From 421bf64703b5c0fae7196923da0561f6a4c6ec79 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 20 Nov 2019 18:18:40 +0100 Subject: [PATCH] [Material] Update the Slider and RangeSlider to the latest Material spec (#44351) Update the Slider and RangeSlider to the latest Material spec. This introduces an updated track, updated thumbs, updated tick marks, and a new value indicator shape. The old paddle value indicator shape is also updated so that text scaling works consistently. --- .../lib/demo/material/slider_demo.dart | 6 +- .../flutter_gallery/lib/gallery/themes.dart | 1 + examples/flutter_gallery/pubspec.yaml | 42 +- .../lib/src/material/range_slider.dart | 81 ++- packages/flutter/lib/src/material/slider.dart | 82 ++- .../lib/src/material/slider_theme.dart | 659 +++++++++++++++--- .../test/material/inherited_theme_test.dart | 4 +- .../test/material/range_slider_test.dart | 78 +-- .../flutter/test/material/slider_test.dart | 181 +++-- .../test/material/slider_theme_test.dart | 339 ++++++++- 10 files changed, 1191 insertions(+), 282 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/slider_demo.dart b/examples/flutter_gallery/lib/demo/material/slider_demo.dart index 3ba93ecc0e..aae6417ccf 100644 --- a/examples/flutter_gallery/lib/demo/material/slider_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/slider_demo.dart @@ -69,6 +69,7 @@ class _CustomRangeThumbShape extends RangeSliderThumbShape { @required SliderThemeData sliderTheme, TextDirection textDirection, Thumb thumb, + bool isPressed, }) { final Canvas canvas = context.canvas; final ColorTween colorTween = ColorTween( @@ -130,6 +131,8 @@ class _CustomThumbShape extends SliderComponentShape { SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { final Canvas canvas = context.canvas; final ColorTween colorTween = ColorTween( @@ -169,6 +172,8 @@ class _CustomValueIndicatorShape extends SliderComponentShape { SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { final Canvas canvas = context.canvas; final ColorTween enableColor = ColorTween( @@ -438,4 +443,3 @@ class _RangeSlidersState extends State<_RangeSliders> { ); } } - diff --git a/examples/flutter_gallery/lib/gallery/themes.dart b/examples/flutter_gallery/lib/gallery/themes.dart index 78430128b2..3e66950c97 100644 --- a/examples/flutter_gallery/lib/gallery/themes.dart +++ b/examples/flutter_gallery/lib/gallery/themes.dart @@ -25,6 +25,7 @@ ThemeData _buildDarkTheme() { final ThemeData base = ThemeData( brightness: Brightness.dark, accentColorBrightness: Brightness.dark, + colorScheme: colorScheme, primaryColor: primaryColor, primaryColorDark: const Color(0xFF0050a0), primaryColorLight: secondaryColor, diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index a16a27273b..94a3801315 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -257,31 +257,31 @@ flutter: weight: 400 - family: LibreFranklin fonts: - - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Bold.ttf - - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Light.ttf - - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Medium.ttf - - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Regular.ttf + - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Bold.ttf + - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Light.ttf + - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Medium.ttf + - asset: packages/flutter_gallery_assets/fonts/librefranklin/LibreFranklin-Regular.ttf - family: Merriweather fonts: - - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-BlackItalic.ttf - - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Italic.ttf - - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf - - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf + - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-BlackItalic.ttf + - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Italic.ttf + - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf + - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf - family: Roboto Condensed fonts: - - asset: packages/rally_assets/RobotoCondensed-Light.ttf - weight: 400 - - asset: packages/rally_assets/RobotoCondensed-Regular.ttf - weight: 500 - - asset: packages/rally_assets/RobotoCondensed-Bold.ttf - weight: 700 + - asset: packages/rally_assets/RobotoCondensed-Light.ttf + weight: 400 + - asset: packages/rally_assets/RobotoCondensed-Regular.ttf + weight: 500 + - asset: packages/rally_assets/RobotoCondensed-Bold.ttf + weight: 700 - family: Eczar fonts: - - asset: packages/rally_assets/Eczar-Regular.ttf - weight: 400 - - asset: packages/rally_assets/Eczar-SemiBold.ttf - weight: 600 - - asset: packages/rally_assets/Eczar-Bold.ttf - weight: 700 + - asset: packages/rally_assets/Eczar-Regular.ttf + weight: 400 + - asset: packages/rally_assets/Eczar-SemiBold.ttf + weight: 600 + - asset: packages/rally_assets/Eczar-Bold.ttf + weight: 700 -# PUBSPEC CHECKSUM: de7e +# PUBSPEC CHECKSUM: de7e \ No newline at end of file diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index 565a108aff..9905277e57 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -519,12 +519,12 @@ class _RangeSliderState extends State with TickerProviderStateMixin return null; }; - static const double _defaultTrackHeight = 2; + static const double _defaultTrackHeight = 4; static const RangeSliderTrackShape _defaultTrackShape = RoundedRectRangeSliderTrackShape(); static const RangeSliderTickMarkShape _defaultTickMarkShape = RoundRangeSliderTickMarkShape(); static const SliderComponentShape _defaultOverlayShape = RoundSliderOverlayShape(); static const RangeSliderThumbShape _defaultThumbShape = RoundRangeSliderThumbShape(); - static const RangeSliderValueIndicatorShape _defaultValueIndicatorShape = PaddleRangeSliderValueIndicatorShape(); + static const RangeSliderValueIndicatorShape _defaultValueIndicatorShape = RectangularRangeSliderValueIndicatorShape(); static const ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete; static const double _defaultMinThumbSeparation = 8; @@ -542,6 +542,19 @@ class _RangeSliderState extends State with TickerProviderStateMixin // colors come from the ThemeData.colorScheme. These colors, along with // the default shapes and text styles are aligned to the Material // Guidelines. + + // The value indicator color is not the same as the thumb and active track + // (which can be defined by activeColor) if the + // RectangularSliderValueIndicatorShape is used. In all other cases, the + // value indicator is assumed to be the same as the active color. + final RangeSliderValueIndicatorShape valueIndicatorShape = sliderTheme.rangeValueIndicatorShape ?? _defaultValueIndicatorShape; + Color valueIndicatorColor; + if (valueIndicatorShape is RectangularRangeSliderValueIndicatorShape) { + valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(0.60), theme.colorScheme.surface.withOpacity(0.90)); + } else { + valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary; + } + sliderTheme = sliderTheme.copyWith( trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, @@ -556,12 +569,13 @@ class _RangeSliderState extends State with TickerProviderStateMixin overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface, disabledThumbColor: sliderTheme.disabledThumbColor ?? theme.colorScheme.onSurface.withOpacity(0.38), overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12), - valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary, + valueIndicatorColor: valueIndicatorColor, + surfaceColor: sliderTheme.surfaceColor ?? theme.colorScheme.surface, rangeTrackShape: sliderTheme.rangeTrackShape ?? _defaultTrackShape, rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? _defaultTickMarkShape, rangeThumbShape: sliderTheme.rangeThumbShape ?? _defaultThumbShape, overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape, - rangeValueIndicatorShape: sliderTheme.rangeValueIndicatorShape ?? _defaultValueIndicatorShape, + rangeValueIndicatorShape: valueIndicatorShape, showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator, valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.body2.copyWith( color: theme.colorScheme.onPrimary, @@ -570,12 +584,18 @@ class _RangeSliderState extends State with TickerProviderStateMixin thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector, ); + // This size is used as the max bounds for the painting of the value + // indicators It must be kept in sync with the function with the same name + // in slider.dart. + Size _sizeWithOverflow() => MediaQuery.of(context).size; + return _RangeSliderRenderObjectWidget( values: _unlerpRangeValues(widget.values), divisions: widget.divisions, labels: widget.labels, sliderTheme: sliderTheme, textScaleFactor: MediaQuery.of(context).textScaleFactor, + sizeWithOverflow: _sizeWithOverflow(), onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, @@ -593,6 +613,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { this.labels, this.sliderTheme, this.textScaleFactor, + this.sizeWithOverflow, this.onChanged, this.onChangeStart, this.onChangeEnd, @@ -605,6 +626,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { final RangeLabels labels; final SliderThemeData sliderTheme; final double textScaleFactor; + final Size sizeWithOverflow; final ValueChanged onChanged; final ValueChanged onChangeStart; final ValueChanged onChangeEnd; @@ -620,6 +642,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { sliderTheme: sliderTheme, theme: Theme.of(context), textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, onChanged: onChanged, onChangeStart: onChangeStart, onChangeEnd: onChangeEnd, @@ -639,6 +662,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ..sliderTheme = sliderTheme ..theme = Theme.of(context) ..textScaleFactor = textScaleFactor + ..sizeWithOverflow = sizeWithOverflow ..onChanged = onChanged ..onChangeStart = onChangeStart ..onChangeEnd = onChangeEnd @@ -656,6 +680,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix SliderThemeData sliderTheme, ThemeData theme, double textScaleFactor, + Size sizeWithOverflow, TargetPlatform platform, ValueChanged onChanged, RangeSemanticFormatterCallback semanticFormatterCallback, @@ -676,6 +701,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix _sliderTheme = sliderTheme, _theme = theme, _textScaleFactor = textScaleFactor, + _sizeWithOverflow = sizeWithOverflow, _onChanged = onChanged, _state = state, _textDirection = textDirection { @@ -848,6 +874,15 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix _updateLabelPainters(); } + Size get sizeWithOverflow => _sizeWithOverflow; + Size _sizeWithOverflow; + set sizeWithOverflow(Size value) { + if (value == sizeWithOverflow) + return; + _sizeWithOverflow = value; + markNeedsPaint(); + } + ValueChanged get onChanged => _onChanged; ValueChanged _onChanged; set onChanged(ValueChanged value) { @@ -1195,8 +1230,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix isEnabled: isEnabled, ); + final bool startThumbSelected = _lastThumbSelection == Thumb.start; + final bool endThumbSelected = _lastThumbSelection == Thumb.end; + if (!_overlayAnimation.isDismissed) { - if (_lastThumbSelection == Thumb.start) { + if (startThumbSelected) { _sliderTheme.overlayShape.paint( context, startThumbCenter, @@ -1210,7 +1248,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix value: startValue, ); } - if (_lastThumbSelection == Thumb.end) { + if (endThumbSelected) { _sliderTheme.overlayShape.paint( context, endThumbCenter, @@ -1231,7 +1269,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix isEnabled: isEnabled, sliderTheme: _sliderTheme, ).width; - final double adjustedTrackWidth = trackRect.width - tickMarkWidth; + final double adjustedTrackWidth = trackRect.width - trackRect.height; // If the tick marks would be too dense, don't bother painting them. if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) { final double dy = trackRect.center.dy; @@ -1239,7 +1277,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix final double value = i / divisions; // The ticks are mapped to be within the track, so the tick mark width // must be subtracted from the track width. - final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2; + final double dx = trackRect.left + value * adjustedTrackWidth + trackRect.height / 2; final Offset tickMarkOffset = Offset(dx, dy); _sliderTheme.rangeTickMarkShape.paint( context, @@ -1268,6 +1306,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix final double bottomValue = isLastThumbStart ? endValue : startValue; final double topValue = isLastThumbStart ? startValue : endValue; final bool shouldPaintValueIndicators = isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator; + final Size resolvedSizeWithOverflow = sizeWithOverflow.isEmpty ? size : sizeWithOverflow; if (shouldPaintValueIndicators) { _sliderTheme.rangeValueIndicatorShape.paint( @@ -1283,6 +1322,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix textDirection: _textDirection, thumb: bottomThumb, value: bottomValue, + textScaleFactor: textScaleFactor, + sizeWithOverflow: resolvedSizeWithOverflow, ); } @@ -1296,6 +1337,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix textDirection: textDirection, sliderTheme: _sliderTheme, thumb: bottomThumb, + isPressed: bottomThumb == Thumb.start ? startThumbSelected : endThumbSelected, ); if (shouldPaintValueIndicators) { @@ -1304,15 +1346,29 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix center: startThumbCenter, labelPainter: _startLabelPainter, activationAnimation: _valueIndicatorAnimation, + textScaleFactor: textScaleFactor, + sizeWithOverflow: resolvedSizeWithOverflow, ); final double endOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift( parentBox: this, center: endThumbCenter, labelPainter: _endLabelPainter, activationAnimation: _valueIndicatorAnimation, + textScaleFactor: textScaleFactor, + sizeWithOverflow: resolvedSizeWithOverflow, ); - final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _startLabelPainter).width / 2; - final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _endLabelPainter).width / 2; + final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize( + isEnabled, + isDiscrete, + labelPainter: _startLabelPainter, + textScaleFactor: textScaleFactor, + ).width / 2; + final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize( + isEnabled, + isDiscrete, + labelPainter: _endLabelPainter, + textScaleFactor: textScaleFactor, + ).width / 2; double innerOverflow = startHalfWidth + endHalfWidth; switch (textDirection) { case TextDirection.ltr: @@ -1338,19 +1394,22 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix textDirection: _textDirection, thumb: topThumb, value: topValue, + textScaleFactor: textScaleFactor, + sizeWithOverflow: resolvedSizeWithOverflow, ); } _sliderTheme.rangeThumbShape.paint( context, topThumbCenter, - activationAnimation: _valueIndicatorAnimation, + activationAnimation: _overlayAnimation, enableAnimation: _enableAnimation, isDiscrete: isDiscrete, isOnTop: thumbDelta < sliderTheme.rangeThumbShape.getPreferredSize(isEnabled, isDiscrete).width, textDirection: textDirection, sliderTheme: _sliderTheme, thumb: topThumb, + isPressed: topThumb == Thumb.start ? startThumbSelected : endThumbSelected, ); } diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 025b68833d..25db8471b1 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -477,12 +477,12 @@ class _SliderState extends State with TickerProviderStateMixin { return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0; } - static const double _defaultTrackHeight = 2; + static const double _defaultTrackHeight = 4; static const SliderTrackShape _defaultTrackShape = RoundedRectSliderTrackShape(); static const SliderTickMarkShape _defaultTickMarkShape = RoundSliderTickMarkShape(); static const SliderComponentShape _defaultOverlayShape = RoundSliderOverlayShape(); static const SliderComponentShape _defaultThumbShape = RoundSliderThumbShape(); - static const SliderComponentShape _defaultValueIndicatorShape = PaddleSliderValueIndicatorShape(); + static const SliderComponentShape _defaultValueIndicatorShape = RectangularSliderValueIndicatorShape(); static const ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete; @override @@ -520,6 +520,19 @@ class _SliderState extends State with TickerProviderStateMixin { // colors come from the ThemeData.colorScheme. These colors, along with // the default shapes and text styles are aligned to the Material // Guidelines. + + // The value indicator color is not the same as the thumb and active track + // (which can be defined by activeColor) if the + // RectangularSliderValueIndicatorShape is used. In all other cases, the + // value indicator is assumed to be the same as the active color. + final SliderComponentShape valueIndicatorShape = sliderTheme.valueIndicatorShape ?? _defaultValueIndicatorShape; + Color valueIndicatorColor; + if (valueIndicatorShape is RectangularSliderValueIndicatorShape) { + valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(0.60), theme.colorScheme.surface.withOpacity(0.90)); + } else { + valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary; + } + sliderTheme = sliderTheme.copyWith( trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, @@ -533,24 +546,31 @@ class _SliderState extends State with TickerProviderStateMixin { thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary, disabledThumbColor: sliderTheme.disabledThumbColor ?? theme.colorScheme.onSurface.withOpacity(0.38), overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12), - valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary, + valueIndicatorColor: valueIndicatorColor, + surfaceColor: sliderTheme.surfaceColor ?? theme.colorScheme.surface, trackShape: sliderTheme.trackShape ?? _defaultTrackShape, tickMarkShape: sliderTheme.tickMarkShape ?? _defaultTickMarkShape, thumbShape: sliderTheme.thumbShape ?? _defaultThumbShape, overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape, - valueIndicatorShape: sliderTheme.valueIndicatorShape ?? _defaultValueIndicatorShape, + valueIndicatorShape: valueIndicatorShape, showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator, valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.body2.copyWith( color: theme.colorScheme.onPrimary, ), ); + // This size is used as the max bounds for the painting of the value + // indicators It must be kept in sync with the function with the same name + // in range_slider.dart. + Size _sizeWithOverflow() => MediaQuery.of(context).size; + return _SliderRenderObjectWidget( value: _unlerp(widget.value), divisions: widget.divisions, label: widget.label, sliderTheme: sliderTheme, - mediaQueryData: MediaQuery.of(context), + textScaleFactor: MediaQuery.of(context).textScaleFactor, + sizeWithOverflow: _sizeWithOverflow(), onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, @@ -586,7 +606,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { this.divisions, this.label, this.sliderTheme, - this.mediaQueryData, + this.textScaleFactor, + this.sizeWithOverflow, this.onChanged, this.onChangeStart, this.onChangeEnd, @@ -598,7 +619,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { final int divisions; final String label; final SliderThemeData sliderTheme; - final MediaQueryData mediaQueryData; + final double textScaleFactor; + final Size sizeWithOverflow; final ValueChanged onChanged; final ValueChanged onChangeStart; final ValueChanged onChangeEnd; @@ -612,7 +634,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { divisions: divisions, label: label, sliderTheme: sliderTheme, - mediaQueryData: mediaQueryData, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, onChanged: onChanged, onChangeStart: onChangeStart, onChangeEnd: onChangeEnd, @@ -631,7 +654,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ..label = label ..sliderTheme = sliderTheme ..theme = Theme.of(context) - ..mediaQueryData = mediaQueryData + ..textScaleFactor = textScaleFactor + ..sizeWithOverflow = sizeWithOverflow ..onChanged = onChanged ..onChangeStart = onChangeStart ..onChangeEnd = onChangeEnd @@ -649,7 +673,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { int divisions, String label, SliderThemeData sliderTheme, - MediaQueryData mediaQueryData, + double textScaleFactor, + Size sizeWithOverflow, TargetPlatform platform, ValueChanged onChanged, SemanticFormatterCallback semanticFormatterCallback, @@ -666,7 +691,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _value = value, _divisions = divisions, _sliderTheme = sliderTheme, - _mediaQueryData = mediaQueryData, + _textScaleFactor = textScaleFactor, + _sizeWithOverflow = sizeWithOverflow, _onChanged = onChanged, _state = state, _textDirection = textDirection { @@ -821,18 +847,28 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { markNeedsPaint(); } - MediaQueryData get mediaQueryData => _mediaQueryData; - MediaQueryData _mediaQueryData; - set mediaQueryData(MediaQueryData value) { - if (value == _mediaQueryData) { + double get textScaleFactor => _textScaleFactor; + double _textScaleFactor; + set textScaleFactor(double value) { + if (value == _textScaleFactor) { return; } - _mediaQueryData = value; + _textScaleFactor = value; // Media query data includes the textScaleFactor, so we need to update the // label painter. _updateLabelPainter(); } + Size get sizeWithOverflow => _sizeWithOverflow; + Size _sizeWithOverflow; + set sizeWithOverflow(Size value) { + if (value == _sizeWithOverflow) { + return; + } + _sizeWithOverflow = value; + markNeedsPaint(); + } + ValueChanged get onChanged => _onChanged; ValueChanged _onChanged; set onChanged(ValueChanged value) { @@ -906,7 +942,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { text: label, ) ..textDirection = textDirection - ..textScaleFactor = _mediaQueryData.textScaleFactor + ..textScaleFactor = textScaleFactor ..layout(); } else { _labelPainter.text = null; @@ -1087,7 +1123,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { sliderTheme: _sliderTheme, isDiscrete: isDiscrete, ); - final Offset thumbCenter = Offset(trackRect.left + visualPosition * trackRect.width, trackRect.center.dy); + final Offset thumbCenter = Offset( + trackRect.left + visualPosition * trackRect.width, trackRect.center.dy); _sliderTheme.trackShape.paint( context, @@ -1121,7 +1158,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { isEnabled: isInteractive, sliderTheme: _sliderTheme, ).width; - final double adjustedTrackWidth = trackRect.width - tickMarkWidth; + final double adjustedTrackWidth = trackRect.width - trackRect.height; // If the tick marks would be too dense, don't bother painting them. if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) { final double dy = trackRect.center.dy; @@ -1129,7 +1166,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { final double value = i / divisions; // The ticks are mapped to be within the track, so the tick mark width // must be subtracted from the track width. - final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2; + final double dx = trackRect.left + value * adjustedTrackWidth + trackRect.height / 2; final Offset tickMarkOffset = Offset(dx, dy); _sliderTheme.tickMarkShape.paint( context, @@ -1158,6 +1195,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { sliderTheme: _sliderTheme, textDirection: _textDirection, value: _value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow.isEmpty ? size : sizeWithOverflow, ); } } @@ -1165,13 +1204,14 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _sliderTheme.thumbShape.paint( context, thumbCenter, - activationAnimation: _valueIndicatorAnimation, + activationAnimation: _overlayAnimation, enableAnimation: _enableAnimation, isDiscrete: isDiscrete, labelPainter: _labelPainter, parentBox: this, sliderTheme: _sliderTheme, textDirection: _textDirection, + sizeWithOverflow: sizeWithOverflow.isEmpty ? size : sizeWithOverflow, value: _value, ); } diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 52282c19ec..a0a7d24ac0 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -278,7 +278,7 @@ enum Thumb { /// by creating subclasses of [SliderTrackShape], /// [SliderComponentShape], and/or [SliderTickMarkShape]. See /// [RoundSliderThumbShape], [RectangularSliderTrackShape], -/// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and +/// [RoundSliderTickMarkShape], [RectangularSliderValueIndicatorShape], and /// [RoundSliderOverlayShape] for examples. /// /// The track painting can be skipped by specifying 0 for [trackHeight]. @@ -355,6 +355,7 @@ class SliderThemeData extends Diagnosticable { this.disabledThumbColor, this.overlayColor, this.valueIndicatorColor, + this.surfaceColor, this.overlayShape, this.tickMarkShape, this.thumbShape, @@ -490,6 +491,14 @@ class SliderThemeData extends Diagnosticable { /// The color given to the [valueIndicatorShape] to draw itself with. final Color valueIndicatorColor; + /// The surface color that the slider is resting on. + /// + /// This is used to clear painting regions for possibly transparent thumbs and + /// thumb shadows. + /// + /// Defaults to [ColorScheme.surface]. + final Color surfaceColor; + /// The shape that will be used to draw the [Slider]'s overlay. /// /// Both the [overlayColor] and a non default [overlayShape] may be specified. @@ -656,6 +665,7 @@ class SliderThemeData extends Diagnosticable { Color disabledThumbColor, Color overlayColor, Color valueIndicatorColor, + Color surfaceColor, SliderComponentShape overlayShape, SliderTickMarkShape tickMarkShape, SliderComponentShape thumbShape, @@ -685,6 +695,7 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor, overlayColor: overlayColor ?? this.overlayColor, valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor, + surfaceColor: surfaceColor ?? this.surfaceColor, overlayShape: overlayShape ?? this.overlayShape, tickMarkShape: tickMarkShape ?? this.tickMarkShape, thumbShape: thumbShape ?? this.thumbShape, @@ -725,6 +736,7 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t), overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t), valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t), + surfaceColor: Color.lerp(a.surfaceColor, b.surfaceColor, t), overlayShape: t < 0.5 ? a.overlayShape : b.overlayShape, tickMarkShape: t < 0.5 ? a.tickMarkShape : b.tickMarkShape, thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape, @@ -758,6 +770,7 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor, overlayColor, valueIndicatorColor, + surfaceColor, overlayShape, tickMarkShape, thumbShape, @@ -797,6 +810,7 @@ class SliderThemeData extends Diagnosticable { && otherData.disabledThumbColor == disabledThumbColor && otherData.overlayColor == overlayColor && otherData.valueIndicatorColor == valueIndicatorColor + && otherData.surfaceColor == surfaceColor && otherData.overlayShape == overlayShape && otherData.tickMarkShape == tickMarkShape && otherData.thumbShape == thumbShape @@ -830,6 +844,7 @@ class SliderThemeData extends Diagnosticable { properties.add(ColorProperty('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor)); properties.add(ColorProperty('overlayColor', overlayColor, defaultValue: defaultData.overlayColor)); properties.add(ColorProperty('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor)); + properties.add(ColorProperty('surfaceColor', surfaceColor, defaultValue: defaultData.surfaceColor)); properties.add(DiagnosticsProperty('overlayShape', overlayShape, defaultValue: defaultData.overlayShape)); properties.add(DiagnosticsProperty('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape)); properties.add(DiagnosticsProperty('thumbShape', thumbShape, defaultValue: defaultData.thumbShape)); @@ -978,6 +993,16 @@ abstract class SliderComponentShape { /// [labelPainter] already has the [textDirection] set. /// /// [value] is the current parametric value (from 0.0 to 1.0) of the slider. + /// + /// [textScaleFactor] can be used to determine whether the component should + /// paint larger or smaller, depending on whether [textScaleFactor] is greater + /// than 1 for larger, and between 0 and 1 for smaller. It usually comes from + /// [MediaQueryData.textScaleFactor]. + /// + /// [sizeWithOverflow] can be used to determine the bounds the drawing of the + /// components that are outside of the regular slider bounds. It's the size of + /// the box, whose center is aligned with the slider's bounds, that the value + /// indicators must be drawn within. Typically, it is bigger than the slider. void paint( PaintingContext context, Offset center, { @@ -989,6 +1014,8 @@ abstract class SliderComponentShape { SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }); /// Special instance of [SliderComponentShape] to skip the thumb drawing. @@ -1204,6 +1231,9 @@ abstract class RangeSliderThumbShape { /// left and right thumb. /// /// {@macro flutter.material.rangeSlider.shape.thumb} + /// + /// [isPressed] can be used to give the selected thumb additional selected + /// or pressed state visual feedback, such as a larger shadow. void paint( PaintingContext context, Offset center, { @@ -1215,6 +1245,7 @@ abstract class RangeSliderThumbShape { TextDirection textDirection, SliderThemeData sliderTheme, Thumb thumb, + bool isPressed, }); } @@ -1240,7 +1271,17 @@ abstract class RangeSliderValueIndicatorShape { /// /// [labelPainter] helps determine the width of the shape. It is variable /// width because it is derived from a formatted string. - Size getPreferredSize(bool isEnabled, bool isDiscrete, { TextPainter labelPainter }); + /// + /// [textScaleFactor] can be used to determine whether the component should + /// paint larger or smaller, depending on whether [textScaleFactor] is greater + /// than 1 for larger, and between 0 and 1 for smaller. It usually comes from + /// [MediaQueryData.textScaleFactor]. + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + TextPainter labelPainter, + double textScaleFactor, + }); /// Determines the best offset to keep this shape on the screen. /// @@ -1251,6 +1292,8 @@ abstract class RangeSliderValueIndicatorShape { Offset center, TextPainter labelPainter, Animation activationAnimation, + double textScaleFactor, + Size sizeWithOverflow, }) { return 0; } @@ -1272,6 +1315,18 @@ abstract class RangeSliderValueIndicatorShape { /// the default case, this is used to paint a stroke around the top indicator /// for better visibility between the two indicators. /// + /// [textScaleFactor] can be used to determine whether the component should + /// paint larger or smaller, depending on whether [textScaleFactor] is greater + /// than 1 for larger, and between 0 and 1 for smaller. It usually comes from + /// [MediaQueryData.textScaleFactor]. + /// + /// [sizeWithOverflow] can be used to determine the bounds the drawing of the + /// components that are outside of the regular slider bounds. It's the size of + /// the box, whose center is aligned with the slider's bounds, that the value + /// indicators must be drawn within. Typically, it is bigger than the slider. + /// + /// {@macro flutter.material.rangeSlider.shape.parentBox} + /// /// {@macro flutter.material.rangeSlider.shape.sliderTheme} /// /// [textDirection] can be used to determine how any extra text or graphics, @@ -1289,6 +1344,8 @@ abstract class RangeSliderValueIndicatorShape { bool isDiscrete, bool isOnTop, TextPainter labelPainter, + double textScaleFactor, + Size sizeWithOverflow, RenderBox parentBox, SliderThemeData sliderTheme, TextDirection textDirection, @@ -1554,17 +1611,17 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS assert(thumbCenter != null); assert(isEnabled != null); assert(isDiscrete != null); - // If the slider track height is less than or equal to 0, then it makes no - // difference whether the track is painted or not, therefore the painting - // can be a no-op. + // If the slider [SliderThemeData.trackHeight] is less than or equal to 0, + // then it makes no difference whether the track is painted or not, + // therefore the painting can be a no-op. if (sliderTheme.trackHeight <= 0) { return; } // Assign the track segment paints, which are left: active, right: inactive, // but reversed for right to left text. - final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); - final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor); + final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor); + final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor); final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation); final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation); Paint leftTrackPaint; @@ -1588,11 +1645,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS isDiscrete: isDiscrete, ); - final Size thumbSize = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete); - final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx - thumbSize.width / 2, trackRect.bottom); + final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx, trackRect.bottom); if (!leftTrackSegment.isEmpty) context.canvas.drawRect(leftTrackSegment, leftTrackPaint); - final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + thumbSize.width / 2, trackRect.top, trackRect.right, trackRect.bottom); + final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx, trackRect.top, trackRect.right, trackRect.bottom); if (!rightTrackSegment.isEmpty) context.canvas.drawRect(rightTrackSegment, rightTrackPaint); } @@ -1648,9 +1704,9 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS assert(enableAnimation != null); assert(textDirection != null); assert(thumbCenter != null); - // If the slider track height is less than or equal to 0, then it makes no - // difference whether the track is painted or not, therefore the painting - // can be a no-op. + // If the slider [SliderThemeData.trackHeight] is less than or equal to 0, + // then it makes no difference whether the track is painted or not, + // therefore the painting can be a no-op. if (sliderTheme.trackHeight <= 0) { return; } @@ -1681,22 +1737,30 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS isEnabled: isEnabled, isDiscrete: isDiscrete, ); + final Radius trackRadius = Radius.circular(trackRect.height / 2); - // The arc rects create a semi-circle with radius equal to track height. - final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height); - if (!leftTrackArcRect.isEmpty) - context.canvas.drawArc(leftTrackArcRect, math.pi / 2, math.pi, false, leftTrackPaint); - final Rect rightTrackArcRect = Rect.fromLTWH(trackRect.right - trackRect.height / 2, trackRect.top, trackRect.height, trackRect.height); - if (!rightTrackArcRect.isEmpty) - context.canvas.drawArc(rightTrackArcRect, -math.pi / 2, math.pi, false, rightTrackPaint); - - final Size thumbSize = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete); - final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx - thumbSize.width / 2, trackRect.bottom); - if (!leftTrackSegment.isEmpty) - context.canvas.drawRect(leftTrackSegment, leftTrackPaint); - final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + thumbSize.width / 2, trackRect.top, trackRect.right, trackRect.bottom); - if (!rightTrackSegment.isEmpty) - context.canvas.drawRect(rightTrackSegment, rightTrackPaint); + context.canvas.drawRRect( + RRect.fromLTRBAndCorners( + trackRect.left, + trackRect.top, + thumbCenter.dx, + trackRect.bottom, + topLeft: trackRadius, + bottomLeft: trackRadius, + ), + leftTrackPaint, + ); + context.canvas.drawRRect( + RRect.fromLTRBAndCorners( + thumbCenter.dx, + trackRect.top, + trackRect.right, + trackRect.bottom, + topRight: trackRadius, + bottomRight: trackRadius, + ), + rightTrackPaint, + ); } } @@ -1912,6 +1976,11 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { assert(isEnabled != null); assert(isDiscrete != null); assert(textDirection != null); + + if (sliderTheme.trackHeight <= 0) { + return; + } + // Assign the track segment paints, which are left: active, right: inactive, // but reversed for right to left text. final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); @@ -1942,25 +2011,39 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { isEnabled: isEnabled, isDiscrete: isDiscrete, ); - final double trackRadius = trackRect.height / 2; + final Radius trackRadius = Radius.circular(trackRect.height / 2); - final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height); - if (!leftTrackArcRect.isEmpty) - context.canvas.drawArc(leftTrackArcRect, math.pi / 2, math.pi, false, inactivePaint); - - final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRadius, trackRect.top, leftThumbOffset.dx - thumbRadius, trackRect.bottom); - if (!leftTrackSegment.isEmpty) - context.canvas.drawRect(leftTrackSegment, inactivePaint); - final Rect middleTrackSegment = Rect.fromLTRB(leftThumbOffset.dx + thumbRadius, trackRect.top, rightThumbOffset.dx - thumbRadius, trackRect.bottom); - if (!middleTrackSegment.isEmpty) - context.canvas.drawRect(middleTrackSegment, activePaint); - final Rect rightTrackSegment = Rect.fromLTRB(rightThumbOffset.dx + thumbRadius, trackRect.top, trackRect.right - trackRadius, trackRect.bottom); - if (!rightTrackSegment.isEmpty) - context.canvas.drawRect(rightTrackSegment, inactivePaint); - - final Rect rightTrackArcRect = Rect.fromLTWH(trackRect.right - trackRect.height, trackRect.top, trackRect.height, trackRect.height); - if (!rightTrackArcRect.isEmpty) - context.canvas.drawArc(rightTrackArcRect, -math.pi / 2, math.pi, false, inactivePaint); + context.canvas.drawRRect( + RRect.fromLTRBAndCorners( + trackRect.left, + trackRect.top, + leftThumbOffset.dx, + trackRect.bottom, + topLeft: trackRadius, + bottomLeft: trackRadius, + ), + inactivePaint, + ); + context.canvas.drawRect( + Rect.fromLTRB( + leftThumbOffset.dx, + trackRect.top, + rightThumbOffset.dx, + trackRect.bottom, + ), + activePaint, + ); + context.canvas.drawRRect( + RRect.fromLTRBAndCorners( + rightThumbOffset.dx, + trackRect.top, + trackRect.right, + trackRect.bottom, + topRight: trackRadius, + bottomRight: trackRadius, + ), + inactivePaint, + ); } } @@ -1988,7 +2071,8 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape { /// The preferred radius of the round tick mark. /// - /// If it is not provided, then half of the track height is used. + /// If it is not provided, then 1/4 of the [SliderThemeData.trackHeight] is + /// used. final double tickMarkRadius; @override @@ -1999,9 +2083,10 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape { assert(sliderTheme != null); assert(sliderTheme.trackHeight != null); assert(isEnabled != null); - // The tick marks are tiny circles. If no radius is provided, then they are - // defaulted to be the same height as the track. - return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2); + // The tick marks are tiny circles. If no radius is provided, then the + // radius is defaulted to be 1/4 of the [SliderThemeData.trackHeight], or a + // diameter of half the [SliderThemeData.trackHeight]. + return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 4); } @override @@ -2080,7 +2165,8 @@ class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape { /// The preferred radius of the round tick mark. /// - /// If it is not provided, then half of the track height is used. + /// If it is not provided, then 1/4 of the [SliderThemeData.trackHeight] is + /// used. final double tickMarkRadius; @override @@ -2091,7 +2177,7 @@ class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape { assert(sliderTheme != null); assert(sliderTheme.trackHeight != null); assert(isEnabled != null); - return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2); + return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 4); } @override @@ -2200,6 +2286,8 @@ class _EmptySliderComponentShape extends SliderComponentShape { SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { // no-op. } @@ -2217,6 +2305,8 @@ class RoundSliderThumbShape extends SliderComponentShape { const RoundSliderThumbShape({ this.enabledThumbRadius = 10.0, this.disabledThumbRadius, + this.elevation = 1.0, + this.pressedElevation = 6.0, }); /// The preferred radius of the round thumb shape when the slider is enabled. @@ -2231,6 +2321,22 @@ class RoundSliderThumbShape extends SliderComponentShape { final double disabledThumbRadius; double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; + /// The resting elevation adds shadow to the unpressed thumb. + /// + /// The default is 1. + /// + /// Use 0 for no shadow. The higher the value, the larger the shadow. For + /// example, a value of 12 will create a very large shadow. + final double elevation; + + /// The pressed elevation adds shadow to the pressed thumb. + /// + /// The default is 6. + /// + /// Use 0 for no shadow. The higher the value, the larger the shadow. For + /// example, a value of 12 will create a very large shadow. + final double pressedElevation; + @override Size getPreferredSize(bool isEnabled, bool isDiscrete) { return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); @@ -2248,6 +2354,8 @@ class RoundSliderThumbShape extends SliderComponentShape { @required SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { assert(context != null); assert(center != null); @@ -2255,6 +2363,7 @@ class RoundSliderThumbShape extends SliderComponentShape { assert(sliderTheme != null); assert(sliderTheme.disabledThumbColor != null); assert(sliderTheme.thumbColor != null); + assert(!sizeWithOverflow.isEmpty); final Canvas canvas = context.canvas; final Tween radiusTween = Tween( @@ -2265,10 +2374,31 @@ class RoundSliderThumbShape extends SliderComponentShape { begin: sliderTheme.disabledThumbColor, end: sliderTheme.thumbColor, ); + final Tween elevationTween = Tween( + begin: elevation, + end: pressedElevation, + ); + + final double evaluatedElevation = elevationTween.evaluate(activationAnimation); + final Color color = colorTween.evaluate(enableAnimation); + final double radius = radiusTween.evaluate(enableAnimation); + final Path path = Path()..addArc(Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), 0, math.pi * 2); + canvas.drawShadow(path, Colors.black, evaluatedElevation, true); + + // If the thumb is translucent, clear the space of the track and shadow so + // the thumb can be drawn without interference. + if (color.alpha != 0xff) { + canvas.drawCircle( + center, + radius, + Paint()..color = sliderTheme.surfaceColor.withOpacity(1), + ); + } + canvas.drawCircle( center, - radiusTween.evaluate(enableAnimation), - Paint()..color = colorTween.evaluate(enableAnimation), + radius, + Paint()..color = color, ); } } @@ -2285,6 +2415,8 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { const RoundRangeSliderThumbShape({ this.enabledThumbRadius = 10.0, this.disabledThumbRadius, + this.elevation = 1.0, + this.pressedElevation = 6.0, }) : assert(enabledThumbRadius != null); /// The preferred radius of the round thumb shape when the slider is enabled. @@ -2299,6 +2431,16 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { final double disabledThumbRadius; double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; + /// The resting elevation adds shadow to the unpressed thumb. + /// + /// The default is 1. + final double elevation; + + /// The pressed elevation adds shadow to the pressed thumb. + /// + /// The default is 6. + final double pressedElevation; + @override Size getPreferredSize(bool isEnabled, bool isDiscrete) { return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); @@ -2316,6 +2458,7 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { @required SliderThemeData sliderTheme, TextDirection textDirection, Thumb thumb, + bool isPressed, }) { assert(context != null); assert(center != null); @@ -2334,6 +2477,10 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { end: sliderTheme.thumbColor, ); final double radius = radiusTween.evaluate(enableAnimation); + final Tween elevationTween = Tween( + begin: elevation, + end: pressedElevation, + ); // Add a stroke of 1dp around the circle if this thumb would overlap // the other thumb. @@ -2363,10 +2510,26 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { } } + final double evaluatedElevation = isPressed ? elevationTween.evaluate(activationAnimation) : elevation; + final Color color = colorTween.evaluate(enableAnimation); + final Path shadowPath = Path()..addArc(Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), 0, math.pi * 2); + canvas.drawShadow(shadowPath, Colors.black, evaluatedElevation, true); + + // If the thumb is translucent, clear the space of the track and shadow so + // the thumb can be drawn without interference. + if (color.alpha != 0xff) { + canvas.drawCircle( + center, + radius, + Paint() + ..color = sliderTheme.surfaceColor.withOpacity(1), + ); + } + canvas.drawCircle( center, radius, - Paint()..color = colorTween.evaluate(enableAnimation), + Paint()..color = color, ); } } @@ -2392,7 +2555,8 @@ class RoundSliderOverlayShape extends SliderComponentShape { /// The preferred radius of the round thumb shape when enabled. /// - /// If it is not provided, then half of the track height is used. + /// If it is not provided, then half of the [SliderThemeData.trackHeight] is + /// used. final double overlayRadius; @override @@ -2412,6 +2576,8 @@ class RoundSliderOverlayShape extends SliderComponentShape { @required SliderThemeData sliderTheme, @required TextDirection textDirection, @required double value, + double textScaleFactor, + Size sizeWithOverflow, }) { assert(context != null); assert(center != null); @@ -2444,16 +2610,20 @@ class RoundSliderOverlayShape extends SliderComponentShape { /// * [Slider], which includes a value indicator defined by this shape. /// * [SliderTheme], which can be used to configure the slider value indicator /// of all sliders in a widget subtree. -class PaddleSliderValueIndicatorShape extends SliderComponentShape { - /// Create a slider value indicator in the shape of an upside-down pear. - const PaddleSliderValueIndicatorShape(); +class RectangularSliderValueIndicatorShape extends SliderComponentShape { + /// Create a slider value indicator that resembles a rectangular tooltip. + const RectangularSliderValueIndicatorShape(); - static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter(); + static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter(); @override - Size getPreferredSize(bool isEnabled, bool isDiscrete, { @required TextPainter labelPainter }) { + Size getPreferredSize(bool isEnabled, bool isDiscrete, { + @required TextPainter labelPainter, + @required double textScaleFactor, + }) { assert(labelPainter != null); - return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter); + assert(textScaleFactor != null && textScaleFactor >= 0); + return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor); } @override @@ -2468,26 +2638,20 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { @required SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { - assert(context != null); - assert(center != null); - assert(activationAnimation != null); - assert(enableAnimation != null); - assert(labelPainter != null); - assert(parentBox != null); - assert(sliderTheme != null); - final ColorTween enableColor = ColorTween( - begin: sliderTheme.disabledThumbColor, - end: sliderTheme.valueIndicatorColor, - ); - _pathPainter.drawValueIndicator( - parentBox, - context.canvas, - center, - Paint()..color = enableColor.evaluate(enableAnimation), - activationAnimation.value, - labelPainter, - null, + final Canvas canvas = context.canvas; + final double scale = activationAnimation.value; + _pathPainter.paint( + parentBox: parentBox, + canvas: canvas, + center: center, + scale: scale, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + backgroundPaintColor: sliderTheme.valueIndicatorColor ); } } @@ -2499,16 +2663,22 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { /// * [RangeSlider], which includes value indicators defined by this shape. /// * [SliderTheme], which can be used to configure the range slider value /// indicator of all sliders in a widget subtree. -class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { - /// Create a slider value indicator in the shape of an upside-down pear. - const PaddleRangeSliderValueIndicatorShape(); +class RectangularRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { + /// Create a range slider value indicator that resembles a rectangular tooltip. + const RectangularRangeSliderValueIndicatorShape(); - static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter(); + static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter(); @override - Size getPreferredSize(bool isEnabled, bool isDiscrete, { @required TextPainter labelPainter }) { + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + @required TextPainter labelPainter, + @required double textScaleFactor, + }) { assert(labelPainter != null); - return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter); + assert(textScaleFactor != null && textScaleFactor >= 0); + return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor); } @override @@ -2517,12 +2687,277 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap Offset center, TextPainter labelPainter, Animation activationAnimation, + double textScaleFactor, + Size sizeWithOverflow, + }) { + return _pathPainter.getHorizontalShift( + parentBox: parentBox, + center: center, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + scale: activationAnimation.value, + ); + } + + @override + void paint( + PaintingContext context, + Offset center, { + Animation activationAnimation, + Animation enableAnimation, + bool isDiscrete, + bool isOnTop, + TextPainter labelPainter, + double textScaleFactor, + Size sizeWithOverflow, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + Thumb thumb, + }) { + final Canvas canvas = context.canvas; + final double scale = activationAnimation.value; + _pathPainter.paint( + parentBox: parentBox, + canvas: canvas, + center: center, + scale: scale, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + backgroundPaintColor: sliderTheme.valueIndicatorColor, + strokePaintColor: isOnTop ? sliderTheme.overlappingShapeStrokeColor : null, + ); + } +} + +class _RectangularSliderValueIndicatorPathPainter { + const _RectangularSliderValueIndicatorPathPainter(); + + static const double _triangleHeight = 8.0; + static const double _labelPadding = 16.0; + static const double _preferredHeight = 32.0; + static const double _minLabelWidth = 16.0; + static const double _bottomTipYOffset = 14.0; + static const double _preferredHalfHeight = _preferredHeight / 2; + static const double _upperRectRadius = 4; + + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, + TextPainter labelPainter, + double textScaleFactor, + ) { + assert(labelPainter != null); + return Size( + _upperRectangleWidth(labelPainter, 1, textScaleFactor), + labelPainter.height + _labelPadding, + ); + } + + double getHorizontalShift({ + RenderBox parentBox, + Offset center, + TextPainter labelPainter, + double textScaleFactor, + Size sizeWithOverflow, + double scale, + }) { + assert(!sizeWithOverflow.isEmpty); + const double edgePadding = 8.0; + final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); + + // The rectangle must be shifted towards the center so that it minimizes the + // chance of it rendering outside the bounds of the render box. If the shift + // is negative, then the lobe is shifted from right to left, and if it is + // positive, then the lobe is shifted from left to right. + final double overflowLeft = math.max(0, rectangleWidth / 2 - center.dx + edgePadding); + final double overflowRight = math.max(0, rectangleWidth / 2 - (sizeWithOverflow.width - center.dx - edgePadding)); + return overflowLeft - overflowRight; + } + + double _upperRectangleWidth(TextPainter labelPainter, double scale, double textScaleFactor) { + final double unscaledWidth = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2; + return unscaledWidth * scale; + } + + void paint({ + RenderBox parentBox, + Canvas canvas, + Offset center, + double scale, + TextPainter labelPainter, + double textScaleFactor, + Size sizeWithOverflow, + Color backgroundPaintColor, + Color strokePaintColor, + }) { + if (scale == 0.0) { + // Zero scale essentially means "do not draw anything", so it's safe to just return. + return; + } + assert(!sizeWithOverflow.isEmpty); + + final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); + final double horizontalShift = getHorizontalShift( + parentBox: parentBox, + center: center, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + scale: scale, + ); + + final double rectHeight = labelPainter.height + _labelPadding; + final Rect upperRect = Rect.fromLTWH( + -rectangleWidth / 2 + horizontalShift, + -_triangleHeight - rectHeight, + rectangleWidth, + rectHeight, + ); + + final Path trianglePath = Path() + ..lineTo(-_triangleHeight, -_triangleHeight) + ..lineTo(_triangleHeight, -_triangleHeight) + ..close(); + final Paint fillPaint = Paint()..color = backgroundPaintColor; + final RRect upperRRect = RRect.fromRectAndRadius(upperRect, const Radius.circular(_upperRectRadius)); + trianglePath.addRRect(upperRRect); + + canvas.save(); + // Prepare the canvas for the base of the tooltip, which is relative to the + // center of the thumb. + canvas.translate(center.dx, center.dy - _bottomTipYOffset); + canvas.scale(scale, scale); + if (strokePaintColor != null) { + final Paint strokePaint = Paint() + ..color = strokePaintColor + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + canvas.drawPath(trianglePath, strokePaint); + } + canvas.drawPath(trianglePath, fillPaint); + + // The label text is centered within the value indicator. + final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height; + canvas.translate(0, bottomTipToUpperRectTranslateY); + final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2); + final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2); + final Offset labelOffset = boxCenter - halfLabelPainterOffset; + labelPainter.paint(canvas, labelOffset); + canvas.restore(); + } +} + +/// A variant shape of a [Slider]'s value indicator . +/// +/// See also: +/// +/// * [Slider], which includes a value indicator defined by this shape. +/// * [SliderTheme], which can be used to configure the slider value indicator +/// of all sliders in a widget subtree. +class PaddleSliderValueIndicatorShape extends SliderComponentShape { + /// Create a slider value indicator in the shape of an upside-down pear. + const PaddleSliderValueIndicatorShape(); + + static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter(); + + @override + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + @required TextPainter labelPainter, + @required double textScaleFactor, + }) { + assert(labelPainter != null); + assert(textScaleFactor != null && textScaleFactor >= 0); + return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor); + } + + @override + void paint( + PaintingContext context, + Offset center, { + @required Animation activationAnimation, + @required Animation enableAnimation, + bool isDiscrete, + @required TextPainter labelPainter, + @required RenderBox parentBox, + @required SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + double textScaleFactor, + Size sizeWithOverflow, + }) { + assert(context != null); + assert(center != null); + assert(activationAnimation != null); + assert(enableAnimation != null); + assert(labelPainter != null); + assert(parentBox != null); + assert(sliderTheme != null); + assert(!sizeWithOverflow.isEmpty); + final ColorTween enableColor = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.valueIndicatorColor, + ); + _pathPainter.paint( + parentBox, + context.canvas, + center, + Paint()..color = enableColor.evaluate(enableAnimation), + activationAnimation.value, + labelPainter, + textScaleFactor, + sizeWithOverflow, + null, + ); + } +} + +/// A variant shape of a [RangeSlider]'s value indicators. +/// +/// See also: +/// +/// * [RangeSlider], which includes value indicators defined by this shape. +/// * [SliderTheme], which can be used to configure the range slider value +/// indicator of all sliders in a widget subtree. +class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { + /// Create a slider value indicator in the shape of an upside-down pear. + const PaddleRangeSliderValueIndicatorShape(); + + static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter(); + + @override + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + @required TextPainter labelPainter, + @required double textScaleFactor, + }) { + assert(labelPainter != null); + assert(textScaleFactor != null && textScaleFactor >= 0); + return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor); + } + + @override + double getHorizontalShift({ + RenderBox parentBox, + Offset center, + TextPainter labelPainter, + Animation activationAnimation, + double textScaleFactor, + Size sizeWithOverflow, }) { return _pathPainter.getHorizontalShift( parentBox: parentBox, center: center, labelPainter: labelPainter, scale: activationAnimation.value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, ); } @@ -2540,6 +2975,8 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap TextDirection textDirection, Thumb thumb, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { assert(context != null); assert(center != null); @@ -2548,25 +2985,28 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap assert(labelPainter != null); assert(parentBox != null); assert(sliderTheme != null); + assert(!sizeWithOverflow.isEmpty); final ColorTween enableColor = ColorTween( begin: sliderTheme.disabledThumbColor, end: sliderTheme.valueIndicatorColor, ); // Add a stroke of 1dp around the top paddle. - _pathPainter.drawValueIndicator( + _pathPainter.paint( parentBox, context.canvas, center, Paint()..color = enableColor.evaluate(enableAnimation), activationAnimation.value, labelPainter, + textScaleFactor, + sizeWithOverflow, isOnTop ? sliderTheme.overlappingShapeStrokeColor : null, ); } } -class _PaddleSliderTrackShapePathPainter { - const _PaddleSliderTrackShapePathPainter(); +class _PaddleSliderValueIndicatorPathPainter { + const _PaddleSliderValueIndicatorPathPainter(); // These constants define the shape of the default value indicator. // The value indicator changes shape based on the size of @@ -2576,14 +3016,12 @@ class _PaddleSliderTrackShapePathPainter { // Radius of the top lobe of the value indicator. static const double _topLobeRadius = 16.0; - // Designed size of the label text. This is the size that the value indicator - // was designed to contain. We scale it from here to fit other sizes. - static const double _labelTextDesignSize = 14.0; + static const double _minLabelWidth = 16.0; // Radius of the bottom lobe of the value indicator. static const double _bottomLobeRadius = 10.0; static const double _labelPadding = 8.0; static const double _distanceBetweenTopBottomCenters = 40.0; - static const double _middleNeckWidth = 2.0; + static const double _middleNeckWidth = 3.0; static const double _bottomNeckRadius = 4.5; // The base of the triangle between the top lobe center and the centers of // the two top neck arcs. @@ -2611,10 +3049,12 @@ class _PaddleSliderTrackShapePathPainter { bool isEnabled, bool isDiscrete, TextPainter labelPainter, + double textScaleFactor, ) { assert(labelPainter != null); - final double textScaleFactor = labelPainter.height / _labelTextDesignSize; - return Size(labelPainter.width + 2 * _labelPadding * textScaleFactor, _preferredHeight * textScaleFactor); + assert(textScaleFactor != null && textScaleFactor >= 0); + final double width = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2 * textScaleFactor; + return Size(width, _preferredHeight * textScaleFactor); } // Adds an arc to the path that has the attributes passed in. This is @@ -2630,15 +3070,17 @@ class _PaddleSliderTrackShapePathPainter { Offset center, TextPainter labelPainter, double scale, + double textScaleFactor, + Size sizeWithOverflow, }) { - final double textScaleFactor = labelPainter.height / _labelTextDesignSize; + assert(!sizeWithOverflow.isEmpty); final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0; final double labelHalfWidth = labelPainter.width / 2.0; final double halfWidthNeeded = math.max( 0.0, inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding), ); - final double shift = _getIdealOffset(parentBox, halfWidthNeeded, textScaleFactor * scale, center); + final double shift = _getIdealOffset(parentBox, halfWidthNeeded, textScaleFactor * scale, center, sizeWithOverflow.width); return shift * textScaleFactor; } @@ -2649,8 +3091,9 @@ class _PaddleSliderTrackShapePathPainter { double halfWidthNeeded, double scale, Offset center, + double widthWithOverflow, ) { - const double edgeMargin = 4.0; + const double edgeMargin = 8.0; final Rect topLobeRect = Rect.fromLTWH( -_topLobeRadius - halfWidthNeeded, -_topLobeRadius - _distanceBetweenTopBottomCenters, @@ -2663,12 +3106,11 @@ class _PaddleSliderTrackShapePathPainter { final Offset bottomRight = (topLobeRect.bottomRight * scale) + center; double shift = 0.0; - final double startGlobal = parentBox.localToGlobal(Offset.zero).dx; - if (topLeft.dx < startGlobal + edgeMargin) { - shift = startGlobal + edgeMargin - topLeft.dx; + if (topLeft.dx < edgeMargin) { + shift = edgeMargin - topLeft.dx; } - final double endGlobal = parentBox.localToGlobal(Offset(parentBox.size.width, parentBox.size.height)).dx; + final double endGlobal = widthWithOverflow; if (bottomRight.dx > endGlobal - edgeMargin) { shift = endGlobal - edgeMargin - bottomRight.dx; } @@ -2684,13 +3126,15 @@ class _PaddleSliderTrackShapePathPainter { return shift; } - void drawValueIndicator( + void paint( RenderBox parentBox, Canvas canvas, Offset center, Paint paint, double scale, TextPainter labelPainter, + double textScaleFactor, + Size sizeWithOverflow, Color strokePaintColor, ) { if (scale == 0.0) { @@ -2698,10 +3142,10 @@ class _PaddleSliderTrackShapePathPainter { // our math below will attempt to divide by zero and send needless NaNs to the engine. return; } + assert(!sizeWithOverflow.isEmpty); // The entire value indicator should scale with the size of the label, // to keep it large enough to encompass the label text. - final double textScaleFactor = labelPainter.height / _labelTextDesignSize; final double overallScale = scale * textScaleFactor; final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0; final double labelHalfWidth = labelPainter.width / 2.0; @@ -2743,7 +3187,7 @@ class _PaddleSliderTrackShapePathPainter { inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding), ); - final double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center); + final double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center, sizeWithOverflow.width); final double leftWidthNeeded = halfWidthNeeded - shift; final double rightWidthNeeded = halfWidthNeeded + shift; @@ -2752,7 +3196,8 @@ class _PaddleSliderTrackShapePathPainter { 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. + // and vertical. The base amount is chosen so that the neck is smooth, + // even when the lobe is shifted due to its size. final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees; final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees; // The center of the top left neck arc. diff --git a/packages/flutter/test/material/inherited_theme_test.dart b/packages/flutter/test/material/inherited_theme_test.dart index 07ea0673cc..dde79202ff 100644 --- a/packages/flutter/test/material/inherited_theme_test.dart +++ b/packages/flutter/test/material/inherited_theme_test.dart @@ -525,7 +525,7 @@ void main() { await tester.tap(find.text('push wrapped')); await tester.pumpAndSettle(); // route animation RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); - expect(sliderBox, paints..rect(color: activeTrackColor)..rect(color: inactiveTrackColor)); + expect(sliderBox, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)); expect(sliderBox, paints..circle(color: thumbColor)); Navigator.of(navigatorContext).pop(); @@ -534,7 +534,7 @@ void main() { await tester.tap(find.text('push unwrapped')); await tester.pumpAndSettle(); // route animation sliderBox = tester.firstRenderObject(find.byType(Slider)); - expect(sliderBox, isNot(paints..rect(color: activeTrackColor)..rect(color: inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor))); expect(sliderBox, isNot(paints..circle(color: thumbColor))); }); diff --git a/packages/flutter/test/material/range_slider_test.dart b/packages/flutter/test/material/range_slider_test.dart index 4972ea0808..b9b3afe134 100644 --- a/packages/flutter/test/material/range_slider_test.dart +++ b/packages/flutter/test/material/range_slider_test.dart @@ -1033,15 +1033,17 @@ void main() { // Check default theme for enabled widget. expect(sliderBox, paints - ..rect(color: sliderTheme.inactiveTrackColor) + ..rrect(color: sliderTheme.inactiveTrackColor) ..rect(color: sliderTheme.activeTrackColor) - ..rect(color: sliderTheme.inactiveTrackColor)); + ..rrect(color: sliderTheme.inactiveTrackColor)); expect(sliderBox, paints ..circle(color: sliderTheme.thumbColor) ..circle(color: sliderTheme.thumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); }); @@ -1058,18 +1060,22 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.inactiveTrackColor) + ..rrect(color: sliderTheme.inactiveTrackColor) ..rect(color: activeColor) - ..rect(color: sliderTheme.inactiveTrackColor)); + ..rrect(color: sliderTheme.inactiveTrackColor)); expect( sliderBox, paints + ..circle(color: Colors.white) ..circle(color: activeColor) + ..circle(color: Colors.white) ..circle(color: activeColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); }); testWidgets('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async { @@ -1084,9 +1090,9 @@ void main() { expect( sliderBox, paints - ..rect(color: inactiveColor) + ..rrect(color: inactiveColor) ..rect(color: sliderTheme.activeTrackColor) - ..rect(color: inactiveColor)); + ..rrect(color: inactiveColor)); expect( sliderBox, paints @@ -1095,6 +1101,8 @@ void main() { expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); }); testWidgets('Range Slider uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async { @@ -1114,18 +1122,20 @@ void main() { expect( sliderBox, paints - ..rect(color: inactiveColor) + ..rrect(color: inactiveColor) ..rect(color: activeColor) - ..rect(color: inactiveColor)); + ..rrect(color: inactiveColor)); expect( sliderBox, paints + ..circle(color: Colors.white) ..circle(color: activeColor) + ..circle(color: Colors.white) ..circle(color: activeColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); }); testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async { @@ -1139,9 +1149,9 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.inactiveTrackColor) + ..rrect(color: sliderTheme.inactiveTrackColor) ..rect(color: sliderTheme.activeTrackColor) - ..rect(color: sliderTheme.inactiveTrackColor)); + ..rrect(color: sliderTheme.inactiveTrackColor)); expect( sliderBox, paints @@ -1154,6 +1164,8 @@ void main() { expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); }); testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async { @@ -1175,9 +1187,9 @@ void main() { expect( sliderBox, paints - ..rect(color: inactiveColor) + ..rrect(color: inactiveColor) ..rect(color: activeColor) - ..rect(color: inactiveColor)); + ..rrect(color: inactiveColor)); expect( sliderBox, paints @@ -1185,12 +1197,16 @@ void main() { ..circle(color: activeColor) ..circle(color: inactiveColor) ..circle(color: activeColor) + ..circle(color: Colors.white) ..circle(color: activeColor) + ..circle(color: Colors.white) ..circle(color: activeColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); }); @@ -1206,12 +1222,14 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.disabledInactiveTrackColor) + ..rrect(color: sliderTheme.disabledInactiveTrackColor) ..rect(color: sliderTheme.disabledActiveTrackColor) - ..rect(color: sliderTheme.disabledInactiveTrackColor)); + ..rrect(color: sliderTheme.disabledInactiveTrackColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.activeTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor))); }); @@ -1233,17 +1251,17 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.disabledInactiveTrackColor) + ..rrect(color: sliderTheme.disabledInactiveTrackColor) ..rect(color: sliderTheme.disabledActiveTrackColor) - ..rect(color: sliderTheme.disabledInactiveTrackColor)); + ..rrect(color: sliderTheme.disabledInactiveTrackColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.activeTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor))); }); testWidgets('Range Slider uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async { - const Color customColor1 = Color(0xcafefeed); - const Color customColor2 = Color(0xdeadbeef); final ThemeData theme = _buildTheme(); final SliderThemeData sliderTheme = theme.sliderTheme; RangeValues values = const RangeValues(0.5, 0.75); @@ -1285,7 +1303,7 @@ void main() { final RenderBox sliderBox = tester.firstRenderObject(find.byType(RangeSlider)); final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0); - TestGesture gesture = await tester.startGesture(topRight); + final TestGesture gesture = await tester.startGesture(topRight); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect(values.end, equals(1)); @@ -1298,24 +1316,6 @@ void main() { await gesture.up(); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); - - // Testing the custom colors are used for the indicator. - await tester.pumpWidget(buildApp( - divisions: 3, - activeColor: customColor1, - inactiveColor: customColor2, - )); - gesture = await tester.startGesture(topRight); - // Wait for value indicator animation to finish. - await tester.pumpAndSettle(); - expect(values.end, equals(1)); - expect( - sliderBox, - paints - ..path(color: customColor1) - ..path(color: customColor1), - ); - await gesture.up(); }); testWidgets('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index 40416727c9..f1019eed1d 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -39,6 +39,8 @@ class LoggingThumbShape extends SliderComponentShape { SliderThemeData sliderTheme, TextDirection textDirection, double value, + double textScaleFactor, + Size sizeWithOverflow, }) { log.add(thumbCenter); final Paint thumbPaint = Paint()..color = Colors.red; @@ -582,6 +584,7 @@ void main() { overlayColor: Color(0xff000010), thumbColor: Color(0xff000011), valueIndicatorColor: Color(0xff000012), + surfaceColor: Colors.white, ), ); final SliderThemeData sliderTheme = theme.sliderTheme; @@ -625,43 +628,47 @@ void main() { final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); // Check default theme for enabled widget. - expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor)); + expect(sliderBox, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor)); + expect(sliderBox, paints..shadow(color: const Color(0xff000000))); expect(sliderBox, paints..circle(color: sliderTheme.thumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); 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.inactiveTrackColor)); - expect(sliderBox, paints..circle(color: customColor1)); + expect(sliderBox, paints..rrect(color: customColor1)..rrect(color: sliderTheme.inactiveTrackColor)); + expect(sliderBox, paints..shadow(color: Colors.black)); + expect(sliderBox, paints..circle(color: Colors.white)..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.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); // Test setting only the inactiveColor. await tester.pumpWidget(buildApp(inactiveColor: customColor1)); - expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: customColor1)); + expect(sliderBox, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: customColor1)); + expect(sliderBox, paints..shadow(color: Colors.black)); expect(sliderBox, paints..circle(color: sliderTheme.thumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); // 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, paints..rrect(color: customColor1)..rrect(color: customColor2)); + expect(sliderBox, paints..shadow(color: Colors.black)); + expect(sliderBox, paints..circle(color: Colors.white)..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.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); // Test colors for discrete slider. await tester.pumpWidget(buildApp(divisions: 3)); - expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor)); + expect(sliderBox, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor)); expect( sliderBox, paints @@ -669,10 +676,12 @@ void main() { ..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) - ..circle(color: sliderTheme.thumbColor)); + ..shadow(color: Colors.black) + ..circle(color: sliderTheme.thumbColor) + ); expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); // Test colors for discrete slider with inactiveColor and activeColor set. await tester.pumpWidget(buildApp( @@ -680,7 +689,7 @@ void main() { inactiveColor: customColor2, divisions: 3, )); - expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2)); + expect(sliderBox, paints..rrect(color: customColor1)..rrect(color: customColor2)); expect( sliderBox, paints @@ -688,11 +697,13 @@ void main() { ..circle(color: customColor2) ..circle(color: customColor1) ..circle(color: customColor1) + ..shadow(color: Colors.black) + ..circle(color: Colors.white) ..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.disabledActiveTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); @@ -702,24 +713,24 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.disabledActiveTrackColor) - ..rect(color: sliderTheme.disabledInactiveTrackColor)); - expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor)); + ..rrect(color: sliderTheme.disabledActiveTrackColor) + ..rrect(color: sliderTheme.disabledInactiveTrackColor)); + expect(sliderBox, paints..shadow(color: Colors.black)..circle(color: sliderTheme.disabledThumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.activeTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor))); // 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.disabledActiveTrackColor) - ..rect(color: sliderTheme.disabledInactiveTrackColor)); - expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor)); + ..rrect(color: sliderTheme.disabledActiveTrackColor) + ..rrect(color: sliderTheme.disabledInactiveTrackColor)); + expect(sliderBox, paints..shadow(color: Colors.black)..circle(color: sliderTheme.disabledThumbColor)); expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); - expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.activeTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor))); // Test that the default value indicator has the right colors. await tester.pumpWidget(buildApp(divisions: 3)); @@ -731,14 +742,15 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.activeTrackColor) - ..rect(color: sliderTheme.inactiveTrackColor) + ..rrect(color: sliderTheme.activeTrackColor) + ..rrect(color: sliderTheme.inactiveTrackColor) ..circle(color: sliderTheme.overlayColor) ..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) ..path(color: sliderTheme.valueIndicatorColor) + ..shadow(color: Colors.black) ..circle(color: sliderTheme.thumbColor), ); await gesture.up(); @@ -759,14 +771,16 @@ void main() { expect( sliderBox, paints - ..rect(color: customColor1) // active track - ..rect(color: customColor2) // inactive track + ..rrect(color: customColor1) // active track + ..rrect(color: customColor2) // inactive track ..circle(color: customColor1.withOpacity(0.12)) // overlay ..circle(color: customColor2) // 1st tick mark ..circle(color: customColor2) // 2nd tick mark ..circle(color: customColor2) // 3rd tick mark ..circle(color: customColor1) // 4th tick mark - ..path(color: customColor1) // indicator + ..path(color: sliderTheme.valueIndicatorColor) // indicator + ..shadow(color: Colors.black) + ..circle(color: Colors.white) ..circle(color: customColor1), // thumb ); await gesture.up(); @@ -970,7 +984,19 @@ void main() { TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); - expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0)); + expect( + tester.renderObject(find.byType(Slider)), + paints + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(0.0, -38.0), + Offset(-30.0, -16.0), + Offset(30.0, -16.0), + ], + color: const Color(0xf55f5f5f), + ), + ); await gesture.up(); await tester.pumpAndSettle(); @@ -980,7 +1006,19 @@ void main() { gesture = await tester.startGesture(center); await tester.pumpAndSettle(); - expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0)); + expect( + tester.renderObject(find.byType(Slider)), + paints + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(0.0, -52.0), + Offset(-44.0, -16.0), + Offset(44.0, -16.0), + ], + color: const Color(0xf55f5f5f), + ), + ); await gesture.up(); await tester.pumpAndSettle(); @@ -995,7 +1033,18 @@ void main() { gesture = await tester.startGesture(center); await tester.pumpAndSettle(); - expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0)); + expect(tester.renderObject(find.byType(Slider)), + paints + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(0.0, -38.0), + Offset(-30.0, -16.0), + Offset(30.0, -16.0), + ], + color: const Color(0xf55f5f5f), + ), + ); await gesture.up(); await tester.pumpAndSettle(); @@ -1009,7 +1058,19 @@ void main() { gesture = await tester.startGesture(center); await tester.pumpAndSettle(); - expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0)); + expect( + tester.renderObject(find.byType(Slider)), + paints + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(0.0, -52.0), + Offset(-44.0, -16.0), + Offset(44.0, -16.0), + ], + color: const Color(0xf55f5f5f), + ), + ); await gesture.up(); await tester.pumpAndSettle(); @@ -1114,11 +1175,11 @@ void main() { expect( sliderBox, paints - ..circle(x: 25.0, y: 24.0, radius: 1.0) - ..circle(x: 212.5, y: 24.0, radius: 1.0) + ..circle(x: 26.0, y: 24.0, radius: 1.0) + ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) - ..circle(x: 587.5, y: 24.0, radius: 1.0) - ..circle(x: 775.0, y: 24.0, radius: 1.0) + ..circle(x: 587.0, y: 24.0, radius: 1.0) + ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 24.0, y: 24.0, radius: 10.0), ); @@ -1131,11 +1192,11 @@ void main() { sliderBox, paints ..circle(x: 111.20703125, y: 24.0, radius: 5.687664985656738) - ..circle(x: 25.0, y: 24.0, radius: 1.0) - ..circle(x: 212.5, y: 24.0, radius: 1.0) + ..circle(x: 26.0, y: 24.0, radius: 1.0) + ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) - ..circle(x: 587.5, y: 24.0, radius: 1.0) - ..circle(x: 775.0, y: 24.0, radius: 1.0) + ..circle(x: 587.0, y: 24.0, radius: 1.0) + ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 111.20703125, y: 24.0, radius: 10.0), ); @@ -1151,11 +1212,11 @@ void main() { sliderBox, paints ..circle(x: 190.0135726928711, y: 24.0, radius: 12.0) - ..circle(x: 25.0, y: 24.0, radius: 1.0) - ..circle(x: 212.5, y: 24.0, radius: 1.0) + ..circle(x: 26.0, y: 24.0, radius: 1.0) + ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) - ..circle(x: 587.5, y: 24.0, radius: 1.0) - ..circle(x: 775.0, y: 24.0, radius: 1.0) + ..circle(x: 587.0, y: 24.0, radius: 1.0) + ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 190.0135726928711, y: 24.0, radius: 10.0), ); // Wait for animations to finish. @@ -1165,11 +1226,11 @@ void main() { sliderBox, paints ..circle(x: 400.0, y: 24.0, radius: 24.0) - ..circle(x: 25.0, y: 24.0, radius: 1.0) - ..circle(x: 212.5, y: 24.0, radius: 1.0) + ..circle(x: 26.0, y: 24.0, radius: 1.0) + ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) - ..circle(x: 587.5, y: 24.0, radius: 1.0) - ..circle(x: 775.0, y: 24.0, radius: 1.0) + ..circle(x: 587.0, y: 24.0, radius: 1.0) + ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 10.0), ); await gesture.up(); @@ -1178,11 +1239,11 @@ void main() { expect( sliderBox, paints - ..circle(x: 25.0, y: 24.0, radius: 1.0) - ..circle(x: 212.5, y: 24.0, radius: 1.0) + ..circle(x: 26.0, y: 24.0, radius: 1.0) + ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) - ..circle(x: 587.5, y: 24.0, radius: 1.0) - ..circle(x: 775.0, y: 24.0, radius: 1.0) + ..circle(x: 587.0, y: 24.0, radius: 1.0) + ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 10.0), ); } diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 0fe0688602..8783b59018 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -107,8 +107,8 @@ void main() { expect( sliderBox, paints - ..rect(color: sliderTheme.disabledActiveTrackColor) - ..rect(color: sliderTheme.disabledInactiveTrackColor), + ..rrect(color: sliderTheme.disabledActiveTrackColor) + ..rrect(color: sliderTheme.disabledInactiveTrackColor), ); }); @@ -129,8 +129,8 @@ void main() { expect( sliderBox, paints - ..rect(color: customTheme.disabledActiveTrackColor) - ..rect(color: customTheme.disabledInactiveTrackColor), + ..rrect(color: customTheme.disabledActiveTrackColor) + ..rrect(color: customTheme.disabledInactiveTrackColor), ); }); @@ -228,13 +228,15 @@ void main() { await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + const Radius radius = Radius.circular(2); + // The enabled slider thumb has track segments that extend to and from // the center of the thumb. expect( sliderBox, paints - ..rect(rect: const Rect.fromLTRB(25.0, 299.0, 202.0, 301.0), color: sliderTheme.activeTrackColor) - ..rect(rect: const Rect.fromLTRB(222.0, 299.0, 776.0, 301.0), color: sliderTheme.inactiveTrackColor), + ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 298.0, 212.0, 302.0, topLeft: radius, bottomLeft: radius), color: sliderTheme.activeTrackColor) + ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); @@ -249,8 +251,8 @@ void main() { expect( sliderBox, paints - ..rect(rect: const Rect.fromLTRB(25.0, 299.0, 202.0, 301.0), color: sliderTheme.disabledActiveTrackColor) - ..rect(rect: const Rect.fromLTRB(222.0, 299.0, 776.0, 301.0), color: sliderTheme.disabledInactiveTrackColor), + ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 298.0, 212.0, 302.0, topLeft: radius, bottomLeft: radius), color: sliderTheme.disabledActiveTrackColor) + ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor), ); }); @@ -364,7 +366,189 @@ void main() { platform: TargetPlatform.android, primarySwatch: Colors.blue, ); - final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always); + final SliderThemeData sliderTheme = theme.sliderTheme.copyWith( + thumbColor: Colors.red.shade500, + showValueIndicator: ShowValueIndicator.always, + ); + Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) { + return Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale), + child: Material( + child: Row( + children: [ + Expanded( + child: SliderTheme( + data: sliderTheme, + child: Slider( + value: sliderValue, + label: '$value', + divisions: 3, + onChanged: (double d) { }, + ), + ), + ), + ], + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp('1')); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + Offset center = tester.getCenter(find.byType(Slider)); + TestGesture gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-20.0, -12.0), + Offset(20.0, -34.0), + Offset(0.0, -38.0), + ], + color: const Color(0xf55f5f5f), + ), + ); + + 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); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..rrect() + ..rrect() + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-30.0, -12.0), + Offset(30.0, -34.0), + Offset(0.0, -38.0), + ], + color: const Color(0xf55f5f5f), + ), + ); + 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); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..rrect() + ..rrect() + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-12.0, -12.0), + Offset(110.0, -34.0), + Offset(0.0, -38.0), + ], + color: const Color(0xf55f5f5f), + ) + ); + 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); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..rrect() + ..rrect() + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-110.0, -12.0), + Offset(12.0, -34.0), + Offset(0.0, -38.0), + ], + color: const Color(0xf55f5f5f), + ) + ); + await gesture.up(); + + // Test that the box decreases in height when the text scale gets smaller. + await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 0.5)); + center = tester.getCenter(find.byType(Slider)); + gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..rrect() + ..rrect() + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-12.0, -12.0), + Offset(61.0, -16.0), + Offset(0.0, -20.0), + ], + excludes: const [ + Offset(0.0, -38.0) + ], + color: const Color(0xf55f5f5f), + ) + ); + await gesture.up(); + + // Test that the box increases in height when the text scale gets bigger. + await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 2.0)); + center = tester.getCenter(find.byType(Slider)); + gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + expect( + sliderBox, + paints + ..rrect() + ..rrect() + ..path( + includes: const [ + Offset(0.0, 0.0), + Offset(-12.0, -16.0), + Offset(208.0, -40.0), + Offset(0.0, -50.0), + ], + color: const Color(0xf55f5f5f), + ) + ); + await gesture.up(); + }, skip: isBrowser); + + testWidgets('Paddle slider value indicator shape draws correctly', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + platform: TargetPlatform.android, + primarySwatch: Colors.blue, + ); + final SliderThemeData sliderTheme = theme.sliderTheme.copyWith( + thumbColor: Colors.red.shade500, + showValueIndicator: ShowValueIndicator.always, + valueIndicatorShape: const PaddleSliderValueIndicatorShape(), + ); Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) { return Directionality( textDirection: TextDirection.ltr, @@ -532,6 +716,7 @@ void main() { testWidgets('The slider track height can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(trackHeight: 16); + const Radius radius = Radius.circular(8); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); @@ -541,8 +726,8 @@ void main() { expect( sliderBox, paints - ..rect(rect: const Rect.fromLTRB(32.0, 292.0, 202.0, 308.0), color: sliderTheme.activeTrackColor) - ..rect(rect: const Rect.fromLTRB(222.0, 292.0, 776.0, 308.0), color: sliderTheme.inactiveTrackColor), + ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 292.0, 212.0, 308.0, topLeft: radius, bottomLeft: radius), color: sliderTheme.activeTrackColor) + ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); @@ -553,8 +738,8 @@ void main() { expect( sliderBox, paints - ..rect(rect: const Rect.fromLTRB(32.0, 292.0, 202.0, 308.0), color: sliderTheme.disabledActiveTrackColor) - ..rect(rect: const Rect.fromLTRB(222.0, 292.0, 776.0, 308.0), color: sliderTheme.disabledInactiveTrackColor), + ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 292.0, 212.0, 308.0, topLeft: radius, bottomLeft: radius), color: sliderTheme.disabledActiveTrackColor) + ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor), ); }); @@ -623,9 +808,9 @@ void main() { expect( sliderBox, paints - ..circle(x: 29, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor) + ..circle(x: 26, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor) ..circle(x: 400, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor) - ..circle(x: 771, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor), + ..circle(x: 774, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false)); @@ -634,9 +819,9 @@ void main() { expect( sliderBox, paints - ..circle(x: 29, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor) + ..circle(x: 26, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor) ..circle(x: 400, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor) - ..circle(x: 771, y: 300, radius: 5, color: sliderTheme.disabledInactiveTickMarkColor), + ..circle(x: 774, y: 300, radius: 5, color: sliderTheme.disabledInactiveTickMarkColor), ); }); @@ -708,7 +893,7 @@ void main() { final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); // Only 2 track segments. - expect(sliderBox, paintsExactlyCountTimes(#drawRect, 2)); + expect(sliderBox, paintsExactlyCountTimes(#drawRRect, 2)); expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 0)); expect(sliderBox, paintsExactlyCountTimes(#drawPath, 0)); }); @@ -815,7 +1000,7 @@ void main() { await gesture.up(); }); - testWidgets('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { + testWidgets('PaddleSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( @@ -824,7 +1009,7 @@ void main() { thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.always, - rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(), + valueIndicatorShape: const PaddleSliderValueIndicatorShape(), ), value: 0.5, divisions: 4, @@ -846,6 +1031,96 @@ void main() { await gesture.up(); }); + + testWidgets('Default slider value indicator shape skips all painting at zero scale', (WidgetTester tester) async { + // Pump a slider with just a value indicator. + await tester.pumpWidget(_buildApp( + ThemeData().sliderTheme.copyWith( + trackHeight: 0, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: SliderComponentShape.noThumb, + tickMarkShape: SliderTickMarkShape.noTickMark, + showValueIndicator: ShowValueIndicator.always, + ), + value: 0.5, + divisions: 4, + )); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + // Tap the center of the track to kick off the animation of the value indicator. + final Offset center = tester.getCenter(find.byType(Slider)); + final TestGesture gesture = await tester.startGesture(center); + + // Nothing to paint at scale 0. + await tester.pump(); + expect(sliderBox, paintsNothing); + + // Painting a path for the value indicator. + await tester.pump(const Duration(milliseconds: 16)); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 1)); + + await gesture.up(); + }); + + testWidgets('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { + // Pump a slider with just a value indicator. + await tester.pumpWidget(_buildRangeApp( + ThemeData().sliderTheme.copyWith( + trackHeight: 0, + rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(), + ), + values: const RangeValues(0, 0.5), + divisions: 4, + )); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(RangeSlider)); + + // Tap the center of the track to kick off the animation of the value indicator. + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + + // No value indicator path to paint at scale 0. + await tester.pump(); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 0)); + + // Painting a path for each value indicator. + await tester.pump(const Duration(milliseconds: 16)); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 2)); + + await gesture.up(); + }); + + testWidgets('Default range indicator shape skips all painting at zero scale', (WidgetTester tester) async { + // Pump a slider with just a value indicator. + await tester.pumpWidget(_buildRangeApp( + ThemeData().sliderTheme.copyWith( + trackHeight: 0, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: SliderComponentShape.noThumb, + tickMarkShape: SliderTickMarkShape.noTickMark, + showValueIndicator: ShowValueIndicator.always, + ), + values: const RangeValues(0, 0.5), + divisions: 4, + )); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(RangeSlider)); + + // Tap the center of the track to kick off the animation of the value indicator. + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + + // No value indicator path to paint at scale 0. + await tester.pump(); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 0)); + + // Painting a path for each value indicator. + await tester.pump(const Duration(milliseconds: 16)); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 2)); + + await gesture.up(); + }); } Widget _buildApp( @@ -871,3 +1146,27 @@ Widget _buildApp( ), ); } + +Widget _buildRangeApp( + SliderThemeData sliderTheme, { + RangeValues values = const RangeValues(0, 0), + bool enabled = true, + int divisions, + }) { + final ValueChanged onChanged = enabled ? (RangeValues d) => values = d : null; + return MaterialApp( + home: Scaffold( + body: Center( + child: SliderTheme( + data: sliderTheme, + child: RangeSlider( + values: values, + labels: RangeLabels(values.start.toString(), values.end.toString()), + onChanged: onChanged, + divisions: divisions, + ), + ), + ), + ), + ); +}