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.
diff --git a/examples/flutter_gallery/lib/demo/material/slider_demo.dart b/examples/flutter_gallery/lib/demo/material/slider_demo.dart
index fbd2212..0746601 100644
--- a/examples/flutter_gallery/lib/demo/material/slider_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/slider_demo.dart
@@ -31,7 +31,6 @@
value: _value,
min: 0.0,
max: 100.0,
- thumbOpenAtMin: true,
onChanged: (double value) {
setState(() {
_value = value;
@@ -44,7 +43,7 @@
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 @@
max: 100.0,
divisions: 5,
label: '${_discreteValue.round()}',
- thumbOpenAtMin: true,
onChanged: (double value) {
setState(() {
_discreteValue = value;
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index 675262c..7e857c4 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -77,6 +77,7 @@
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';
diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart
index 50570f3..9529ceb 100644
--- a/packages/flutter/lib/src/material/button_theme.dart
+++ b/packages/flutter/lib/src/material/button_theme.dart
@@ -135,7 +135,7 @@
/// 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 @@
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));
+ }
}
diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart
index 263d658..cff8cac 100644
--- a/packages/flutter/lib/src/material/ink_well.dart
+++ b/packages/flutter/lib/src/material/ink_well.dart
@@ -312,7 +312,7 @@
/// * [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.
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index c51480f..dfc4969 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -53,7 +53,7 @@
// 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 @@
@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 @@
/// 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 @@
/// * [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));
+ }
}
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index 8b11c61..f1b2898 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -9,39 +9,61 @@
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 @@
///
/// * [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 @@
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 @@
/// 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 _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 @@
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 @@
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 @@
..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 @@
..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 @@
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 @@
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 @@
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 @@
}
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 @@
}
}
- 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 @@
@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 @@
@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 @@
}
}
- 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));
+ }
}
}
diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart
new file mode 100644
index 0000000..3efbacb
--- /dev/null
+++ b/packages/flutter/lib/src/material/slider_theme.dart
@@ -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,
+ );
+ }
+}
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 0949c51..318151a 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -12,6 +12,7 @@
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 @@
///
/// 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 @@
MaterialColor primarySwatch,
Color primaryColor,
Brightness primaryColorBrightness,
+ Color primaryColorLight,
+ Color primaryColorDark,
Color accentColor,
Brightness accentColorBrightness,
Color canvasColor,
@@ -109,13 +112,16 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
assert(iconTheme != null),
assert(primaryIconTheme != null),
assert(accentIconTheme != null),
+ assert(sliderTheme != null),
assert(platform != null);
/// A default light blue theme.
@@ -311,6 +337,12 @@
/// 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 @@
///
/// * [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 @@
/// 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 @@
Brightness brightness,
Color primaryColor,
Brightness primaryColorBrightness,
+ Color primaryColorLight,
+ Color primaryColorDark,
Color accentColor,
Brightness accentColorBrightness,
Color canvasColor,
@@ -468,12 +507,15 @@
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 @@
iconTheme: iconTheme ?? this.iconTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentIconTheme: accentIconTheme ?? this.accentIconTheme,
+ sliderTheme: sliderTheme ?? this.sliderTheme,
platform: platform ?? this.platform,
);
}
@@ -515,7 +558,8 @@
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 @@
// 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 @@
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 @@
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 @@
(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 @@
// 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 @@
/// 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
diff --git a/packages/flutter/lib/src/material/typography.dart b/packages/flutter/lib/src/material/typography.dart
index 2e131af..80b0f32 100644
--- a/packages/flutter/lib/src/material/typography.dart
+++ b/packages/flutter/lib/src/material/typography.dart
@@ -33,7 +33,7 @@
/// 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 @@
TextStyle body2,
TextStyle body1,
TextStyle caption,
- TextStyle button
+ TextStyle button,
}) {
return new TextTheme(
display4: display4 ?? this.display4,
@@ -353,6 +353,34 @@
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 @@
/// * <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:
diff --git a/packages/flutter/lib/src/widgets/icon_theme_data.dart b/packages/flutter/lib/src/widgets/icon_theme_data.dart
index 0dc5f41..e13aea9 100644
--- a/packages/flutter/lib/src/widgets/icon_theme_data.dart
+++ b/packages/flutter/lib/src/widgets/icon_theme_data.dart
@@ -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 @@
/// 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 @@
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 @@
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));
}
}
diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart
index 6d2fb5b..d9438e8 100644
--- a/packages/flutter/test/material/slider_test.dart
+++ b/packages/flutter/test/material/slider_test.dart
@@ -150,8 +150,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
);
}
- 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 @@
),
),
));
- 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 @@
),
),
));
- 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 @@
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 @@
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 @@
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 @@
),
));
- 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 @@
),
));
- 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);
+ });
}
diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart
new file mode 100644
index 0000000..0c7c1b7
--- /dev/null
+++ b/packages/flutter/test/material/slider_theme_test.dart
@@ -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();
+ });
+}