Slider Visual Update (#14901)

This implements an update to the look of the Slider widget.

Specifically, it does the following:

* Adds the ability to customize the colors of all components of the slider
* Adds the ability to customize the shape of the slider thumb and value indicator
* Adds the ability to show the value indicator on continuous sliders
* Updates the default value indicator to be a "paddle" shape with new animations.
* Changes the tick marks to be visible all the time on discrete sliders
* Fixes a memory leak of an animation controller.
* Removes "thumbOpenAtMin" flag, which is no longer needed, and can be emulated by the
custom thumb shape support. It was not widely used.
* Adds tests for all of the new features.
This commit is contained in:
Greg Spencer 2018-03-01 13:03:20 -08:00 committed by GitHub
parent d2dcec22ce
commit 701eff4ac5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 2026 additions and 535 deletions

View file

@ -31,7 +31,6 @@ class _SliderDemoState extends State<SliderDemo> {
value: _value,
min: 0.0,
max: 100.0,
thumbOpenAtMin: true,
onChanged: (double value) {
setState(() {
_value = value;
@ -44,7 +43,7 @@ class _SliderDemoState extends State<SliderDemo> {
new Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget> [
const Slider(value: 0.25, thumbOpenAtMin: true, onChanged: null),
const Slider(value: 0.25, onChanged: null),
const Text('Disabled'),
]
),
@ -57,7 +56,6 @@ class _SliderDemoState extends State<SliderDemo> {
max: 100.0,
divisions: 5,
label: '${_discreteValue.round()}',
thumbOpenAtMin: true,
onChanged: (double value) {
setState(() {
_discreteValue = value;

View file

@ -77,6 +77,7 @@ export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart';
export 'src/material/shadows.dart';
export 'src/material/slider.dart';
export 'src/material/slider_theme.dart';
export 'src/material/snack_bar.dart';
export 'src/material/stepper.dart';
export 'src/material/switch.dart';

View file

@ -135,7 +135,7 @@ class ButtonTheme extends InheritedWidget {
/// A button theme can be specified as part of the overall Material theme
/// using [ThemeData.buttomTheme]. The Material theme's button theme data
/// can be overridden with [ButtonTheme].
class ButtonThemeData {
class ButtonThemeData extends Diagnosticable {
/// Create a button theme object that can be used with [ButtonTheme]
/// or [ThemeData].
///
@ -251,4 +251,18 @@ class ButtonThemeData {
shape,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
final ButtonThemeData defaultTheme = const ButtonThemeData();
description.add(new EnumProperty<ButtonTextTheme>('textTheme', textTheme,
defaultValue: defaultTheme.textTheme));
description.add(new DoubleProperty('minWidth', minWidth, defaultValue: defaultTheme.minWidth));
description.add(new DoubleProperty('height', height, defaultValue: defaultTheme.height));
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
defaultValue: defaultTheme.padding));
description.add(
new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultTheme.shape));
}
}

View file

@ -312,7 +312,7 @@ class InkResponse extends StatefulWidget {
/// * [highlightColor], the color of the highlight.
/// * [InkSplash.splashFactory], which defines the default splash.
/// * [InkRipple.splashFactory], which defines a splash that spreads out
/// more aggresively than the default.
/// more aggressively than the default.
final InteractiveInkFeatureFactory splashFactory;
/// Whether detected gestures should provide acoustic and/or haptic feedback.

View file

@ -53,7 +53,7 @@ class _InputBorderGap extends ChangeNotifier {
// Used to interpolate between two InputBorders.
class _InputBorderTween extends Tween<InputBorder> {
_InputBorderTween({ InputBorder begin, InputBorder end }) : super(begin: begin, end: end);
_InputBorderTween({InputBorder begin, InputBorder end}) : super(begin: begin, end: end);
@override
InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t);
@ -108,7 +108,7 @@ class _BorderContainer extends StatefulWidget {
@required this.border,
@required this.gap,
@required this.gapAnimation,
this.child
this.child,
}) : assert(border != null),
assert(gap != null),
super(key: key);
@ -2164,7 +2164,7 @@ class InputDecoration {
/// The [InputDecoration.applyDefaults] method is used to combine a input
/// decoration theme with an [InputDecoration] object.
@immutable
class InputDecorationTheme {
class InputDecorationTheme extends Diagnosticable {
/// Creates a value for [ThemeData.inputDecorationTheme] that
/// defines default values for [InputDecorator].
///
@ -2300,4 +2300,36 @@ class InputDecorationTheme {
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container.
final InputBorder border;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
final InputDecorationTheme defaultTheme = const InputDecorationTheme();
description.add(new DiagnosticsProperty<TextStyle>('labelStyle', labelStyle,
defaultValue: defaultTheme.labelStyle));
description.add(new DiagnosticsProperty<TextStyle>('helperStyle', helperStyle,
defaultValue: defaultTheme.helperStyle));
description.add(new DiagnosticsProperty<TextStyle>('hintStyle', hintStyle,
defaultValue: defaultTheme.hintStyle));
description.add(new DiagnosticsProperty<TextStyle>('errorStyle', errorStyle,
defaultValue: defaultTheme.errorStyle));
description
.add(new DiagnosticsProperty<bool>('isDense', isDense, defaultValue: defaultTheme.isDense));
description.add(new DiagnosticsProperty<EdgeInsets>('contentPadding', contentPadding,
defaultValue: defaultTheme.contentPadding));
description.add(new DiagnosticsProperty<bool>('isCollapsed', isCollapsed,
defaultValue: defaultTheme.isCollapsed));
description.add(new DiagnosticsProperty<TextStyle>('prefixStyle', prefixStyle,
defaultValue: defaultTheme.prefixStyle));
description.add(new DiagnosticsProperty<TextStyle>('suffixStyle', suffixStyle,
defaultValue: defaultTheme.suffixStyle));
description.add(new DiagnosticsProperty<TextStyle>('counterStyle', counterStyle,
defaultValue: defaultTheme.counterStyle));
description
.add(new DiagnosticsProperty<bool>('filled', filled, defaultValue: defaultTheme.filled));
description.add(new DiagnosticsProperty<Color>('fillColor', fillColor,
defaultValue: defaultTheme.fillColor));
description.add(
new DiagnosticsProperty<InputBorder>('border', border, defaultValue: defaultTheme.border));
}
}

View file

@ -9,39 +9,61 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'material.dart';
import 'slider_theme.dart';
import 'theme.dart';
import 'typography.dart';
/// A material design slider.
/// A Material Design slider.
///
/// Used to select from a range of values.
///
/// A slider can be used to select from either a continuous or a discrete set of
/// values. The default is use a continuous range of values from [min] to [max].
/// To use discrete values, use a non-null value for [divisions], which
/// values. The default is to use a continuous range of values from [min] to
/// [max]. To use discrete values, use a non-null value for [divisions], which
/// indicates the number of discrete intervals. For example, if [min] is 0.0 and
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the values
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the
/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0.
///
/// The terms for the parts of a slider are:
///
/// * The "thumb", which is a shape that slides horizontally when the user
/// drags it.
/// * The "rail", which is the line that the slider thumb slides along.
/// * The "value indicator", which is a shape that pops up when the user
/// is dragging the thumb to indicate the value being selected.
/// * The "active" side of the slider is the side between the thumb and the
/// minimum value.
/// * The "inactive" side of the slider is the side between the thumb and the
/// maximum value.
///
/// The slider will be disabled if [onChanged] is null or if the range given by
/// [min]..[max] is empty (i.e. if [min] is equal to [max]).
///
/// The slider itself does not maintain any state. Instead, when the state of
/// the slider changes, the widget calls the [onChanged] callback. Most widgets
/// that use a slider will listen for the [onChanged] callback and rebuild the
/// slider with a new [value] to update the visual appearance of the slider.
/// The slider widget itself does not maintain any state. Instead, when the state
/// of the slider changes, the widget calls the [onChanged] callback. Most
/// widgets that use a slider will listen for the [onChanged] callback and
/// rebuild the slider with a new [value] to update the visual appearance of the
/// slider.
///
/// By default, a slider will be as wide as possible, centered vertically. When
/// given unbounded constraints, it will attempt to make the track 144 pixels
/// given unbounded constraints, it will attempt to make the rail 144 pixels
/// wide (with margins on each side) and will shrink-wrap vertically.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// To determine how it should be displayed (e.g. colors, thumb shape, etc.),
/// a slider uses the [SliderThemeData] available from either a [SliderTheme]
/// widget or the [ThemeData.sliderTheme] a [Theme] widget above it in the
/// widget tree. You can also override some of the colors with the [activeColor]
/// and [inactiveColor] properties, although more fine-grained control of the
/// look is achieved using a [SliderThemeData].
///
/// See also:
///
/// * [SliderTheme] and [SliderThemeData] for information about controlling
/// the visual appearance of the slider.
/// * [Radio], for selecting among a set of explicit values.
/// * [Checkbox] and [Switch], for toggling a particular value on or off.
/// * <https://material.google.com/components/sliders.html>
@ -55,6 +77,10 @@ class Slider extends StatefulWidget {
///
/// * [value] determines currently selected value for this slider.
/// * [onChanged] is called when the user selects a new value for the slider.
///
/// You can override some of the colors with the [activeColor] and
/// [inactiveColor] properties, although more fine-grained control of the
/// appearance is achieved using a [SliderThemeData].
const Slider({
Key key,
@required this.value,
@ -65,14 +91,12 @@ class Slider extends StatefulWidget {
this.label,
this.activeColor,
this.inactiveColor,
this.thumbOpenAtMin: false,
}) : assert(value != null),
assert(min != null),
assert(max != null),
assert(min <= max),
assert(value >= min && value <= max),
assert(divisions == null || divisions > 0),
assert(thumbOpenAtMin != null),
super(key: key);
/// The currently selected value for this slider.
@ -131,32 +155,43 @@ class Slider extends StatefulWidget {
/// A label to show above the slider when the slider is active.
///
/// Typically used to display the value of a discrete slider.
/// It is used to display the value of a discrete slider, and it is displayed
/// as part of the value indicator shape.
///
/// The label is rendered using the active [ThemeData]'s
/// [ThemeData.accentTextTheme.body2] text style.
///
/// If null, then the value indicator will not be displayed.
///
/// See also:
///
/// * [SliderComponentShape] for how to create a custom value indicator
/// shape.
final String label;
/// The color to use for the portion of the slider that has been selected.
/// The color to use for the portion of the slider rail that is active.
///
/// Defaults to accent color of the current [Theme].
/// The "active" side of the slider is the side between the thumb and the
/// minimum value.
///
/// Defaults to [SliderTheme.activeRailColor] of the current [SliderTheme].
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
final Color activeColor;
/// The color for the unselected portion of the slider.
/// The color for the inactive portion of the slider rail.
///
/// Defaults to the unselected widget color of the current [Theme].
/// The "inactive" side of the slider is the side between the thumb and the
/// maximum value.
///
/// Defaults to the [SliderTheme.inactiveRailColor] of the current
/// [SliderTheme].
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
final Color inactiveColor;
/// Whether the thumb should be an open circle when the slider is at its minimum position.
///
/// When this property is false, the thumb does not change when it the slider
/// reaches its minimum position.
///
/// This property is useful, for example, when the minimum value represents a
/// qualitatively different state. For a slider that controls the volume of
/// a sound, for example, the minimum value represents "no sound at all,"
/// which is qualitatively different from even a very soft sound.
///
/// Defaults to false.
final bool thumbOpenAtMin;
@override
_SliderState createState() => new _SliderState();
@ -170,44 +205,88 @@ class Slider extends StatefulWidget {
}
class _SliderState extends State<Slider> with TickerProviderStateMixin {
_SliderState() {
_reactionController = new AnimationController(
static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
static const Duration positionAnimationDuration = const Duration(milliseconds: 75);
AnimationController reactionController;
AnimationController enableController;
AnimationController positionController;
@override
void initState() {
super.initState();
reactionController = new AnimationController(
duration: kRadialReactionDuration,
vsync: this,
);
enableController = new AnimationController(
duration: enableAnimationDuration,
vsync: this,
);
positionController = new AnimationController(
duration: positionAnimationDuration,
vsync: this,
);
}
void _handleChanged(double value) {
assert(widget.onChanged != null);
widget.onChanged(value * (widget.max - widget.min) + widget.min);
}
@override
void dispose() {
_reactionController?.dispose();
reactionController.dispose();
enableController.dispose();
positionController.dispose();
super.dispose();
}
// Have to keep the reaction controller here so that we may dispose of it
// properly.
AnimationController _reactionController;
void _handleChanged(double value) {
assert(widget.onChanged != null);
final double transformedValue = _lerp(value);
widget.onChanged(transformedValue);
}
// Returns a number between min and max, proportional to value, which must
// be between 0.0 and 1.0.
double _lerp(double value) {
assert(value >= 0.0);
assert(value <= 1.0);
return value * (widget.max - widget.min) + widget.min;
}
// Returns a number between 0.0 and 1.0, given a value between min and max.
double _unlerp(double value) {
assert(value <= widget.max);
assert(value >= widget.min);
return widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
SliderThemeData sliderTheme = SliderTheme.of(context);
// If the widget has active or inactive colors specified, then we plug them
// in to the slider theme as best we can. If the developer wants more
// control than that, then they need to use a SliderTheme.
if (widget.activeColor != null || widget.inactiveColor != null) {
sliderTheme = sliderTheme.copyWith(
activeRailColor: widget.activeColor,
inactiveRailColor: widget.inactiveColor,
activeTickMarkColor: widget.inactiveColor,
inactiveTickMarkColor: widget.activeColor,
thumbColor: widget.activeColor,
valueIndicatorColor: widget.activeColor,
overlayColor: widget.activeColor?.withAlpha(0x29),
);
}
return new _SliderRenderObjectWidget(
value: widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0,
value: _unlerp(widget.value),
divisions: widget.divisions,
label: widget.label,
activeColor: widget.activeColor ?? theme.accentColor,
inactiveColor: widget.inactiveColor ?? theme.unselectedWidgetColor,
thumbOpenAtMin: widget.thumbOpenAtMin,
textTheme: theme.accentTextTheme,
sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
vsync: this,
reactionController: _reactionController,
state: this,
);
}
}
@ -218,27 +297,19 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
this.value,
this.divisions,
this.label,
this.activeColor,
this.inactiveColor,
this.thumbOpenAtMin,
this.textTheme,
this.sliderTheme,
this.textScaleFactor,
this.onChanged,
this.vsync,
this.reactionController,
this.state,
}) : super(key: key);
final double value;
final int divisions;
final String label;
final Color activeColor;
final Color inactiveColor;
final bool thumbOpenAtMin;
final TextTheme textTheme;
final SliderThemeData sliderTheme;
final double textScaleFactor;
final ValueChanged<double> onChanged;
final TickerProvider vsync;
final AnimationController reactionController;
final _SliderState state;
@override
_RenderSlider createRenderObject(BuildContext context) {
@ -246,14 +317,11 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
value: value,
divisions: divisions,
label: label,
activeColor: activeColor,
inactiveColor: inactiveColor,
thumbOpenAtMin: thumbOpenAtMin,
textTheme: textTheme,
sliderTheme: sliderTheme,
theme: Theme.of(context),
textScaleFactor: textScaleFactor,
onChanged: onChanged,
vsync: vsync,
reactionController: reactionController,
state: state,
textDirection: Directionality.of(context),
);
}
@ -264,74 +332,47 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..value = value
..divisions = divisions
..label = label
..activeColor = activeColor
..inactiveColor = inactiveColor
..thumbOpenAtMin = thumbOpenAtMin
..textTheme = textTheme
..sliderTheme = sliderTheme
..theme = Theme.of(context)
..textScaleFactor = textScaleFactor
..onChanged = onChanged
..textDirection = Directionality.of(context);
// Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object.
// Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object.
}
}
const double _kThumbRadius = 6.0;
const double _kActiveThumbRadius = 9.0;
const double _kDisabledThumbRadius = 4.0;
const double _kReactionRadius = 16.0;
const double _kPreferredTrackWidth = 144.0;
const double _kMinimumTrackWidth = _kActiveThumbRadius; // biggest of the thumb radii
const double _kPreferredTotalWidth = _kPreferredTrackWidth + 2 * _kReactionRadius;
const double _kMinimumTotalWidth = _kMinimumTrackWidth + 2 * _kReactionRadius;
const double _overlayRadius = 16.0;
const double _overlayDiameter = _overlayRadius * 2.0;
const double _railHeight = 2.0;
const double _preferredRailWidth = 144.0;
const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
final Color _kActiveTrackColor = Colors.grey;
final Tween<double> _kReactionRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kReactionRadius);
final Tween<double> _kThumbRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kActiveThumbRadius);
final ColorTween _kTickColorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);
const Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500);
const double _kLabelBalloonRadius = 14.0;
final Tween<double> _kLabelBalloonCenterTween = new Tween<double>(begin: 0.0, end: -_kLabelBalloonRadius * 2.0);
final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kLabelBalloonRadius);
final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0);
final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0);
const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider.
double _getAdditionalHeightForLabel(String label) {
return label == null ? 0.0 : _kLabelBalloonRadius * 2.0;
}
double _getPreferredTotalHeight(String label) {
return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label);
}
const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
class _RenderSlider extends RenderBox {
_RenderSlider({
@required double value,
int divisions,
String label,
Color activeColor,
Color inactiveColor,
bool thumbOpenAtMin,
TextTheme textTheme,
SliderThemeData sliderTheme,
ThemeData theme,
double textScaleFactor,
ValueChanged<double> onChanged,
TickerProvider vsync,
@required _SliderState state,
@required TextDirection textDirection,
@required AnimationController reactionController,
}) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(state != null),
assert(textDirection != null),
_label = label,
_value = value,
_divisions = divisions,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_thumbOpenAtMin = thumbOpenAtMin,
_textTheme = textTheme,
_sliderTheme = sliderTheme,
_theme = theme,
_textScaleFactor = textScaleFactor,
_onChanged = onChanged,
_state = state,
_textDirection = textDirection {
_updateLabelPainter();
final GestureArenaTeam team = new GestureArenaTeam();
@ -342,104 +383,106 @@ class _RenderSlider extends RenderBox {
..onEnd = _handleDragEnd;
_tap = new TapGestureRecognizer()
..team = team
..onTapCancel = _endInteraction
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp;
_reactionController = reactionController;
_reaction = new CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn
)..addListener(markNeedsPaint);
_position = new AnimationController(
value: value,
duration: _kDiscreteTransitionDuration,
vsync: vsync,
)..addListener(markNeedsPaint);
_reaction = new CurvedAnimation(parent: state.reactionController, curve: Curves.fastOutSlowIn)
..addListener(markNeedsPaint);
state.enableController.value = isInteractive ? 1.0 : 0.0;
_enableAnimation = new CurvedAnimation(parent: state.enableController, curve: Curves.easeInOut)
..addListener(markNeedsPaint);
state.positionController.value = _value;
}
double get value => _value;
double _value;
_SliderState _state;
set value(double newValue) {
assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
if (newValue == _value)
final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
if (convertedValue == _value) {
return;
_value = newValue;
if (divisions != null)
_position.animateTo(newValue, curve: Curves.fastOutSlowIn);
else
_position.value = newValue;
}
_value = convertedValue;
if (isDiscrete) {
_state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
} else {
_state.positionController.value = convertedValue;
}
}
int get divisions => _divisions;
int _divisions;
set divisions(int value) {
if (value == _divisions)
if (value == _divisions) {
return;
}
_divisions = value;
markNeedsPaint();
}
String get label => _label;
String _label;
set label(String value) {
if (value == _label)
if (value == _label) {
return;
}
_label = value;
_updateLabelPainter();
}
Color get activeColor => _activeColor;
Color _activeColor;
set activeColor(Color value) {
if (value == _activeColor)
SliderThemeData get sliderTheme => _sliderTheme;
SliderThemeData _sliderTheme;
set sliderTheme(SliderThemeData value) {
if (value == _sliderTheme) {
return;
_activeColor = value;
}
_sliderTheme = value;
markNeedsPaint();
}
Color get inactiveColor => _inactiveColor;
Color _inactiveColor;
set inactiveColor(Color value) {
if (value == _inactiveColor)
return;
_inactiveColor = value;
markNeedsPaint();
}
ThemeData get theme => _theme;
ThemeData _theme;
bool get thumbOpenAtMin => _thumbOpenAtMin;
bool _thumbOpenAtMin;
set thumbOpenAtMin(bool value) {
if (value == _thumbOpenAtMin)
set theme(ThemeData value) {
if (value == _theme) {
return;
_thumbOpenAtMin = value;
markNeedsPaint();
}
TextTheme get textTheme => _textTheme;
TextTheme _textTheme;
set textTheme(TextTheme value) {
if (value == _textTheme)
return;
_textTheme = value;
}
_theme = value;
markNeedsPaint();
}
double get textScaleFactor => _textScaleFactor;
double _textScaleFactor;
set textScaleFactor(double value) {
if (value == _textScaleFactor)
if (value == _textScaleFactor) {
return;
}
_textScaleFactor = value;
_updateLabelPainter();
markNeedsPaint();
}
ValueChanged<double> get onChanged => _onChanged;
ValueChanged<double> _onChanged;
set onChanged(ValueChanged<double> value) {
if (value == _onChanged)
if (value == _onChanged) {
return;
}
final bool wasInteractive = isInteractive;
_onChanged = value;
if (wasInteractive != isInteractive) {
if (isInteractive) {
_state.enableController.forward();
} else {
_state.enableController.reverse();
}
markNeedsPaint();
markNeedsSemanticsUpdate();
}
@ -447,21 +490,23 @@ class _RenderSlider extends RenderBox {
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (value == _textDirection)
if (value == _textDirection) {
return;
}
_textDirection = value;
_updateLabelPainter();
}
void _updateLabelPainter() {
if (label != null) {
// We have to account for the text scale factor in the supplied theme.
final TextStyle style = _theme.accentTextTheme.body2
.copyWith(fontSize: _theme.accentTextTheme.body2.fontSize * _textScaleFactor);
_labelPainter
..text = new TextSpan(
style: _textTheme.body1.copyWith(fontSize: 10.0 * _textScaleFactor),
text: label,
)
..text = new TextSpan(style: style, text: label)
..textDirection = textDirection
..layout();
} else {
@ -473,14 +518,11 @@ class _RenderSlider extends RenderBox {
markNeedsLayout();
}
double get _trackLength => size.width - 2.0 * _kReactionRadius;
double get _railLength => size.width - _overlayDiameter;
Animation<double> _reaction;
AnimationController _reactionController;
AnimationController _position;
Animation<double> _enableAnimation;
final TextPainter _labelPainter = new TextPainter();
HorizontalDragGestureRecognizer _drag;
TapGestureRecognizer _tap;
bool _active = false;
@ -488,6 +530,8 @@ class _RenderSlider extends RenderBox {
bool get isInteractive => onChanged != null;
bool get isDiscrete => divisions != null && divisions > 0;
double _getValueFromVisualPosition(double visualPosition) {
switch (textDirection) {
case TextDirection.rtl:
@ -499,29 +543,40 @@ class _RenderSlider extends RenderBox {
}
double _getValueFromGlobalPosition(Offset globalPosition) {
final double visualPosition = (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength;
final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
return _getValueFromVisualPosition(visualPosition);
}
double _discretize(double value) {
double result = value.clamp(0.0, 1.0);
if (divisions != null)
if (isDiscrete) {
result = (result * divisions).round() / divisions;
}
return result;
}
void _handleDragStart(DragStartDetails details) {
void _startInteraction(Offset globalPosition) {
if (isInteractive) {
_active = true;
_currentDragValue = _getValueFromGlobalPosition(details.globalPosition);
_currentDragValue = _getValueFromGlobalPosition(globalPosition);
onChanged(_discretize(_currentDragValue));
_reactionController.forward();
_state.reactionController.forward();
}
}
void _endInteraction() {
if (_active) {
_active = false;
_currentDragValue = 0.0;
_state.reactionController.reverse();
}
}
void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
final double valueDelta = details.primaryDelta / _trackLength;
final double valueDelta = details.primaryDelta / _railLength;
switch (textDirection) {
case TextDirection.rtl:
_currentDragValue -= valueDelta;
@ -534,18 +589,11 @@ class _RenderSlider extends RenderBox {
}
}
void _handleDragEnd(DragEndDetails details) {
if (_active) {
_active = false;
_currentDragValue = 0.0;
_reactionController.reverse();
}
}
void _handleDragEnd(DragEndDetails details) => _endInteraction();
void _handleTapUp(TapUpDetails details) {
if (isInteractive && !_active)
onChanged(_discretize(_getValueFromGlobalPosition(details.globalPosition)));
}
void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);
void _handleTapUp(TapUpDetails details) => _endInteraction();
@override
bool hitTestSelf(Offset position) => true;
@ -562,25 +610,22 @@ class _RenderSlider extends RenderBox {
@override
double computeMinIntrinsicWidth(double height) {
return _kMinimumTotalWidth;
return math.max(_overlayDiameter,
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width);
}
@override
double computeMaxIntrinsicWidth(double height) {
// This doesn't quite match the definition of computeMaxIntrinsicWidth,
// but it seems within the spirit...
return _kPreferredTotalWidth;
return _preferredTotalWidth;
}
@override
double computeMinIntrinsicHeight(double width) {
return _getPreferredTotalHeight(label);
}
double computeMinIntrinsicHeight(double width) => _overlayDiameter;
@override
double computeMaxIntrinsicHeight(double width) {
return _getPreferredTotalHeight(label);
}
double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
@override
bool get sizedByParent => true;
@ -588,123 +633,174 @@ class _RenderSlider extends RenderBox {
@override
void performResize() {
size = new Size(
constraints.hasBoundedWidth ? constraints.maxWidth : _kPreferredTotalWidth,
constraints.hasBoundedHeight ? constraints.maxHeight : _getPreferredTotalHeight(label),
constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter,
);
}
void _paintTickMarks(
Canvas canvas, Rect railLeft, Rect railRight, Paint leftPaint, Paint rightPaint) {
if (isDiscrete) {
// The ticks are tiny circles that are the same height as the rail.
const double tickRadius = _railHeight / 2.0;
final double railWidth = railRight.right - railLeft.left;
final double dx = (railWidth - _railHeight) / divisions;
// If the ticks would be too dense, don't bother painting them.
if (dx >= 3.0 * _railHeight) {
for (int i = 0; i <= divisions; i += 1) {
final double left = railLeft.left + i * dx;
final Offset center = new Offset(left + tickRadius, railLeft.top + tickRadius);
if (railLeft.contains(center)) {
canvas.drawCircle(center, tickRadius, leftPaint);
} else if (railRight.contains(center)) {
canvas.drawCircle(center, tickRadius, rightPaint);
}
}
}
}
}
void _paintOverlay(Canvas canvas, Offset center) {
if (!_reaction.isDismissed) {
// TODO(gspencer) : We don't really follow the spec here for overlays.
// The spec says to use 16% opacity for drawing over light material,
// and 32% for colored material, but we don't really have a way to
// know what the underlying color is, so there's no easy way to
// implement this. Choosing the "light" version for now.
final Paint reactionPaint = new Paint()..color = _sliderTheme.overlayColor;
final double radius = _overlayRadiusTween.evaluate(_reaction);
canvas.drawCircle(center, radius, reactionPaint);
}
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final double trackLength = size.width - 2 * _kReactionRadius;
final bool enabled = isInteractive;
final double value = _position.value;
final bool thumbAtMin = value == 0.0;
final double railLength = size.width - 2 * _overlayRadius;
final double value = _state.positionController.value;
final ColorTween activeRailEnableColor = new ColorTween(
begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor);
final ColorTween inactiveRailEnableColor = new ColorTween(
begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor);
final ColorTween activeTickMarkEnableColor = new ColorTween(
begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor);
final ColorTween inactiveTickMarkEnableColor = new ColorTween(
begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor);
final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor;
final Paint trackPaint = new Paint()..color = _inactiveColor;
final Paint activeRailPaint = new Paint()
..color = activeRailEnableColor.evaluate(_enableAnimation);
final Paint inactiveRailPaint = new Paint()
..color = inactiveRailEnableColor.evaluate(_enableAnimation);
final Paint activeTickMarkPaint = new Paint()
..color = activeTickMarkEnableColor.evaluate(_enableAnimation);
final Paint inactiveTickMarkPaint = new Paint()
..color = inactiveTickMarkEnableColor.evaluate(_enableAnimation);
double visualPosition;
Paint leftPaint;
Paint rightPaint;
Paint leftRailPaint;
Paint rightRailPaint;
Paint leftTickMarkPaint;
Paint rightTickMarkPaint;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - value;
leftPaint = trackPaint;
rightPaint = primaryPaint;
leftRailPaint = inactiveRailPaint;
rightRailPaint = activeRailPaint;
leftTickMarkPaint = inactiveTickMarkPaint;
rightTickMarkPaint = activeTickMarkPaint;
break;
case TextDirection.ltr:
visualPosition = value;
leftPaint = primaryPaint;
rightPaint = trackPaint;
leftRailPaint = activeRailPaint;
rightRailPaint = inactiveRailPaint;
leftTickMarkPaint = activeTickMarkPaint;
rightTickMarkPaint = inactiveTickMarkPaint;
break;
}
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label);
final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel;
final double trackLeft = offset.dx + _kReactionRadius;
final double trackTop = trackCenter - 1.0;
final double trackBottom = trackCenter + 1.0;
final double trackRight = trackLeft + trackLength;
final double trackActive = trackLeft + trackLength * visualPosition;
const double railRadius = _railHeight / 2.0;
const double thumbGap = 2.0;
final Offset thumbCenter = new Offset(trackActive, trackCenter);
final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius;
final double railVerticalCenter = offset.dy + (size.height) / 2.0;
final double railLeft = offset.dx + _overlayRadius;
final double railTop = railVerticalCenter - railRadius;
final double railBottom = railVerticalCenter + railRadius;
final double railRight = railLeft + railLength;
final double railActive = railLeft + railLength * visualPosition;
final double thumbRadius =
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0;
final double railActiveLeft =
math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value));
final double railActiveRight =
math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight);
final Rect railLeftRect = new Rect.fromLTRB(railLeft, railTop, railActiveLeft, railBottom);
final Rect railRightRect = new Rect.fromLTRB(railActiveRight, railTop, railRight, railBottom);
if (enabled) {
if (visualPosition > 0.0)
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), leftPaint);
if (visualPosition < 1.0) {
final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null;
final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0;
canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), rightPaint);
}
} else {
if (visualPosition > 0.0)
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint);
if (visualPosition < 1.0)
canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint);
final Offset thumbCenter = new Offset(railActive, railVerticalCenter);
// Paint the rail.
if (visualPosition > 0.0) {
canvas.drawRect(railLeftRect, leftRailPaint);
}
if (visualPosition < 1.0) {
canvas.drawRect(railRightRect, rightRailPaint);
}
if (_reaction.status != AnimationStatus.dismissed) {
final int divisions = this.divisions;
if (divisions != null) {
const double tickWidth = 2.0;
final double dx = (trackLength - tickWidth) / divisions;
// If the ticks would be too dense, don't bother painting them.
if (dx >= 3 * tickWidth) {
final Paint tickPaint = new Paint()..color = _kTickColorTween.evaluate(_reaction);
for (int i = 0; i <= divisions; i += 1) {
final double left = trackLeft + i * dx;
canvas.drawRect(new Rect.fromLTRB(left, trackTop, left + tickWidth, trackBottom), tickPaint);
}
}
_paintOverlay(canvas, thumbCenter);
_paintTickMarks(
canvas,
railLeftRect,
railRightRect,
leftTickMarkPaint,
rightTickMarkPaint,
);
if (isInteractive && _reaction.status != AnimationStatus.dismissed && label != null) {
bool showValueIndicator;
switch (_sliderTheme.showValueIndicator) {
case ShowValueIndicator.onlyForDiscrete:
showValueIndicator = isDiscrete;
break;
case ShowValueIndicator.onlyForContinuous:
showValueIndicator = !isDiscrete;
break;
case ShowValueIndicator.always:
showValueIndicator = true;
break;
case ShowValueIndicator.never:
showValueIndicator = false;
break;
}
if (label != null) {
final Offset center = new Offset(
trackActive,
_kLabelBalloonCenterTween.evaluate(_reaction) * textScaleFactor + trackCenter
if (showValueIndicator) {
_sliderTheme.valueIndicatorShape.paint(
context,
isDiscrete,
thumbCenter,
_reaction,
_enableAnimation,
_labelPainter,
_sliderTheme,
_textDirection,
_textScaleFactor,
value,
);
final double radius = _kLabelBalloonRadiusTween.evaluate(_reaction) * textScaleFactor;
final Offset tip = new Offset(
trackActive,
_kLabelBalloonTipTween.evaluate(_reaction) * textScaleFactor + trackCenter
);
final double tipAttachment = _kLabelBalloonTipAttachmentRatio * radius;
canvas.drawCircle(center, radius, primaryPaint);
final Path path = new Path()
..moveTo(tip.dx, tip.dy)
..lineTo(center.dx - tipAttachment, center.dy + tipAttachment)
..lineTo(center.dx + tipAttachment, center.dy + tipAttachment)
..close();
canvas.drawPath(path, primaryPaint);
final Offset labelOffset = new Offset(
center.dx - _labelPainter.width / 2.0,
center.dy - _labelPainter.height / 2.0
);
_labelPainter.paint(canvas, labelOffset);
return;
} else {
final Color reactionBaseColor = thumbAtMin ? _kActiveTrackColor : _activeColor;
final Paint reactionPaint = new Paint()..color = reactionBaseColor.withAlpha(kRadialReactionAlpha);
canvas.drawCircle(thumbCenter, _kReactionRadiusTween.evaluate(_reaction), reactionPaint);
}
}
Paint thumbPaint = primaryPaint;
double thumbRadiusDelta = 0.0;
if (thumbAtMin && thumbOpenAtMin) {
thumbPaint = trackPaint;
// This is destructive to trackPaint.
thumbPaint
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
thumbRadiusDelta = -1.0;
}
canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint);
_sliderTheme.thumbShape.paint(
context,
isDiscrete,
thumbCenter,
_reaction,
_enableAnimation,
label != null ? _labelPainter : null,
_sliderTheme,
_textDirection,
_textScaleFactor,
value,
);
}
@override
@ -718,15 +814,17 @@ class _RenderSlider extends RenderBox {
}
}
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
void _increaseAction() {
if (isInteractive)
if (isInteractive) {
onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
}
}
void _decreaseAction() {
if (isInteractive)
if (isInteractive) {
onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
}
}
}

View file

@ -0,0 +1,729 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show Path;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
import 'theme_data.dart';
/// Applies a slider theme to descendant [Slider] widgets.
///
/// A slider theme describes the colors and shape choices of the slider
/// components.
///
/// Descendant widgets obtain the current theme's [SliderThemeData] object using
/// [SliderTheme.of]. When a widget uses [SliderTheme.of], it is automatically
/// rebuilt if the theme later changes.
///
/// See also:
///
/// * [SliderThemeData], which describes the actual configuration of a slider
/// theme.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the slider thumb and value indicator.
class SliderTheme extends InheritedWidget {
/// Applies the given theme [data] to [child].
///
/// The [data] and [child] arguments must not be null.
const SliderTheme({
Key key,
@required this.data,
@required Widget child,
}) : assert(child != null),
assert(data != null),
super(key: key, child: child);
/// Specifies the color and shape values for descendant slider widgets.
final SliderThemeData data;
/// Returns the data from the closest [SliderTheme] instance that encloses
/// the given context.
///
/// Defaults to the ambient [ThemeData.sliderTheme] if there is no
/// [SliderTheme] in the given build context.
///
/// Typical usage is as follows:
///
/// ```dart
/// double _rocketThrust;
///
/// @override
/// Widget build(BuildContext context) {
/// return new SliderTheme(
/// data: SliderTheme.of(context).copyWith(activeRail: Colors.orange),
/// child: new Slider(
/// onChanged: (double value) => setState(() => _rocketThrust = value),
/// value: _rocketThrust;
/// ),
/// );
/// }
/// ```
///
/// See also:
///
/// * [SliderThemeData], which describes the actual configuration of a slider
/// theme.
static SliderThemeData of(BuildContext context) {
final SliderTheme inheritedTheme = context.inheritFromWidgetOfExactType(SliderTheme);
return inheritedTheme != null ? inheritedTheme.data : Theme.of(context).sliderTheme;
}
@override
bool updateShouldNotify(SliderTheme old) => data != old.data;
}
/// Describes the conditions under which the value indicator on a [Slider]
/// will be shown. Used with [SliderThemeData.showValueIndicator].
///
/// See also:
///
/// * [Slider], a Material Design slider widget.
/// * [SliderThemeData], which describes the actual configuration of a slider
/// theme.
enum ShowValueIndicator {
/// The value indicator will only be shown for discrete sliders (sliders
/// where [Slider.divisions] is non-null).
onlyForDiscrete,
/// The value indicator will only be shown for continuous sliders (sliders
/// where [Slider.divisions] is null).
onlyForContinuous,
/// The value indicator will be shown for all types of sliders.
always,
/// The value indicator will never be shown.
never,
}
/// Holds the color, shape, and typography values for a material design slider
/// theme.
///
/// Use this class to configure a [SliderTheme] widget, or to set the
/// [ThemeData.sliderTheme] for a [Theme] widget.
///
/// To obtain the current ambient slider theme, use [SliderTheme.of].
///
/// The parts of a slider are:
///
/// * The "thumb", which is a shape that slides horizontally when the user
/// drags it.
/// * The "rail", which is the line that the slider thumb slides along.
/// * The "value indicator", which is a shape that pops up when the user
/// is dragging the thumb to indicate the value being selected.
/// * The "active" side of the slider is the side between the thumb and the
/// minimum value.
/// * The "inactive" side of the slider is the side between the thumb and the
/// maximum value.
/// * The [Slider] is disabled when it is not accepting user input. See
/// [Slider] for details on when this happens.
///
/// The thumb and the value indicator may have their shapes and behavior
/// customized by creating your own [SliderComponentShape] that does what
/// you want. See [RoundSliderThumbShape] and
/// [PaddleSliderValueIndicatorShape] for examples.
///
/// See also:
///
/// * [SliderTheme] widget, which can override the slider theme of its
/// children.
/// * [Theme] widget, which performs a similar function to [SliderTheme],
/// but for overall themes.
/// * [ThemeData], which has a default [SliderThemeData].
/// * [SliderComponentShape], to define custom slider component shapes.
class SliderThemeData extends Diagnosticable {
/// Create a [SliderThemeData] given a set of exact values. All the values
/// must be specified.
///
/// This will rarely be used directly. It is used by [lerp] to
/// create intermediate themes based on two themes.
///
/// The simplest way to create a SliderThemeData is to use
/// [copyWith] on the one you get from [SliderTheme.of], or create an
/// entirely new one with [SliderThemeData.materialDefaults].
const SliderThemeData({
@required this.activeRailColor,
@required this.inactiveRailColor,
@required this.disabledActiveRailColor,
@required this.disabledInactiveRailColor,
@required this.activeTickMarkColor,
@required this.inactiveTickMarkColor,
@required this.disabledActiveTickMarkColor,
@required this.disabledInactiveTickMarkColor,
@required this.thumbColor,
@required this.disabledThumbColor,
@required this.overlayColor,
@required this.valueIndicatorColor,
@required this.thumbShape,
@required this.valueIndicatorShape,
@required this.showValueIndicator,
}) : assert(activeRailColor != null),
assert(inactiveRailColor != null),
assert(disabledActiveRailColor != null),
assert(disabledInactiveRailColor != null),
assert(activeTickMarkColor != null),
assert(inactiveTickMarkColor != null),
assert(disabledActiveTickMarkColor != null),
assert(disabledInactiveTickMarkColor != null),
assert(thumbColor != null),
assert(disabledThumbColor != null),
assert(overlayColor != null),
assert(valueIndicatorColor != null),
assert(thumbShape != null),
assert(valueIndicatorShape != null),
assert(showValueIndicator != null);
/// Generates a SliderThemeData from three main colors.
///
/// Usually these are the primary, dark and light colors from
/// a [ThemeData].
///
/// The opacities of these colors will be overridden with the Material Design
/// defaults when assigning them to the slider theme component colors.
///
/// This is used to generate the default slider theme for a [ThemeData].
factory SliderThemeData.materialDefaults({
@required Color primaryColor,
@required Color primaryColorDark,
@required Color primaryColorLight,
}) {
assert(primaryColor != null);
assert(primaryColorDark != null);
assert(primaryColorLight != null);
// These are Material Design defaults, and are used to derive
// component Colors (with opacity) from base colors.
const int activeRailAlpha = 0xff;
const int inactiveRailAlpha = 0x3d; // 24% opacity
const int disabledActiveRailAlpha = 0x52; // 32% opacity
const int disabledInactiveRailAlpha = 0x1f; // 12% opacity
const int activeTickMarkAlpha = 0x8a; // 54% opacity
const int inactiveTickMarkAlpha = 0x8a; // 54% opacity
const int disabledActiveTickMarkAlpha = 0x1f; // 12% opacity
const int disabledInactiveTickMarkAlpha = 0x1f; // 12% opacity
const int thumbAlpha = 0xff;
const int disabledThumbAlpha = 0x52; // 32% opacity
const int valueIndicatorAlpha = 0xff;
// TODO(gspencer): We don't really follow the spec here for overlays.
// The spec says to use 16% opacity for drawing over light material,
// and 32% for colored material, but we don't really have a way to
// know what the underlying color is, so there's no easy way to
// implement this. Choosing the "light" version for now.
const int overlayLightAlpha = 0x29; // 16% opacity
return new SliderThemeData(
activeRailColor: primaryColor.withAlpha(activeRailAlpha),
inactiveRailColor: primaryColor.withAlpha(inactiveRailAlpha),
disabledActiveRailColor: primaryColorDark.withAlpha(disabledActiveRailAlpha),
disabledInactiveRailColor: primaryColorDark.withAlpha(disabledInactiveRailAlpha),
activeTickMarkColor: primaryColorLight.withAlpha(activeTickMarkAlpha),
inactiveTickMarkColor: primaryColor.withAlpha(inactiveTickMarkAlpha),
disabledActiveTickMarkColor: primaryColorLight.withAlpha(disabledActiveTickMarkAlpha),
disabledInactiveTickMarkColor: primaryColorDark.withAlpha(disabledInactiveTickMarkAlpha),
thumbColor: primaryColor.withAlpha(thumbAlpha),
disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha),
overlayColor: primaryColor.withAlpha(overlayLightAlpha),
valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha),
thumbShape: const RoundSliderThumbShape(),
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
showValueIndicator: ShowValueIndicator.onlyForDiscrete,
);
}
final Color activeRailColor;
final Color inactiveRailColor;
final Color disabledActiveRailColor;
final Color disabledInactiveRailColor;
final Color activeTickMarkColor;
final Color inactiveTickMarkColor;
final Color disabledActiveTickMarkColor;
final Color disabledInactiveTickMarkColor;
final Color thumbColor;
final Color disabledThumbColor;
final Color overlayColor;
final Color valueIndicatorColor;
final SliderComponentShape thumbShape;
final SliderComponentShape valueIndicatorShape;
/// Whether the value indicator should be shown for different types of sliders.
///
/// By default, [showValueIndicator] is set to
/// [ShowValueIndicator.onlyForDiscrete]. The value indicator is only shown
/// when the thumb is being touched.
final ShowValueIndicator showValueIndicator;
SliderThemeData copyWith({
Color activeRailColor,
Color inactiveRailColor,
Color disabledActiveRailColor,
Color disabledInactiveRailColor,
Color activeTickMarkColor,
Color inactiveTickMarkColor,
Color disabledActiveTickMarkColor,
Color disabledInactiveTickMarkColor,
Color thumbColor,
Color disabledThumbColor,
Color overlayColor,
Color valueIndicatorColor,
SliderComponentShape thumbShape,
SliderComponentShape valueIndicatorShape,
ShowValueIndicator showValueIndicator,
}) {
return new SliderThemeData(
activeRailColor: activeRailColor ?? this.activeRailColor,
inactiveRailColor: inactiveRailColor ?? this.inactiveRailColor,
disabledActiveRailColor: disabledActiveRailColor ?? this.disabledActiveRailColor,
disabledInactiveRailColor: disabledInactiveRailColor ?? this.disabledInactiveRailColor,
activeTickMarkColor: activeTickMarkColor ?? this.activeTickMarkColor,
inactiveTickMarkColor: inactiveTickMarkColor ?? this.inactiveTickMarkColor,
disabledActiveTickMarkColor: disabledActiveTickMarkColor ?? this.disabledActiveTickMarkColor,
disabledInactiveTickMarkColor:
disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor,
thumbColor: thumbColor ?? this.thumbColor,
disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor,
overlayColor: overlayColor ?? this.overlayColor,
valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor,
thumbShape: thumbShape ?? this.thumbShape,
valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape,
showValueIndicator: showValueIndicator ?? this.showValueIndicator,
);
}
/// Linearly interpolate between two slider themes.
///
/// The arguments must not be null.
///
/// The `t` argument represents position on the timeline, with 0.0 meaning
/// that the interpolation has not started, returning `a` (or something
/// equivalent to `a`), 1.0 meaning that the interpolation has finished,
/// returning `b` (or something equivalent to `b`), and values in between
/// meaning that the interpolation is at the relevant point on the timeline
/// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
/// 1.0, so negative values and values greater than 1.0 are valid (and can
/// easily be generated by curves such as [Curves.elasticInOut]).
///
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
assert(a != null);
assert(b != null);
assert(t != null);
return new SliderThemeData(
activeRailColor: Color.lerp(a.activeRailColor, b.activeRailColor, t),
inactiveRailColor: Color.lerp(a.inactiveRailColor, b.inactiveRailColor, t),
disabledActiveRailColor: Color.lerp(a.disabledActiveRailColor, b.disabledActiveRailColor, t),
disabledInactiveRailColor:
Color.lerp(a.disabledInactiveRailColor, b.disabledInactiveRailColor, t),
activeTickMarkColor: Color.lerp(a.activeTickMarkColor, b.activeTickMarkColor, t),
inactiveTickMarkColor: Color.lerp(a.inactiveTickMarkColor, b.inactiveTickMarkColor, t),
disabledActiveTickMarkColor:
Color.lerp(a.disabledActiveTickMarkColor, b.disabledActiveTickMarkColor, t),
disabledInactiveTickMarkColor:
Color.lerp(a.disabledInactiveTickMarkColor, b.disabledInactiveTickMarkColor, t),
thumbColor: Color.lerp(a.thumbColor, b.thumbColor, t),
disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t),
overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t),
valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t),
thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape,
valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape,
showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator,
);
}
@override
int get hashCode {
return hashValues(
activeRailColor,
inactiveRailColor,
disabledActiveRailColor,
disabledInactiveRailColor,
activeTickMarkColor,
inactiveTickMarkColor,
disabledActiveTickMarkColor,
disabledInactiveTickMarkColor,
thumbColor,
disabledThumbColor,
overlayColor,
valueIndicatorColor,
thumbShape,
valueIndicatorShape,
showValueIndicator,
);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
final SliderThemeData otherData = other;
return otherData.activeRailColor == activeRailColor &&
otherData.inactiveRailColor == inactiveRailColor &&
otherData.disabledActiveRailColor == disabledActiveRailColor &&
otherData.disabledInactiveRailColor == disabledInactiveRailColor &&
otherData.activeTickMarkColor == activeTickMarkColor &&
otherData.inactiveTickMarkColor == inactiveTickMarkColor &&
otherData.disabledActiveTickMarkColor == disabledActiveTickMarkColor &&
otherData.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor &&
otherData.thumbColor == thumbColor &&
otherData.disabledThumbColor == disabledThumbColor &&
otherData.overlayColor == overlayColor &&
otherData.valueIndicatorColor == valueIndicatorColor &&
otherData.thumbShape == thumbShape &&
otherData.valueIndicatorShape == valueIndicatorShape &&
otherData.showValueIndicator == showValueIndicator;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
final ThemeData defaultTheme = new ThemeData.fallback();
final SliderThemeData defaultData = new SliderThemeData.materialDefaults(
primaryColor: defaultTheme.primaryColor,
primaryColorDark: defaultTheme.primaryColorDark,
primaryColorLight: defaultTheme.primaryColorLight,
);
description.add(new DiagnosticsProperty<Color>('activeRailColor', activeRailColor,
defaultValue: defaultData.activeRailColor));
description.add(new DiagnosticsProperty<Color>('inactiveRailColor', inactiveRailColor,
defaultValue: defaultData.inactiveRailColor));
description.add(new DiagnosticsProperty<Color>(
'disabledActiveRailColor', disabledActiveRailColor,
defaultValue: defaultData.disabledActiveRailColor));
description.add(new DiagnosticsProperty<Color>(
'disabledInactiveRailColor', disabledInactiveRailColor,
defaultValue: defaultData.disabledInactiveRailColor));
description.add(new DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor,
defaultValue: defaultData.activeTickMarkColor));
description.add(new DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor,
defaultValue: defaultData.inactiveTickMarkColor));
description.add(new DiagnosticsProperty<Color>(
'disabledActiveTickMarkColor', disabledActiveTickMarkColor,
defaultValue: defaultData.disabledActiveTickMarkColor));
description.add(new DiagnosticsProperty<Color>(
'disabledInactiveTickMarkColor', disabledInactiveTickMarkColor,
defaultValue: defaultData.disabledInactiveTickMarkColor));
description.add(new DiagnosticsProperty<Color>('thumbColor', thumbColor,
defaultValue: defaultData.thumbColor));
description.add(new DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor,
defaultValue: defaultData.disabledThumbColor));
description.add(new DiagnosticsProperty<Color>('overlayColor', overlayColor,
defaultValue: defaultData.overlayColor));
description.add(new DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor,
defaultValue: defaultData.valueIndicatorColor));
description.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape,
defaultValue: defaultData.thumbShape));
description.add(new DiagnosticsProperty<SliderComponentShape>(
'valueIndicatorShape', valueIndicatorShape,
defaultValue: defaultData.valueIndicatorShape));
description.add(new DiagnosticsProperty<ShowValueIndicator>(
'showValueIndicator', showValueIndicator,
defaultValue: defaultData.showValueIndicator));
}
}
/// Base class for slider thumb and value indicator shapes.
///
/// Create a subclass of this if you would like a custom slider thumb or
/// value indicator shape.
///
/// See also:
///
/// * [RoundSliderThumbShape] for a simple example of a thumb shape.
/// * [PaddleSliderValueIndicatorShape], for a complex example of a value
/// indicator shape.
abstract class SliderComponentShape {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliderComponentShape();
/// Returns the preferred size of the shape, based on the given conditions.
Size getPreferredSize(bool isEnabled, bool isDiscrete);
/// Paints the shape, taking into account the state passed to it.
///
/// [activationAnimation] is an animation triggered when the user beings
/// to interact with the slider. It reverses when the user stops interacting
/// with the slider.
/// [enableAnimation] is an animation triggered when the [Slider] is enabled,
/// and it reverses when the slider is disabled.
/// If [labelPainter] is non-null, then [labelPainter.paint] should be
/// called with the location that the label should appear. If the labelPainter
/// passed is null, then no label was supplied to the [Slider].
/// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
void paint(
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Animation<double> activationAnimation,
Animation<double> enableAnimation,
TextPainter labelPainter,
SliderThemeData sliderTheme,
TextDirection textDirection,
double textScaleFactor,
double value,
);
}
/// This is the default shape to a [Slider]'s thumb if no
/// other shape is specified.
///
/// See also:
///
/// * [Slider] for the component that this is meant to display this shape.
/// * [SliderThemeData] where an instance of this class is set to inform the
/// slider of the shape of the its thumb.
class RoundSliderThumbShape extends SliderComponentShape {
const RoundSliderThumbShape();
static const double _thumbRadius = 6.0;
static const double _disabledThumbRadius = 4.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return new Size.fromRadius(isEnabled ? _thumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Animation<double> activationAnimation,
Animation<double> enableAnimation,
TextPainter labelPainter,
SliderThemeData sliderTheme,
TextDirection textDirection,
double textScaleFactor,
double value,
) {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween =
new Tween<double>(begin: _disabledThumbRadius, end: _thumbRadius);
final ColorTween colorTween =
new ColorTween(begin: sliderTheme.disabledThumbColor, end: sliderTheme.thumbColor);
canvas.drawCircle(
thumbCenter,
radiusTween.evaluate(enableAnimation),
new Paint()..color = colorTween.evaluate(enableAnimation),
);
}
}
/// This is the default shape to a [Slider]'s value indicator if no
/// other shape is specified.
///
/// See also:
///
/// * [Slider] for the component that this is meant to display this shape.
/// * [SliderThemeData] where an instance of this class is set to inform the
/// slider of the shape of the its value indicator.
class PaddleSliderValueIndicatorShape extends SliderComponentShape {
const PaddleSliderValueIndicatorShape();
// These constants define the shape of the default value indicator.
// The value indicator changes shape based on the size of
// the label: The top lobe spreads horizontally, and the
// top arc on the neck moves down to keep it merging smoothly
// with the top lobe as it expands.
// Radius of the top lobe of the value indicator.
static const double _topLobeRadius = 16.0;
// Radius of the bottom lobe of the value indicator.
static const double _bottomLobeRadius = 6.0;
// The starting angle for the bottom lobe. Picked to get the desired
// thickness for the neck.
static const double _bottomLobeStartAngle = -1.1 * math.pi / 4.0;
// The ending angle for the bottom lobe. Picked to get the desired
// thickness for the neck.
static const double _bottomLobeEndAngle = 1.1 * 5 * math.pi / 4.0;
// The padding on either side of the label.
static const double _labelPadding = 8.0;
static const double _distanceBetweenTopBottomCenters = 40.0;
static const Offset _topLobeCenter = const Offset(0.0, -_distanceBetweenTopBottomCenters);
static const double _topNeckRadius = 14.0;
// The length of the hypotenuse of the triangle formed by the center
// of the left top lobe arc and the center of the top left neck arc.
// Used to calculate the position of the center of the arc.
static const double _neckTriangleHypotenuse = _topLobeRadius + _topNeckRadius;
// Some convenience values to help readability.
static const double _twoSeventyDegrees = 3.0 * math.pi / 2.0;
static const double _ninetyDegrees = math.pi / 2.0;
static const double _thirtyDegrees = math.pi / 6.0;
static const Size preferredSize =
const Size.fromHeight(_distanceBetweenTopBottomCenters + _topLobeRadius + _bottomLobeRadius);
static final Tween<double> _slideUpTween = new Tween<double>(begin: 0.0, end: 1.0);
static Path _bottomLobePath; // Initialized by _generateBottomLobe
static Offset _bottomLobeEnd; // Initialized by _generateBottomLobe
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => preferredSize;
// Adds an arc to the path that has the attributes passed in. This is
// a convenience to make adding arcs have less boilerplate.
static void _addArc(Path path, Offset center, double radius, double startAngle, double endAngle) {
final Rect arcRect = new Rect.fromCircle(center: center, radius: radius);
path.arcTo(arcRect, startAngle, endAngle - startAngle, false);
}
// Generates the bottom lobe path, which is the same for all instances of
// the value indicator, so we reuse it for each one.
static void _generateBottomLobe() {
const double bottomNeckRadius = 4.5;
const double bottomNeckStartAngle = _bottomLobeEndAngle - math.pi;
const double bottomNeckEndAngle = 0.0;
final Path path = new Path();
final Offset bottomKnobStart = new Offset(
_bottomLobeRadius * math.cos(_bottomLobeStartAngle),
_bottomLobeRadius * math.sin(_bottomLobeStartAngle),
);
final Offset bottomNeckRightCenter = bottomKnobStart +
new Offset(
bottomNeckRadius * math.cos(bottomNeckStartAngle),
-bottomNeckRadius * math.sin(bottomNeckStartAngle),
);
final Offset bottomNeckLeftCenter = new Offset(
-bottomNeckRightCenter.dx,
bottomNeckRightCenter.dy,
);
final Offset bottomNeckStartRight = new Offset(
bottomNeckRightCenter.dx - bottomNeckRadius,
bottomNeckRightCenter.dy,
);
path.moveTo(bottomNeckStartRight.dx, bottomNeckStartRight.dy);
_addArc(
path,
bottomNeckRightCenter,
bottomNeckRadius,
math.pi - bottomNeckEndAngle,
math.pi - bottomNeckStartAngle,
);
_addArc(
path,
Offset.zero,
_bottomLobeRadius,
_bottomLobeStartAngle,
_bottomLobeEndAngle,
);
_addArc(
path,
bottomNeckLeftCenter,
bottomNeckRadius,
bottomNeckStartAngle,
bottomNeckEndAngle,
);
_bottomLobeEnd = new Offset(
-bottomNeckStartRight.dx,
bottomNeckStartRight.dy,
);
_bottomLobePath = path;
}
Offset _addBottomLobe(Path path) {
if (_bottomLobePath == null || _bottomLobeEnd == null) {
// Generate this lazily so as to not slow down app startup.
_generateBottomLobe();
}
path.extendWithPath(_bottomLobePath, Offset.zero);
return _bottomLobeEnd;
}
void _drawValueIndicator(Canvas canvas, Offset center, Paint paint, double scale,
TextPainter labelPainter, double textScaleFactor) {
canvas.save();
canvas.translate(center.dx, center.dy);
// The entire value indicator should scale with the text scale factor,
// to keep it large enough to encompass the label text.
canvas.scale(scale * textScaleFactor, scale * textScaleFactor);
final double inverseTextScale = 1.0 / textScaleFactor;
final double labelHalfWidth = labelPainter.width / 2.0;
// This is the needed extra width for the label. It is only positive when
// the label exceeds the minimum size contained by the round top lobe.
final double halfWidthNeeded =
math.max(0.0, inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding));
final Path path = new Path();
final Offset bottomLobeEnd = _addBottomLobe(path);
// The base of the triangle between the top lobe center and the centers of
// the two top neck arcs.
final double neckTriangleBase = _topNeckRadius - bottomLobeEnd.dx;
// The parameter that describes how far along the transition from round to
// stretched we are.
final double t = math.max(0.0, math.min(1.0, halfWidthNeeded / neckTriangleBase));
// The angle between the top neck arc's center and the top lobe's center
// and vertical.
final double theta = (1.0 - t) * _thirtyDegrees;
// The center of the top left neck arc.
final Offset neckLeftCenter = new Offset(
-neckTriangleBase, _topLobeCenter.dy + math.cos(theta) * _neckTriangleHypotenuse);
final Offset topLobeShift = new Offset(halfWidthNeeded, 0.0);
final double neckArcAngle = _ninetyDegrees - theta;
_addArc(
path,
neckLeftCenter,
_topNeckRadius,
0.0,
-neckArcAngle,
);
_addArc(path, _topLobeCenter - topLobeShift, _topLobeRadius, _ninetyDegrees + theta,
_twoSeventyDegrees);
_addArc(path, _topLobeCenter + topLobeShift, _topLobeRadius, _twoSeventyDegrees,
_twoSeventyDegrees + math.pi - theta);
final Offset neckRightCenter = new Offset(-neckLeftCenter.dx, neckLeftCenter.dy);
_addArc(
path,
neckRightCenter,
_topNeckRadius,
math.pi + neckArcAngle,
math.pi,
);
canvas.drawPath(path, paint);
// Draw the label.
canvas.save();
canvas.translate(0.0, -_distanceBetweenTopBottomCenters);
canvas.scale(inverseTextScale, inverseTextScale);
labelPainter.paint(canvas, Offset.zero - new Offset(labelHalfWidth, labelPainter.height / 2.0));
canvas.restore();
canvas.restore();
}
@override
void paint(
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Animation<double> activationAnimation,
Animation<double> enableAnimation,
TextPainter labelPainter,
SliderThemeData sliderTheme,
TextDirection textDirection,
double textScaleFactor,
double value,
) {
assert(labelPainter != null);
final ColorTween colorTween =
new ColorTween(begin: Colors.transparent, end: sliderTheme.valueIndicatorColor);
final ColorTween enableColor = new ColorTween(
begin: sliderTheme.disabledThumbColor, end: colorTween.evaluate(activationAnimation));
_drawValueIndicator(
context.canvas,
thumbCenter,
new Paint()..color = enableColor.evaluate(enableAnimation),
_slideUpTween.evaluate(activationAnimation),
labelPainter,
textScaleFactor,
);
}
}

View file

@ -12,6 +12,7 @@ import 'colors.dart';
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'slider_theme.dart';
import 'typography.dart';
/// Describes the contrast needs of a color.
@ -52,8 +53,8 @@ const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC);
///
/// To obtain the current theme, use [Theme.of].
@immutable
class ThemeData {
/// Create a ThemeData given a set of preferred values.
class ThemeData extends Diagnosticable {
/// Create a [ThemeData] given a set of preferred values.
///
/// Default values will be derived for arguments that are omitted.
///
@ -78,6 +79,8 @@ class ThemeData {
MaterialColor primarySwatch,
Color primaryColor,
Brightness primaryColorBrightness,
Color primaryColorLight,
Color primaryColorDark,
Color accentColor,
Brightness accentColorBrightness,
Color canvasColor,
@ -109,13 +112,16 @@ class ThemeData {
IconThemeData iconTheme,
IconThemeData primaryIconTheme,
IconThemeData accentIconTheme,
TargetPlatform platform
SliderThemeData sliderTheme,
TargetPlatform platform,
}) {
brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark;
primarySwatch ??= Colors.blue;
primaryColor ??= isDark ? Colors.grey[900] : primarySwatch[500];
primaryColorBrightness ??= estimateBrightnessForColor(primaryColor);
primaryColorLight ??= isDark ? Colors.grey[500] : primarySwatch[100];
primaryColorDark ??= isDark ? Colors.black : primarySwatch[700];
final bool primaryIsDark = primaryColorBrightness == Brightness.dark;
accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500];
accentColorBrightness ??= estimateBrightnessForColor(accentColor);
@ -143,9 +149,15 @@ class ThemeData {
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
errorColor ??= Colors.red[700];
inputDecorationTheme ??= const InputDecorationTheme();
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
iconTheme ??= isDark
? const IconThemeData(color: Colors.white)
: const IconThemeData(color: Colors.black);
primaryIconTheme ??= primaryIsDark
? const IconThemeData(color: Colors.white)
: const IconThemeData(color: Colors.black);
accentIconTheme ??= accentIsDark
? const IconThemeData(color: Colors.white)
: const IconThemeData(color: Colors.black);
platform ??= defaultTargetPlatform;
final Typography typography = new Typography(platform: platform);
textTheme ??= isDark ? typography.white : typography.black;
@ -156,10 +168,17 @@ class ThemeData {
primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily);
accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily);
}
sliderTheme ??= new SliderThemeData.materialDefaults(
primaryColor: primaryColor,
primaryColorLight: primaryColorLight,
primaryColorDark: primaryColorDark,
);
return new ThemeData.raw(
brightness: brightness,
primaryColor: primaryColor,
primaryColorBrightness: primaryColorBrightness,
primaryColorLight: primaryColorLight,
primaryColorDark: primaryColorDark,
accentColor: accentColor,
accentColorBrightness: accentColorBrightness,
canvasColor: canvasColor,
@ -190,11 +209,12 @@ class ThemeData {
iconTheme: iconTheme,
primaryIconTheme: primaryIconTheme,
accentIconTheme: accentIconTheme,
platform: platform
sliderTheme: sliderTheme,
platform: platform,
);
}
/// Create a ThemeData given a set of exact values. All the values
/// Create a [ThemeData] given a set of exact values. All the values
/// must be specified.
///
/// This will rarely be used directly. It is used by [lerp] to
@ -204,6 +224,8 @@ class ThemeData {
@required this.brightness,
@required this.primaryColor,
@required this.primaryColorBrightness,
@required this.primaryColorLight,
@required this.primaryColorDark,
@required this.accentColor,
@required this.accentColorBrightness,
@required this.canvasColor,
@ -234,10 +256,13 @@ class ThemeData {
@required this.iconTheme,
@required this.primaryIconTheme,
@required this.accentIconTheme,
@required this.platform
@required this.sliderTheme,
@required this.platform,
}) : assert(brightness != null),
assert(primaryColor != null),
assert(primaryColorBrightness != null),
assert(primaryColorLight != null),
assert(primaryColorDark != null),
assert(accentColor != null),
assert(accentColorBrightness != null),
assert(canvasColor != null),
@ -267,6 +292,7 @@ class ThemeData {
assert(iconTheme != null),
assert(primaryIconTheme != null),
assert(accentIconTheme != null),
assert(sliderTheme != null),
assert(platform != null);
/// A default light blue theme.
@ -311,6 +337,12 @@ class ThemeData {
/// icons placed on top of the primary color (e.g. toolbar text).
final Brightness primaryColorBrightness;
/// A lighter version of the [primaryColor].
final Color primaryColorLight;
/// A darker version of the [primaryColor].
final Color primaryColorDark;
/// The foreground color for widgets (knobs, text, overscroll edge effect, etc).
final Color accentColor;
@ -352,7 +384,7 @@ class ThemeData {
///
/// * [InkSplash.splashFactory], which defines the default splash.
/// * [InkRipple.splashFactory], which defines a splash that spreads out
/// more aggresively than the default.
/// more aggressively than the default.
final InteractiveInkFeatureFactory splashFactory;
/// The color used to highlight selected rows.
@ -428,6 +460,11 @@ class ThemeData {
/// An icon theme that contrasts with the accent color.
final IconThemeData accentIconTheme;
/// The colors and shapes used to render [Slider].
///
/// This is the value returned from [SliderTheme.of].
final SliderThemeData sliderTheme;
/// The platform the material widgets should adapt to target.
///
/// Defaults to the current platform.
@ -438,6 +475,8 @@ class ThemeData {
Brightness brightness,
Color primaryColor,
Brightness primaryColorBrightness,
Color primaryColorLight,
Color primaryColorDark,
Color accentColor,
Brightness accentColorBrightness,
Color canvasColor,
@ -468,12 +507,15 @@ class ThemeData {
IconThemeData iconTheme,
IconThemeData primaryIconTheme,
IconThemeData accentIconTheme,
SliderThemeData sliderTheme,
TargetPlatform platform,
}) {
return new ThemeData.raw(
brightness: brightness ?? this.brightness,
primaryColor: primaryColor ?? this.primaryColor,
primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness,
primaryColorLight: primaryColorLight ?? this.primaryColorLight,
primaryColorDark: primaryColorDark ?? this.primaryColorDark,
accentColor: accentColor ?? this.accentColor,
accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness,
canvasColor: canvasColor ?? this.canvasColor,
@ -504,6 +546,7 @@ class ThemeData {
iconTheme: iconTheme ?? this.iconTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentIconTheme: accentIconTheme ?? this.accentIconTheme,
sliderTheme: sliderTheme ?? this.sliderTheme,
platform: platform ?? this.platform,
);
}
@ -515,7 +558,8 @@ class ThemeData {
static const int _localizedThemeDataCacheSize = 5;
/// Caches localized themes to speed up the [localize] method.
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache = new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
/// Returns a new theme built by merging the text geometry provided by the
/// [localTextGeometry] theme with the [baseTheme].
@ -567,7 +611,7 @@ class ThemeData {
// Design spec shows for its color palette on
// <https://material.io/guidelines/style/color.html#color-color-palette>.
const double kThreshold = 0.15;
if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold )
if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold)
return Brightness.light;
return Brightness.dark;
}
@ -595,6 +639,8 @@ class ThemeData {
brightness: t < 0.5 ? a.brightness : b.brightness,
primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t),
primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
primaryColorLight: Color.lerp(a.primaryColorLight, b.primaryColorLight, t),
primaryColorDark: Color.lerp(a.primaryColorDark, b.primaryColorDark, t),
canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
@ -625,6 +671,7 @@ class ThemeData {
iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
platform: t < 0.5 ? a.platform : b.platform,
);
}
@ -667,53 +714,120 @@ class ThemeData {
(otherData.iconTheme == iconTheme) &&
(otherData.primaryIconTheme == primaryIconTheme) &&
(otherData.accentIconTheme == accentIconTheme) &&
(otherData.sliderTheme == sliderTheme) &&
(otherData.platform == platform);
}
@override
int get hashCode {
return hashValues(
brightness,
primaryColor,
primaryColorBrightness,
canvasColor,
scaffoldBackgroundColor,
bottomAppBarColor,
cardColor,
dividerColor,
highlightColor,
splashColor,
splashFactory,
selectedRowColor,
unselectedWidgetColor,
disabledColor,
buttonColor,
buttonTheme,
secondaryHeaderColor,
textSelectionColor,
textSelectionHandleColor,
hashValues( // Too many values.
backgroundColor,
accentColor,
accentColorBrightness,
indicatorColor,
dialogBackgroundColor,
hintColor,
errorColor,
textTheme,
primaryTextTheme,
accentTextTheme,
iconTheme,
inputDecorationTheme,
primaryIconTheme,
accentIconTheme,
platform,
)
brightness,
primaryColor,
primaryColorBrightness,
canvasColor,
scaffoldBackgroundColor,
bottomAppBarColor,
cardColor,
dividerColor,
highlightColor,
splashColor,
splashFactory,
selectedRowColor,
unselectedWidgetColor,
disabledColor,
buttonColor,
buttonTheme,
secondaryHeaderColor,
textSelectionColor,
textSelectionHandleColor,
hashValues( // Too many values.
backgroundColor,
accentColor,
accentColorBrightness,
indicatorColor,
dialogBackgroundColor,
hintColor,
errorColor,
textTheme,
primaryTextTheme,
accentTextTheme,
iconTheme,
inputDecorationTheme,
primaryIconTheme,
accentIconTheme,
sliderTheme,
platform,
),
);
}
@override
String toString() => '$runtimeType(${ platform != defaultTargetPlatform ? "$platform " : ''}$brightness $primaryColor etc...)';
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
final ThemeData defaultData = new ThemeData.fallback();
description.add(new EnumProperty<TargetPlatform>('platform', platform,
defaultValue: defaultTargetPlatform));
description.add(new EnumProperty<Brightness>('brightness', brightness,
defaultValue: defaultData.brightness));
description.add(new DiagnosticsProperty<Color>('primaryColor', primaryColor,
defaultValue: defaultData.primaryColor));
description.add(new EnumProperty<Brightness>('primaryColorBrightness', primaryColorBrightness,
defaultValue: defaultData.primaryColorBrightness));
description.add(new DiagnosticsProperty<Color>('accentColor', accentColor,
defaultValue: defaultData.accentColor));
description.add(new EnumProperty<Brightness>('accentColorBrightness', accentColorBrightness,
defaultValue: defaultData.accentColorBrightness));
description.add(new DiagnosticsProperty<Color>('canvasColor', canvasColor,
defaultValue: defaultData.canvasColor));
description.add(new DiagnosticsProperty<Color>(
'scaffoldBackgroundColor', scaffoldBackgroundColor,
defaultValue: defaultData.scaffoldBackgroundColor));
description.add(new DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor,
defaultValue: defaultData.bottomAppBarColor));
description.add(new DiagnosticsProperty<Color>('cardColor', cardColor,
defaultValue: defaultData.cardColor));
description.add(new DiagnosticsProperty<Color>('dividerColor', dividerColor,
defaultValue: defaultData.dividerColor));
description.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor,
defaultValue: defaultData.highlightColor));
description.add(new DiagnosticsProperty<Color>('splashColor', splashColor,
defaultValue: defaultData.splashColor));
description.add(new DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor,
defaultValue: defaultData.selectedRowColor));
description.add(new DiagnosticsProperty<Color>('unselectedWidgetColor', unselectedWidgetColor,
defaultValue: defaultData.unselectedWidgetColor));
description.add(new DiagnosticsProperty<Color>('disabledColor', disabledColor,
defaultValue: defaultData.disabledColor));
description.add(new DiagnosticsProperty<Color>('buttonColor', buttonColor,
defaultValue: defaultData.buttonColor));
description.add(new DiagnosticsProperty<Color>('secondaryHeaderColor', secondaryHeaderColor,
defaultValue: defaultData.secondaryHeaderColor));
description.add(new DiagnosticsProperty<Color>('textSelectionColor', textSelectionColor,
defaultValue: defaultData.textSelectionColor));
description.add(new DiagnosticsProperty<Color>(
'textSelectionHandleColor', textSelectionHandleColor,
defaultValue: defaultData.textSelectionHandleColor));
description.add(new DiagnosticsProperty<Color>('backgroundColor', backgroundColor,
defaultValue: defaultData.backgroundColor));
description.add(new DiagnosticsProperty<Color>('dialogBackgroundColor', dialogBackgroundColor,
defaultValue: defaultData.dialogBackgroundColor));
description.add(new DiagnosticsProperty<Color>('indicatorColor', indicatorColor,
defaultValue: defaultData.indicatorColor));
description.add(new DiagnosticsProperty<Color>('hintColor', hintColor,
defaultValue: defaultData.hintColor));
description.add(new DiagnosticsProperty<Color>('errorColor', errorColor,
defaultValue: defaultData.errorColor));
description.add(new DiagnosticsProperty<ButtonThemeData>('buttonTheme', buttonTheme));
description.add(new DiagnosticsProperty<TextTheme>('textTheme', textTheme));
description.add(new DiagnosticsProperty<TextTheme>('primaryTextTheme', primaryTextTheme));
description.add(new DiagnosticsProperty<TextTheme>('accentTextTheme', accentTextTheme));
description.add(new DiagnosticsProperty<InputDecorationTheme>(
'inputDecorationTheme', inputDecorationTheme));
description.add(new DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme));
description.add(new DiagnosticsProperty<IconThemeData>('primaryIconTheme', primaryIconTheme));
description.add(new DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme));
description.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
}
}
class _IdentityThemeDataCacheKey {
@ -732,7 +846,8 @@ class _IdentityThemeDataCacheKey {
// We are explicitly ignoring the possibility that the types might not
// match in the interests of speed.
final _IdentityThemeDataCacheKey otherKey = other;
return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry);
return identical(baseTheme, otherKey.baseTheme) &&
identical(localTextGeometry, otherKey.localTextGeometry);
}
}
@ -742,8 +857,7 @@ class _IdentityThemeDataCacheKey {
/// The key that was inserted before all other keys is evicted first, i.e. the
/// one inserted least recently.
class _FifoCache<K, V> {
_FifoCache(this._maximumSize)
: assert(_maximumSize != null && _maximumSize > 0);
_FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
/// In Dart the map literal uses a linked hash-map implementation, whose keys
/// are stored such that [Map.keys] returns them in the order they were

View file

@ -33,7 +33,7 @@ import 'colors.dart';
/// globally adjusted, such as the color scheme.
/// * <http://material.google.com/style/typography.html>
@immutable
class TextTheme {
class TextTheme extends Diagnosticable {
/// Creates a text theme that uses the given values.
///
/// Rather than creating a new text theme, consider using [Typography.black]
@ -114,7 +114,7 @@ class TextTheme {
TextStyle body2,
TextStyle body1,
TextStyle caption,
TextStyle button
TextStyle button,
}) {
return new TextTheme(
display4: display4 ?? this.display4,
@ -353,6 +353,34 @@ class TextTheme {
button,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
final TextTheme defaultTheme = new Typography(platform: defaultTargetPlatform).black;
description.add(new DiagnosticsProperty<TextStyle>('display4', display4,
defaultValue: defaultTheme.display4));
description.add(new DiagnosticsProperty<TextStyle>('display3', display3,
defaultValue: defaultTheme.display3));
description.add(new DiagnosticsProperty<TextStyle>('display2', display2,
defaultValue: defaultTheme.display2));
description.add(new DiagnosticsProperty<TextStyle>('display1', display1,
defaultValue: defaultTheme.display1));
description.add(new DiagnosticsProperty<TextStyle>('headline', headline,
defaultValue: defaultTheme.headline));
description
.add(new DiagnosticsProperty<TextStyle>('title', title, defaultValue: defaultTheme.title));
description.add(
new DiagnosticsProperty<TextStyle>('subhead', subhead, defaultValue: defaultTheme.subhead));
description
.add(new DiagnosticsProperty<TextStyle>('body2', body2, defaultValue: defaultTheme.body2));
description
.add(new DiagnosticsProperty<TextStyle>('body1', body1, defaultValue: defaultTheme.body1));
description.add(
new DiagnosticsProperty<TextStyle>('caption', caption, defaultValue: defaultTheme.caption));
description.add(
new DiagnosticsProperty<TextStyle>('button', button, defaultValue: defaultTheme.button));
}
}
/// The two material design text themes.
@ -373,7 +401,7 @@ class TextTheme {
/// * <http://material.google.com/style/typography.html>
class Typography {
/// Creates the default typography for the specified platform.
factory Typography({ @required TargetPlatform platform }) {
factory Typography({@required TargetPlatform platform}) {
assert(platform != null);
switch (platform) {
case TargetPlatform.android:

View file

@ -5,6 +5,8 @@
import 'dart:ui' show Color, hashValues;
import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/foundation.dart';
/// Defines the color, opacity, and size of icons.
///
/// Used by [IconTheme] to control the color, opacity, and size of icons in a
@ -13,29 +15,26 @@ import 'dart:ui' as ui show lerpDouble;
/// To obtain the current icon theme, use [IconTheme.of]. To convert an icon
/// theme to a version with all the fields filled in, use [new
/// IconThemeData.fallback].
class IconThemeData {
class IconThemeData extends Diagnosticable {
/// Creates an icon theme data.
///
/// The opacity applies to both explicit and default icon colors. The value
/// is clamped between 0.0 and 1.0.
const IconThemeData({ this.color, double opacity, this.size }) : _opacity = opacity;
const IconThemeData({this.color, double opacity, this.size}) : _opacity = opacity;
/// Creates an icon them with some reasonable default values.
///
/// The [color] is black, the [opacity] is 1.0, and the [size] is 24.0.
const IconThemeData.fallback()
: color = const Color(0xFF000000),
_opacity = 1.0,
size = 24.0;
: color = const Color(0xFF000000),
_opacity = 1.0,
size = 24.0;
/// Creates a copy of this icon theme but with the given fields replaced with
/// the new values.
IconThemeData copyWith({ Color color, double opacity, double size }) {
IconThemeData copyWith({Color color, double opacity, double size}) {
return new IconThemeData(
color: color ?? this.color,
opacity: opacity ?? this.opacity,
size: size ?? this.size
);
color: color ?? this.color, opacity: opacity ?? this.opacity, size: size ?? this.size);
}
/// Returns a new icon theme that matches this icon theme but with some values
@ -44,11 +43,7 @@ class IconThemeData {
IconThemeData merge(IconThemeData other) {
if (other == null)
return this;
return copyWith(
color: other.color,
opacity: other.opacity,
size: other.size
);
return copyWith(color: other.color, opacity: other.opacity, size: other.size);
}
/// Whether all the properties of this object are non-null.
@ -100,16 +95,13 @@ class IconThemeData {
int get hashCode => hashValues(color, opacity, size);
@override
String toString() {
final List<String> result = <String>[];
if (color != null)
result.add('color: $color');
if (_opacity != null)
result.add('opacity: $_opacity');
if (size != null)
result.add('size: $size');
if (result.isEmpty)
return '<no theme>';
return result.join(', ');
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
if (color == null && _opacity == null && size == null) {
return;
}
description.add(new DiagnosticsProperty<Color>('color', color));
description.add(new DoubleProperty('opacity', _opacity));
description.add(new DoubleProperty('size', size));
}
}

View file

@ -150,8 +150,7 @@ void main() {
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider can be given zero values',
(WidgetTester tester) async {
testWidgets('Slider can be given zero values', (WidgetTester tester) async {
final List<double> log = <double>[];
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
@ -160,7 +159,9 @@ void main() {
value: 0.0,
min: 0.0,
max: 1.0,
onChanged: (double newValue) { log.add(newValue); },
onChanged: (double newValue) {
log.add(newValue);
},
),
),
));
@ -176,7 +177,9 @@ void main() {
value: 0.0,
min: 0.0,
max: 0.0,
onChanged: (double newValue) { log.add(newValue); },
onChanged: (double newValue) {
log.add(newValue);
},
),
),
));
@ -186,11 +189,27 @@ void main() {
log.clear();
});
testWidgets('Slider has a customizable active color',
testWidgets('Slider uses the right theme colors for the right components',
(WidgetTester tester) async {
const Color customColor = const Color(0xFF4CD964);
final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
Widget buildApp(Color activeColor) {
const Color customColor1 = const Color(0xcafefeed);
const Color customColor2 = const Color(0xdeadbeef);
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
double value = 0.45;
Widget buildApp({
Color activeColor,
Color inactiveColor,
int divisions,
bool enabled: true,
}) {
final ValueChanged<double> onChanged = !enabled
? null
: (double d) {
value = d;
};
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
@ -198,47 +217,12 @@ void main() {
child: new Theme(
data: theme,
child: new Slider(
value: 0.5,
value: value,
label: '$value',
divisions: divisions,
activeColor: activeColor,
onChanged: (double newValue) {},
),
),
),
),
);
}
await tester.pumpWidget(buildApp(null));
final RenderBox sliderBox =
tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor));
expect(sliderBox, paints..circle(color: theme.accentColor));
expect(sliderBox, isNot(paints..circle(color: customColor)));
expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor)));
await tester.pumpWidget(buildApp(customColor));
expect(sliderBox, paints..rect(color: customColor)..rect(color: theme.unselectedWidgetColor));
expect(sliderBox, paints..circle(color: customColor));
expect(sliderBox, isNot(paints..circle(color: theme.accentColor)));
expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor)));
});
testWidgets('Slider has a customizable inactive color',
(WidgetTester tester) async {
const Color customColor = const Color(0xFF4CD964);
final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
Widget buildApp(Color inactiveColor) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Theme(
data: theme,
child: new Slider(
value: 0.5,
inactiveColor: inactiveColor,
onChanged: (double newValue) {},
onChanged: onChanged,
),
),
),
@ -246,78 +230,168 @@ void main() {
);
}
await tester.pumpWidget(buildApp(null));
await tester.pumpWidget(buildApp());
final RenderBox sliderBox =
tester.firstRenderObject<RenderBox>(find.byType(Slider));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor));
expect(sliderBox, paints..circle(color: theme.accentColor));
await tester.pumpWidget(buildApp(customColor));
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: customColor));
expect(sliderBox, paints..circle(color: theme.accentColor));
// Check default theme for enabled widget.
expect(
sliderBox,
paints
..rect(color: sliderTheme.activeRailColor)
..rect(color: sliderTheme.inactiveRailColor));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
// Test setting only the activeColor.
await tester.pumpWidget(buildApp(activeColor: customColor1));
expect(
sliderBox, paints..rect(color: customColor1)..rect(color: sliderTheme.inactiveRailColor));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
// Test setting only the inactiveColor.
await tester.pumpWidget(buildApp(inactiveColor: customColor1));
expect(sliderBox, paints..rect(color: sliderTheme.activeRailColor)..rect(color: customColor1));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
// Test setting both activeColor and inactiveColor.
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2));
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
// Test colors for discrete slider.
await tester.pumpWidget(buildApp(divisions: 3));
expect(
sliderBox,
paints
..rect(color: sliderTheme.activeRailColor)
..rect(color: sliderTheme.inactiveRailColor));
expect(
sliderBox,
paints
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
// Test colors for discrete slider with inactiveColor and activeColor set.
await tester
.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, divisions: 3));
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
expect(
sliderBox,
paints
..circle(color: customColor2)
..circle(color: customColor2)
..circle(color: customColor1)
..circle(color: customColor1)
..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
// Test default theme for disabled widget.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pump(const Duration(seconds: 1)); // wait for disable animation to finish.
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveRailColor)
..rect(color: sliderTheme.disabledInactiveRailColor));
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveRailColor)));
// Test setting the activeColor and inactiveColor for disabled widget.
await tester.pumpWidget(
buildApp(activeColor: customColor1, inactiveColor: customColor2, enabled: false));
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveRailColor)
..rect(color: sliderTheme.disabledInactiveRailColor));
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeRailColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveRailColor)));
// Test that the default value indicator has the right colors.
await tester.pumpWidget(buildApp(divisions: 3));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
await tester.pump();
await tester
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
paints
..rect(color: sliderTheme.activeRailColor)
..rect(color: sliderTheme.inactiveRailColor)
..circle(color: sliderTheme.overlayColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..path(color: sliderTheme.valueIndicatorColor)
..circle(color: sliderTheme.thumbColor),
);
await gesture.up();
await tester.pump();
await tester
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
// Testing the custom colors are used for the indicator.
await tester.pumpWidget(buildApp(
divisions: 3,
activeColor: customColor1,
inactiveColor: customColor2,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pump();
await tester
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
paints
..rect(color: customColor1)
..rect(color: customColor2)
..circle(color: customColor1.withAlpha(0x29))
..circle(color: customColor2)
..circle(color: customColor2)
..circle(color: customColor1)
..path(color: customColor1)
..circle(color: customColor1),
);
await gesture.up();
});
testWidgets('Slider can draw an open thumb at min (LTR)',
(WidgetTester tester) async {
Widget buildApp(bool thumbOpenAtMin) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Slider(
value: 0.0,
thumbOpenAtMin: thumbOpenAtMin,
onChanged: (double newValue) {},
),
),
),
);
}
await tester.pumpWidget(buildApp(false));
final RenderBox sliderBox =
tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(style: PaintingStyle.fill));
expect(sliderBox, isNot(paints..circle()..circle()));
await tester.pumpWidget(buildApp(true));
expect(sliderBox, paints..circle(style: PaintingStyle.stroke));
expect(sliderBox, isNot(paints..circle()..circle()));
});
testWidgets('Slider can draw an open thumb at min (RTL)',
(WidgetTester tester) async {
Widget buildApp(bool thumbOpenAtMin) {
return new Directionality(
textDirection: TextDirection.rtl,
child: new Material(
child: new Center(
child: new Slider(
value: 0.0,
thumbOpenAtMin: thumbOpenAtMin,
onChanged: (double newValue) {},
),
),
),
);
}
await tester.pumpWidget(buildApp(false));
final RenderBox sliderBox =
tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(style: PaintingStyle.fill));
expect(sliderBox, isNot(paints..circle()..circle()));
await tester.pumpWidget(buildApp(true));
expect(sliderBox, paints..circle(style: PaintingStyle.stroke));
expect(sliderBox, isNot(paints..circle()..circle()));
});
testWidgets('Slider can tap in vertical scroller',
(WidgetTester tester) async {
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
@ -425,7 +499,8 @@ void main() {
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
const Size(144.0 + 2.0 * 16.0, 600.0));
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
@ -442,14 +517,18 @@ void main() {
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 32.0));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
const Size(144.0 + 2.0 * 16.0, 32.0));
});
testWidgets('discrete Slider respects textScaleFactor', (WidgetTester tester) async {
testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
Widget buildSlider({ double textScaleFactor }) {
Widget buildSlider(
{double textScaleFactor,
bool isDiscrete: true,
ShowValueIndicator show: ShowValueIndicator.onlyForDiscrete}) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new StatefulBuilder(
@ -457,22 +536,27 @@ void main() {
return new MediaQuery(
data: new MediaQueryData(textScaleFactor: textScaleFactor),
child: new Material(
child: new Center(
child: new OverflowBox(
maxWidth: double.INFINITY,
maxHeight: double.INFINITY,
child: new Slider(
key: sliderKey,
min: 0.0,
max: 100.0,
divisions: 10,
label: '${value.round()}',
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
child: new Theme(
data: Theme.of(context).copyWith(
sliderTheme:
Theme.of(context).sliderTheme.copyWith(showValueIndicator: show)),
child: new Center(
child: new OverflowBox(
maxWidth: double.INFINITY,
maxHeight: double.INFINITY,
child: new Slider(
key: sliderKey,
min: 0.0,
max: 100.0,
divisions: isDiscrete ? 10 : null,
label: '${value.round()}',
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
@ -486,12 +570,10 @@ void main() {
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
await gesture.moveBy(const Offset(10.0, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(
tester.renderObject(find.byType(Slider)),
paints..circle(radius: 6.0, x: 16.0, y: 44.0)
);
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
@ -499,12 +581,41 @@ void main() {
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await gesture.moveBy(const Offset(10.0, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(
tester.renderObject(find.byType(Slider)),
paints..circle(radius: 12.0, x: 16.0, y: 44.0)
);
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
// Check continuous
await tester.pumpWidget(buildSlider(
textScaleFactor: 1.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
await tester.pumpWidget(buildSlider(
textScaleFactor: 2.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pump(const Duration(seconds: 1));
@ -523,18 +634,18 @@ void main() {
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
expect(
semantics,
hasSemantics(
new TestSemantics.root(children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
]
),
ignoreRect: true,
ignoreTransform: true,
));
]),
ignoreRect: true,
ignoreTransform: true,
));
// Disable slider
await tester.pumpWidget(const Directionality(
@ -547,12 +658,92 @@ void main() {
),
));
expect(semantics, hasSemantics(
new TestSemantics.root(),
ignoreRect: true,
ignoreTransform: true,
));
expect(
semantics,
hasSemantics(
new TestSemantics.root(),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {
final ThemeData baseTheme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
SliderThemeData theme = baseTheme.sliderTheme;
double value = 0.45;
Widget buildApp({SliderThemeData sliderTheme, int divisions, bool enabled: true}) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Theme(
data: baseTheme,
child: new SliderTheme(
data: sliderTheme,
child: new Slider(
value: value,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
),
);
}
Future<Null> expectValueIndicator(
{bool isVisible, SliderThemeData theme, int divisions, bool enabled: true}) async {
// discrete enabled widget.
await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled));
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
await tester
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
isVisible
? (paints..path(color: theme.valueIndicatorColor))
: isNot(paints..path(color: theme.valueIndicatorColor)),
);
await gesture.up();
}
// Default (showValueIndicator set to onlyForDiscrete).
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// With showValueIndicator set to onlyForContinuous.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onlyForContinuous);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// discrete enabled widget with showValueIndicator set to always.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.always);
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// discrete enabled widget with showValueIndicator set to never.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.never);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
});
}

View file

@ -0,0 +1,294 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/painting.dart';
import '../rendering/mock_canvas.dart';
void main() {
testWidgets('Slider theme is built by ThemeData', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.red,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
expect(sliderTheme.activeRailColor.value, equals(Colors.red.value));
expect(sliderTheme.inactiveRailColor.value, equals(Colors.red.withAlpha(0x3d).value));
});
testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.red,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
Widget buildSlider(SliderThemeData data) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Theme(
data: theme,
child: const Slider(
value: 0.5,
label: '0.5',
onChanged: null,
),
),
),
),
);
}
await tester.pumpWidget(buildSlider(sliderTheme));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveRailColor)
..rect(color: sliderTheme.disabledInactiveRailColor));
});
testWidgets('Slider overrides ThemeData theme if SliderTheme present',
(WidgetTester tester) async {
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.red,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
final SliderThemeData customTheme = sliderTheme.copyWith(
activeRailColor: Colors.purple,
inactiveRailColor: Colors.purple.withAlpha(0x3d),
);
Widget buildSlider(SliderThemeData data) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new Theme(
data: theme,
child: new SliderTheme(
data: customTheme,
child: const Slider(
value: 0.5,
label: '0.5',
onChanged: null,
),
),
),
),
),
);
}
await tester.pumpWidget(buildSlider(sliderTheme));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..rect(color: customTheme.disabledActiveRailColor)
..rect(color: customTheme.disabledInactiveRailColor));
});
testWidgets('SliderThemeData generates correct opacities for materialDefaults',
(WidgetTester tester) async {
const Color customColor1 = const Color(0xcafefeed);
const Color customColor2 = const Color(0xdeadbeef);
const Color customColor3 = const Color(0xdecaface);
final SliderThemeData sliderTheme = new SliderThemeData.materialDefaults(
primaryColor: customColor1,
primaryColorDark: customColor2,
primaryColorLight: customColor3,
);
expect(sliderTheme.activeRailColor, equals(customColor1.withAlpha(0xff)));
expect(sliderTheme.inactiveRailColor, equals(customColor1.withAlpha(0x3d)));
expect(sliderTheme.disabledActiveRailColor, equals(customColor2.withAlpha(0x52)));
expect(sliderTheme.disabledInactiveRailColor, equals(customColor2.withAlpha(0x1f)));
expect(sliderTheme.activeTickMarkColor, equals(customColor3.withAlpha(0x8a)));
expect(sliderTheme.inactiveTickMarkColor, equals(customColor1.withAlpha(0x8a)));
expect(sliderTheme.disabledActiveTickMarkColor, equals(customColor3.withAlpha(0x1f)));
expect(sliderTheme.disabledInactiveTickMarkColor, equals(customColor2.withAlpha(0x1f)));
expect(sliderTheme.thumbColor, equals(customColor1.withAlpha(0xff)));
expect(sliderTheme.disabledThumbColor, equals(customColor2.withAlpha(0x52)));
expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x29)));
expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff)));
expect(sliderTheme.thumbShape, equals(const isInstanceOf<RoundSliderThumbShape>()));
expect(sliderTheme.valueIndicatorShape,
equals(const isInstanceOf<PaddleSliderValueIndicatorShape>()));
expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete));
});
testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async {
final SliderThemeData sliderThemeBlack = new SliderThemeData.materialDefaults(
primaryColor: Colors.black,
primaryColorDark: Colors.black,
primaryColorLight: Colors.black,
);
final SliderThemeData sliderThemeWhite = new SliderThemeData.materialDefaults(
primaryColor: Colors.white,
primaryColorDark: Colors.white,
primaryColorLight: Colors.white,
);
final SliderThemeData lerp = SliderThemeData.lerp(sliderThemeBlack, sliderThemeWhite, 0.5);
const Color middleGrey = const Color(0xff7f7f7f);
expect(lerp.activeRailColor, equals(middleGrey.withAlpha(0xff)));
expect(lerp.inactiveRailColor, equals(middleGrey.withAlpha(0x3d)));
expect(lerp.disabledActiveRailColor, equals(middleGrey.withAlpha(0x52)));
expect(lerp.disabledInactiveRailColor, equals(middleGrey.withAlpha(0x1f)));
expect(lerp.activeTickMarkColor, equals(middleGrey.withAlpha(0x8a)));
expect(lerp.inactiveTickMarkColor, equals(middleGrey.withAlpha(0x8a)));
expect(lerp.disabledActiveTickMarkColor, equals(middleGrey.withAlpha(0x1f)));
expect(lerp.disabledInactiveTickMarkColor, equals(middleGrey.withAlpha(0x1f)));
expect(lerp.thumbColor, equals(middleGrey.withAlpha(0xff)));
expect(lerp.disabledThumbColor, equals(middleGrey.withAlpha(0x52)));
expect(lerp.overlayColor, equals(middleGrey.withAlpha(0x29)));
expect(lerp.valueIndicatorColor, equals(middleGrey.withAlpha(0xff)));
});
testWidgets('Default slider thumb shape draws correctly', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500);
double value = 0.45;
Widget buildApp({
int divisions,
bool enabled: true,
}) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new SliderTheme(
data: sliderTheme,
child: new Slider(
value: value,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor, radius: 6.0));
await tester.pumpWidget(buildApp(enabled: false));
await tester.pump(const Duration(milliseconds: 500)); // wait for disable animation
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor, radius: 4.0));
await tester.pumpWidget(buildApp(divisions: 3));
await tester.pump(const Duration(milliseconds: 500)); // wait for disable animation
expect(
sliderBox,
paints
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.thumbColor, radius: 6.0));
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
await tester.pump(const Duration(milliseconds: 500)); // wait for disable animation
expect(
sliderBox,
paints
..circle(color: sliderTheme.disabledActiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledThumbColor, radius: 4.0));
});
testWidgets('Default slider value indicator shape draws correctly', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme
.copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always);
Widget buildApp(String value) {
return new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
child: new SliderTheme(
data: sliderTheme,
child: new Slider(
value: 0.5,
label: '$value',
divisions: 3,
onChanged: (double d) {},
),
),
),
),
);
}
await tester.pumpWidget(buildApp('1'));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
await tester.pump();
// Wait for value indicator animation to finish.
await tester.pump(const Duration(milliseconds: 500));
expect(
sliderBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
includes: <Offset>[
const Offset(0.0, -40.0),
const Offset(15.9, -40.0),
const Offset(-15.9, -40.0),
],
excludes: <Offset>[const Offset(16.1, -40.0), const Offset(-16.1, -40.0)],
));
await gesture.up();
// Test that it expands with a larger label.
await tester.pumpWidget(buildApp('1000'));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pump();
// Wait for value indicator animation to finish.
await tester.pump(const Duration(milliseconds: 500));
expect(
sliderBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
includes: <Offset>[
const Offset(0.0, -40.0),
const Offset(35.9, -40.0),
const Offset(-35.9, -40.0),
],
excludes: <Offset>[const Offset(36.1, -40.0), const Offset(-36.1, -40.0)],
));
await gesture.up();
});
}