Unverified Commit 701eff4a authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent d2dcec22
...@@ -31,7 +31,6 @@ class _SliderDemoState extends State<SliderDemo> { ...@@ -31,7 +31,6 @@ class _SliderDemoState extends State<SliderDemo> {
value: _value, value: _value,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
thumbOpenAtMin: true,
onChanged: (double value) { onChanged: (double value) {
setState(() { setState(() {
_value = value; _value = value;
...@@ -44,7 +43,7 @@ class _SliderDemoState extends State<SliderDemo> { ...@@ -44,7 +43,7 @@ class _SliderDemoState extends State<SliderDemo> {
new Column( new Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: const <Widget> [ children: const <Widget> [
const Slider(value: 0.25, thumbOpenAtMin: true, onChanged: null), const Slider(value: 0.25, onChanged: null),
const Text('Disabled'), const Text('Disabled'),
] ]
), ),
...@@ -57,7 +56,6 @@ class _SliderDemoState extends State<SliderDemo> { ...@@ -57,7 +56,6 @@ class _SliderDemoState extends State<SliderDemo> {
max: 100.0, max: 100.0,
divisions: 5, divisions: 5,
label: '${_discreteValue.round()}', label: '${_discreteValue.round()}',
thumbOpenAtMin: true,
onChanged: (double value) { onChanged: (double value) {
setState(() { setState(() {
_discreteValue = value; _discreteValue = value;
......
...@@ -77,6 +77,7 @@ export 'src/material/scaffold.dart'; ...@@ -77,6 +77,7 @@ export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart'; export 'src/material/scrollbar.dart';
export 'src/material/shadows.dart'; export 'src/material/shadows.dart';
export 'src/material/slider.dart'; export 'src/material/slider.dart';
export 'src/material/slider_theme.dart';
export 'src/material/snack_bar.dart'; export 'src/material/snack_bar.dart';
export 'src/material/stepper.dart'; export 'src/material/stepper.dart';
export 'src/material/switch.dart'; export 'src/material/switch.dart';
......
...@@ -135,7 +135,7 @@ class ButtonTheme extends InheritedWidget { ...@@ -135,7 +135,7 @@ class ButtonTheme extends InheritedWidget {
/// A button theme can be specified as part of the overall Material theme /// A button theme can be specified as part of the overall Material theme
/// using [ThemeData.buttomTheme]. The Material theme's button theme data /// using [ThemeData.buttomTheme]. The Material theme's button theme data
/// can be overridden with [ButtonTheme]. /// can be overridden with [ButtonTheme].
class ButtonThemeData { class ButtonThemeData extends Diagnosticable {
/// Create a button theme object that can be used with [ButtonTheme] /// Create a button theme object that can be used with [ButtonTheme]
/// or [ThemeData]. /// or [ThemeData].
/// ///
...@@ -251,4 +251,18 @@ class ButtonThemeData { ...@@ -251,4 +251,18 @@ class ButtonThemeData {
shape, 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));
}
} }
...@@ -312,7 +312,7 @@ class InkResponse extends StatefulWidget { ...@@ -312,7 +312,7 @@ class InkResponse extends StatefulWidget {
/// * [highlightColor], the color of the highlight. /// * [highlightColor], the color of the highlight.
/// * [InkSplash.splashFactory], which defines the default splash. /// * [InkSplash.splashFactory], which defines the default splash.
/// * [InkRipple.splashFactory], which defines a splash that spreads out /// * [InkRipple.splashFactory], which defines a splash that spreads out
/// more aggresively than the default. /// more aggressively than the default.
final InteractiveInkFeatureFactory splashFactory; final InteractiveInkFeatureFactory splashFactory;
/// Whether detected gestures should provide acoustic and/or haptic feedback. /// Whether detected gestures should provide acoustic and/or haptic feedback.
......
...@@ -53,7 +53,7 @@ class _InputBorderGap extends ChangeNotifier { ...@@ -53,7 +53,7 @@ class _InputBorderGap extends ChangeNotifier {
// Used to interpolate between two InputBorders. // Used to interpolate between two InputBorders.
class _InputBorderTween extends Tween<InputBorder> { 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 @override
InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t); InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t);
...@@ -108,7 +108,7 @@ class _BorderContainer extends StatefulWidget { ...@@ -108,7 +108,7 @@ class _BorderContainer extends StatefulWidget {
@required this.border, @required this.border,
@required this.gap, @required this.gap,
@required this.gapAnimation, @required this.gapAnimation,
this.child this.child,
}) : assert(border != null), }) : assert(border != null),
assert(gap != null), assert(gap != null),
super(key: key); super(key: key);
...@@ -2164,7 +2164,7 @@ class InputDecoration { ...@@ -2164,7 +2164,7 @@ class InputDecoration {
/// The [InputDecoration.applyDefaults] method is used to combine a input /// The [InputDecoration.applyDefaults] method is used to combine a input
/// decoration theme with an [InputDecoration] object. /// decoration theme with an [InputDecoration] object.
@immutable @immutable
class InputDecorationTheme { class InputDecorationTheme extends Diagnosticable {
/// Creates a value for [ThemeData.inputDecorationTheme] that /// Creates a value for [ThemeData.inputDecorationTheme] that
/// defines default values for [InputDecorator]. /// defines default values for [InputDecorator].
/// ///
...@@ -2300,4 +2300,36 @@ class InputDecorationTheme { ...@@ -2300,4 +2300,36 @@ class InputDecorationTheme {
/// * [OutlineInputBorder], an [InputDecorator] border which draws a /// * [OutlineInputBorder], an [InputDecorator] border which draws a
/// rounded rectangle around the input decorator's container. /// rounded rectangle around the input decorator's container.
final InputBorder border; 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));
}
} }
...@@ -9,39 +9,61 @@ import 'package:flutter/gestures.dart'; ...@@ -9,39 +9,61 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material.dart';
import 'slider_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart';
/// A material design slider. /// A Material Design slider.
/// ///
/// Used to select from a range of values. /// Used to select from a range of values.
/// ///
/// A slider can be used to select from either a continuous or a discrete set of /// 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]. /// values. The default is to use a continuous range of values from [min] to
/// To use discrete values, use a non-null value for [divisions], which /// [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 /// 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. /// 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 /// 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]). /// [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 widget itself does not maintain any state. Instead, when the state
/// the slider changes, the widget calls the [onChanged] callback. Most widgets /// of the slider changes, the widget calls the [onChanged] callback. Most
/// that use a slider will listen for the [onChanged] callback and rebuild the /// widgets that use a slider will listen for the [onChanged] callback and
/// slider with a new [value] to update the visual appearance of the slider. /// 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 /// 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. /// wide (with margins on each side) and will shrink-wrap vertically.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// 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: /// See also:
/// ///
/// * [SliderTheme] and [SliderThemeData] for information about controlling
/// the visual appearance of the slider.
/// * [Radio], for selecting among a set of explicit values. /// * [Radio], for selecting among a set of explicit values.
/// * [Checkbox] and [Switch], for toggling a particular value on or off. /// * [Checkbox] and [Switch], for toggling a particular value on or off.
/// * <https://material.google.com/components/sliders.html> /// * <https://material.google.com/components/sliders.html>
...@@ -55,6 +77,10 @@ class Slider extends StatefulWidget { ...@@ -55,6 +77,10 @@ class Slider extends StatefulWidget {
/// ///
/// * [value] determines currently selected value for this slider. /// * [value] determines currently selected value for this slider.
/// * [onChanged] is called when the user selects a new value for the 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({ const Slider({
Key key, Key key,
@required this.value, @required this.value,
...@@ -65,14 +91,12 @@ class Slider extends StatefulWidget { ...@@ -65,14 +91,12 @@ class Slider extends StatefulWidget {
this.label, this.label,
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.thumbOpenAtMin: false,
}) : assert(value != null), }) : assert(value != null),
assert(min != null), assert(min != null),
assert(max != null), assert(max != null),
assert(min <= max), assert(min <= max),
assert(value >= min && value <= max), assert(value >= min && value <= max),
assert(divisions == null || divisions > 0), assert(divisions == null || divisions > 0),
assert(thumbOpenAtMin != null),
super(key: key); super(key: key);
/// The currently selected value for this slider. /// The currently selected value for this slider.
...@@ -131,31 +155,42 @@ class Slider extends StatefulWidget { ...@@ -131,31 +155,42 @@ class Slider extends StatefulWidget {
/// A label to show above the slider when the slider is active. /// 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; 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
final Color activeColor; /// minimum value.
/// The color for the unselected portion of the slider.
/// ///
/// Defaults to the unselected widget color of the current [Theme]. /// Defaults to [SliderTheme.activeRailColor] of the current [SliderTheme].
final Color inactiveColor; ///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
final Color activeColor;
/// Whether the thumb should be an open circle when the slider is at its minimum position. /// The color for the inactive portion of the slider rail.
/// ///
/// When this property is false, the thumb does not change when it the slider /// The "inactive" side of the slider is the side between the thumb and the
/// reaches its minimum position. /// maximum value.
/// ///
/// This property is useful, for example, when the minimum value represents a /// Defaults to the [SliderTheme.inactiveRailColor] of the current
/// qualitatively different state. For a slider that controls the volume of /// [SliderTheme].
/// 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. /// Using a [SliderTheme] gives much more fine-grained control over the
final bool thumbOpenAtMin; /// appearance of various components of the slider.
final Color inactiveColor;
@override @override
_SliderState createState() => new _SliderState(); _SliderState createState() => new _SliderState();
...@@ -170,44 +205,88 @@ class Slider extends StatefulWidget { ...@@ -170,44 +205,88 @@ class Slider extends StatefulWidget {
} }
class _SliderState extends State<Slider> with TickerProviderStateMixin { class _SliderState extends State<Slider> with TickerProviderStateMixin {
_SliderState() { static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
_reactionController = new AnimationController( static const Duration positionAnimationDuration = const Duration(milliseconds: 75);
AnimationController reactionController;
AnimationController enableController;
AnimationController positionController;
@override
void initState() {
super.initState();
reactionController = new AnimationController(
duration: kRadialReactionDuration, duration: kRadialReactionDuration,
vsync: this, vsync: this,
); );
enableController = new AnimationController(
duration: enableAnimationDuration,
vsync: this,
);
positionController = new AnimationController(
duration: positionAnimationDuration,
vsync: this,
);
}
@override
void dispose() {
reactionController.dispose();
enableController.dispose();
positionController.dispose();
super.dispose();
} }
void _handleChanged(double value) { void _handleChanged(double value) {
assert(widget.onChanged != null); assert(widget.onChanged != null);
widget.onChanged(value * (widget.max - widget.min) + widget.min); final double transformedValue = _lerp(value);
widget.onChanged(transformedValue);
} }
@override // Returns a number between min and max, proportional to value, which must
void dispose() { // be between 0.0 and 1.0.
_reactionController?.dispose(); double _lerp(double value) {
super.dispose(); assert(value >= 0.0);
assert(value <= 1.0);
return value * (widget.max - widget.min) + widget.min;
} }
// Have to keep the reaction controller here so that we may dispose of it // Returns a number between 0.0 and 1.0, given a value between min and max.
// properly. double _unlerp(double value) {
AnimationController _reactionController; assert(value <= widget.max);
assert(value >= widget.min);
return widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(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( 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, divisions: widget.divisions,
label: widget.label, label: widget.label,
activeColor: widget.activeColor ?? theme.accentColor, sliderTheme: sliderTheme,
inactiveColor: widget.inactiveColor ?? theme.unselectedWidgetColor,
thumbOpenAtMin: widget.thumbOpenAtMin,
textTheme: theme.accentTextTheme,
textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
vsync: this, state: this,
reactionController: _reactionController,
); );
} }
} }
...@@ -218,27 +297,19 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -218,27 +297,19 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
this.value, this.value,
this.divisions, this.divisions,
this.label, this.label,
this.activeColor, this.sliderTheme,
this.inactiveColor,
this.thumbOpenAtMin,
this.textTheme,
this.textScaleFactor, this.textScaleFactor,
this.onChanged, this.onChanged,
this.vsync, this.state,
this.reactionController,
}) : super(key: key); }) : super(key: key);
final double value; final double value;
final int divisions; final int divisions;
final String label; final String label;
final Color activeColor; final SliderThemeData sliderTheme;
final Color inactiveColor;
final bool thumbOpenAtMin;
final TextTheme textTheme;
final double textScaleFactor; final double textScaleFactor;
final ValueChanged<double> onChanged; final ValueChanged<double> onChanged;
final TickerProvider vsync; final _SliderState state;
final AnimationController reactionController;
@override @override
_RenderSlider createRenderObject(BuildContext context) { _RenderSlider createRenderObject(BuildContext context) {
...@@ -246,14 +317,11 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -246,14 +317,11 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
value: value, value: value,
divisions: divisions, divisions: divisions,
label: label, label: label,
activeColor: activeColor, sliderTheme: sliderTheme,
inactiveColor: inactiveColor, theme: Theme.of(context),
thumbOpenAtMin: thumbOpenAtMin,
textTheme: textTheme,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
onChanged: onChanged, onChanged: onChanged,
vsync: vsync, state: state,
reactionController: reactionController,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
); );
} }
...@@ -264,74 +332,47 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -264,74 +332,47 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..value = value ..value = value
..divisions = divisions ..divisions = divisions
..label = label ..label = label
..activeColor = activeColor ..sliderTheme = sliderTheme
..inactiveColor = inactiveColor ..theme = Theme.of(context)
..thumbOpenAtMin = thumbOpenAtMin
..textTheme = textTheme
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
..onChanged = onChanged ..onChanged = onChanged
..textDirection = Directionality.of(context); ..textDirection = Directionality.of(context);
// Ticker provider cannot change since there's a 1:1 relationship between // Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object. // the _SliderRenderObjectWidget object and the _SliderState object.
} }
} }
const double _kThumbRadius = 6.0; const double _overlayRadius = 16.0;
const double _kActiveThumbRadius = 9.0; const double _overlayDiameter = _overlayRadius * 2.0;
const double _kDisabledThumbRadius = 4.0; const double _railHeight = 2.0;
const double _kReactionRadius = 16.0; const double _preferredRailWidth = 144.0;
const double _kPreferredTrackWidth = 144.0; const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
const double _kMinimumTrackWidth = _kActiveThumbRadius; // biggest of the thumb radii
const double _kPreferredTotalWidth = _kPreferredTrackWidth + 2 * _kReactionRadius;
const double _kMinimumTotalWidth = _kMinimumTrackWidth + 2 * _kReactionRadius;
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) { const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label); final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
}
class _RenderSlider extends RenderBox { class _RenderSlider extends RenderBox {
_RenderSlider({ _RenderSlider({
@required double value, @required double value,
int divisions, int divisions,
String label, String label,
Color activeColor, SliderThemeData sliderTheme,
Color inactiveColor, ThemeData theme,
bool thumbOpenAtMin,
TextTheme textTheme,
double textScaleFactor, double textScaleFactor,
ValueChanged<double> onChanged, ValueChanged<double> onChanged,
TickerProvider vsync, @required _SliderState state,
@required TextDirection textDirection, @required TextDirection textDirection,
@required AnimationController reactionController,
}) : assert(value != null && value >= 0.0 && value <= 1.0), }) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(state != null),
assert(textDirection != null), assert(textDirection != null),
_label = label, _label = label,
_value = value, _value = value,
_divisions = divisions, _divisions = divisions,
_activeColor = activeColor, _sliderTheme = sliderTheme,
_inactiveColor = inactiveColor, _theme = theme,
_thumbOpenAtMin = thumbOpenAtMin,
_textTheme = textTheme,
_textScaleFactor = textScaleFactor, _textScaleFactor = textScaleFactor,
_onChanged = onChanged, _onChanged = onChanged,
_state = state,
_textDirection = textDirection { _textDirection = textDirection {
_updateLabelPainter(); _updateLabelPainter();
final GestureArenaTeam team = new GestureArenaTeam(); final GestureArenaTeam team = new GestureArenaTeam();
...@@ -342,104 +383,106 @@ class _RenderSlider extends RenderBox { ...@@ -342,104 +383,106 @@ class _RenderSlider extends RenderBox {
..onEnd = _handleDragEnd; ..onEnd = _handleDragEnd;
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..team = team ..team = team
..onTapCancel = _endInteraction
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp; ..onTapUp = _handleTapUp;
_reactionController = reactionController; _reaction = new CurvedAnimation(parent: state.reactionController, curve: Curves.fastOutSlowIn)
_reaction = new CurvedAnimation( ..addListener(markNeedsPaint);
parent: _reactionController, state.enableController.value = isInteractive ? 1.0 : 0.0;
curve: Curves.fastOutSlowIn _enableAnimation = new CurvedAnimation(parent: state.enableController, curve: Curves.easeInOut)
)..addListener(markNeedsPaint); ..addListener(markNeedsPaint);
_position = new AnimationController( state.positionController.value = _value;
value: value,
duration: _kDiscreteTransitionDuration,
vsync: vsync,
)..addListener(markNeedsPaint);
} }
double get value => _value; double get value => _value;
double _value; double _value;
_SliderState _state;
set value(double newValue) { set value(double newValue) {
assert(newValue != null && newValue >= 0.0 && newValue <= 1.0); assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
if (newValue == _value) final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
if (convertedValue == _value) {
return; return;
_value = newValue; }
if (divisions != null) _value = convertedValue;
_position.animateTo(newValue, curve: Curves.fastOutSlowIn); if (isDiscrete) {
else _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
_position.value = newValue; } else {
_state.positionController.value = convertedValue;
}
} }
int get divisions => _divisions; int get divisions => _divisions;
int _divisions; int _divisions;
set divisions(int value) { set divisions(int value) {
if (value == _divisions) if (value == _divisions) {
return; return;
}
_divisions = value; _divisions = value;
markNeedsPaint(); markNeedsPaint();
} }
String get label => _label; String get label => _label;
String _label; String _label;
set label(String value) { set label(String value) {
if (value == _label) if (value == _label) {
return; return;
}
_label = value; _label = value;
_updateLabelPainter(); _updateLabelPainter();
} }
Color get activeColor => _activeColor; SliderThemeData get sliderTheme => _sliderTheme;
Color _activeColor; SliderThemeData _sliderTheme;
set activeColor(Color value) {
if (value == _activeColor)
return;
_activeColor = value;
markNeedsPaint();
}
Color get inactiveColor => _inactiveColor; set sliderTheme(SliderThemeData value) {
Color _inactiveColor; if (value == _sliderTheme) {
set inactiveColor(Color value) {
if (value == _inactiveColor)
return; return;
_inactiveColor = value; }
_sliderTheme = value;
markNeedsPaint(); markNeedsPaint();
} }
bool get thumbOpenAtMin => _thumbOpenAtMin; ThemeData get theme => _theme;
bool _thumbOpenAtMin; ThemeData _theme;
set thumbOpenAtMin(bool value) {
if (value == _thumbOpenAtMin)
return;
_thumbOpenAtMin = value;
markNeedsPaint();
}
TextTheme get textTheme => _textTheme; set theme(ThemeData value) {
TextTheme _textTheme; if (value == _theme) {
set textTheme(TextTheme value) {
if (value == _textTheme)
return; return;
_textTheme = value; }
_theme = value;
markNeedsPaint(); markNeedsPaint();
} }
double get textScaleFactor => _textScaleFactor; double get textScaleFactor => _textScaleFactor;
double _textScaleFactor; double _textScaleFactor;
set textScaleFactor(double value) { set textScaleFactor(double value) {
if (value == _textScaleFactor) if (value == _textScaleFactor) {
return; return;
}
_textScaleFactor = value; _textScaleFactor = value;
_updateLabelPainter(); _updateLabelPainter();
markNeedsPaint();
} }
ValueChanged<double> get onChanged => _onChanged; ValueChanged<double> get onChanged => _onChanged;
ValueChanged<double> _onChanged; ValueChanged<double> _onChanged;
set onChanged(ValueChanged<double> value) { set onChanged(ValueChanged<double> value) {
if (value == _onChanged) if (value == _onChanged) {
return; return;
}
final bool wasInteractive = isInteractive; final bool wasInteractive = isInteractive;
_onChanged = value; _onChanged = value;
if (wasInteractive != isInteractive) { if (wasInteractive != isInteractive) {
if (isInteractive) {
_state.enableController.forward();
} else {
_state.enableController.reverse();
}
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -447,21 +490,23 @@ class _RenderSlider extends RenderBox { ...@@ -447,21 +490,23 @@ class _RenderSlider extends RenderBox {
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
assert(value != null); assert(value != null);
if (value == _textDirection) if (value == _textDirection) {
return; return;
}
_textDirection = value; _textDirection = value;
_updateLabelPainter(); _updateLabelPainter();
} }
void _updateLabelPainter() { void _updateLabelPainter() {
if (label != null) { 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 _labelPainter
..text = new TextSpan( ..text = new TextSpan(style: style, text: label)
style: _textTheme.body1.copyWith(fontSize: 10.0 * _textScaleFactor),
text: label,
)
..textDirection = textDirection ..textDirection = textDirection
..layout(); ..layout();
} else { } else {
...@@ -473,14 +518,11 @@ class _RenderSlider extends RenderBox { ...@@ -473,14 +518,11 @@ class _RenderSlider extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
double get _trackLength => size.width - 2.0 * _kReactionRadius; double get _railLength => size.width - _overlayDiameter;
Animation<double> _reaction; Animation<double> _reaction;
AnimationController _reactionController; Animation<double> _enableAnimation;
AnimationController _position;
final TextPainter _labelPainter = new TextPainter(); final TextPainter _labelPainter = new TextPainter();
HorizontalDragGestureRecognizer _drag; HorizontalDragGestureRecognizer _drag;
TapGestureRecognizer _tap; TapGestureRecognizer _tap;
bool _active = false; bool _active = false;
...@@ -488,6 +530,8 @@ class _RenderSlider extends RenderBox { ...@@ -488,6 +530,8 @@ class _RenderSlider extends RenderBox {
bool get isInteractive => onChanged != null; bool get isInteractive => onChanged != null;
bool get isDiscrete => divisions != null && divisions > 0;
double _getValueFromVisualPosition(double visualPosition) { double _getValueFromVisualPosition(double visualPosition) {
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
...@@ -499,29 +543,40 @@ class _RenderSlider extends RenderBox { ...@@ -499,29 +543,40 @@ class _RenderSlider extends RenderBox {
} }
double _getValueFromGlobalPosition(Offset globalPosition) { double _getValueFromGlobalPosition(Offset globalPosition) {
final double visualPosition = (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength; final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
return _getValueFromVisualPosition(visualPosition); return _getValueFromVisualPosition(visualPosition);
} }
double _discretize(double value) { double _discretize(double value) {
double result = value.clamp(0.0, 1.0); double result = value.clamp(0.0, 1.0);
if (divisions != null) if (isDiscrete) {
result = (result * divisions).round() / divisions; result = (result * divisions).round() / divisions;
}
return result; return result;
} }
void _handleDragStart(DragStartDetails details) { void _startInteraction(Offset globalPosition) {
if (isInteractive) { if (isInteractive) {
_active = true; _active = true;
_currentDragValue = _getValueFromGlobalPosition(details.globalPosition); _currentDragValue = _getValueFromGlobalPosition(globalPosition);
onChanged(_discretize(_currentDragValue)); 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) { void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) { if (isInteractive) {
final double valueDelta = details.primaryDelta / _trackLength; final double valueDelta = details.primaryDelta / _railLength;
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
_currentDragValue -= valueDelta; _currentDragValue -= valueDelta;
...@@ -534,18 +589,11 @@ class _RenderSlider extends RenderBox { ...@@ -534,18 +589,11 @@ class _RenderSlider extends RenderBox {
} }
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) => _endInteraction();
if (_active) {
_active = false;
_currentDragValue = 0.0;
_reactionController.reverse();
}
}
void _handleTapUp(TapUpDetails details) { void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);
if (isInteractive && !_active)
onChanged(_discretize(_getValueFromGlobalPosition(details.globalPosition))); void _handleTapUp(TapUpDetails details) => _endInteraction();
}
@override @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => true;
...@@ -562,25 +610,22 @@ class _RenderSlider extends RenderBox { ...@@ -562,25 +610,22 @@ class _RenderSlider extends RenderBox {
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return _kMinimumTotalWidth; return math.max(_overlayDiameter,
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width);
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
// This doesn't quite match the definition of computeMaxIntrinsicWidth, // This doesn't quite match the definition of computeMaxIntrinsicWidth,
// but it seems within the spirit... // but it seems within the spirit...
return _kPreferredTotalWidth; return _preferredTotalWidth;
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) => _overlayDiameter;
return _getPreferredTotalHeight(label);
}
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
return _getPreferredTotalHeight(label);
}
@override @override
bool get sizedByParent => true; bool get sizedByParent => true;
...@@ -588,123 +633,174 @@ class _RenderSlider extends RenderBox { ...@@ -588,123 +633,174 @@ class _RenderSlider extends RenderBox {
@override @override
void performResize() { void performResize() {
size = new Size( size = new Size(
constraints.hasBoundedWidth ? constraints.maxWidth : _kPreferredTotalWidth, constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
constraints.hasBoundedHeight ? constraints.maxHeight : _getPreferredTotalHeight(label), 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 @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final double trackLength = size.width - 2 * _kReactionRadius; final double railLength = size.width - 2 * _overlayRadius;
final bool enabled = isInteractive; final double value = _state.positionController.value;
final double value = _position.value; final ColorTween activeRailEnableColor = new ColorTween(
final bool thumbAtMin = value == 0.0; begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor);
final ColorTween inactiveRailEnableColor = new ColorTween(
final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor; begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor);
final Paint trackPaint = new Paint()..color = _inactiveColor; final ColorTween activeTickMarkEnableColor = new ColorTween(
begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor);
final ColorTween inactiveTickMarkEnableColor = new ColorTween(
begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor);
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; double visualPosition;
Paint leftPaint; Paint leftRailPaint;
Paint rightPaint; Paint rightRailPaint;
Paint leftTickMarkPaint;
Paint rightTickMarkPaint;
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
visualPosition = 1.0 - value; visualPosition = 1.0 - value;
leftPaint = trackPaint; leftRailPaint = inactiveRailPaint;
rightPaint = primaryPaint; rightRailPaint = activeRailPaint;
leftTickMarkPaint = inactiveTickMarkPaint;
rightTickMarkPaint = activeTickMarkPaint;
break; break;
case TextDirection.ltr: case TextDirection.ltr:
visualPosition = value; visualPosition = value;
leftPaint = primaryPaint; leftRailPaint = activeRailPaint;
rightPaint = trackPaint; rightRailPaint = inactiveRailPaint;
leftTickMarkPaint = activeTickMarkPaint;
rightTickMarkPaint = inactiveTickMarkPaint;
break; break;
} }
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label); const double railRadius = _railHeight / 2.0;
final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel; const double thumbGap = 2.0;
final double trackLeft = offset.dx + _kReactionRadius;
final double trackTop = trackCenter - 1.0; final double railVerticalCenter = offset.dy + (size.height) / 2.0;
final double trackBottom = trackCenter + 1.0; final double railLeft = offset.dx + _overlayRadius;
final double trackRight = trackLeft + trackLength; final double railTop = railVerticalCenter - railRadius;
final double trackActive = trackLeft + trackLength * visualPosition; final double railBottom = railVerticalCenter + railRadius;
final double railRight = railLeft + railLength;
final Offset thumbCenter = new Offset(trackActive, trackCenter); final double railActive = railLeft + railLength * visualPosition;
final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius; final double thumbRadius =
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0;
if (enabled) { final double railActiveLeft =
if (visualPosition > 0.0) math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value));
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), leftPaint); final double railActiveRight =
if (visualPosition < 1.0) { math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight);
final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null; final Rect railLeftRect = new Rect.fromLTRB(railLeft, railTop, railActiveLeft, railBottom);
final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0; final Rect railRightRect = new Rect.fromLTRB(railActiveRight, railTop, railRight, railBottom);
canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), rightPaint);
} final Offset thumbCenter = new Offset(railActive, railVerticalCenter);
} else {
if (visualPosition > 0.0) // Paint the rail.
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint); if (visualPosition > 0.0) {
if (visualPosition < 1.0) canvas.drawRect(railLeftRect, leftRailPaint);
canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint); }
if (visualPosition < 1.0) {
canvas.drawRect(railRightRect, rightRailPaint);
} }
if (_reaction.status != AnimationStatus.dismissed) { _paintOverlay(canvas, thumbCenter);
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);
}
}
}
if (label != null) { _paintTickMarks(
final Offset center = new Offset( canvas,
trackActive, railLeftRect,
_kLabelBalloonCenterTween.evaluate(_reaction) * textScaleFactor + trackCenter railRightRect,
); leftTickMarkPaint,
final double radius = _kLabelBalloonRadiusTween.evaluate(_reaction) * textScaleFactor; rightTickMarkPaint,
final Offset tip = new Offset( );
trackActive,
_kLabelBalloonTipTween.evaluate(_reaction) * textScaleFactor + trackCenter if (isInteractive && _reaction.status != AnimationStatus.dismissed && label != null) {
); bool showValueIndicator;
final double tipAttachment = _kLabelBalloonTipAttachmentRatio * radius; switch (_sliderTheme.showValueIndicator) {
case ShowValueIndicator.onlyForDiscrete:
canvas.drawCircle(center, radius, primaryPaint); showValueIndicator = isDiscrete;
final Path path = new Path() break;
..moveTo(tip.dx, tip.dy) case ShowValueIndicator.onlyForContinuous:
..lineTo(center.dx - tipAttachment, center.dy + tipAttachment) showValueIndicator = !isDiscrete;
..lineTo(center.dx + tipAttachment, center.dy + tipAttachment) break;
..close(); case ShowValueIndicator.always:
canvas.drawPath(path, primaryPaint); showValueIndicator = true;
final Offset labelOffset = new Offset( break;
center.dx - _labelPainter.width / 2.0, case ShowValueIndicator.never:
center.dy - _labelPainter.height / 2.0 showValueIndicator = false;
break;
}
if (showValueIndicator) {
_sliderTheme.valueIndicatorShape.paint(
context,
isDiscrete,
thumbCenter,
_reaction,
_enableAnimation,
_labelPainter,
_sliderTheme,
_textDirection,
_textScaleFactor,
value,
); );
_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; _sliderTheme.thumbShape.paint(
double thumbRadiusDelta = 0.0; context,
if (thumbAtMin && thumbOpenAtMin) { isDiscrete,
thumbPaint = trackPaint; thumbCenter,
// This is destructive to trackPaint. _reaction,
thumbPaint _enableAnimation,
..style = PaintingStyle.stroke label != null ? _labelPainter : null,
..strokeWidth = 2.0; _sliderTheme,
thumbRadiusDelta = -1.0; _textDirection,
} _textScaleFactor,
canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint); value,
);
} }
@override @override
...@@ -718,15 +814,17 @@ class _RenderSlider extends RenderBox { ...@@ -718,15 +814,17 @@ class _RenderSlider extends RenderBox {
} }
} }
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit; double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
void _increaseAction() { void _increaseAction() {
if (isInteractive) if (isInteractive) {
onChanged((value + _semanticActionUnit).clamp(0.0, 1.0)); onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
}
} }
void _decreaseAction() { void _decreaseAction() {
if (isInteractive) if (isInteractive) {
onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
}
} }
} }
// 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,
);
}
}
...@@ -12,6 +12,7 @@ import 'colors.dart'; ...@@ -12,6 +12,7 @@ import 'colors.dart';
import 'ink_splash.dart'; import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'slider_theme.dart';
import 'typography.dart'; import 'typography.dart';
/// Describes the contrast needs of a color. /// Describes the contrast needs of a color.
...@@ -52,8 +53,8 @@ const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC); ...@@ -52,8 +53,8 @@ const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC);
/// ///
/// To obtain the current theme, use [Theme.of]. /// To obtain the current theme, use [Theme.of].
@immutable @immutable
class ThemeData { class ThemeData extends Diagnosticable {
/// Create a ThemeData given a set of preferred values. /// Create a [ThemeData] given a set of preferred values.
/// ///
/// Default values will be derived for arguments that are omitted. /// Default values will be derived for arguments that are omitted.
/// ///
...@@ -78,6 +79,8 @@ class ThemeData { ...@@ -78,6 +79,8 @@ class ThemeData {
MaterialColor primarySwatch, MaterialColor primarySwatch,
Color primaryColor, Color primaryColor,
Brightness primaryColorBrightness, Brightness primaryColorBrightness,
Color primaryColorLight,
Color primaryColorDark,
Color accentColor, Color accentColor,
Brightness accentColorBrightness, Brightness accentColorBrightness,
Color canvasColor, Color canvasColor,
...@@ -109,13 +112,16 @@ class ThemeData { ...@@ -109,13 +112,16 @@ class ThemeData {
IconThemeData iconTheme, IconThemeData iconTheme,
IconThemeData primaryIconTheme, IconThemeData primaryIconTheme,
IconThemeData accentIconTheme, IconThemeData accentIconTheme,
TargetPlatform platform SliderThemeData sliderTheme,
TargetPlatform platform,
}) { }) {
brightness ??= Brightness.light; brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark; final bool isDark = brightness == Brightness.dark;
primarySwatch ??= Colors.blue; primarySwatch ??= Colors.blue;
primaryColor ??= isDark ? Colors.grey[900] : primarySwatch[500]; primaryColor ??= isDark ? Colors.grey[900] : primarySwatch[500];
primaryColorBrightness ??= estimateBrightnessForColor(primaryColor); primaryColorBrightness ??= estimateBrightnessForColor(primaryColor);
primaryColorLight ??= isDark ? Colors.grey[500] : primarySwatch[100];
primaryColorDark ??= isDark ? Colors.black : primarySwatch[700];
final bool primaryIsDark = primaryColorBrightness == Brightness.dark; final bool primaryIsDark = primaryColorBrightness == Brightness.dark;
accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500]; accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500];
accentColorBrightness ??= estimateBrightnessForColor(accentColor); accentColorBrightness ??= estimateBrightnessForColor(accentColor);
...@@ -143,9 +149,15 @@ class ThemeData { ...@@ -143,9 +149,15 @@ class ThemeData {
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000); hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
errorColor ??= Colors.red[700]; errorColor ??= Colors.red[700];
inputDecorationTheme ??= const InputDecorationTheme(); inputDecorationTheme ??= const InputDecorationTheme();
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); iconTheme ??= isDark
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); ? const IconThemeData(color: Colors.white)
accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); : 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; platform ??= defaultTargetPlatform;
final Typography typography = new Typography(platform: platform); final Typography typography = new Typography(platform: platform);
textTheme ??= isDark ? typography.white : typography.black; textTheme ??= isDark ? typography.white : typography.black;
...@@ -156,10 +168,17 @@ class ThemeData { ...@@ -156,10 +168,17 @@ class ThemeData {
primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily); primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily);
accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily); accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily);
} }
sliderTheme ??= new SliderThemeData.materialDefaults(
primaryColor: primaryColor,
primaryColorLight: primaryColorLight,
primaryColorDark: primaryColorDark,
);
return new ThemeData.raw( return new ThemeData.raw(
brightness: brightness, brightness: brightness,
primaryColor: primaryColor, primaryColor: primaryColor,
primaryColorBrightness: primaryColorBrightness, primaryColorBrightness: primaryColorBrightness,
primaryColorLight: primaryColorLight,
primaryColorDark: primaryColorDark,
accentColor: accentColor, accentColor: accentColor,
accentColorBrightness: accentColorBrightness, accentColorBrightness: accentColorBrightness,
canvasColor: canvasColor, canvasColor: canvasColor,
...@@ -190,11 +209,12 @@ class ThemeData { ...@@ -190,11 +209,12 @@ class ThemeData {
iconTheme: iconTheme, iconTheme: iconTheme,
primaryIconTheme: primaryIconTheme, primaryIconTheme: primaryIconTheme,
accentIconTheme: accentIconTheme, 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. /// must be specified.
/// ///
/// This will rarely be used directly. It is used by [lerp] to /// This will rarely be used directly. It is used by [lerp] to
...@@ -204,6 +224,8 @@ class ThemeData { ...@@ -204,6 +224,8 @@ class ThemeData {
@required this.brightness, @required this.brightness,
@required this.primaryColor, @required this.primaryColor,
@required this.primaryColorBrightness, @required this.primaryColorBrightness,
@required this.primaryColorLight,
@required this.primaryColorDark,
@required this.accentColor, @required this.accentColor,
@required this.accentColorBrightness, @required this.accentColorBrightness,
@required this.canvasColor, @required this.canvasColor,
...@@ -234,10 +256,13 @@ class ThemeData { ...@@ -234,10 +256,13 @@ class ThemeData {
@required this.iconTheme, @required this.iconTheme,
@required this.primaryIconTheme, @required this.primaryIconTheme,
@required this.accentIconTheme, @required this.accentIconTheme,
@required this.platform @required this.sliderTheme,
@required this.platform,
}) : assert(brightness != null), }) : assert(brightness != null),
assert(primaryColor != null), assert(primaryColor != null),
assert(primaryColorBrightness != null), assert(primaryColorBrightness != null),
assert(primaryColorLight != null),
assert(primaryColorDark != null),
assert(accentColor != null), assert(accentColor != null),
assert(accentColorBrightness != null), assert(accentColorBrightness != null),
assert(canvasColor != null), assert(canvasColor != null),
...@@ -267,6 +292,7 @@ class ThemeData { ...@@ -267,6 +292,7 @@ class ThemeData {
assert(iconTheme != null), assert(iconTheme != null),
assert(primaryIconTheme != null), assert(primaryIconTheme != null),
assert(accentIconTheme != null), assert(accentIconTheme != null),
assert(sliderTheme != null),
assert(platform != null); assert(platform != null);
/// A default light blue theme. /// A default light blue theme.
...@@ -311,6 +337,12 @@ class ThemeData { ...@@ -311,6 +337,12 @@ class ThemeData {
/// icons placed on top of the primary color (e.g. toolbar text). /// icons placed on top of the primary color (e.g. toolbar text).
final Brightness primaryColorBrightness; 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). /// The foreground color for widgets (knobs, text, overscroll edge effect, etc).
final Color accentColor; final Color accentColor;
...@@ -352,7 +384,7 @@ class ThemeData { ...@@ -352,7 +384,7 @@ class ThemeData {
/// ///
/// * [InkSplash.splashFactory], which defines the default splash. /// * [InkSplash.splashFactory], which defines the default splash.
/// * [InkRipple.splashFactory], which defines a splash that spreads out /// * [InkRipple.splashFactory], which defines a splash that spreads out
/// more aggresively than the default. /// more aggressively than the default.
final InteractiveInkFeatureFactory splashFactory; final InteractiveInkFeatureFactory splashFactory;
/// The color used to highlight selected rows. /// The color used to highlight selected rows.
...@@ -428,6 +460,11 @@ class ThemeData { ...@@ -428,6 +460,11 @@ class ThemeData {
/// An icon theme that contrasts with the accent color. /// An icon theme that contrasts with the accent color.
final IconThemeData accentIconTheme; 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. /// The platform the material widgets should adapt to target.
/// ///
/// Defaults to the current platform. /// Defaults to the current platform.
...@@ -438,6 +475,8 @@ class ThemeData { ...@@ -438,6 +475,8 @@ class ThemeData {
Brightness brightness, Brightness brightness,
Color primaryColor, Color primaryColor,
Brightness primaryColorBrightness, Brightness primaryColorBrightness,
Color primaryColorLight,
Color primaryColorDark,
Color accentColor, Color accentColor,
Brightness accentColorBrightness, Brightness accentColorBrightness,
Color canvasColor, Color canvasColor,
...@@ -468,12 +507,15 @@ class ThemeData { ...@@ -468,12 +507,15 @@ class ThemeData {
IconThemeData iconTheme, IconThemeData iconTheme,
IconThemeData primaryIconTheme, IconThemeData primaryIconTheme,
IconThemeData accentIconTheme, IconThemeData accentIconTheme,
SliderThemeData sliderTheme,
TargetPlatform platform, TargetPlatform platform,
}) { }) {
return new ThemeData.raw( return new ThemeData.raw(
brightness: brightness ?? this.brightness, brightness: brightness ?? this.brightness,
primaryColor: primaryColor ?? this.primaryColor, primaryColor: primaryColor ?? this.primaryColor,
primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness, primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness,
primaryColorLight: primaryColorLight ?? this.primaryColorLight,
primaryColorDark: primaryColorDark ?? this.primaryColorDark,
accentColor: accentColor ?? this.accentColor, accentColor: accentColor ?? this.accentColor,
accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness, accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness,
canvasColor: canvasColor ?? this.canvasColor, canvasColor: canvasColor ?? this.canvasColor,
...@@ -504,6 +546,7 @@ class ThemeData { ...@@ -504,6 +546,7 @@ class ThemeData {
iconTheme: iconTheme ?? this.iconTheme, iconTheme: iconTheme ?? this.iconTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme, primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentIconTheme: accentIconTheme ?? this.accentIconTheme, accentIconTheme: accentIconTheme ?? this.accentIconTheme,
sliderTheme: sliderTheme ?? this.sliderTheme,
platform: platform ?? this.platform, platform: platform ?? this.platform,
); );
} }
...@@ -515,7 +558,8 @@ class ThemeData { ...@@ -515,7 +558,8 @@ class ThemeData {
static const int _localizedThemeDataCacheSize = 5; static const int _localizedThemeDataCacheSize = 5;
/// Caches localized themes to speed up the [localize] method. /// 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 /// Returns a new theme built by merging the text geometry provided by the
/// [localTextGeometry] theme with the [baseTheme]. /// [localTextGeometry] theme with the [baseTheme].
...@@ -567,7 +611,7 @@ class ThemeData { ...@@ -567,7 +611,7 @@ class ThemeData {
// Design spec shows for its color palette on // Design spec shows for its color palette on
// <https://material.io/guidelines/style/color.html#color-color-palette>. // <https://material.io/guidelines/style/color.html#color-color-palette>.
const double kThreshold = 0.15; 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.light;
return Brightness.dark; return Brightness.dark;
} }
...@@ -595,6 +639,8 @@ class ThemeData { ...@@ -595,6 +639,8 @@ class ThemeData {
brightness: t < 0.5 ? a.brightness : b.brightness, brightness: t < 0.5 ? a.brightness : b.brightness,
primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t), primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t),
primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness, 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), canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t), scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t), bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
...@@ -625,6 +671,7 @@ class ThemeData { ...@@ -625,6 +671,7 @@ class ThemeData {
iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t), iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t), primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, 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, platform: t < 0.5 ? a.platform : b.platform,
); );
} }
...@@ -667,53 +714,120 @@ class ThemeData { ...@@ -667,53 +714,120 @@ class ThemeData {
(otherData.iconTheme == iconTheme) && (otherData.iconTheme == iconTheme) &&
(otherData.primaryIconTheme == primaryIconTheme) && (otherData.primaryIconTheme == primaryIconTheme) &&
(otherData.accentIconTheme == accentIconTheme) && (otherData.accentIconTheme == accentIconTheme) &&
(otherData.sliderTheme == sliderTheme) &&
(otherData.platform == platform); (otherData.platform == platform);
} }
@override @override
int get hashCode { int get hashCode {
return hashValues( return hashValues(
brightness, brightness,
primaryColor, primaryColor,
primaryColorBrightness, primaryColorBrightness,
canvasColor, canvasColor,
scaffoldBackgroundColor, scaffoldBackgroundColor,
bottomAppBarColor, bottomAppBarColor,
cardColor, cardColor,
dividerColor, dividerColor,
highlightColor, highlightColor,
splashColor, splashColor,
splashFactory, splashFactory,
selectedRowColor, selectedRowColor,
unselectedWidgetColor, unselectedWidgetColor,
disabledColor, disabledColor,
buttonColor, buttonColor,
buttonTheme, buttonTheme,
secondaryHeaderColor, secondaryHeaderColor,
textSelectionColor, textSelectionColor,
textSelectionHandleColor, textSelectionHandleColor,
hashValues( // Too many values. hashValues( // Too many values.
backgroundColor, backgroundColor,
accentColor, accentColor,
accentColorBrightness, accentColorBrightness,
indicatorColor, indicatorColor,
dialogBackgroundColor, dialogBackgroundColor,
hintColor, hintColor,
errorColor, errorColor,
textTheme, textTheme,
primaryTextTheme, primaryTextTheme,
accentTextTheme, accentTextTheme,
iconTheme, iconTheme,
inputDecorationTheme, inputDecorationTheme,
primaryIconTheme, primaryIconTheme,
accentIconTheme, accentIconTheme,
platform, sliderTheme,
) platform,
),
); );
} }
@override @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 { class _IdentityThemeDataCacheKey {
...@@ -732,7 +846,8 @@ class _IdentityThemeDataCacheKey { ...@@ -732,7 +846,8 @@ class _IdentityThemeDataCacheKey {
// We are explicitly ignoring the possibility that the types might not // We are explicitly ignoring the possibility that the types might not
// match in the interests of speed. // match in the interests of speed.
final _IdentityThemeDataCacheKey otherKey = other; final _IdentityThemeDataCacheKey otherKey = other;
return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry); return identical(baseTheme, otherKey.baseTheme) &&
identical(localTextGeometry, otherKey.localTextGeometry);
} }
} }
...@@ -742,8 +857,7 @@ class _IdentityThemeDataCacheKey { ...@@ -742,8 +857,7 @@ class _IdentityThemeDataCacheKey {
/// The key that was inserted before all other keys is evicted first, i.e. the /// The key that was inserted before all other keys is evicted first, i.e. the
/// one inserted least recently. /// one inserted least recently.
class _FifoCache<K, V> { class _FifoCache<K, V> {
_FifoCache(this._maximumSize) _FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
: assert(_maximumSize != null && _maximumSize > 0);
/// In Dart the map literal uses a linked hash-map implementation, whose keys /// 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 /// are stored such that [Map.keys] returns them in the order they were
......
...@@ -33,7 +33,7 @@ import 'colors.dart'; ...@@ -33,7 +33,7 @@ import 'colors.dart';
/// globally adjusted, such as the color scheme. /// globally adjusted, such as the color scheme.
/// * <http://material.google.com/style/typography.html> /// * <http://material.google.com/style/typography.html>
@immutable @immutable
class TextTheme { class TextTheme extends Diagnosticable {
/// Creates a text theme that uses the given values. /// Creates a text theme that uses the given values.
/// ///
/// Rather than creating a new text theme, consider using [Typography.black] /// Rather than creating a new text theme, consider using [Typography.black]
...@@ -114,7 +114,7 @@ class TextTheme { ...@@ -114,7 +114,7 @@ class TextTheme {
TextStyle body2, TextStyle body2,
TextStyle body1, TextStyle body1,
TextStyle caption, TextStyle caption,
TextStyle button TextStyle button,
}) { }) {
return new TextTheme( return new TextTheme(
display4: display4 ?? this.display4, display4: display4 ?? this.display4,
...@@ -353,6 +353,34 @@ class TextTheme { ...@@ -353,6 +353,34 @@ class TextTheme {
button, 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. /// The two material design text themes.
...@@ -373,7 +401,7 @@ class TextTheme { ...@@ -373,7 +401,7 @@ class TextTheme {
/// * <http://material.google.com/style/typography.html> /// * <http://material.google.com/style/typography.html>
class Typography { class Typography {
/// Creates the default typography for the specified platform. /// Creates the default typography for the specified platform.
factory Typography({ @required TargetPlatform platform }) { factory Typography({@required TargetPlatform platform}) {
assert(platform != null); assert(platform != null);
switch (platform) { switch (platform) {
case TargetPlatform.android: case TargetPlatform.android:
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'dart:ui' show Color, hashValues; import 'dart:ui' show Color, hashValues;
import 'dart:ui' as ui show lerpDouble; import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/foundation.dart';
/// Defines the color, opacity, and size of icons. /// Defines the color, opacity, and size of icons.
/// ///
/// Used by [IconTheme] to control the color, opacity, and size of icons in a /// Used by [IconTheme] to control the color, opacity, and size of icons in a
...@@ -13,29 +15,26 @@ import 'dart:ui' as ui show lerpDouble; ...@@ -13,29 +15,26 @@ import 'dart:ui' as ui show lerpDouble;
/// To obtain the current icon theme, use [IconTheme.of]. To convert an icon /// 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 /// theme to a version with all the fields filled in, use [new
/// IconThemeData.fallback]. /// IconThemeData.fallback].
class IconThemeData { class IconThemeData extends Diagnosticable {
/// Creates an icon theme data. /// Creates an icon theme data.
/// ///
/// The opacity applies to both explicit and default icon colors. The value /// The opacity applies to both explicit and default icon colors. The value
/// is clamped between 0.0 and 1.0. /// 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. /// Creates an icon them with some reasonable default values.
/// ///
/// The [color] is black, the [opacity] is 1.0, and the [size] is 24.0. /// The [color] is black, the [opacity] is 1.0, and the [size] is 24.0.
const IconThemeData.fallback() const IconThemeData.fallback()
: color = const Color(0xFF000000), : color = const Color(0xFF000000),
_opacity = 1.0, _opacity = 1.0,
size = 24.0; size = 24.0;
/// Creates a copy of this icon theme but with the given fields replaced with /// Creates a copy of this icon theme but with the given fields replaced with
/// the new values. /// the new values.
IconThemeData copyWith({ Color color, double opacity, double size }) { IconThemeData copyWith({Color color, double opacity, double size}) {
return new IconThemeData( return new IconThemeData(
color: color ?? this.color, color: color ?? this.color, opacity: opacity ?? this.opacity, size: size ?? this.size);
opacity: opacity ?? this.opacity,
size: size ?? this.size
);
} }
/// Returns a new icon theme that matches this icon theme but with some values /// Returns a new icon theme that matches this icon theme but with some values
...@@ -44,11 +43,7 @@ class IconThemeData { ...@@ -44,11 +43,7 @@ class IconThemeData {
IconThemeData merge(IconThemeData other) { IconThemeData merge(IconThemeData other) {
if (other == null) if (other == null)
return this; return this;
return copyWith( return copyWith(color: other.color, opacity: other.opacity, size: other.size);
color: other.color,
opacity: other.opacity,
size: other.size
);
} }
/// Whether all the properties of this object are non-null. /// Whether all the properties of this object are non-null.
...@@ -100,16 +95,13 @@ class IconThemeData { ...@@ -100,16 +95,13 @@ class IconThemeData {
int get hashCode => hashValues(color, opacity, size); int get hashCode => hashValues(color, opacity, size);
@override @override
String toString() { void debugFillProperties(DiagnosticPropertiesBuilder description) {
final List<String> result = <String>[]; super.debugFillProperties(description);
if (color != null) if (color == null && _opacity == null && size == null) {
result.add('color: $color'); return;
if (_opacity != null) }
result.add('opacity: $_opacity'); description.add(new DiagnosticsProperty<Color>('color', color));
if (size != null) description.add(new DoubleProperty('opacity', _opacity));
result.add('size: $size'); description.add(new DoubleProperty('size', size));
if (result.isEmpty)
return '<no theme>';
return result.join(', ');
} }
} }
...@@ -150,8 +150,7 @@ void main() { ...@@ -150,8 +150,7 @@ void main() {
expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
}); });
testWidgets('Slider can be given zero values', testWidgets('Slider can be given zero values', (WidgetTester tester) async {
(WidgetTester tester) async {
final List<double> log = <double>[]; final List<double> log = <double>[];
await tester.pumpWidget(new Directionality( await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -160,7 +159,9 @@ void main() { ...@@ -160,7 +159,9 @@ void main() {
value: 0.0, value: 0.0,
min: 0.0, min: 0.0,
max: 1.0, max: 1.0,
onChanged: (double newValue) { log.add(newValue); }, onChanged: (double newValue) {
log.add(newValue);
},
), ),
), ),
)); ));
...@@ -176,7 +177,9 @@ void main() { ...@@ -176,7 +177,9 @@ void main() {
value: 0.0, value: 0.0,
min: 0.0, min: 0.0,
max: 0.0, max: 0.0,
onChanged: (double newValue) { log.add(newValue); }, onChanged: (double newValue) {
log.add(newValue);
},
), ),
), ),
)); ));
...@@ -186,11 +189,27 @@ void main() { ...@@ -186,11 +189,27 @@ void main() {
log.clear(); log.clear();
}); });
testWidgets('Slider has a customizable active color', testWidgets('Slider uses the right theme colors for the right components',
(WidgetTester tester) async { (WidgetTester tester) async {
const Color customColor = const Color(0xFF4CD964); const Color customColor1 = const Color(0xcafefeed);
final ThemeData theme = new ThemeData(platform: TargetPlatform.android); const Color customColor2 = const Color(0xdeadbeef);
Widget buildApp(Color activeColor) { 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( return new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Material( child: new Material(
...@@ -198,47 +217,12 @@ void main() { ...@@ -198,47 +217,12 @@ void main() {
child: new Theme( child: new Theme(
data: theme, data: theme,
child: new Slider( child: new Slider(
value: 0.5, value: value,
label: '$value',
divisions: divisions,
activeColor: activeColor, 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, inactiveColor: inactiveColor,
onChanged: (double newValue) {}, onChanged: onChanged,
), ),
), ),
), ),
...@@ -246,78 +230,168 @@ void main() { ...@@ -246,78 +230,168 @@ void main() {
); );
} }
await tester.pumpWidget(buildApp(null)); await tester.pumpWidget(buildApp());
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));
});
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 = final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(style: PaintingStyle.fill)); // Check default theme for enabled widget.
expect(sliderBox, isNot(paints..circle()..circle())); expect(
await tester.pumpWidget(buildApp(true)); sliderBox,
expect(sliderBox, paints..circle(style: PaintingStyle.stroke)); paints
expect(sliderBox, isNot(paints..circle()..circle())); ..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 tap in vertical scroller', testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
(WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(new Directionality( await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -425,7 +499,8 @@ void main() { ...@@ -425,7 +499,8 @@ void main() {
), ),
), ),
)); ));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
const Size(144.0 + 2.0 * 16.0, 600.0));
await tester.pumpWidget(const Directionality( await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -442,14 +517,18 @@ void main() { ...@@ -442,14 +517,18 @@ void main() {
), ),
), ),
)); ));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 32.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
const Size(144.0 + 2.0 * 16.0, 32.0));
}); });
testWidgets('discrete Slider respects textScaleFactor', (WidgetTester tester) async { testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey(); final Key sliderKey = new UniqueKey();
double value = 0.0; double value = 0.0;
Widget buildSlider({ double textScaleFactor }) { Widget buildSlider(
{double textScaleFactor,
bool isDiscrete: true,
ShowValueIndicator show: ShowValueIndicator.onlyForDiscrete}) {
return new Directionality( return new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new StatefulBuilder( child: new StatefulBuilder(
...@@ -457,22 +536,27 @@ void main() { ...@@ -457,22 +536,27 @@ void main() {
return new MediaQuery( return new MediaQuery(
data: new MediaQueryData(textScaleFactor: textScaleFactor), data: new MediaQueryData(textScaleFactor: textScaleFactor),
child: new Material( child: new Material(
child: new Center( child: new Theme(
child: new OverflowBox( data: Theme.of(context).copyWith(
maxWidth: double.INFINITY, sliderTheme:
maxHeight: double.INFINITY, Theme.of(context).sliderTheme.copyWith(showValueIndicator: show)),
child: new Slider( child: new Center(
key: sliderKey, child: new OverflowBox(
min: 0.0, maxWidth: double.INFINITY,
max: 100.0, maxHeight: double.INFINITY,
divisions: 10, child: new Slider(
label: '${value.round()}', key: sliderKey,
value: value, min: 0.0,
onChanged: (double newValue) { max: 100.0,
setState(() { divisions: isDiscrete ? 10 : null,
value = newValue; label: '${value.round()}',
}); value: value,
}, onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
), ),
), ),
), ),
...@@ -486,12 +570,10 @@ void main() { ...@@ -486,12 +570,10 @@ void main() {
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0)); await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
Offset center = tester.getCenter(find.byType(Slider)); Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center); 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( expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
tester.renderObject(find.byType(Slider)),
paints..circle(radius: 6.0, x: 16.0, y: 44.0)
);
await gesture.up(); await gesture.up();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
...@@ -499,12 +581,41 @@ void main() { ...@@ -499,12 +581,41 @@ void main() {
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0)); await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
center = tester.getCenter(find.byType(Slider)); center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center); 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( expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
tester.renderObject(find.byType(Slider)),
paints..circle(radius: 12.0, x: 16.0, y: 44.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 gesture.up();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
...@@ -523,18 +634,18 @@ void main() { ...@@ -523,18 +634,18 @@ void main() {
), ),
)); ));
expect(semantics, hasSemantics( expect(
new TestSemantics.root( semantics,
children: <TestSemantics>[ hasSemantics(
new TestSemantics.root(children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: 1, id: 1,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index, actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
), ),
] ]),
), ignoreRect: true,
ignoreRect: true, ignoreTransform: true,
ignoreTransform: true, ));
));
// Disable slider // Disable slider
await tester.pumpWidget(const Directionality( await tester.pumpWidget(const Directionality(
...@@ -547,12 +658,92 @@ void main() { ...@@ -547,12 +658,92 @@ void main() {
), ),
)); ));
expect(semantics, hasSemantics( expect(
new TestSemantics.root(), semantics,
ignoreRect: true, hasSemantics(
ignoreTransform: true, new TestSemantics.root(),
)); ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose(); 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);
});
} }
// 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();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment