diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index cf78bb9dd05..c53cb778087 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -53,6 +53,10 @@ import 'theme.dart'; /// /// Requires one of its ancestors to be a [Material] widget. /// +/// Requires one of its ancestors to be a [MediaQuery] widget. Typically, these +/// are introduced by the [MaterialApp] or [WidgetsApp] widget at the top of +/// your application widget tree. +/// /// 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 @@ -67,13 +71,15 @@ import 'theme.dart'; /// * [Radio], for selecting among a set of explicit values. /// * [Checkbox] and [Switch], for toggling a particular value on or off. /// * +/// * [MediaQuery], from which the text scale factor is obtained. class Slider extends StatefulWidget { /// Creates a material design slider. /// /// The slider itself does not maintain any state. Instead, when the state of - /// the slider changes, the widget calls the [onChanged] callback. Most widgets - /// that use a slider will listen for the [onChanged] callback and rebuild the - /// slider with a new [value] to update the visual appearance of the slider. + /// the slider changes, the widget calls the [onChanged] callback. Most + /// widgets that use a slider will listen for the [onChanged] callback and + /// rebuild the slider with a new [value] to update the visual appearance of + /// the slider. /// /// * [value] determines currently selected value for this slider. /// * [onChanged] is called when the user selects a new value for the slider. @@ -208,8 +214,13 @@ class _SliderState extends State with TickerProviderStateMixin { static const Duration enableAnimationDuration = const Duration(milliseconds: 75); static const Duration positionAnimationDuration = const Duration(milliseconds: 75); + // Animation controller that is run when interactions occur (taps, drags, + // etc.). AnimationController reactionController; + // Animation controller that is run when enabling/disabling the slider. AnimationController enableController; + // Animation controller that is run when transitioning between one value + // and the next on a discrete slider. AnimationController positionController; @override @@ -227,6 +238,8 @@ class _SliderState extends State with TickerProviderStateMixin { duration: positionAnimationDuration, vsync: this, ); + enableController.value = widget.onChanged != null ? 1.0 : 0.0; + positionController.value = widget.value; } @override @@ -263,6 +276,7 @@ class _SliderState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); + assert(debugCheckHasMediaQuery(context)); SliderThemeData sliderTheme = SliderTheme.of(context); @@ -286,7 +300,7 @@ class _SliderState extends State with TickerProviderStateMixin { divisions: widget.divisions, label: widget.label, sliderTheme: sliderTheme, - textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, + mediaQueryData: MediaQuery.of(context), onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, state: this, ); @@ -300,7 +314,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { this.divisions, this.label, this.sliderTheme, - this.textScaleFactor, + this.mediaQueryData, this.onChanged, this.state, }) : super(key: key); @@ -309,7 +323,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { final int divisions; final String label; final SliderThemeData sliderTheme; - final double textScaleFactor; + final MediaQueryData mediaQueryData; final ValueChanged onChanged; final _SliderState state; @@ -321,7 +335,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { label: label, sliderTheme: sliderTheme, theme: Theme.of(context), - textScaleFactor: textScaleFactor, + mediaQueryData: mediaQueryData, onChanged: onChanged, state: state, textDirection: Directionality.of(context), @@ -336,7 +350,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ..label = label ..sliderTheme = sliderTheme ..theme = Theme.of(context) - ..textScaleFactor = textScaleFactor + ..mediaQueryData = mediaQueryData ..onChanged = onChanged ..textDirection = Directionality.of(context); // Ticker provider cannot change since there's a 1:1 relationship between @@ -360,7 +374,7 @@ class _RenderSlider extends RenderBox { String label, SliderThemeData sliderTheme, ThemeData theme, - double textScaleFactor, + MediaQueryData mediaQueryData, ValueChanged onChanged, @required _SliderState state, @required TextDirection textDirection, @@ -372,7 +386,7 @@ class _RenderSlider extends RenderBox { _divisions = divisions, _sliderTheme = sliderTheme, _theme = theme, - _textScaleFactor = textScaleFactor, + _mediaQueryData = mediaQueryData, _onChanged = onChanged, _state = state, _textDirection = textDirection { @@ -389,12 +403,14 @@ class _RenderSlider extends RenderBox { ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp ..onTapCancel = _endInteraction; - _reaction = new CurvedAnimation(parent: state.reactionController, curve: Curves.fastOutSlowIn) - ..addListener(markNeedsPaint); - state.enableController.value = isInteractive ? 1.0 : 0.0; - _enableAnimation = new CurvedAnimation(parent: state.enableController, curve: Curves.easeInOut) - ..addListener(markNeedsPaint); - state.positionController.value = _value; + _reaction = new CurvedAnimation( + parent: _state.reactionController, + curve: Curves.fastOutSlowIn, + ); + _enableAnimation = new CurvedAnimation( + parent: _state.enableController, + curve: Curves.easeInOut, + ); } double get value => _value; @@ -418,7 +434,6 @@ class _RenderSlider extends RenderBox { int get divisions => _divisions; int _divisions; - set divisions(int value) { if (value == _divisions) { return; @@ -429,7 +444,6 @@ class _RenderSlider extends RenderBox { String get label => _label; String _label; - set label(String value) { if (value == _label) { return; @@ -440,7 +454,6 @@ class _RenderSlider extends RenderBox { SliderThemeData get sliderTheme => _sliderTheme; SliderThemeData _sliderTheme; - set sliderTheme(SliderThemeData value) { if (value == _sliderTheme) { return; @@ -451,7 +464,6 @@ class _RenderSlider extends RenderBox { ThemeData get theme => _theme; ThemeData _theme; - set theme(ThemeData value) { if (value == _theme) { return; @@ -460,20 +472,20 @@ class _RenderSlider extends RenderBox { markNeedsPaint(); } - double get textScaleFactor => _textScaleFactor; - double _textScaleFactor; - - set textScaleFactor(double value) { - if (value == _textScaleFactor) { + MediaQueryData get mediaQueryData => _mediaQueryData; + MediaQueryData _mediaQueryData; + set mediaQueryData(MediaQueryData value) { + if (value == _mediaQueryData) { return; } - _textScaleFactor = value; + _mediaQueryData = value; + // Media query data includes the textScaleFactor, so we need to update the + // label painter. _updateLabelPainter(); } ValueChanged get onChanged => _onChanged; ValueChanged _onChanged; - set onChanged(ValueChanged value) { if (value == _onChanged) { return; @@ -493,7 +505,6 @@ class _RenderSlider extends RenderBox { TextDirection get textDirection => _textDirection; TextDirection _textDirection; - set textDirection(TextDirection value) { assert(value != null); if (value == _textDirection) { @@ -505,12 +516,10 @@ class _RenderSlider extends RenderBox { void _updateLabelPainter() { if (label != null) { - // We have to account for the text scale factor in the supplied theme. - final TextStyle style = _theme.accentTextTheme.body2 - .copyWith(fontSize: _theme.accentTextTheme.body2.fontSize * _textScaleFactor); _labelPainter - ..text = new TextSpan(style: style, text: label) + ..text = new TextSpan(style: _theme.accentTextTheme.body2, text: label) ..textDirection = textDirection + ..textScaleFactor = _mediaQueryData.textScaleFactor ..layout(); } else { _labelPainter.text = null; @@ -535,6 +544,22 @@ class _RenderSlider extends RenderBox { bool get isDiscrete => divisions != null && divisions > 0; + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _reaction.addListener(markNeedsPaint); + _enableAnimation.addListener(markNeedsPaint); + _state.positionController.addListener(markNeedsPaint); + } + + @override + void detach() { + _reaction.removeListener(markNeedsPaint); + _enableAnimation.removeListener(markNeedsPaint); + _state.positionController.removeListener(markNeedsPaint); + super.detach(); + } + double _getValueFromVisualPosition(double visualPosition) { switch (textDirection) { case TextDirection.rtl: @@ -589,7 +614,6 @@ class _RenderSlider extends RenderBox { break; } onChanged(_discretize(_currentDragValue)); - markNeedsPaint(); } } @@ -614,8 +638,10 @@ class _RenderSlider extends RenderBox { @override double computeMinIntrinsicWidth(double height) { - return math.max(_overlayDiameter, - _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width); + return math.max( + _overlayDiameter, + _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width, + ); } @override @@ -643,7 +669,12 @@ class _RenderSlider extends RenderBox { } void _paintTickMarks( - Canvas canvas, Rect railLeft, Rect railRight, Paint leftPaint, Paint rightPaint) { + 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; @@ -683,23 +714,15 @@ class _RenderSlider extends RenderBox { final double railLength = size.width - 2 * _overlayRadius; final double value = _state.positionController.value; - final ColorTween activeRailEnableColor = new ColorTween( - begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor); - final ColorTween inactiveRailEnableColor = new ColorTween( - begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor); - final ColorTween activeTickMarkEnableColor = new ColorTween( - begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor); - final ColorTween inactiveTickMarkEnableColor = new ColorTween( - begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor); + final ColorTween activeRailEnableColor = new ColorTween(begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor); + final ColorTween inactiveRailEnableColor = new ColorTween(begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor); + final ColorTween activeTickMarkEnableColor = new ColorTween(begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor); + final ColorTween inactiveTickMarkEnableColor = new ColorTween(begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor); - final Paint 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); + final Paint activeRailPaint = new Paint()..color = activeRailEnableColor.evaluate(_enableAnimation); + final Paint inactiveRailPaint = new Paint()..color = inactiveRailEnableColor.evaluate(_enableAnimation); + final Paint activeTickMarkPaint = new Paint()..color = activeTickMarkEnableColor.evaluate(_enableAnimation); + final Paint inactiveTickMarkPaint = new Paint()..color = inactiveTickMarkEnableColor.evaluate(_enableAnimation); double visualPosition; Paint leftRailPaint; @@ -732,12 +755,9 @@ class _RenderSlider extends RenderBox { final double railBottom = railVerticalCenter + railRadius; final double railRight = railLeft + railLength; final double railActive = railLeft + railLength * visualPosition; - final double thumbRadius = - _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0; - final double railActiveLeft = - math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value)); - final double railActiveRight = - math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight); + final double thumbRadius = _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0; + final double railActiveLeft = math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value)); + final double railActiveRight = math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight); final Rect railLeftRect = new Rect.fromLTRB(railLeft, railTop, railActiveLeft, railBottom); final Rect railRightRect = new Rect.fromLTRB(railActiveRight, railTop, railRight, railBottom); @@ -779,6 +799,7 @@ class _RenderSlider extends RenderBox { } if (showValueIndicator) { _sliderTheme.valueIndicatorShape.paint( + this, context, isDiscrete, thumbCenter, @@ -787,13 +808,13 @@ class _RenderSlider extends RenderBox { _labelPainter, _sliderTheme, _textDirection, - _textScaleFactor, value, ); } } _sliderTheme.thumbShape.paint( + this, context, isDiscrete, thumbCenter, @@ -802,7 +823,6 @@ class _RenderSlider extends RenderBox { label != null ? _labelPainter : null, _sliderTheme, _textDirection, - _textScaleFactor, value, ); } diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 3efbacbe159..9497aa8b8ba 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -9,7 +9,6 @@ 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'; @@ -49,20 +48,27 @@ class SliderTheme extends InheritedWidget { /// Defaults to the ambient [ThemeData.sliderTheme] if there is no /// [SliderTheme] in the given build context. /// - /// Typical usage is as follows: + /// ## Sample code /// /// ```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; - /// ), - /// ); + /// class Launch extends StatefulWidget { + /// @override + /// State createState() => new LaunchState(); + /// } + /// + /// class LaunchState extends State { + /// double _rocketThrust; + /// + /// @override + /// Widget build(BuildContext context) { + /// return new SliderTheme( + /// data: SliderTheme.of(context).copyWith(activeRailColor: const Color(0xff804040)), + /// child: new Slider( + /// onChanged: (double value) { setState(() { _rocketThrust = value; }); }, + /// value: _rocketThrust, + /// ), + /// ); + /// } /// } /// ``` /// @@ -147,7 +153,31 @@ class SliderThemeData extends Diagnosticable { /// /// 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]. + /// entirely new one with [SliderThemeData.fromPrimaryColors]. + /// + /// ## Sample code + /// + /// ```dart + /// class Blissful extends StatefulWidget { + /// @override + /// State createState() => new BlissfulState(); + /// } + /// + /// class BlissfulState extends State { + /// double _bliss; + /// + /// @override + /// Widget build(BuildContext context) { + /// return new SliderTheme( + /// data: SliderTheme.of(context).copyWith(activeRailColor: const Color(0xff404080)), + /// child: new Slider( + /// onChanged: (double value) { setState(() { _bliss = value; }); }, + /// value: _bliss, + /// ), + /// ); + /// } + /// } + /// ``` const SliderThemeData({ @required this.activeRailColor, @required this.inactiveRailColor, @@ -189,7 +219,7 @@ class SliderThemeData extends Diagnosticable { /// 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({ + factory SliderThemeData.fromPrimaryColors({ @required Color primaryColor, @required Color primaryColorDark, @required Color primaryColorLight, @@ -238,22 +268,69 @@ class SliderThemeData extends Diagnosticable { ); } + /// The color of the [Slider] rail between the [Slider.min] position and the + /// current thumb position. final Color activeRailColor; + + /// The color of the [Slider] rail between the current thumb position and the + /// [Slider.max] position. final Color inactiveRailColor; + + /// The color of the [Slider] rail between the [Slider.min] position and the + /// current thumb position when the [Slider] is disabled. final Color disabledActiveRailColor; + + /// The color of the [Slider] rail between the current thumb position and the + /// [Slider.max] position when the [Slider] is disabled. final Color disabledInactiveRailColor; + + /// The color of the rail's tick marks that are drawn between the [Slider.min] + /// position and the current thumb position. final Color activeTickMarkColor; + + /// The color of the rail's tick marks that are drawn between the current + /// thumb position and the [Slider.max] position. final Color inactiveTickMarkColor; + + /// The color of the rail's tick marks that are drawn between the [Slider.min] + /// position and the current thumb position when the [Slider] is disabled. final Color disabledActiveTickMarkColor; + + /// The color of the rail's tick marks that are drawn between the current + /// thumb position and the [Slider.max] position when the [Slider] is + /// disabled. final Color disabledInactiveTickMarkColor; + + /// The color given to the [thumbShape] to draw itself with. final Color thumbColor; + + /// The color given to the [thumbShape] to draw itself with when the + /// [Slider] is disabled. final Color disabledThumbColor; + + /// The color of the overlay drawn around the slider thumb when it is pressed. + /// + /// This is typically a semi-transparent color. final Color overlayColor; + + /// The color given to the [valueIndicatorShape] to draw itself with. final Color valueIndicatorColor; + + /// The shape and behavior that will be used to draw the [Slider]'s thumb. + /// + /// This can be customized by implementing a subclass of + /// [SliderComponentShape]. final SliderComponentShape thumbShape; + + /// The shape and behavior that will be used to draw the [Slider]'s value + /// indicator. + /// + /// This can be customized by implementing a subclass of + /// [SliderComponentShape]. final SliderComponentShape valueIndicatorShape; - /// Whether the value indicator should be shown for different types of sliders. + /// 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 @@ -285,8 +362,7 @@ class SliderThemeData extends Diagnosticable { activeTickMarkColor: activeTickMarkColor ?? this.activeTickMarkColor, inactiveTickMarkColor: inactiveTickMarkColor ?? this.inactiveTickMarkColor, disabledActiveTickMarkColor: disabledActiveTickMarkColor ?? this.disabledActiveTickMarkColor, - disabledInactiveTickMarkColor: - disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor, + disabledInactiveTickMarkColor: disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor, thumbColor: thumbColor ?? this.thumbColor, disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor, overlayColor: overlayColor ?? this.overlayColor, @@ -320,14 +396,11 @@ class SliderThemeData extends Diagnosticable { 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), + 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), + 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), @@ -361,6 +434,9 @@ class SliderThemeData extends Diagnosticable { @override bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } if (other.runtimeType != runtimeType) { return false; } @@ -386,47 +462,26 @@ class SliderThemeData extends Diagnosticable { void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); final ThemeData defaultTheme = new ThemeData.fallback(); - final SliderThemeData defaultData = new SliderThemeData.materialDefaults( + final SliderThemeData defaultData = new SliderThemeData.fromPrimaryColors( primaryColor: defaultTheme.primaryColor, primaryColorDark: defaultTheme.primaryColorDark, primaryColorLight: defaultTheme.primaryColorLight, ); - description.add(new DiagnosticsProperty('activeRailColor', activeRailColor, - defaultValue: defaultData.activeRailColor)); - description.add(new DiagnosticsProperty('inactiveRailColor', inactiveRailColor, - defaultValue: defaultData.inactiveRailColor)); - description.add(new DiagnosticsProperty( - 'disabledActiveRailColor', disabledActiveRailColor, - defaultValue: defaultData.disabledActiveRailColor)); - description.add(new DiagnosticsProperty( - 'disabledInactiveRailColor', disabledInactiveRailColor, - defaultValue: defaultData.disabledInactiveRailColor)); - description.add(new DiagnosticsProperty('activeTickMarkColor', activeTickMarkColor, - defaultValue: defaultData.activeTickMarkColor)); - description.add(new DiagnosticsProperty('inactiveTickMarkColor', inactiveTickMarkColor, - defaultValue: defaultData.inactiveTickMarkColor)); - description.add(new DiagnosticsProperty( - 'disabledActiveTickMarkColor', disabledActiveTickMarkColor, - defaultValue: defaultData.disabledActiveTickMarkColor)); - description.add(new DiagnosticsProperty( - 'disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, - defaultValue: defaultData.disabledInactiveTickMarkColor)); - description.add(new DiagnosticsProperty('thumbColor', thumbColor, - defaultValue: defaultData.thumbColor)); - description.add(new DiagnosticsProperty('disabledThumbColor', disabledThumbColor, - defaultValue: defaultData.disabledThumbColor)); - description.add(new DiagnosticsProperty('overlayColor', overlayColor, - defaultValue: defaultData.overlayColor)); - description.add(new DiagnosticsProperty('valueIndicatorColor', valueIndicatorColor, - defaultValue: defaultData.valueIndicatorColor)); - description.add(new DiagnosticsProperty('thumbShape', thumbShape, - defaultValue: defaultData.thumbShape)); - description.add(new DiagnosticsProperty( - 'valueIndicatorShape', valueIndicatorShape, - defaultValue: defaultData.valueIndicatorShape)); - description.add(new DiagnosticsProperty( - 'showValueIndicator', showValueIndicator, - defaultValue: defaultData.showValueIndicator)); + description.add(new DiagnosticsProperty('activeRailColor', activeRailColor, defaultValue: defaultData.activeRailColor)); + description.add(new DiagnosticsProperty('inactiveRailColor', inactiveRailColor, defaultValue: defaultData.inactiveRailColor)); + description.add(new DiagnosticsProperty('disabledActiveRailColor', disabledActiveRailColor, defaultValue: defaultData.disabledActiveRailColor)); + description.add(new DiagnosticsProperty('disabledInactiveRailColor', disabledInactiveRailColor, defaultValue: defaultData.disabledInactiveRailColor)); + description.add(new DiagnosticsProperty('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor)); + description.add(new DiagnosticsProperty('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor)); + description.add(new DiagnosticsProperty('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor)); + description.add(new DiagnosticsProperty('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor)); + description.add(new DiagnosticsProperty('thumbColor', thumbColor, defaultValue: defaultData.thumbColor)); + description.add(new DiagnosticsProperty('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor)); + description.add(new DiagnosticsProperty('overlayColor', overlayColor, defaultValue: defaultData.overlayColor)); + description.add(new DiagnosticsProperty('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor)); + description.add(new DiagnosticsProperty('thumbShape', thumbShape, defaultValue: defaultData.thumbShape)); + description.add(new DiagnosticsProperty('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape)); + description.add(new DiagnosticsProperty('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator)); } } @@ -460,6 +515,7 @@ abstract class SliderComponentShape { /// 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( + RenderBox parentBox, PaintingContext context, bool isDiscrete, Offset thumbCenter, @@ -468,7 +524,6 @@ abstract class SliderComponentShape { TextPainter labelPainter, SliderThemeData sliderTheme, TextDirection textDirection, - double textScaleFactor, double value, ); } @@ -493,6 +548,7 @@ class RoundSliderThumbShape extends SliderComponentShape { @override void paint( + RenderBox parentBox, PaintingContext context, bool isDiscrete, Offset thumbCenter, @@ -501,14 +557,17 @@ class RoundSliderThumbShape extends SliderComponentShape { TextPainter labelPainter, SliderThemeData sliderTheme, TextDirection textDirection, - double textScaleFactor, double value, ) { final Canvas canvas = context.canvas; - final Tween radiusTween = - new Tween(begin: _disabledThumbRadius, end: _thumbRadius); - final ColorTween colorTween = - new ColorTween(begin: sliderTheme.disabledThumbColor, end: sliderTheme.thumbColor); + final Tween radiusTween = new Tween( + begin: _disabledThumbRadius, + end: _thumbRadius, + ); + final ColorTween colorTween = new ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); canvas.drawCircle( thumbCenter, radiusTween.evaluate(enableAnimation), @@ -536,6 +595,9 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { // Radius of the top lobe of the value indicator. static const double _topLobeRadius = 16.0; + // Baseline size of the label text. This is the size that the value indicator + // was designed to contain. We scale if from here to fit other sizes. + static const double _labelTextDesignSize = 14.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 @@ -557,15 +619,14 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { 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 = + static const Size _preferredSize = const Size.fromHeight(_distanceBetweenTopBottomCenters + _topLobeRadius + _bottomLobeRadius); - static final Tween _slideUpTween = new Tween(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; + 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. @@ -638,20 +699,71 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { return _bottomLobeEnd; } - void _drawValueIndicator(Canvas canvas, Offset center, Paint paint, double scale, - TextPainter labelPainter, double textScaleFactor) { + // Determines the "best" offset to keep the bubble on the screen. The calling + // code will bound that with the available movement in the paddle shape. + double _getIdealOffset( + RenderBox parentBox, + double halfWidthNeeded, + double scale, + Offset center, + ) { + const double edgeMargin = 4.0; + final Rect topLobeRect = new Rect.fromLTWH( + -_topLobeRadius - halfWidthNeeded, + -_topLobeRadius - _distanceBetweenTopBottomCenters, + 2.0 * (_topLobeRadius + halfWidthNeeded), + 2.0 * _topLobeRadius, + ); + // We can just multiply by scale instead of a transform, since we're scaling + // around (0, 0). + final Offset topLeft = (topLobeRect.topLeft * scale) + center; + final Offset bottomRight = (topLobeRect.bottomRight * scale) + center; + double shift = 0.0; + if (topLeft.dx < edgeMargin) { + shift = edgeMargin - topLeft.dx; + } + if (bottomRight.dx > parentBox.size.width - edgeMargin) { + shift = parentBox.size.width - bottomRight.dx - edgeMargin; + } + shift = (scale == 0.0 ? 0.0 : shift / scale); + return shift; + } + + void _drawValueIndicator( + RenderBox parentBox, + Canvas canvas, + Offset center, + Paint paint, + double scale, + TextPainter labelPainter, + ) { canvas.save(); canvas.translate(center.dx, center.dy); - // The entire value indicator should scale with the text scale factor, + // The entire value indicator should scale with the size of the label, // to keep it large enough to encompass the label text. - canvas.scale(scale * textScaleFactor, scale * textScaleFactor); - final double inverseTextScale = 1.0 / textScaleFactor; + final double textScaleFactor = labelPainter.height / _labelTextDesignSize; + final double overallScale = scale * textScaleFactor; + canvas.scale(overallScale, overallScale); + final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0; 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 double halfWidthNeeded = math.max( + 0.0, + inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding), + ); + + double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center); + double leftWidthNeeded; + double rightWidthNeeded; + if (shift < 0.0) { // shifting to the left + shift = math.max(shift, -halfWidthNeeded); + } else { // shifting to the right + shift = math.min(shift, halfWidthNeeded); + } + rightWidthNeeded = halfWidthNeeded + shift; + leftWidthNeeded = halfWidthNeeded - shift; final Path path = new Path(); final Offset bottomLobeEnd = _addBottomLobe(path); @@ -660,39 +772,57 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { 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)); + final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / neckTriangleBase)); + final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / 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; + final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees; + final double rightTheta = (1.0 - rightAmount) * _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; + -neckTriangleBase, + _topLobeCenter.dy + math.cos(leftTheta) * _neckTriangleHypotenuse, + ); + final Offset neckRightCenter = new Offset( + neckTriangleBase, + _topLobeCenter.dy + math.cos(rightTheta) * _neckTriangleHypotenuse, + ); + final double leftNeckArcAngle = _ninetyDegrees - leftTheta; + final double rightNeckArcAngle = math.pi + _ninetyDegrees - rightTheta; + _addArc( path, neckLeftCenter, _topNeckRadius, 0.0, - -neckArcAngle, + -leftNeckArcAngle, + ); + _addArc( + path, + _topLobeCenter - new Offset(leftWidthNeeded, 0.0), + _topLobeRadius, + _ninetyDegrees + leftTheta, + _twoSeventyDegrees, + ); + _addArc( + path, + _topLobeCenter + new Offset(rightWidthNeeded, 0.0), + _topLobeRadius, + _twoSeventyDegrees, + _twoSeventyDegrees + math.pi - rightTheta, ); - _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, + rightNeckArcAngle, math.pi, ); canvas.drawPath(path, paint); // Draw the label. canvas.save(); - canvas.translate(0.0, -_distanceBetweenTopBottomCenters); + canvas.translate(shift, -_distanceBetweenTopBottomCenters); canvas.scale(inverseTextScale, inverseTextScale); labelPainter.paint(canvas, Offset.zero - new Offset(labelHalfWidth, labelPainter.height / 2.0)); canvas.restore(); @@ -701,6 +831,7 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { @override void paint( + RenderBox parentBox, PaintingContext context, bool isDiscrete, Offset thumbCenter, @@ -709,21 +840,20 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { 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)); + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.valueIndicatorColor, + ); _drawValueIndicator( + parentBox, context.canvas, thumbCenter, new Paint()..color = enableColor.evaluate(enableAnimation), - _slideUpTween.evaluate(activationAnimation), + activationAnimation.value, labelPainter, - textScaleFactor, ); } } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 32134be1110..8d26191674d 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -149,15 +149,9 @@ class ThemeData extends Diagnosticable { hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000); errorColor ??= Colors.red[700]; inputDecorationTheme ??= const InputDecorationTheme(); - iconTheme ??= isDark - ? const IconThemeData(color: Colors.white) - : const IconThemeData(color: Colors.black); - primaryIconTheme ??= primaryIsDark - ? const IconThemeData(color: Colors.white) - : const IconThemeData(color: Colors.black); - accentIconTheme ??= accentIsDark - ? const IconThemeData(color: Colors.white) - : const IconThemeData(color: Colors.black); + iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); + primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); + accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); platform ??= defaultTargetPlatform; final Typography typography = new Typography(platform: platform); textTheme ??= isDark ? typography.white : typography.black; @@ -168,7 +162,7 @@ class ThemeData extends Diagnosticable { primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily); accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily); } - sliderTheme ??= new SliderThemeData.materialDefaults( + sliderTheme ??= new SliderThemeData.fromPrimaryColors( primaryColor: primaryColor, primaryColorLight: primaryColorLight, primaryColorDark: primaryColorDark, @@ -721,43 +715,43 @@ class ThemeData extends Diagnosticable { @override int get hashCode { return hashValues( - brightness, - primaryColor, - primaryColorBrightness, - canvasColor, - scaffoldBackgroundColor, - bottomAppBarColor, - cardColor, - dividerColor, - highlightColor, - splashColor, - splashFactory, - selectedRowColor, - unselectedWidgetColor, - disabledColor, - buttonColor, - buttonTheme, - secondaryHeaderColor, - textSelectionColor, - textSelectionHandleColor, - hashValues( // Too many values. - backgroundColor, - accentColor, - accentColorBrightness, - indicatorColor, - dialogBackgroundColor, - hintColor, - errorColor, - textTheme, - primaryTextTheme, - accentTextTheme, - iconTheme, - inputDecorationTheme, - primaryIconTheme, - accentIconTheme, - sliderTheme, - platform, - ), + brightness, + primaryColor, + primaryColorBrightness, + canvasColor, + scaffoldBackgroundColor, + bottomAppBarColor, + cardColor, + dividerColor, + highlightColor, + splashColor, + splashFactory, + selectedRowColor, + unselectedWidgetColor, + disabledColor, + buttonColor, + buttonTheme, + secondaryHeaderColor, + textSelectionColor, + textSelectionHandleColor, + hashValues( // Too many values. + backgroundColor, + accentColor, + accentColorBrightness, + indicatorColor, + dialogBackgroundColor, + hintColor, + errorColor, + textTheme, + primaryTextTheme, + accentTextTheme, + iconTheme, + inputDecorationTheme, + primaryIconTheme, + accentIconTheme, + sliderTheme, + platform, + ), ); } @@ -765,64 +759,36 @@ class ThemeData extends Diagnosticable { void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); final ThemeData defaultData = new ThemeData.fallback(); - description.add(new EnumProperty('platform', platform, - defaultValue: defaultTargetPlatform)); - description.add(new EnumProperty('brightness', brightness, - defaultValue: defaultData.brightness)); - description.add(new DiagnosticsProperty('primaryColor', primaryColor, - defaultValue: defaultData.primaryColor)); - description.add(new EnumProperty('primaryColorBrightness', primaryColorBrightness, - defaultValue: defaultData.primaryColorBrightness)); - description.add(new DiagnosticsProperty('accentColor', accentColor, - defaultValue: defaultData.accentColor)); - description.add(new EnumProperty('accentColorBrightness', accentColorBrightness, - defaultValue: defaultData.accentColorBrightness)); - description.add(new DiagnosticsProperty('canvasColor', canvasColor, - defaultValue: defaultData.canvasColor)); - description.add(new DiagnosticsProperty( - 'scaffoldBackgroundColor', scaffoldBackgroundColor, - defaultValue: defaultData.scaffoldBackgroundColor)); - description.add(new DiagnosticsProperty('bottomAppBarColor', bottomAppBarColor, - defaultValue: defaultData.bottomAppBarColor)); - description.add(new DiagnosticsProperty('cardColor', cardColor, - defaultValue: defaultData.cardColor)); - description.add(new DiagnosticsProperty('dividerColor', dividerColor, - defaultValue: defaultData.dividerColor)); - description.add(new DiagnosticsProperty('highlightColor', highlightColor, - defaultValue: defaultData.highlightColor)); - description.add(new DiagnosticsProperty('splashColor', splashColor, - defaultValue: defaultData.splashColor)); - description.add(new DiagnosticsProperty('selectedRowColor', selectedRowColor, - defaultValue: defaultData.selectedRowColor)); - description.add(new DiagnosticsProperty('unselectedWidgetColor', unselectedWidgetColor, - defaultValue: defaultData.unselectedWidgetColor)); - description.add(new DiagnosticsProperty('disabledColor', disabledColor, - defaultValue: defaultData.disabledColor)); - description.add(new DiagnosticsProperty('buttonColor', buttonColor, - defaultValue: defaultData.buttonColor)); - description.add(new DiagnosticsProperty('secondaryHeaderColor', secondaryHeaderColor, - defaultValue: defaultData.secondaryHeaderColor)); - description.add(new DiagnosticsProperty('textSelectionColor', textSelectionColor, - defaultValue: defaultData.textSelectionColor)); - description.add(new DiagnosticsProperty( - 'textSelectionHandleColor', textSelectionHandleColor, - defaultValue: defaultData.textSelectionHandleColor)); - description.add(new DiagnosticsProperty('backgroundColor', backgroundColor, - defaultValue: defaultData.backgroundColor)); - description.add(new DiagnosticsProperty('dialogBackgroundColor', dialogBackgroundColor, - defaultValue: defaultData.dialogBackgroundColor)); - description.add(new DiagnosticsProperty('indicatorColor', indicatorColor, - defaultValue: defaultData.indicatorColor)); - description.add(new DiagnosticsProperty('hintColor', hintColor, - defaultValue: defaultData.hintColor)); - description.add(new DiagnosticsProperty('errorColor', errorColor, - defaultValue: defaultData.errorColor)); + description.add(new EnumProperty('platform', platform, defaultValue: defaultTargetPlatform)); + description.add(new EnumProperty('brightness', brightness, defaultValue: defaultData.brightness)); + description.add(new DiagnosticsProperty('primaryColor', primaryColor, defaultValue: defaultData.primaryColor)); + description.add(new EnumProperty('primaryColorBrightness', primaryColorBrightness, defaultValue: defaultData.primaryColorBrightness)); + description.add(new DiagnosticsProperty('accentColor', accentColor, defaultValue: defaultData.accentColor)); + description.add(new EnumProperty('accentColorBrightness', accentColorBrightness, defaultValue: defaultData.accentColorBrightness)); + description.add(new DiagnosticsProperty('canvasColor', canvasColor, defaultValue: defaultData.canvasColor)); + description.add(new DiagnosticsProperty('scaffoldBackgroundColor', scaffoldBackgroundColor, defaultValue: defaultData.scaffoldBackgroundColor)); + description.add(new DiagnosticsProperty('bottomAppBarColor', bottomAppBarColor, defaultValue: defaultData.bottomAppBarColor)); + description.add(new DiagnosticsProperty('cardColor', cardColor, defaultValue: defaultData.cardColor)); + description.add(new DiagnosticsProperty('dividerColor', dividerColor, defaultValue: defaultData.dividerColor)); + description.add(new DiagnosticsProperty('highlightColor', highlightColor, defaultValue: defaultData.highlightColor)); + description.add(new DiagnosticsProperty('splashColor', splashColor, defaultValue: defaultData.splashColor)); + description.add(new DiagnosticsProperty('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor)); + description.add(new DiagnosticsProperty('unselectedWidgetColor', unselectedWidgetColor, defaultValue: defaultData.unselectedWidgetColor)); + description.add(new DiagnosticsProperty('disabledColor', disabledColor, defaultValue: defaultData.disabledColor)); + description.add(new DiagnosticsProperty('buttonColor', buttonColor, defaultValue: defaultData.buttonColor)); + description.add(new DiagnosticsProperty('secondaryHeaderColor', secondaryHeaderColor, defaultValue: defaultData.secondaryHeaderColor)); + description.add(new DiagnosticsProperty('textSelectionColor', textSelectionColor, defaultValue: defaultData.textSelectionColor)); + description.add(new DiagnosticsProperty('textSelectionHandleColor', textSelectionHandleColor, defaultValue: defaultData.textSelectionHandleColor)); + description.add(new DiagnosticsProperty('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor)); + description.add(new DiagnosticsProperty('dialogBackgroundColor', dialogBackgroundColor, defaultValue: defaultData.dialogBackgroundColor)); + description.add(new DiagnosticsProperty('indicatorColor', indicatorColor, defaultValue: defaultData.indicatorColor)); + description.add(new DiagnosticsProperty('hintColor', hintColor, defaultValue: defaultData.hintColor)); + description.add(new DiagnosticsProperty('errorColor', errorColor, defaultValue: defaultData.errorColor)); description.add(new DiagnosticsProperty('buttonTheme', buttonTheme)); description.add(new DiagnosticsProperty('textTheme', textTheme)); description.add(new DiagnosticsProperty('primaryTextTheme', primaryTextTheme)); description.add(new DiagnosticsProperty('accentTextTheme', accentTextTheme)); - description.add(new DiagnosticsProperty( - 'inputDecorationTheme', inputDecorationTheme)); + description.add(new DiagnosticsProperty('inputDecorationTheme', inputDecorationTheme)); description.add(new DiagnosticsProperty('iconTheme', iconTheme)); description.add(new DiagnosticsProperty('primaryIconTheme', primaryIconTheme)); description.add(new DiagnosticsProperty('accentIconTheme', accentIconTheme)); @@ -846,8 +812,7 @@ class _IdentityThemeDataCacheKey { // We are explicitly ignoring the possibility that the types might not // match in the interests of speed. final _IdentityThemeDataCacheKey otherKey = other; - return identical(baseTheme, otherKey.baseTheme) && - identical(localTextGeometry, otherKey.localTextGeometry); + return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry); } } diff --git a/packages/flutter/lib/src/widgets/icon_theme_data.dart b/packages/flutter/lib/src/widgets/icon_theme_data.dart index e13aea9f023..ac3b817e7ba 100644 --- a/packages/flutter/lib/src/widgets/icon_theme_data.dart +++ b/packages/flutter/lib/src/widgets/icon_theme_data.dart @@ -34,7 +34,10 @@ class IconThemeData extends Diagnosticable { /// the new values. IconThemeData copyWith({Color color, double opacity, double size}) { return new IconThemeData( - color: color ?? this.color, opacity: opacity ?? this.opacity, size: size ?? this.size); + color: color ?? this.color, + opacity: opacity ?? this.opacity, + size: size ?? this.size, + ); } /// Returns a new icon theme that matches this icon theme but with some values @@ -43,7 +46,11 @@ class IconThemeData extends Diagnosticable { IconThemeData merge(IconThemeData other) { if (other == null) return this; - return copyWith(color: other.color, opacity: other.opacity, size: other.size); + return copyWith( + color: other.color, + opacity: other.opacity, + size: other.size, + ); } /// Whether all the properties of this object are non-null. @@ -97,11 +104,8 @@ class IconThemeData extends Diagnosticable { @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); - if (color == null && _opacity == null && size == null) { - return; - } - description.add(new DiagnosticsProperty('color', color)); - description.add(new DoubleProperty('opacity', _opacity)); - description.add(new DoubleProperty('size', size)); + description.add(new DiagnosticsProperty('color', color, defaultValue: null)); + description.add(new DoubleProperty('opacity', opacity, defaultValue: null)); + description.add(new DoubleProperty('size', size, defaultValue: null)); } } diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index d9438e89c51..7505b86b245 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -12,6 +12,36 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; +// A thumb shape that also logs its repaint center. +class LoggingThumbShape extends SliderComponentShape { + LoggingThumbShape(this.log); + + final List log; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return const Size(10.0, 10.0); + } + + @override + void paint( + RenderBox parentBox, + PaintingContext context, + bool isDiscrete, + Offset thumbCenter, + Animation activationAnimation, + Animation enableAnimation, + TextPainter labelPainter, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + ) { + log.add(thumbCenter); + final Paint thumbPaint = new Paint()..color = Colors.red; + context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint); + } +} + void main() { testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { final Key sliderKey = new UniqueKey(); @@ -22,16 +52,19 @@ void main() { textDirection: TextDirection.ltr, child: new StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return new Material( - child: new Center( - child: new Slider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ), ), ), ); @@ -65,16 +98,19 @@ void main() { textDirection: TextDirection.rtl, child: new StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return new Material( - child: new Center( - child: new Slider( - key: sliderKey, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ), ), ), ); @@ -99,6 +135,117 @@ void main() { expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); + testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async { + final Key sliderKey = new UniqueKey(); + double value = 0.0; + int updates = 0; + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + key: sliderKey, + value: value, + onChanged: (double newValue) { + setState(() { + updates++; + value = newValue; + }); + }, + ), + ), + ), + ); + }, + ), + ), + ); + + expect(value, equals(0.0)); + await tester.tap(find.byKey(sliderKey)); + expect(value, equals(0.5)); + await tester.pump(); + await tester.tap(find.byKey(sliderKey)); + expect(value, equals(0.5)); + await tester.pump(); + expect(updates, equals(1)); + }); + + testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async { + final Key sliderKey = new UniqueKey(); + double value = 0.0; + final List log = []; + final LoggingThumbShape loggingThumb = new LoggingThumbShape(log); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb); + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new SliderTheme( + data: sliderTheme, + child: new Slider( + key: sliderKey, + value: value, + divisions: 4, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ), + ), + ), + ), + ); + }, + ), + ), + ); + + final List expectedLog = [ + const Offset(16.0, 300.0), + const Offset(16.0, 300.0), + const Offset(400.0, 300.0), + ]; + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + expect(value, equals(0.5)); + expect(log.length, 3); + expect(log, orderedEquals(expectedLog)); + await gesture.moveBy(const Offset(-500.0, 0.0)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + expect(value, equals(0.0)); + expect(log.length, 5); + expect(log.last.dx, closeTo(343.3, 0.1)); + // With no more gesture or value changes, the thumb position should still + // be redrawn in the animated position. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + expect(value, equals(0.0)); + expect(log.length, 7); + expect(log.last.dx, closeTo(185.5, 0.1)); + // Final position. + await tester.pump(const Duration(milliseconds: 80)); + expectedLog.add(const Offset(16.0, 300.0)); + expect(value, equals(0.0)); + expect(log.length, 8); + expect(log.last.dx, closeTo(16.0, 0.1)); + await gesture.up(); + }); + testWidgets('Slider take on discrete values', (WidgetTester tester) async { final Key sliderKey = new UniqueKey(); double value = 0.0; @@ -108,21 +255,24 @@ void main() { textDirection: TextDirection.ltr, child: new StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return new Material( - child: new Center( - child: new SizedBox( - width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth - child: new Slider( - key: sliderKey, - min: 0.0, - max: 100.0, - divisions: 10, - value: value, - onChanged: (double newValue) { - setState(() { - value = newValue; - }); - }, + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new SizedBox( + width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth + child: new Slider( + key: sliderKey, + min: 0.0, + max: 100.0, + divisions: 10, + value: value, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ), ), ), ), @@ -154,14 +304,17 @@ void main() { final List log = []; await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Slider( - value: 0.0, - min: 0.0, - max: 1.0, - onChanged: (double newValue) { - log.add(newValue); - }, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Slider( + value: 0.0, + min: 0.0, + max: 1.0, + onChanged: (double newValue) { + log.add(newValue); + }, + ), ), ), )); @@ -172,14 +325,17 @@ void main() { await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Slider( - value: 0.0, - min: 0.0, - max: 0.0, - onChanged: (double newValue) { - log.add(newValue); - }, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Slider( + value: 0.0, + min: 0.0, + max: 0.0, + onChanged: (double newValue) { + log.add(newValue); + }, + ), ), ), )); @@ -189,8 +345,7 @@ void main() { log.clear(); }); - testWidgets('Slider uses the right theme colors for the right components', - (WidgetTester tester) async { + testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async { const Color customColor1 = const Color(0xcafefeed); const Color customColor2 = const Color(0xdeadbeef); final ThemeData theme = new ThemeData( @@ -212,17 +367,20 @@ void main() { }; return new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Center( - child: new Theme( - data: theme, - child: new Slider( - value: value, - label: '$value', - divisions: divisions, - activeColor: activeColor, - inactiveColor: inactiveColor, - onChanged: onChanged, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Theme( + data: theme, + child: new Slider( + value: value, + label: '$value', + divisions: divisions, + activeColor: activeColor, + inactiveColor: inactiveColor, + onChanged: onChanged, + ), ), ), ), @@ -235,11 +393,7 @@ void main() { final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); // Check default theme for enabled widget. - expect( - sliderBox, - paints - ..rect(color: sliderTheme.activeRailColor) - ..rect(color: sliderTheme.inactiveRailColor)); + expect(sliderBox, paints..rect(color: sliderTheme.activeRailColor)..rect(color: sliderTheme.inactiveRailColor)); expect(sliderBox, paints..circle(color: sliderTheme.thumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor))); @@ -249,8 +403,7 @@ void main() { // Test setting only the activeColor. await tester.pumpWidget(buildApp(activeColor: customColor1)); - expect( - sliderBox, paints..rect(color: customColor1)..rect(color: sliderTheme.inactiveRailColor)); + 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))); @@ -276,11 +429,7 @@ void main() { // 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..rect(color: sliderTheme.activeRailColor)..rect(color: sliderTheme.inactiveRailColor)); expect( sliderBox, paints @@ -294,8 +443,7 @@ void main() { 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)); + await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, divisions: 3)); expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2)); expect( sliderBox, @@ -326,8 +474,7 @@ void main() { 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)); + await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, enabled: false)); expect( sliderBox, paints @@ -343,8 +490,8 @@ void main() { 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. + // Wait for value indicator animation to finish. + await tester.pump(const Duration(milliseconds: 500)); expect(value, equals(2.0 / 3.0)); expect( sliderBox, @@ -361,8 +508,8 @@ void main() { ); await gesture.up(); await tester.pump(); - await tester - .pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish. + // Wait for value indicator animation to finish. + await tester.pump(const Duration(milliseconds: 500)); // Testing the custom colors are used for the indicator. await tester.pumpWidget(buildApp( @@ -373,8 +520,8 @@ void main() { 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. + // Wait for value indicator animation to finish. + await tester.pump(const Duration(milliseconds: 500)); expect(value, equals(2.0 / 3.0)); expect( sliderBox, @@ -395,19 +542,22 @@ void main() { double value = 0.0; await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new ListView( - children: [ - new Slider( - value: value, - onChanged: (double newValue) { - value = newValue; - }, - ), - new Container( - height: 2000.0, - ), - ], + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new ListView( + children: [ + new Slider( + value: value, + onChanged: (double newValue) { + value = newValue; + }, + ), + new Container( + height: 2000.0, + ), + ], + ), ), ), )); @@ -420,13 +570,16 @@ void main() { double value = 0.0; await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Center( - child: new Slider( - value: value, - onChanged: (double newValue) { - value = newValue; - }, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + value: value, + onChanged: (double newValue) { + value = newValue; + }, + ), ), ), ), @@ -448,13 +601,16 @@ void main() { double value = 0.0; await tester.pumpWidget(new Directionality( textDirection: TextDirection.rtl, - child: new Material( - child: new Center( - child: new Slider( - value: value, - onChanged: (double newValue) { - value = newValue; - }, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + value: value, + onChanged: (double newValue) { + value = newValue; + }, + ), ), ), ), @@ -473,62 +629,70 @@ void main() { }); testWidgets('Slider sizing', (WidgetTester tester) async { - await tester.pumpWidget(const Directionality( + await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: const Material( - child: const Center( - child: const Slider( - value: 0.5, - onChanged: null, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: const Material( + child: const Center( + child: const Slider( + value: 0.5, + onChanged: null, + ), ), ), ), )); expect(tester.renderObject(find.byType(Slider)).size, const Size(800.0, 600.0)); - await tester.pumpWidget(const Directionality( + await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: const Material( - child: const Center( - child: const IntrinsicWidth( - child: const Slider( - value: 0.5, - onChanged: null, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: const Material( + child: const Center( + child: const IntrinsicWidth( + child: const Slider( + value: 0.5, + onChanged: null, + ), ), ), ), ), )); - expect(tester.renderObject(find.byType(Slider)).size, - const Size(144.0 + 2.0 * 16.0, 600.0)); + expect(tester.renderObject(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0)); - await tester.pumpWidget(const Directionality( + await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: const Material( - child: const Center( - child: const OverflowBox( - maxWidth: double.INFINITY, - maxHeight: double.INFINITY, - child: const Slider( - value: 0.5, - onChanged: null, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: const Material( + child: const Center( + child: const OverflowBox( + maxWidth: double.INFINITY, + maxHeight: double.INFINITY, + child: const Slider( + value: 0.5, + onChanged: null, + ), ), ), ), ), )); - expect(tester.renderObject(find.byType(Slider)).size, - const Size(144.0 + 2.0 * 16.0, 32.0)); + expect(tester.renderObject(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 32.0)); }); testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async { final Key sliderKey = new UniqueKey(); double value = 0.0; - Widget buildSlider( - {double textScaleFactor, - bool isDiscrete: true, - ShowValueIndicator show: ShowValueIndicator.onlyForDiscrete}) { + Widget buildSlider({ + double textScaleFactor, + bool isDiscrete: true, + ShowValueIndicator show: ShowValueIndicator.onlyForDiscrete, + }) { return new Directionality( textDirection: TextDirection.ltr, child: new StatefulBuilder( @@ -538,8 +702,8 @@ void main() { child: new Material( child: new Theme( data: Theme.of(context).copyWith( - sliderTheme: - Theme.of(context).sliderTheme.copyWith(showValueIndicator: show)), + sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show), + ), child: new Center( child: new OverflowBox( maxWidth: double.INFINITY, @@ -621,15 +785,161 @@ void main() { await tester.pump(const Duration(seconds: 1)); }); + testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async { + final Key sliderKey = new GlobalKey(debugLabel: 'A'); + double value = 0.0; + + Widget buildSlider(int parents) { + Widget createParents(int parents, StateSetter setState) { + Widget slider = new Slider( + key: sliderKey, + value: value, + divisions: 4, + onChanged: (double newValue) { + setState(() { + value = newValue; + }); + }, + ); + + for (int i = 0; i < parents; ++i) { + slider = new Column(children: [slider]); + } + return slider; + } + + return new Directionality( + textDirection: TextDirection.ltr, + child: new StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: createParents(parents, setState), + ), + ); + }, + ), + ); + } + + Future testReparenting(bool reparent) async { + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + final Offset center = tester.getCenter(find.byType(Slider)); + // Move to 0.0. + TestGesture gesture = await tester.startGesture(Offset.zero); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(seconds: 1)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); + expect( + sliderBox, + paints + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 16.0, y: 16.0, radius: 6.0), + ); + + gesture = await tester.startGesture(center); + await tester.pump(); + // Wait for animations to start. + await tester.pump(const Duration(milliseconds: 25)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); + expect( + sliderBox, + paints + ..circle(x: 310.9375, y: 16.0, radius: 3.791776657104492) + ..circle(x: 17.0, y: 16.0, radius: 1.0) + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 310.9375, y: 16.0, radius: 6.0), + ); + + // Reparenting in the middle of an animation should do nothing. + if (reparent) { + await tester.pumpWidget(buildSlider(2)); + } + + // Move a little further in the animations. + await tester.pump(const Duration(milliseconds: 10)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); + expect( + sliderBox, + paints + ..circle(x: 396.6802978515625, y: 16.0, radius: 8.0) + ..circle(x: 17.0, y: 16.0, radius: 1.0) + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 396.6802978515625, y: 16.0, radius: 6.0), + ); + // Wait for animations to finish. + await tester.pump(const Duration(milliseconds: 300)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); + expect( + sliderBox, + paints + ..circle(x: 400.0, y: 16.0, radius: 16.0) + ..circle(x: 17.0, y: 16.0, radius: 1.0) + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 6.0), + ); + await gesture.up(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); + expect( + sliderBox, + paints + ..circle(x: 17.0, y: 16.0, radius: 1.0) + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 6.0), + ); + // Move to 0.0 again. + gesture = await tester.startGesture(Offset.zero); + await tester.pump(); + await gesture.up(); + await tester.pump(const Duration(seconds: 1)); + expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); + expect( + sliderBox, + paints + ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 1.0) + ..circle(x: 591.5, y: 16.0, radius: 1.0) + ..circle(x: 783.0, y: 16.0, radius: 1.0) + ..circle(x: 16.0, y: 16.0, radius: 6.0), + ); + } + + await tester.pumpWidget(buildSlider(1)); + // Do it once without reparenting in the middle of an animation + await testReparenting(false); + // Now do it again with reparenting in the middle of an animation. + await testReparenting(true); + + }); + testWidgets('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Slider( - value: 0.5, - onChanged: (double v) {}, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Slider( + value: 0.5, + onChanged: (double v) {}, + ), ), ), )); @@ -648,12 +958,15 @@ void main() { )); // Disable slider - await tester.pumpWidget(const Directionality( + await tester.pumpWidget(new Directionality( textDirection: TextDirection.ltr, - child: const Material( - child: const Slider( - value: 0.5, - onChanged: null, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: const Material( + child: const Slider( + value: 0.5, + onChanged: null, + ), ), ), )); @@ -680,17 +993,20 @@ void main() { final ValueChanged 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, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + 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, + ), ), ), ), @@ -699,15 +1015,19 @@ void main() { ); } - Future expectValueIndicator( - {bool isVisible, SliderThemeData theme, int divisions, bool enabled: true}) async { - // discrete enabled widget. + Future 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. + // Wait for value indicator animation to finish. + await tester.pump(const Duration(milliseconds: 500)); final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); expect( diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 0c7c1b78643..7e220624de3 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show window; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -31,53 +33,12 @@ void main() { 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(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: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Theme( + data: theme, child: const Slider( value: 0.5, label: '0.5', @@ -94,20 +55,57 @@ void main() { final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); - expect( - sliderBox, - paints - ..rect(color: customTheme.disabledActiveRailColor) - ..rect(color: customTheme.disabledInactiveRailColor)); + expect(sliderBox, paints..rect(color: sliderTheme.disabledActiveRailColor)..rect(color: sliderTheme.disabledInactiveRailColor)); }); - testWidgets('SliderThemeData generates correct opacities for materialDefaults', - (WidgetTester tester) async { + 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 MediaQuery( + data: new MediaQueryData.fromWindow(window), + 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(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( + final SliderThemeData sliderTheme = new SliderThemeData.fromPrimaryColors( primaryColor: customColor1, primaryColorDark: customColor2, primaryColorLight: customColor3, @@ -126,18 +124,17 @@ void main() { expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x29))); expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff))); expect(sliderTheme.thumbShape, equals(const isInstanceOf())); - expect(sliderTheme.valueIndicatorShape, - equals(const isInstanceOf())); + expect(sliderTheme.valueIndicatorShape, equals(const isInstanceOf())); expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete)); }); testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async { - final SliderThemeData sliderThemeBlack = new SliderThemeData.materialDefaults( + final SliderThemeData sliderThemeBlack = new SliderThemeData.fromPrimaryColors( primaryColor: Colors.black, primaryColorDark: Colors.black, primaryColorLight: Colors.black, ); - final SliderThemeData sliderThemeWhite = new SliderThemeData.materialDefaults( + final SliderThemeData sliderThemeWhite = new SliderThemeData.fromPrimaryColors( primaryColor: Colors.white, primaryColorDark: Colors.white, primaryColorLight: Colors.white, @@ -172,15 +169,18 @@ void main() { final ValueChanged 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, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new SliderTheme( + data: sliderTheme, + child: new Slider( + value: value, + label: '$value', + divisions: divisions, + onChanged: onChanged, + ), ), ), ), @@ -225,21 +225,27 @@ void main() { platform: TargetPlatform.android, primarySwatch: Colors.blue, ); - final SliderThemeData sliderTheme = theme.sliderTheme - .copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always); - Widget buildApp(String value) { + final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always); + Widget buildApp(String value, {double sliderValue = 0.5}) { 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) {}, - ), + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Row( + children: [ + new Expanded( + child: new SliderTheme( + data: sliderTheme, + child: new Slider( + value: sliderValue, + label: '$value', + divisions: 3, + onChanged: (double d) {}, + ), + ), + ), + ], ), ), ), @@ -290,5 +296,47 @@ void main() { excludes: [const Offset(36.1, -40.0), const Offset(-36.1, -40.0)], )); await gesture.up(); + + // Test that it avoids the left edge of the screen. + await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0)); + 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: [ + const Offset(0.0, -40.0), + const Offset(98.0, -40.0), + const Offset(-16.0, -40.0), + ], + excludes: [const Offset(98.1, -40.0), const Offset(-16.1, -40.0)], + )); + await gesture.up(); + + // Test that it avoids the right edge of the screen. + await tester.pumpWidget(buildApp('1000000', sliderValue: 1.0)); + 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: [ + const Offset(0.0, -40.0), + const Offset(16.0, -40.0), + const Offset(-98.0, -40.0), + ], + excludes: [const Offset(16.1, -40.0), const Offset(-98.1, -40.0)], + )); + await gesture.up(); }); } diff --git a/packages/flutter/test/widgets/semantics_debugger_test.dart b/packages/flutter/test/widgets/semantics_debugger_test.dart index f25dd4d5b1d..afa26cfb9cf 100644 --- a/packages/flutter/test/widgets/semantics_debugger_test.dart +++ b/packages/flutter/test/widgets/semantics_debugger_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show window; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; @@ -292,13 +294,16 @@ void main() { child: new SemanticsDebugger( child: new Directionality( textDirection: TextDirection.ltr, - child: new Material( - child: new Center( - child: new Slider( - value: value, - onChanged: (double newValue) { - value = newValue; - }, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window), + child: new Material( + child: new Center( + child: new Slider( + value: value, + onChanged: (double newValue) { + value = newValue; + }, + ), ), ), ),