Unverified Commit e71cf1cd authored by Jose Alba's avatar Jose Alba Committed by GitHub

[Slider] Rebase. (#52663)

parent a2d62df3
...@@ -69,6 +69,7 @@ class _CustomRangeThumbShape extends RangeSliderThumbShape { ...@@ -69,6 +69,7 @@ class _CustomRangeThumbShape extends RangeSliderThumbShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
Thumb thumb, Thumb thumb,
bool isPressed,
}) { }) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween( final ColorTween colorTween = ColorTween(
...@@ -130,6 +131,8 @@ class _CustomThumbShape extends SliderComponentShape { ...@@ -130,6 +131,8 @@ class _CustomThumbShape extends SliderComponentShape {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween( final ColorTween colorTween = ColorTween(
...@@ -169,6 +172,8 @@ class _CustomValueIndicatorShape extends SliderComponentShape { ...@@ -169,6 +172,8 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final ColorTween enableColor = ColorTween( final ColorTween enableColor = ColorTween(
...@@ -268,7 +273,12 @@ class _SlidersState extends State<_Sliders> { ...@@ -268,7 +273,12 @@ class _SlidersState extends State<_Sliders> {
), ),
), ),
), ),
Slider.adaptive( SliderTheme(
data: const SliderThemeData(
showValueIndicator: ShowValueIndicator.always,
),
child: Slider.adaptive(
label: _continuousValue.toStringAsFixed(6).toString(),
value: _continuousValue, value: _continuousValue,
min: 0.0, min: 0.0,
max: 100.0, max: 100.0,
...@@ -278,6 +288,7 @@ class _SlidersState extends State<_Sliders> { ...@@ -278,6 +288,7 @@ class _SlidersState extends State<_Sliders> {
}); });
}, },
), ),
),
const Text('Continuous with Editable Numerical Value'), const Text('Continuous with Editable Numerical Value'),
], ],
), ),
......
...@@ -21,10 +21,12 @@ ThemeData _buildDarkTheme() { ...@@ -21,10 +21,12 @@ ThemeData _buildDarkTheme() {
final ColorScheme colorScheme = const ColorScheme.dark().copyWith( final ColorScheme colorScheme = const ColorScheme.dark().copyWith(
primary: primaryColor, primary: primaryColor,
secondary: secondaryColor, secondary: secondaryColor,
onPrimary: secondaryColor,
); );
final ThemeData base = ThemeData( final ThemeData base = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
accentColorBrightness: Brightness.dark, accentColorBrightness: Brightness.dark,
colorScheme: colorScheme,
primaryColor: primaryColor, primaryColor: primaryColor,
primaryColorDark: const Color(0xFF0050a0), primaryColorDark: const Color(0xFF0050a0),
primaryColorLight: secondaryColor, primaryColorLight: secondaryColor,
......
...@@ -22,6 +22,11 @@ import 'theme.dart'; ...@@ -22,6 +22,11 @@ import 'theme.dart';
// RangeValues _dollarsRange = RangeValues(50, 100); // RangeValues _dollarsRange = RangeValues(50, 100);
// void setState(VoidCallback fn) { } // void setState(VoidCallback fn) { }
/// [RangeSlider] uses this callback to paint the value indicator on the overlay.
/// Since the value indicator is painted on the Overlay; this method paints the
/// value indicator in a [RenderBox] that appears in the [Overlay].
typedef PaintRangeValueIndicator = void Function(PaintingContext context, Offset offset);
/// A Material Design range slider. /// A Material Design range slider.
/// ///
/// Used to select a range from a range of values. /// Used to select a range from a range of values.
...@@ -127,6 +132,7 @@ class RangeSlider extends StatefulWidget { ...@@ -127,6 +132,7 @@ class RangeSlider extends StatefulWidget {
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.useV2Slider = false,
}) : assert(values != null), }) : assert(values != null),
assert(min != null), assert(min != null),
assert(max != null), assert(max != null),
...@@ -135,6 +141,7 @@ class RangeSlider extends StatefulWidget { ...@@ -135,6 +141,7 @@ class RangeSlider extends StatefulWidget {
assert(values.start >= min && values.start <= max), assert(values.start >= min && values.start <= max),
assert(values.end >= min && values.end <= max), assert(values.end >= min && values.end <= max),
assert(divisions == null || divisions > 0), assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key); super(key: key);
/// The currently selected values for this range slider. /// The currently selected values for this range slider.
...@@ -333,6 +340,19 @@ class RangeSlider extends StatefulWidget { ...@@ -333,6 +340,19 @@ class RangeSlider extends StatefulWidget {
/// {@end-tool} /// {@end-tool}
final RangeSemanticFormatterCallback semanticFormatterCallback; final RangeSemanticFormatterCallback semanticFormatterCallback;
/// Whether to use the updated Material spec version of the [RangeSlider].
/// * The v2 [RangeSlider] has an updated value indicator that matches the latest specs.
/// * The value indicator is painted on the Overlay.
/// * The active track is bigger than the inactive track.
/// * The thumb that is activated has elevation.
/// * Updated value indicators in case they overlap with each other.
/// * <https://groups.google.com/g/flutter-announce/c/69dmlKUL5Ew/m/tQh-ajiEAAAJl>
///
/// This is a temporary flag for migrating the slider from v1 to v2. Currently
/// this defaults to false, because the changes may break existing tests. This
/// value will be defaulted to true in the future.
final bool useV2Slider;
// Touch width for the tap boundary of the slider thumbs. // Touch width for the tap boundary of the slider thumbs.
static const double _minTouchTargetWidth = kMinInteractiveDimension; static const double _minTouchTargetWidth = kMinInteractiveDimension;
...@@ -354,6 +374,7 @@ class RangeSlider extends StatefulWidget { ...@@ -354,6 +374,7 @@ class RangeSlider extends StatefulWidget {
properties.add(StringProperty('labelEnd', labels?.end)); properties.add(StringProperty('labelEnd', labels?.end));
properties.add(ColorProperty('activeColor', activeColor)); properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('inactiveColor', inactiveColor)); properties.add(ColorProperty('inactiveColor', inactiveColor));
properties.add(FlagProperty('useV2Slider', value: useV2Slider, ifFalse: 'useV1Slider'));
properties.add(ObjectFlagProperty<ValueChanged<RangeValues>>.has('semanticFormatterCallback', semanticFormatterCallback)); properties.add(ObjectFlagProperty<ValueChanged<RangeValues>>.has('semanticFormatterCallback', semanticFormatterCallback));
} }
} }
...@@ -377,6 +398,10 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -377,6 +398,10 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
AnimationController startPositionController; AnimationController startPositionController;
AnimationController endPositionController; AnimationController endPositionController;
Timer interactionTimer; Timer interactionTimer;
// Value Indicator paint Animation that appears on the Overlay.
PaintRangeValueIndicator paintTopValueIndicator;
PaintRangeValueIndicator paintBottomValueIndicator;
@override @override
void initState() { void initState() {
...@@ -520,14 +545,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -520,14 +545,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
return null; return null;
}; };
static const double _defaultTrackHeight = 2;
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 ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
static const double _defaultMinThumbSeparation = 8;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -543,6 +561,29 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -543,6 +561,29 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
// colors come from the ThemeData.colorScheme. These colors, along with // colors come from the ThemeData.colorScheme. These colors, along with
// the default shapes and text styles are aligned to the Material // the default shapes and text styles are aligned to the Material
// Guidelines. // Guidelines.
final bool useV2Slider = widget.useV2Slider;
final double _defaultTrackHeight = useV2Slider ? 4 : 2;
final RangeSliderTrackShape _defaultTrackShape = RoundedRectRangeSliderTrackShape(useV2Slider: useV2Slider);
final RangeSliderTickMarkShape _defaultTickMarkShape = RoundRangeSliderTickMarkShape(useV2Slider: useV2Slider);
const SliderComponentShape _defaultOverlayShape = RoundSliderOverlayShape();
final RangeSliderThumbShape _defaultThumbShape = RoundRangeSliderThumbShape(useV2Slider: useV2Slider);
final RangeSliderValueIndicatorShape _defaultValueIndicatorShape = useV2Slider ? const RectangularRangeSliderValueIndicatorShape() : const PaddleRangeSliderValueIndicatorShape();
const ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
const double _defaultMinThumbSeparation = 8;
// The value indicator's 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( sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight, trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
...@@ -555,14 +596,14 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -555,14 +596,14 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12), disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary, thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface, overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
disabledThumbColor: sliderTheme.disabledThumbColor ?? theme.colorScheme.onSurface.withOpacity(0.38), disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), const Color(0xFFFFFFFF)),
overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12), overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12),
valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary, valueIndicatorColor: valueIndicatorColor,
rangeTrackShape: sliderTheme.rangeTrackShape ?? _defaultTrackShape, rangeTrackShape: sliderTheme.rangeTrackShape ?? _defaultTrackShape,
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? _defaultTickMarkShape, rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? _defaultTickMarkShape,
rangeThumbShape: sliderTheme.rangeThumbShape ?? _defaultThumbShape, rangeThumbShape: sliderTheme.rangeThumbShape ?? _defaultThumbShape,
overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape, overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape,
rangeValueIndicatorShape: sliderTheme.rangeValueIndicatorShape ?? _defaultValueIndicatorShape, rangeValueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator, showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator,
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith( valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
...@@ -571,19 +612,49 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -571,19 +612,49 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector, thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector,
); );
return _RangeSliderRenderObjectWidget( // 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 _screenSize() => MediaQuery.of(context).size;
return CompositedTransformTarget(
link: _layerLink,
child: _RangeSliderRenderObjectWidget(
values: _unlerpRangeValues(widget.values), values: _unlerpRangeValues(widget.values),
divisions: widget.divisions, divisions: widget.divisions,
labels: widget.labels, labels: widget.labels,
sliderTheme: sliderTheme, sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.of(context).textScaleFactor, textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this, state: this,
semanticFormatterCallback: widget.semanticFormatterCallback, semanticFormatterCallback: widget.semanticFormatterCallback,
useV2Slider: widget.useV2Slider,
),
); );
} }
final LayerLink _layerLink = LayerLink();
OverlayEntry overlayEntry;
void showValueIndicator() {
if (overlayEntry == null) {
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return CompositedTransformFollower(
link: _layerLink,
child: _ValueIndicatorRenderObjectWidget(
state: this,
),
);
},
);
Overlay.of(context).insert(overlayEntry);
}
}
} }
class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
...@@ -594,11 +665,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -594,11 +665,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
this.labels, this.labels,
this.sliderTheme, this.sliderTheme,
this.textScaleFactor, this.textScaleFactor,
this.screenSize,
this.onChanged, this.onChanged,
this.onChangeStart, this.onChangeStart,
this.onChangeEnd, this.onChangeEnd,
this.state, this.state,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.useV2Slider,
}) : super(key: key); }) : super(key: key);
final RangeValues values; final RangeValues values;
...@@ -606,11 +679,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -606,11 +679,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
final RangeLabels labels; final RangeLabels labels;
final SliderThemeData sliderTheme; final SliderThemeData sliderTheme;
final double textScaleFactor; final double textScaleFactor;
final Size screenSize;
final ValueChanged<RangeValues> onChanged; final ValueChanged<RangeValues> onChanged;
final ValueChanged<RangeValues> onChangeStart; final ValueChanged<RangeValues> onChangeStart;
final ValueChanged<RangeValues> onChangeEnd; final ValueChanged<RangeValues> onChangeEnd;
final RangeSemanticFormatterCallback semanticFormatterCallback; final RangeSemanticFormatterCallback semanticFormatterCallback;
final _RangeSliderState state; final _RangeSliderState state;
final bool useV2Slider;
@override @override
_RenderRangeSlider createRenderObject(BuildContext context) { _RenderRangeSlider createRenderObject(BuildContext context) {
...@@ -621,6 +696,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -621,6 +696,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
sliderTheme: sliderTheme, sliderTheme: sliderTheme,
theme: Theme.of(context), theme: Theme.of(context),
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
screenSize: screenSize,
onChanged: onChanged, onChanged: onChanged,
onChangeStart: onChangeStart, onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd, onChangeEnd: onChangeEnd,
...@@ -628,6 +704,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -628,6 +704,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback, semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform, platform: Theme.of(context).platform,
useV2Slider: useV2Slider,
); );
} }
...@@ -640,6 +717,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -640,6 +717,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
..sliderTheme = sliderTheme ..sliderTheme = sliderTheme
..theme = Theme.of(context) ..theme = Theme.of(context)
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
..screenSize = screenSize
..onChanged = onChanged ..onChanged = onChanged
..onChangeStart = onChangeStart ..onChangeStart = onChangeStart
..onChangeEnd = onChangeEnd ..onChangeEnd = onChangeEnd
...@@ -657,6 +735,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -657,6 +735,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
ThemeData theme, ThemeData theme,
double textScaleFactor, double textScaleFactor,
Size screenSize,
TargetPlatform platform, TargetPlatform platform,
ValueChanged<RangeValues> onChanged, ValueChanged<RangeValues> onChanged,
RangeSemanticFormatterCallback semanticFormatterCallback, RangeSemanticFormatterCallback semanticFormatterCallback,
...@@ -664,6 +743,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -664,6 +743,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
this.onChangeEnd, this.onChangeEnd,
@required _RangeSliderState state, @required _RangeSliderState state,
@required TextDirection textDirection, @required TextDirection textDirection,
bool useV2Slider,
}) : assert(values != null), }) : assert(values != null),
assert(values.start >= 0.0 && values.start <= 1.0), assert(values.start >= 0.0 && values.start <= 1.0),
assert(values.end >= 0.0 && values.end <= 1.0), assert(values.end >= 0.0 && values.end <= 1.0),
...@@ -677,9 +757,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -677,9 +757,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_sliderTheme = sliderTheme, _sliderTheme = sliderTheme,
_theme = theme, _theme = theme,
_textScaleFactor = textScaleFactor, _textScaleFactor = textScaleFactor,
_screenSize = screenSize,
_onChanged = onChanged, _onChanged = onChanged,
_state = state, _state = state,
_textDirection = textDirection { _textDirection = textDirection,
_useV2Slider = useV2Slider {
_updateLabelPainters(); _updateLabelPainters();
final GestureArenaTeam team = GestureArenaTeam(); final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer() _drag = HorizontalDragGestureRecognizer()
...@@ -700,7 +782,12 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -700,7 +782,12 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_valueIndicatorAnimation = CurvedAnimation( _valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController, parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
); )..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed && _state.overlayEntry != null) {
_state.overlayEntry.remove();
_state.overlayEntry = null;
}
});
_enableAnimation = CurvedAnimation( _enableAnimation = CurvedAnimation(
parent: _state.enableController, parent: _state.enableController,
curve: Curves.easeInOut, curve: Curves.easeInOut,
...@@ -849,6 +936,15 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -849,6 +936,15 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_updateLabelPainters(); _updateLabelPainters();
} }
Size get screenSize => _screenSize;
Size _screenSize;
set screenSize(Size value) {
if (value == screenSize)
return;
_screenSize = value;
markNeedsPaint();
}
ValueChanged<RangeValues> get onChanged => _onChanged; ValueChanged<RangeValues> get onChanged => _onChanged;
ValueChanged<RangeValues> _onChanged; ValueChanged<RangeValues> _onChanged;
set onChanged(ValueChanged<RangeValues> value) { set onChanged(ValueChanged<RangeValues> value) {
...@@ -913,6 +1009,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -913,6 +1009,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
return 0.05; return 0.05;
} }
final bool _useV2Slider;
void _updateLabelPainters() { void _updateLabelPainters() {
_updateLabelPainter(Thumb.start); _updateLabelPainter(Thumb.start);
_updateLabelPainter(Thumb.end); _updateLabelPainter(Thumb.end);
...@@ -1009,6 +1107,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1009,6 +1107,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
} }
void _startInteraction(Offset globalPosition) { void _startInteraction(Offset globalPosition) {
_state.showValueIndicator();
final double tapValue = _getValueFromGlobalPosition(globalPosition).clamp(0.0, 1.0) as double; final double tapValue = _getValueFromGlobalPosition(globalPosition).clamp(0.0, 1.0) as double;
_lastThumbSelection = sliderTheme.thumbSelector(textDirection, values, tapValue, _thumbSize, size, 0); _lastThumbSelection = sliderTheme.thumbSelector(textDirection, values, tapValue, _thumbSize, size, 0);
...@@ -1200,8 +1299,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1200,8 +1299,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
isEnabled: isEnabled, isEnabled: isEnabled,
); );
final bool startThumbSelected = _lastThumbSelection == Thumb.start;
final bool endThumbSelected = _lastThumbSelection == Thumb.end;
if (!_overlayAnimation.isDismissed) { if (!_overlayAnimation.isDismissed) {
if (_lastThumbSelection == Thumb.start) { if (startThumbSelected) {
_sliderTheme.overlayShape.paint( _sliderTheme.overlayShape.paint(
context, context,
startThumbCenter, startThumbCenter,
...@@ -1215,7 +1317,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1215,7 +1317,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
value: startValue, value: startValue,
); );
} }
if (_lastThumbSelection == Thumb.end) { if (endThumbSelected) {
_sliderTheme.overlayShape.paint( _sliderTheme.overlayShape.paint(
context, context,
endThumbCenter, endThumbCenter,
...@@ -1236,7 +1338,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1236,7 +1338,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
isEnabled: isEnabled, isEnabled: isEnabled,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
).width; ).width;
final double adjustedTrackWidth = trackRect.width - tickMarkWidth; final double padding = _useV2Slider ? trackRect.height : tickMarkWidth;
final double adjustedTrackWidth = trackRect.width - padding;
// If the tick marks would be too dense, don't bother painting them. // If the tick marks would be too dense, don't bother painting them.
if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) { if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) {
final double dy = trackRect.center.dy; final double dy = trackRect.center.dy;
...@@ -1244,7 +1347,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1244,7 +1347,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final double value = i / divisions; final double value = i / divisions;
// The ticks are mapped to be within the track, so the tick mark width // The ticks are mapped to be within the track, so the tick mark width
// must be subtracted from the track width. // must be subtracted from the track width.
final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2; final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
final Offset tickMarkOffset = Offset(dx, dy); final Offset tickMarkOffset = Offset(dx, dy);
_sliderTheme.rangeTickMarkShape.paint( _sliderTheme.rangeTickMarkShape.paint(
context, context,
...@@ -1273,8 +1376,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1273,8 +1376,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final double bottomValue = isLastThumbStart ? endValue : startValue; final double bottomValue = isLastThumbStart ? endValue : startValue;
final double topValue = isLastThumbStart ? startValue : endValue; final double topValue = isLastThumbStart ? startValue : endValue;
final bool shouldPaintValueIndicators = isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator; final bool shouldPaintValueIndicators = isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator;
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
if (shouldPaintValueIndicators) { if (shouldPaintValueIndicators) {
_state.paintBottomValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.rangeValueIndicatorShape.paint( _sliderTheme.rangeValueIndicatorShape.paint(
context, context,
bottomThumbCenter, bottomThumbCenter,
...@@ -1288,7 +1393,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1288,7 +1393,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: _textDirection, textDirection: _textDirection,
thumb: bottomThumb, thumb: bottomThumb,
value: bottomValue, value: bottomValue,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
); );
};
} }
_sliderTheme.rangeThumbShape.paint( _sliderTheme.rangeThumbShape.paint(
...@@ -1301,6 +1409,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1301,6 +1409,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: textDirection, textDirection: textDirection,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
thumb: bottomThumb, thumb: bottomThumb,
isPressed: bottomThumb == Thumb.start ? startThumbSelected : endThumbSelected,
); );
if (shouldPaintValueIndicators) { if (shouldPaintValueIndicators) {
...@@ -1309,15 +1418,29 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1309,15 +1418,29 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
center: startThumbCenter, center: startThumbCenter,
labelPainter: _startLabelPainter, labelPainter: _startLabelPainter,
activationAnimation: _valueIndicatorAnimation, activationAnimation: _valueIndicatorAnimation,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
); );
final double endOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift( final double endOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift(
parentBox: this, parentBox: this,
center: endThumbCenter, center: endThumbCenter,
labelPainter: _endLabelPainter, labelPainter: _endLabelPainter,
activationAnimation: _valueIndicatorAnimation, activationAnimation: _valueIndicatorAnimation,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
); );
final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _startLabelPainter).width / 2; final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(
final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _endLabelPainter).width / 2; 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; double innerOverflow = startHalfWidth + endHalfWidth;
switch (textDirection) { switch (textDirection) {
case TextDirection.ltr: case TextDirection.ltr:
...@@ -1330,6 +1453,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1330,6 +1453,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
break; break;
} }
_state.paintTopValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.rangeValueIndicatorShape.paint( _sliderTheme.rangeValueIndicatorShape.paint(
context, context,
topThumbCenter, topThumbCenter,
...@@ -1343,19 +1467,23 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1343,19 +1467,23 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: _textDirection, textDirection: _textDirection,
thumb: topThumb, thumb: topThumb,
value: topValue, value: topValue,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
); );
};
} }
_sliderTheme.rangeThumbShape.paint( _sliderTheme.rangeThumbShape.paint(
context, context,
topThumbCenter, topThumbCenter,
activationAnimation: _valueIndicatorAnimation, activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation, enableAnimation: _enableAnimation,
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
isOnTop: thumbDelta < sliderTheme.rangeThumbShape.getPreferredSize(isEnabled, isDiscrete).width, isOnTop: thumbDelta < sliderTheme.rangeThumbShape.getPreferredSize(isEnabled, isDiscrete).width,
textDirection: textDirection, textDirection: textDirection,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
thumb: topThumb, thumb: topThumb,
isPressed: topThumb == Thumb.start ? startThumbSelected : endThumbSelected,
); );
} }
...@@ -1419,3 +1547,66 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1419,3 +1547,66 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
return (value - _semanticActionUnit).clamp(0.0, 1.0) as double; return (value - _semanticActionUnit).clamp(0.0, 1.0) as double;
} }
} }
class _ValueIndicatorRenderObjectWidget extends LeafRenderObjectWidget {
const _ValueIndicatorRenderObjectWidget({
this.state,
});
final _RangeSliderState state;
@override
_RenderValueIndicator createRenderObject(BuildContext context) {
return _RenderValueIndicator(
state: state,
);
}
@override
void updateRenderObject(BuildContext context, _RenderValueIndicator renderObject) {
renderObject._state = state;
}
}
class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_RenderValueIndicator({
_RangeSliderState state,
}) :_state = state {
_valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn,
);
}
Animation<double> _valueIndicatorAnimation;
_RangeSliderState _state;
@override
bool get sizedByParent => true;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_valueIndicatorAnimation.addListener(markNeedsPaint);
_state.startPositionController.addListener(markNeedsPaint);
_state.endPositionController.addListener(markNeedsPaint);
}
@override
void detach() {
_valueIndicatorAnimation.removeListener(markNeedsPaint);
_state.startPositionController.removeListener(markNeedsPaint);
_state.endPositionController.removeListener(markNeedsPaint);
super.detach();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_state.paintBottomValueIndicator != null) {
_state.paintBottomValueIndicator(context, offset);
}
if (_state.paintTopValueIndicator != null) {
_state.paintTopValueIndicator(context, offset);
}
}
}
...@@ -31,6 +31,11 @@ import 'theme.dart'; ...@@ -31,6 +31,11 @@ import 'theme.dart';
/// * [Slider.semanticFormatterCallback], which shows an example use case. /// * [Slider.semanticFormatterCallback], which shows an example use case.
typedef SemanticFormatterCallback = String Function(double value); typedef SemanticFormatterCallback = String Function(double value);
/// [Slider] uses this callback to paint the value indicator on the overlay.
/// Since the value indicator is painted on the Overlay; this method paints the
/// value indicator in a [RenderBox] that appears in the [Overlay].
typedef PaintValueIndicator = void Function(PaintingContext context, Offset offset);
enum _SliderType { material, adaptive } enum _SliderType { material, adaptive }
/// A Material Design slider. /// A Material Design slider.
...@@ -124,6 +129,7 @@ class Slider extends StatefulWidget { ...@@ -124,6 +129,7 @@ class Slider extends StatefulWidget {
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.useV2Slider = false,
}) : _sliderType = _SliderType.material, }) : _sliderType = _SliderType.material,
assert(value != null), assert(value != null),
assert(min != null), assert(min != null),
...@@ -131,6 +137,7 @@ class Slider extends StatefulWidget { ...@@ -131,6 +137,7 @@ class Slider extends StatefulWidget {
assert(min <= max), assert(min <= max),
assert(value >= min && value <= max), assert(value >= min && value <= max),
assert(divisions == null || divisions > 0), assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key); super(key: key);
/// Creates a [CupertinoSlider] if the target platform is iOS, creates a /// Creates a [CupertinoSlider] if the target platform is iOS, creates a
...@@ -153,6 +160,7 @@ class Slider extends StatefulWidget { ...@@ -153,6 +160,7 @@ class Slider extends StatefulWidget {
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.useV2Slider = false,
}) : _sliderType = _SliderType.adaptive, }) : _sliderType = _SliderType.adaptive,
assert(value != null), assert(value != null),
assert(min != null), assert(min != null),
...@@ -160,6 +168,7 @@ class Slider extends StatefulWidget { ...@@ -160,6 +168,7 @@ class Slider extends StatefulWidget {
assert(min <= max), assert(min <= max),
assert(value >= min && value <= max), assert(value >= min && value <= max),
assert(divisions == null || divisions > 0), assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key); super(key: key);
/// The currently selected value for this slider. /// The currently selected value for this slider.
...@@ -374,6 +383,19 @@ class Slider extends StatefulWidget { ...@@ -374,6 +383,19 @@ class Slider extends StatefulWidget {
/// Ignored if this slider is created with [Slider.adaptive] /// Ignored if this slider is created with [Slider.adaptive]
final SemanticFormatterCallback semanticFormatterCallback; final SemanticFormatterCallback semanticFormatterCallback;
/// Whether to use the updated Material spec version of the [Slider].
/// * The v2 Slider has an updated value indicator that matches the latest specs.
/// * The value indicator is painted on the Overlay.
/// * The active track is bigger than the inactive track.
/// * The thumb that is activated has elevation.
/// * Updated value indicators in case they overlap with each other.
/// * <https://groups.google.com/g/flutter-announce/c/69dmlKUL5Ew/m/tQh-ajiEAAAJl>
///
/// This is a temporary flag for migrating the slider from v1 to v2. To avoid
/// unexpected breaking changes, this value should be set to true. Setting
/// this to false is considered deprecated.
final bool useV2Slider;
final _SliderType _sliderType ; final _SliderType _sliderType ;
@override @override
...@@ -392,6 +414,7 @@ class Slider extends StatefulWidget { ...@@ -392,6 +414,7 @@ class Slider extends StatefulWidget {
properties.add(StringProperty('label', label)); properties.add(StringProperty('label', label));
properties.add(ColorProperty('activeColor', activeColor)); properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('inactiveColor', inactiveColor)); properties.add(ColorProperty('inactiveColor', inactiveColor));
properties.add(FlagProperty('useV2Slider', value: useV2Slider, ifFalse: 'useV1Slider'));
properties.add(ObjectFlagProperty<ValueChanged<double>>.has('semanticFormatterCallback', semanticFormatterCallback)); properties.add(ObjectFlagProperty<ValueChanged<double>>.has('semanticFormatterCallback', semanticFormatterCallback));
} }
} }
...@@ -412,6 +435,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -412,6 +435,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// and the next on a discrete slider. // and the next on a discrete slider.
AnimationController positionController; AnimationController positionController;
Timer interactionTimer; Timer interactionTimer;
// Value Indicator Animation that appears on the Overlay.
PaintValueIndicator paintValueIndicator;
@override @override
void initState() { void initState() {
...@@ -479,14 +504,6 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -479,14 +504,6 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0; return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0;
} }
static const double _defaultTrackHeight = 2;
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 ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -525,6 +542,28 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -525,6 +542,28 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// colors come from the ThemeData.colorScheme. These colors, along with // colors come from the ThemeData.colorScheme. These colors, along with
// the default shapes and text styles are aligned to the Material // the default shapes and text styles are aligned to the Material
// Guidelines. // Guidelines.
final bool useV2Slider = widget.useV2Slider;
final double _defaultTrackHeight = useV2Slider ? 4 : 2;
final SliderTrackShape _defaultTrackShape = RoundedRectSliderTrackShape(useV2Slider: useV2Slider);
final SliderTickMarkShape _defaultTickMarkShape = RoundSliderTickMarkShape(useV2Slider: useV2Slider);
const SliderComponentShape _defaultOverlayShape = RoundSliderOverlayShape();
final SliderComponentShape _defaultThumbShape = RoundSliderThumbShape(useV2Slider: useV2Slider);
final SliderComponentShape _defaultValueIndicatorShape = useV2Slider ? const RectangularSliderValueIndicatorShape() : const PaddleSliderValueIndicatorShape();
const ShowValueIndicator _defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
// The value indicator's 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( sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight, trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
...@@ -536,31 +575,41 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -536,31 +575,41 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? theme.colorScheme.onPrimary.withOpacity(0.12), disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? theme.colorScheme.onPrimary.withOpacity(0.12),
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12), disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary, thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
disabledThumbColor: sliderTheme.disabledThumbColor ?? theme.colorScheme.onSurface.withOpacity(0.38), disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), const Color(0xFFFFFFFF)),
overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12), overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12),
valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary, valueIndicatorColor: valueIndicatorColor,
trackShape: sliderTheme.trackShape ?? _defaultTrackShape, trackShape: sliderTheme.trackShape ?? _defaultTrackShape,
tickMarkShape: sliderTheme.tickMarkShape ?? _defaultTickMarkShape, tickMarkShape: sliderTheme.tickMarkShape ?? _defaultTickMarkShape,
thumbShape: sliderTheme.thumbShape ?? _defaultThumbShape, thumbShape: sliderTheme.thumbShape ?? _defaultThumbShape,
overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape, overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape,
valueIndicatorShape: sliderTheme.valueIndicatorShape ?? _defaultValueIndicatorShape, valueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator, showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator,
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith( valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
), ),
); );
return _SliderRenderObjectWidget( // 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 _screenSize() => MediaQuery.of(context).size;
return CompositedTransformTarget(
link: _layerLink,
child: _SliderRenderObjectWidget(
value: _unlerp(widget.value), value: _unlerp(widget.value),
divisions: widget.divisions, divisions: widget.divisions,
label: widget.label, label: widget.label,
sliderTheme: sliderTheme, sliderTheme: sliderTheme,
mediaQueryData: MediaQuery.of(context), textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this, state: this,
semanticFormatterCallback: widget.semanticFormatterCallback, semanticFormatterCallback: widget.semanticFormatterCallback,
useV2Slider: widget.useV2Slider,
),
); );
} }
...@@ -582,8 +631,28 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -582,8 +631,28 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
), ),
); );
} }
final LayerLink _layerLink = LayerLink();
OverlayEntry overlayEntry;
void showValueIndicator() {
if (overlayEntry == null) {
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return CompositedTransformFollower(
link: _layerLink,
child: _ValueIndicatorRenderObjectWidget(
state: this,
),
);
},
);
Overlay.of(context).insert(overlayEntry);
}
}
} }
class _SliderRenderObjectWidget extends LeafRenderObjectWidget { class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
const _SliderRenderObjectWidget({ const _SliderRenderObjectWidget({
Key key, Key key,
...@@ -591,24 +660,28 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -591,24 +660,28 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
this.divisions, this.divisions,
this.label, this.label,
this.sliderTheme, this.sliderTheme,
this.mediaQueryData, this.textScaleFactor,
this.screenSize,
this.onChanged, this.onChanged,
this.onChangeStart, this.onChangeStart,
this.onChangeEnd, this.onChangeEnd,
this.state, this.state,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.useV2Slider,
}) : super(key: key); }) : super(key: key);
final double value; final double value;
final int divisions; final int divisions;
final String label; final String label;
final SliderThemeData sliderTheme; final SliderThemeData sliderTheme;
final MediaQueryData mediaQueryData; final double textScaleFactor;
final Size screenSize;
final ValueChanged<double> onChanged; final ValueChanged<double> onChanged;
final ValueChanged<double> onChangeStart; final ValueChanged<double> onChangeStart;
final ValueChanged<double> onChangeEnd; final ValueChanged<double> onChangeEnd;
final SemanticFormatterCallback semanticFormatterCallback; final SemanticFormatterCallback semanticFormatterCallback;
final _SliderState state; final _SliderState state;
final bool useV2Slider;
@override @override
_RenderSlider createRenderObject(BuildContext context) { _RenderSlider createRenderObject(BuildContext context) {
...@@ -617,7 +690,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -617,7 +690,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
divisions: divisions, divisions: divisions,
label: label, label: label,
sliderTheme: sliderTheme, sliderTheme: sliderTheme,
mediaQueryData: mediaQueryData, textScaleFactor: textScaleFactor,
screenSize: screenSize,
onChanged: onChanged, onChanged: onChanged,
onChangeStart: onChangeStart, onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd, onChangeEnd: onChangeEnd,
...@@ -625,6 +699,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -625,6 +699,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback, semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform, platform: Theme.of(context).platform,
useV2Slider: useV2Slider,
); );
} }
...@@ -636,7 +711,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -636,7 +711,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..label = label ..label = label
..sliderTheme = sliderTheme ..sliderTheme = sliderTheme
..theme = Theme.of(context) ..theme = Theme.of(context)
..mediaQueryData = mediaQueryData ..textScaleFactor = textScaleFactor
..screenSize = screenSize
..onChanged = onChanged ..onChanged = onChanged
..onChangeStart = onChangeStart ..onChangeStart = onChangeStart
..onChangeEnd = onChangeEnd ..onChangeEnd = onChangeEnd
...@@ -654,7 +730,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -654,7 +730,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
int divisions, int divisions,
String label, String label,
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
MediaQueryData mediaQueryData, double textScaleFactor,
Size screenSize,
TargetPlatform platform, TargetPlatform platform,
ValueChanged<double> onChanged, ValueChanged<double> onChanged,
SemanticFormatterCallback semanticFormatterCallback, SemanticFormatterCallback semanticFormatterCallback,
...@@ -662,6 +739,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -662,6 +739,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
this.onChangeEnd, this.onChangeEnd,
@required _SliderState state, @required _SliderState state,
@required TextDirection textDirection, @required TextDirection textDirection,
bool useV2Slider,
}) : assert(value != null && value >= 0.0 && value <= 1.0), }) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(state != null), assert(state != null),
assert(textDirection != null), assert(textDirection != null),
...@@ -671,10 +749,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -671,10 +749,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_value = value, _value = value,
_divisions = divisions, _divisions = divisions,
_sliderTheme = sliderTheme, _sliderTheme = sliderTheme,
_mediaQueryData = mediaQueryData, _textScaleFactor = textScaleFactor,
_screenSize = screenSize,
_onChanged = onChanged, _onChanged = onChanged,
_state = state, _state = state,
_textDirection = textDirection { _textDirection = textDirection,
_useV2Slider = useV2Slider {
_updateLabelPainter(); _updateLabelPainter();
final GestureArenaTeam team = GestureArenaTeam(); final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer() _drag = HorizontalDragGestureRecognizer()
...@@ -695,7 +775,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -695,7 +775,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_valueIndicatorAnimation = CurvedAnimation( _valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController, parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
); )..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed && _state.overlayEntry != null) {
_state.overlayEntry.remove();
_state.overlayEntry = null;
}
});
_enableAnimation = CurvedAnimation( _enableAnimation = CurvedAnimation(
parent: _state.enableController, parent: _state.enableController,
curve: Curves.easeInOut, curve: Curves.easeInOut,
...@@ -826,18 +911,26 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -826,18 +911,26 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
markNeedsPaint(); markNeedsPaint();
} }
MediaQueryData get mediaQueryData => _mediaQueryData; double get textScaleFactor => _textScaleFactor;
MediaQueryData _mediaQueryData; double _textScaleFactor;
set mediaQueryData(MediaQueryData value) { set textScaleFactor(double value) {
if (value == _mediaQueryData) { if (value == _textScaleFactor) {
return; return;
} }
_mediaQueryData = value; _textScaleFactor = value;
// Media query data includes the textScaleFactor, so we need to update the
// label painter.
_updateLabelPainter(); _updateLabelPainter();
} }
Size get screenSize => _screenSize;
Size _screenSize;
set screenSize(Size value) {
if (value == _screenSize) {
return;
}
_screenSize = value;
markNeedsPaint();
}
ValueChanged<double> get onChanged => _onChanged; ValueChanged<double> get onChanged => _onChanged;
ValueChanged<double> _onChanged; ValueChanged<double> _onChanged;
set onChanged(ValueChanged<double> value) { set onChanged(ValueChanged<double> value) {
...@@ -871,6 +964,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -871,6 +964,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_updateLabelPainter(); _updateLabelPainter();
} }
final bool _useV2Slider;
bool get showValueIndicator { bool get showValueIndicator {
bool showValueIndicator; bool showValueIndicator;
switch (_sliderTheme.showValueIndicator) { switch (_sliderTheme.showValueIndicator) {
...@@ -915,7 +1010,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -915,7 +1010,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
text: label, text: label,
) )
..textDirection = textDirection ..textDirection = textDirection
..textScaleFactor = _mediaQueryData.textScaleFactor ..textScaleFactor = textScaleFactor
..layout(); ..layout();
} else { } else {
_labelPainter.text = null; _labelPainter.text = null;
...@@ -975,6 +1070,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -975,6 +1070,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
void _startInteraction(Offset globalPosition) { void _startInteraction(Offset globalPosition) {
_state.showValueIndicator();
if (isInteractive) { if (isInteractive) {
_active = true; _active = true;
// We supply the *current* value as the start location, so that if we have // We supply the *current* value as the start location, so that if we have
...@@ -1008,6 +1104,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1008,6 +1104,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_active = false; _active = false;
_currentDragValue = 0.0; _currentDragValue = 0.0;
_state.overlayController.reverse(); _state.overlayController.reverse();
if (showValueIndicator && _state.interactionTimer == null) { if (showValueIndicator && _state.interactionTimer == null) {
_state.valueIndicatorController.reverse(); _state.valueIndicatorController.reverse();
} }
...@@ -1130,7 +1227,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1130,7 +1227,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
isEnabled: isInteractive, isEnabled: isInteractive,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
).width; ).width;
final double adjustedTrackWidth = trackRect.width - tickMarkWidth; final double padding = _useV2Slider ? trackRect.height : tickMarkWidth;
final double adjustedTrackWidth = trackRect.width - padding;
// If the tick marks would be too dense, don't bother painting them. // If the tick marks would be too dense, don't bother painting them.
if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) { if (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) {
final double dy = trackRect.center.dy; final double dy = trackRect.center.dy;
...@@ -1138,7 +1236,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1138,7 +1236,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final double value = i / divisions; final double value = i / divisions;
// The ticks are mapped to be within the track, so the tick mark width // The ticks are mapped to be within the track, so the tick mark width
// must be subtracted from the track width. // must be subtracted from the track width.
final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2; final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
final Offset tickMarkOffset = Offset(dx, dy); final Offset tickMarkOffset = Offset(dx, dy);
_sliderTheme.tickMarkShape.paint( _sliderTheme.tickMarkShape.paint(
context, context,
...@@ -1156,9 +1254,10 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1156,9 +1254,10 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) { if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
if (showValueIndicator) { if (showValueIndicator) {
_state.paintValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.valueIndicatorShape.paint( _sliderTheme.valueIndicatorShape.paint(
context, context,
thumbCenter, offset + thumbCenter,
activationAnimation: _valueIndicatorAnimation, activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation, enableAnimation: _enableAnimation,
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
...@@ -1167,20 +1266,24 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1167,20 +1266,24 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
textDirection: _textDirection, textDirection: _textDirection,
value: _value, value: _value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
); );
};
} }
} }
_sliderTheme.thumbShape.paint( _sliderTheme.thumbShape.paint(
context, context,
thumbCenter, thumbCenter,
activationAnimation: _valueIndicatorAnimation, activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation, enableAnimation: _enableAnimation,
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
labelPainter: _labelPainter, labelPainter: _labelPainter,
parentBox: this, parentBox: this,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
textDirection: _textDirection, textDirection: _textDirection,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
value: _value, value: _value,
); );
} }
...@@ -1220,3 +1323,59 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1220,3 +1323,59 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
} }
} }
class _ValueIndicatorRenderObjectWidget extends LeafRenderObjectWidget {
const _ValueIndicatorRenderObjectWidget({
this.state,
});
final _SliderState state;
@override
_RenderValueIndicator createRenderObject(BuildContext context) {
return _RenderValueIndicator(
state: state,
);
}
@override
void updateRenderObject(BuildContext context, _RenderValueIndicator renderObject) {
renderObject._state = state;
}
}
class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_RenderValueIndicator({
_SliderState state,
}) : _state = state {
_valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn,
);
}
Animation<double> _valueIndicatorAnimation;
_SliderState _state;
@override
bool get sizedByParent => true;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_valueIndicatorAnimation.addListener(markNeedsPaint);
_state.positionController.addListener(markNeedsPaint);
}
@override
void detach() {
_valueIndicatorAnimation.removeListener(markNeedsPaint);
_state.positionController.removeListener(markNeedsPaint);
super.detach();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_state.paintValueIndicator != null) {
_state.paintValueIndicator(context, offset);
}
}
}
...@@ -108,6 +108,28 @@ import 'theme_data.dart'; ...@@ -108,6 +108,28 @@ import 'theme_data.dart';
/// track segments. In [TextDirection.ltr], the start of the slider is on the /// track segments. In [TextDirection.ltr], the start of the slider is on the
/// left, and in [TextDirection.rtl], the start of the slider is on the right. /// left, and in [TextDirection.rtl], the start of the slider is on the right.
/// {@endtemplate} /// {@endtemplate}
///
/// {@template flutter.material.slider.useV2Slider}
/// Whether to use the updated Material spec version of the slider shape.
///
/// This is a temporary flag for migrating the slider from v1 to v2. To avoid
/// unexpected breaking changes, this value should be set to true. Setting
/// this to false is considered deprecated.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.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].
/// {@endtemplate}
///
/// {@template flutter.material.rangeSlider.shape.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.
/// {@endtemplate}
/// Applies a slider theme to descendant [Slider] widgets. /// Applies a slider theme to descendant [Slider] widgets.
/// ///
...@@ -278,7 +300,7 @@ enum Thumb { ...@@ -278,7 +300,7 @@ enum Thumb {
/// by creating subclasses of [SliderTrackShape], /// by creating subclasses of [SliderTrackShape],
/// [SliderComponentShape], and/or [SliderTickMarkShape]. See /// [SliderComponentShape], and/or [SliderTickMarkShape]. See
/// [RoundSliderThumbShape], [RectangularSliderTrackShape], /// [RoundSliderThumbShape], [RectangularSliderTrackShape],
/// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and /// [RoundSliderTickMarkShape], [RectangularSliderValueIndicatorShape], and
/// [RoundSliderOverlayShape] for examples. /// [RoundSliderOverlayShape] for examples.
/// ///
/// The track painting can be skipped by specifying 0 for [trackHeight]. /// The track painting can be skipped by specifying 0 for [trackHeight].
...@@ -490,6 +512,7 @@ class SliderThemeData with Diagnosticable { ...@@ -490,6 +512,7 @@ class SliderThemeData with Diagnosticable {
/// The color given to the [valueIndicatorShape] to draw itself with. /// The color given to the [valueIndicatorShape] to draw itself with.
final Color valueIndicatorColor; final Color valueIndicatorColor;
/// The shape that will be used to draw the [Slider]'s overlay. /// The shape that will be used to draw the [Slider]'s overlay.
/// ///
/// Both the [overlayColor] and a non default [overlayShape] may be specified. /// Both the [overlayColor] and a non default [overlayShape] may be specified.
...@@ -976,6 +999,10 @@ abstract class SliderComponentShape { ...@@ -976,6 +999,10 @@ abstract class SliderComponentShape {
/// [labelPainter] already has the [textDirection] set. /// [labelPainter] already has the [textDirection] set.
/// ///
/// [value] is the current parametric value (from 0.0 to 1.0) of the slider. /// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
///
/// {@macro flutter.material.slider.shape.textScaleFactor}
///
/// {@macro flutter.material.slider.shape.sizeWithOverflow}
void paint( void paint(
PaintingContext context, PaintingContext context,
Offset center, { Offset center, {
...@@ -987,6 +1014,8 @@ abstract class SliderComponentShape { ...@@ -987,6 +1014,8 @@ abstract class SliderComponentShape {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}); });
/// Special instance of [SliderComponentShape] to skip the thumb drawing. /// Special instance of [SliderComponentShape] to skip the thumb drawing.
...@@ -1202,6 +1231,9 @@ abstract class RangeSliderThumbShape { ...@@ -1202,6 +1231,9 @@ abstract class RangeSliderThumbShape {
/// left and right thumb. /// left and right thumb.
/// ///
/// {@macro flutter.material.rangeSlider.shape.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( void paint(
PaintingContext context, PaintingContext context,
Offset center, { Offset center, {
...@@ -1213,6 +1245,7 @@ abstract class RangeSliderThumbShape { ...@@ -1213,6 +1245,7 @@ abstract class RangeSliderThumbShape {
TextDirection textDirection, TextDirection textDirection,
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
Thumb thumb, Thumb thumb,
bool isPressed,
}); });
} }
...@@ -1238,7 +1271,14 @@ abstract class RangeSliderValueIndicatorShape { ...@@ -1238,7 +1271,14 @@ abstract class RangeSliderValueIndicatorShape {
/// ///
/// [labelPainter] helps determine the width of the shape. It is variable /// [labelPainter] helps determine the width of the shape. It is variable
/// width because it is derived from a formatted string. /// width because it is derived from a formatted string.
Size getPreferredSize(bool isEnabled, bool isDiscrete, { TextPainter labelPainter }); ///
/// {@macro flutter.material.slider.shape.textScaleFactor}
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
TextPainter labelPainter,
double textScaleFactor,
});
/// Determines the best offset to keep this shape on the screen. /// Determines the best offset to keep this shape on the screen.
/// ///
...@@ -1249,6 +1289,8 @@ abstract class RangeSliderValueIndicatorShape { ...@@ -1249,6 +1289,8 @@ abstract class RangeSliderValueIndicatorShape {
Offset center, Offset center,
TextPainter labelPainter, TextPainter labelPainter,
Animation<double> activationAnimation, Animation<double> activationAnimation,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
return 0; return 0;
} }
...@@ -1270,6 +1312,12 @@ abstract class RangeSliderValueIndicatorShape { ...@@ -1270,6 +1312,12 @@ abstract class RangeSliderValueIndicatorShape {
/// the default case, this is used to paint a stroke around the top indicator /// the default case, this is used to paint a stroke around the top indicator
/// for better visibility between the two indicators. /// for better visibility between the two indicators.
/// ///
/// {@macro flutter.material.slider.shape.textScaleFactor}
///
/// {@macro flutter.material.slider.shape.sizeWithOverflow}
///
/// {@macro flutter.material.rangeSlider.shape.parentBox}
///
/// {@macro flutter.material.rangeSlider.shape.sliderTheme} /// {@macro flutter.material.rangeSlider.shape.sliderTheme}
/// ///
/// [textDirection] can be used to determine how any extra text or graphics, /// [textDirection] can be used to determine how any extra text or graphics,
...@@ -1287,6 +1335,8 @@ abstract class RangeSliderValueIndicatorShape { ...@@ -1287,6 +1335,8 @@ abstract class RangeSliderValueIndicatorShape {
bool isDiscrete, bool isDiscrete,
bool isOnTop, bool isOnTop,
TextPainter labelPainter, TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
RenderBox parentBox, RenderBox parentBox,
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
...@@ -1552,17 +1602,17 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1552,17 +1602,17 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
assert(thumbCenter != null); assert(thumbCenter != null);
assert(isEnabled != null); assert(isEnabled != null);
assert(isDiscrete != null); assert(isDiscrete != null);
// If the slider track height is less than or equal to 0, then it makes no // If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// difference whether the track is painted or not, therefore the painting // then it makes no difference whether the track is painted or not,
// can be a no-op. // therefore the painting can be a no-op.
if (sliderTheme.trackHeight <= 0) { if (sliderTheme.trackHeight <= 0) {
return; return;
} }
// Assign the track segment paints, which are left: active, right: inactive, // Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text. // but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor); final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation); final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation); final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint; Paint leftTrackPaint;
...@@ -1586,11 +1636,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1586,11 +1636,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
); );
final Size thumbSize = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete); final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx, trackRect.bottom);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx - thumbSize.width / 2, trackRect.bottom);
if (!leftTrackSegment.isEmpty) if (!leftTrackSegment.isEmpty)
context.canvas.drawRect(leftTrackSegment, leftTrackPaint); 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) if (!rightTrackSegment.isEmpty)
context.canvas.drawRect(rightTrackSegment, rightTrackPaint); context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
} }
...@@ -1620,7 +1669,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1620,7 +1669,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
/// * [RectangularSliderTrackShape], for a similar track with sharp edges. /// * [RectangularSliderTrackShape], for a similar track with sharp edges.
class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape { class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
/// Create a slider track that draws two rectangles with rounded outer edges. /// Create a slider track that draws two rectangles with rounded outer edges.
const RoundedRectSliderTrackShape(); const RoundedRectSliderTrackShape({ this.useV2Slider = false });
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
void paint( void paint(
...@@ -1633,6 +1685,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1633,6 +1685,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
@required Offset thumbCenter, @required Offset thumbCenter,
bool isDiscrete = false, bool isDiscrete = false,
bool isEnabled = false, bool isEnabled = false,
double additionalActiveTrackHeight = 2,
}) { }) {
assert(context != null); assert(context != null);
assert(offset != null); assert(offset != null);
...@@ -1646,9 +1699,9 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1646,9 +1699,9 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
assert(enableAnimation != null); assert(enableAnimation != null);
assert(textDirection != null); assert(textDirection != null);
assert(thumbCenter != null); assert(thumbCenter != null);
// If the slider track height is less than or equal to 0, then it makes no // If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// difference whether the track is painted or not, therefore the painting // then it makes no difference whether the track is painted or not,
// can be a no-op. // therefore the painting can be a no-op.
if (sliderTheme.trackHeight <= 0) { if (sliderTheme.trackHeight <= 0) {
return; return;
} }
...@@ -1679,7 +1732,33 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1679,7 +1732,33 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
isEnabled: isEnabled, isEnabled: isEnabled,
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
); );
final Radius trackRadius = Radius.circular(trackRect.height / 2);
final Radius activeTrackRadius = Radius.circular(trackRect.height / 2 + 1);
if (useV2Slider) {
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left,
(textDirection == TextDirection.ltr) ? trackRect.top - (additionalActiveTrackHeight / 2): trackRect.top,
thumbCenter.dx,
(textDirection == TextDirection.ltr) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius : trackRadius,
bottomLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius: trackRadius,
),
leftTrackPaint,
);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
thumbCenter.dx,
(textDirection == TextDirection.rtl) ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top,
trackRect.right,
(textDirection == TextDirection.rtl) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
bottomRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
),
rightTrackPaint,
);
} else {
// The arc rects create a semi-circle with radius equal to track height. // 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); final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height);
if (!leftTrackArcRect.isEmpty) if (!leftTrackArcRect.isEmpty)
...@@ -1696,6 +1775,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ...@@ -1696,6 +1775,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
if (!rightTrackSegment.isEmpty) if (!rightTrackSegment.isEmpty)
context.canvas.drawRect(rightTrackSegment, rightTrackPaint); context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
} }
}
} }
/// A [RangeSlider] track that's a simple rectangle. /// A [RangeSlider] track that's a simple rectangle.
...@@ -1726,7 +1806,10 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1726,7 +1806,10 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape {
/// ///
/// The middle track segment is the selected range and is active, and the two /// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive. /// outer track segments are inactive.
const RectangularRangeSliderTrackShape(); const RectangularRangeSliderTrackShape({this.useV2Slider});
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
Rect getPreferredRect({ Rect getPreferredRect({
...@@ -1785,8 +1868,8 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1785,8 +1868,8 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape {
assert(textDirection != null); assert(textDirection != null);
// Assign the track segment paints, which are left: active, right: inactive, // Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text. // but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor); final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation); final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation); final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
...@@ -1852,7 +1935,10 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1852,7 +1935,10 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
/// ///
/// The middle track segment is the selected range and is active, and the two /// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive. /// outer track segments are inactive.
const RoundedRectRangeSliderTrackShape(); const RoundedRectRangeSliderTrackShape({ this.useV2Slider });
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
Rect getPreferredRect({ Rect getPreferredRect({
...@@ -1894,6 +1980,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1894,6 +1980,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
bool isEnabled = false, bool isEnabled = false,
bool isDiscrete = false, bool isDiscrete = false,
@required TextDirection textDirection, @required TextDirection textDirection,
double additionalActiveTrackHeight = 2,
}) { }) {
assert(context != null); assert(context != null);
assert(offset != null); assert(offset != null);
...@@ -1910,12 +1997,23 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1910,12 +1997,23 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
assert(isEnabled != null); assert(isEnabled != null);
assert(isDiscrete != null); assert(isDiscrete != null);
assert(textDirection != null); assert(textDirection != null);
if (sliderTheme.trackHeight <= 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive, // Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text. // but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); final ColorTween activeTrackColorTween = ColorTween(
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor); begin: sliderTheme.disabledActiveTrackColor,
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation); end: sliderTheme.activeTrackColor);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation); 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);
Offset leftThumbOffset; Offset leftThumbOffset;
Offset rightThumbOffset; Offset rightThumbOffset;
...@@ -1940,6 +2038,42 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1940,6 +2038,42 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
isEnabled: isEnabled, isEnabled: isEnabled,
isDiscrete: isDiscrete, isDiscrete: isDiscrete,
); );
if (useV2Slider) {
final Radius trackRadius = Radius.circular(trackRect.height / 2);
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 - (additionalActiveTrackHeight / 2),
rightThumbOffset.dx,
trackRect.bottom + (additionalActiveTrackHeight / 2),
),
activePaint,
);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
rightThumbOffset.dx,
trackRect.top,
trackRect.right,
trackRect.bottom,
topRight: trackRadius,
bottomRight: trackRadius,
),
inactivePaint,
);
} else {
final double trackRadius = trackRect.height / 2; final double trackRadius = trackRect.height / 2;
final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height); final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height);
...@@ -1960,6 +2094,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1960,6 +2094,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
if (!rightTrackArcRect.isEmpty) if (!rightTrackArcRect.isEmpty)
context.canvas.drawArc(rightTrackArcRect, -math.pi / 2, math.pi, false, inactivePaint); context.canvas.drawArc(rightTrackArcRect, -math.pi / 2, math.pi, false, inactivePaint);
} }
}
} }
/// The default shape of each [Slider] tick mark. /// The default shape of each [Slider] tick mark.
...@@ -1982,13 +2117,21 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape { ...@@ -1982,13 +2117,21 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
/// sliders in a widget subtree. /// sliders in a widget subtree.
class RoundSliderTickMarkShape extends SliderTickMarkShape { class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// Create a slider tick mark that draws a circle. /// Create a slider tick mark that draws a circle.
const RoundSliderTickMarkShape({ this.tickMarkRadius }); const RoundSliderTickMarkShape({
this.tickMarkRadius,
this.useV2Slider = false,
});
/// The preferred radius of the round tick mark. /// 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, and [useV2Slider] is true, then 1/4 of the
/// [SliderThemeData.trackHeight] is used. If it is not provided, and
/// [useV2Slider] is false, then half of the track height is used.
final double tickMarkRadius; final double tickMarkRadius;
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
Size getPreferredSize({ Size getPreferredSize({
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
...@@ -1997,9 +2140,11 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape { ...@@ -1997,9 +2140,11 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
assert(sliderTheme != null); assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null); assert(sliderTheme.trackHeight != null);
assert(isEnabled != null); assert(isEnabled != null);
// The tick marks are tiny circles. If no radius is provided, then they are // The tick marks are tiny circles. If no radius is provided, then the
// defaulted to be the same height as the track. // radius is defaulted to be a fraction of the
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2); // [SliderThemeData.trackHeight]. The fraction is 1/4 when [useV2Slider] is
// true, and 1/2 when it is false.
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / (useV2Slider ? 4 : 2));
} }
@override @override
...@@ -2074,13 +2219,21 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape { ...@@ -2074,13 +2219,21 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// sliders in a widget subtree. /// sliders in a widget subtree.
class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape { class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape {
/// Create a range slider tick mark that draws a circle. /// Create a range slider tick mark that draws a circle.
const RoundRangeSliderTickMarkShape({ this.tickMarkRadius }); const RoundRangeSliderTickMarkShape({
this.tickMarkRadius,
this.useV2Slider = false,
});
/// The preferred radius of the round tick mark. /// 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, and [useV2Slider] is true, then 1/4 of the
/// [SliderThemeData.trackHeight] is used. If it is not provided, and
/// [useV2Slider] is false, then half of the track height is used.
final double tickMarkRadius; final double tickMarkRadius;
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
Size getPreferredSize({ Size getPreferredSize({
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
...@@ -2089,7 +2242,7 @@ class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape { ...@@ -2089,7 +2242,7 @@ class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape {
assert(sliderTheme != null); assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null); assert(sliderTheme.trackHeight != null);
assert(isEnabled != null); assert(isEnabled != null);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2); return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / (useV2Slider ? 4 : 2));
} }
@override @override
...@@ -2198,6 +2351,8 @@ class _EmptySliderComponentShape extends SliderComponentShape { ...@@ -2198,6 +2351,8 @@ class _EmptySliderComponentShape extends SliderComponentShape {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
// no-op. // no-op.
} }
...@@ -2205,6 +2360,9 @@ class _EmptySliderComponentShape extends SliderComponentShape { ...@@ -2205,6 +2360,9 @@ class _EmptySliderComponentShape extends SliderComponentShape {
/// The default shape of a [Slider]'s thumb. /// The default shape of a [Slider]'s thumb.
/// ///
/// If [useV2Slider] is true, then there is a shadow for the resting and
/// pressed state.
///
/// See also: /// See also:
/// ///
/// * [Slider], which includes a thumb defined by this shape. /// * [Slider], which includes a thumb defined by this shape.
...@@ -2215,6 +2373,9 @@ class RoundSliderThumbShape extends SliderComponentShape { ...@@ -2215,6 +2373,9 @@ class RoundSliderThumbShape extends SliderComponentShape {
const RoundSliderThumbShape({ const RoundSliderThumbShape({
this.enabledThumbRadius = 10.0, this.enabledThumbRadius = 10.0,
this.disabledThumbRadius, this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
this.useV2Slider = false,
}); });
/// The preferred radius of the round thumb shape when the slider is enabled. /// The preferred radius of the round thumb shape when the slider is enabled.
...@@ -2229,6 +2390,30 @@ class RoundSliderThumbShape extends SliderComponentShape { ...@@ -2229,6 +2390,30 @@ class RoundSliderThumbShape extends SliderComponentShape {
final double disabledThumbRadius; final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// This value is only used when [useV2Slider] is true.
///
/// 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.
///
/// This value is only used when [useV2Slider] is true.
///
/// 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;
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override @override
Size getPreferredSize(bool isEnabled, bool isDiscrete) { Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
...@@ -2246,6 +2431,8 @@ class RoundSliderThumbShape extends SliderComponentShape { ...@@ -2246,6 +2431,8 @@ class RoundSliderThumbShape extends SliderComponentShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
assert(context != null); assert(context != null);
assert(center != null); assert(center != null);
...@@ -2253,6 +2440,7 @@ class RoundSliderThumbShape extends SliderComponentShape { ...@@ -2253,6 +2440,7 @@ class RoundSliderThumbShape extends SliderComponentShape {
assert(sliderTheme != null); assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null); assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null); assert(sliderTheme.thumbColor != null);
assert(!sizeWithOverflow.isEmpty);
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>( final Tween<double> radiusTween = Tween<double>(
...@@ -2263,16 +2451,35 @@ class RoundSliderThumbShape extends SliderComponentShape { ...@@ -2263,16 +2451,35 @@ class RoundSliderThumbShape extends SliderComponentShape {
begin: sliderTheme.disabledThumbColor, begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor, end: sliderTheme.thumbColor,
); );
final Color color = colorTween.evaluate(enableAnimation);
final double radius = radiusTween.evaluate(enableAnimation);
if (useV2Slider) {
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
final double evaluatedElevation = elevationTween.evaluate(activationAnimation);
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);
}
canvas.drawCircle( canvas.drawCircle(
center, center,
radiusTween.evaluate(enableAnimation), radius,
Paint()..color = colorTween.evaluate(enableAnimation), Paint()..color = color,
); );
} }
} }
/// The default shape of a [RangeSlider]'s thumbs. /// The default shape of a [RangeSlider]'s thumbs.
/// ///
/// If [useV2Slider] is true, then there is a shadow for the resting and
/// pressed state.
///
/// See also: /// See also:
/// ///
/// * [RangeSlider], which includes thumbs defined by this shape. /// * [RangeSlider], which includes thumbs defined by this shape.
...@@ -2283,8 +2490,14 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { ...@@ -2283,8 +2490,14 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
const RoundRangeSliderThumbShape({ const RoundRangeSliderThumbShape({
this.enabledThumbRadius = 10.0, this.enabledThumbRadius = 10.0,
this.disabledThumbRadius, this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
this.useV2Slider = false,
}) : assert(enabledThumbRadius != null); }) : assert(enabledThumbRadius != null);
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
/// The preferred radius of the round thumb shape when the slider is enabled. /// The preferred radius of the round thumb shape when the slider is enabled.
/// ///
/// If it is not provided, then the material default of 10 is used. /// If it is not provided, then the material default of 10 is used.
...@@ -2297,6 +2510,16 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { ...@@ -2297,6 +2510,16 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
final double disabledThumbRadius; final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; 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 @override
Size getPreferredSize(bool isEnabled, bool isDiscrete) { Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
...@@ -2314,6 +2537,7 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { ...@@ -2314,6 +2537,7 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
Thumb thumb, Thumb thumb,
bool isPressed,
}) { }) {
assert(context != null); assert(context != null);
assert(center != null); assert(center != null);
...@@ -2332,6 +2556,10 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { ...@@ -2332,6 +2556,10 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
end: sliderTheme.thumbColor, end: sliderTheme.thumbColor,
); );
final double radius = radiusTween.evaluate(enableAnimation); final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
// Add a stroke of 1dp around the circle if this thumb would overlap // Add a stroke of 1dp around the circle if this thumb would overlap
// the other thumb. // the other thumb.
...@@ -2361,10 +2589,19 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape { ...@@ -2361,10 +2589,19 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
} }
} }
final Color color = colorTween.evaluate(enableAnimation);
if (useV2Slider) {
final double evaluatedElevation = isPressed ? elevationTween.evaluate(activationAnimation) : elevation;
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);
}
canvas.drawCircle( canvas.drawCircle(
center, center,
radius, radius,
Paint()..color = colorTween.evaluate(enableAnimation), Paint()..color = color,
); );
} }
} }
...@@ -2390,7 +2627,8 @@ class RoundSliderOverlayShape extends SliderComponentShape { ...@@ -2390,7 +2627,8 @@ class RoundSliderOverlayShape extends SliderComponentShape {
/// The preferred radius of the round thumb shape when enabled. /// 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; final double overlayRadius;
@override @override
...@@ -2410,6 +2648,8 @@ class RoundSliderOverlayShape extends SliderComponentShape { ...@@ -2410,6 +2648,8 @@ class RoundSliderOverlayShape extends SliderComponentShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
@required TextDirection textDirection, @required TextDirection textDirection,
@required double value, @required double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
assert(context != null); assert(context != null);
assert(center != null); assert(center != null);
...@@ -2442,16 +2682,275 @@ class RoundSliderOverlayShape extends SliderComponentShape { ...@@ -2442,16 +2682,275 @@ class RoundSliderOverlayShape extends SliderComponentShape {
/// * [Slider], which includes a value indicator defined by this shape. /// * [Slider], which includes a value indicator defined by this shape.
/// * [SliderTheme], which can be used to configure the slider value indicator /// * [SliderTheme], which can be used to configure the slider value indicator
/// of all sliders in a widget subtree. /// of all sliders in a widget subtree.
class RectangularSliderValueIndicatorShape extends SliderComponentShape {
/// Create a slider value indicator that resembles a rectangular tooltip.
const RectangularSliderValueIndicatorShape();
static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter();
@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<double> activationAnimation,
@required Animation<double> enableAnimation,
bool isDiscrete,
@required TextPainter labelPainter,
@required RenderBox parentBox,
@required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
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);
}
}
/// The default 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 RectangularRangeSliderValueIndicatorShape
extends RangeSliderValueIndicatorShape {
/// Create a range slider value indicator that resembles a rectangular tooltip.
const RectangularRangeSliderValueIndicatorShape();
static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter();
@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<double> 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<double> activationAnimation,
Animation<double> 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));
if (rectangleWidth < sizeWithOverflow.width) {
return overflowLeft - overflowRight;
} else if (overflowLeft - overflowRight > 0) {
return overflowLeft - (edgePadding * textScaleFactor);
} else {
return -overflowRight + (edgePadding * textScaleFactor);
}
}
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 . The value indicator is in
/// the shape of an upside-down pear.
///
/// 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 { class PaddleSliderValueIndicatorShape extends SliderComponentShape {
/// Create a slider value indicator in the shape of an upside-down pear. /// Create a slider value indicator in the shape of an upside-down pear.
const PaddleSliderValueIndicatorShape(); const PaddleSliderValueIndicatorShape();
static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter(); static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override @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); assert(labelPainter != null);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter); assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor);
} }
@override @override
...@@ -2466,6 +2965,8 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { ...@@ -2466,6 +2965,8 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
assert(context != null); assert(context != null);
assert(center != null); assert(center != null);
...@@ -2474,23 +2975,27 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { ...@@ -2474,23 +2975,27 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
assert(labelPainter != null); assert(labelPainter != null);
assert(parentBox != null); assert(parentBox != null);
assert(sliderTheme != null); assert(sliderTheme != null);
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween( final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor, begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor, end: sliderTheme.valueIndicatorColor,
); );
_pathPainter.drawValueIndicator( _pathPainter.paint(
parentBox, parentBox,
context.canvas, context.canvas,
center, center,
Paint()..color = enableColor.evaluate(enableAnimation), Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value, activationAnimation.value,
labelPainter, labelPainter,
textScaleFactor,
sizeWithOverflow,
null, null,
); );
} }
} }
/// The default shape of a [RangeSlider]'s value indicators. /// A variant shape of a [RangeSlider]'s value indicators. The value indicator
/// is in the shape of an upside-down pear.
/// ///
/// See also: /// See also:
/// ///
...@@ -2501,12 +3006,18 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap ...@@ -2501,12 +3006,18 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
/// Create a slider value indicator in the shape of an upside-down pear. /// Create a slider value indicator in the shape of an upside-down pear.
const PaddleRangeSliderValueIndicatorShape(); const PaddleRangeSliderValueIndicatorShape();
static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter(); static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override @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); assert(labelPainter != null);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter); assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor);
} }
@override @override
...@@ -2515,12 +3026,16 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap ...@@ -2515,12 +3026,16 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
Offset center, Offset center,
TextPainter labelPainter, TextPainter labelPainter,
Animation<double> activationAnimation, Animation<double> activationAnimation,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
return _pathPainter.getHorizontalShift( return _pathPainter.getHorizontalShift(
parentBox: parentBox, parentBox: parentBox,
center: center, center: center,
labelPainter: labelPainter, labelPainter: labelPainter,
scale: activationAnimation.value, scale: activationAnimation.value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
); );
} }
...@@ -2538,6 +3053,8 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap ...@@ -2538,6 +3053,8 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
TextDirection textDirection, TextDirection textDirection,
Thumb thumb, Thumb thumb,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
assert(context != null); assert(context != null);
assert(center != null); assert(center != null);
...@@ -2546,25 +3063,28 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap ...@@ -2546,25 +3063,28 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
assert(labelPainter != null); assert(labelPainter != null);
assert(parentBox != null); assert(parentBox != null);
assert(sliderTheme != null); assert(sliderTheme != null);
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween( final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor, begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor, end: sliderTheme.valueIndicatorColor,
); );
// Add a stroke of 1dp around the top paddle. // Add a stroke of 1dp around the top paddle.
_pathPainter.drawValueIndicator( _pathPainter.paint(
parentBox, parentBox,
context.canvas, context.canvas,
center, center,
Paint()..color = enableColor.evaluate(enableAnimation), Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value, activationAnimation.value,
labelPainter, labelPainter,
textScaleFactor,
sizeWithOverflow,
isOnTop ? sliderTheme.overlappingShapeStrokeColor : null, isOnTop ? sliderTheme.overlappingShapeStrokeColor : null,
); );
} }
} }
class _PaddleSliderTrackShapePathPainter { class _PaddleSliderValueIndicatorPathPainter {
const _PaddleSliderTrackShapePathPainter(); const _PaddleSliderValueIndicatorPathPainter();
// These constants define the shape of the default value indicator. // These constants define the shape of the default value indicator.
// The value indicator changes shape based on the size of // The value indicator changes shape based on the size of
...@@ -2574,14 +3094,12 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2574,14 +3094,12 @@ class _PaddleSliderTrackShapePathPainter {
// Radius of the top lobe of the value indicator. // Radius of the top lobe of the value indicator.
static const double _topLobeRadius = 16.0; static const double _topLobeRadius = 16.0;
// Designed size of the label text. This is the size that the value indicator static const double _minLabelWidth = 16.0;
// was designed to contain. We scale it from here to fit other sizes.
static const double _labelTextDesignSize = 14.0;
// Radius of the bottom lobe of the value indicator. // Radius of the bottom lobe of the value indicator.
static const double _bottomLobeRadius = 10.0; static const double _bottomLobeRadius = 10.0;
static const double _labelPadding = 8.0; static const double _labelPadding = 8.0;
static const double _distanceBetweenTopBottomCenters = 40.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; static const double _bottomNeckRadius = 4.5;
// The base of the triangle between the top lobe center and the centers of // The base of the triangle between the top lobe center and the centers of
// the two top neck arcs. // the two top neck arcs.
...@@ -2609,10 +3127,12 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2609,10 +3127,12 @@ class _PaddleSliderTrackShapePathPainter {
bool isEnabled, bool isEnabled,
bool isDiscrete, bool isDiscrete,
TextPainter labelPainter, TextPainter labelPainter,
double textScaleFactor,
) { ) {
assert(labelPainter != null); assert(labelPainter != null);
final double textScaleFactor = labelPainter.height / _labelTextDesignSize; assert(textScaleFactor != null && textScaleFactor >= 0);
return Size(labelPainter.width + 2 * _labelPadding * textScaleFactor, _preferredHeight * textScaleFactor); 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 // Adds an arc to the path that has the attributes passed in. This is
...@@ -2628,15 +3148,17 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2628,15 +3148,17 @@ class _PaddleSliderTrackShapePathPainter {
Offset center, Offset center,
TextPainter labelPainter, TextPainter labelPainter,
double scale, 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 inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0; final double labelHalfWidth = labelPainter.width / 2.0;
final double halfWidthNeeded = math.max( final double halfWidthNeeded = math.max(
0.0, 0.0,
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding), 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; return shift * textScaleFactor;
} }
...@@ -2647,8 +3169,9 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2647,8 +3169,9 @@ class _PaddleSliderTrackShapePathPainter {
double halfWidthNeeded, double halfWidthNeeded,
double scale, double scale,
Offset center, Offset center,
double widthWithOverflow,
) { ) {
const double edgeMargin = 4.0; const double edgeMargin = 8.0;
final Rect topLobeRect = Rect.fromLTWH( final Rect topLobeRect = Rect.fromLTWH(
-_topLobeRadius - halfWidthNeeded, -_topLobeRadius - halfWidthNeeded,
-_topLobeRadius - _distanceBetweenTopBottomCenters, -_topLobeRadius - _distanceBetweenTopBottomCenters,
...@@ -2661,12 +3184,11 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2661,12 +3184,11 @@ class _PaddleSliderTrackShapePathPainter {
final Offset bottomRight = (topLobeRect.bottomRight * scale) + center; final Offset bottomRight = (topLobeRect.bottomRight * scale) + center;
double shift = 0.0; double shift = 0.0;
final double startGlobal = parentBox.localToGlobal(Offset.zero).dx; if (topLeft.dx < edgeMargin) {
if (topLeft.dx < startGlobal + edgeMargin) { shift = edgeMargin - topLeft.dx;
shift = startGlobal + 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) { if (bottomRight.dx > endGlobal - edgeMargin) {
shift = endGlobal - edgeMargin - bottomRight.dx; shift = endGlobal - edgeMargin - bottomRight.dx;
} }
...@@ -2682,13 +3204,15 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2682,13 +3204,15 @@ class _PaddleSliderTrackShapePathPainter {
return shift; return shift;
} }
void drawValueIndicator( void paint(
RenderBox parentBox, RenderBox parentBox,
Canvas canvas, Canvas canvas,
Offset center, Offset center,
Paint paint, Paint paint,
double scale, double scale,
TextPainter labelPainter, TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
Color strokePaintColor, Color strokePaintColor,
) { ) {
if (scale == 0.0) { if (scale == 0.0) {
...@@ -2696,10 +3220,10 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2696,10 +3220,10 @@ class _PaddleSliderTrackShapePathPainter {
// our math below will attempt to divide by zero and send needless NaNs to the engine. // our math below will attempt to divide by zero and send needless NaNs to the engine.
return; return;
} }
assert(!sizeWithOverflow.isEmpty);
// The entire value indicator should scale with the size of the label, // The entire value indicator should scale with the size of the label,
// to keep it large enough to encompass the label text. // to keep it large enough to encompass the label text.
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
final double overallScale = scale * textScaleFactor; final double overallScale = scale * textScaleFactor;
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0; final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0; final double labelHalfWidth = labelPainter.width / 2.0;
...@@ -2741,7 +3265,7 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2741,7 +3265,7 @@ class _PaddleSliderTrackShapePathPainter {
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding), 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 leftWidthNeeded = halfWidthNeeded - shift;
final double rightWidthNeeded = halfWidthNeeded + shift; final double rightWidthNeeded = halfWidthNeeded + shift;
...@@ -2750,7 +3274,8 @@ class _PaddleSliderTrackShapePathPainter { ...@@ -2750,7 +3274,8 @@ class _PaddleSliderTrackShapePathPainter {
final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / _neckTriangleBase)); final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / _neckTriangleBase));
final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / _neckTriangleBase)); 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 // 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 leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees; final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
// The center of the top left neck arc. // The center of the top left neck arc.
......
...@@ -17,7 +17,8 @@ void main() { ...@@ -17,7 +17,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -39,6 +40,7 @@ void main() { ...@@ -39,6 +40,7 @@ void main() {
}, },
), ),
), ),
),
); );
// No thumbs get select when tapping between the thumbs outside the touch // No thumbs get select when tapping between the thumbs outside the touch
...@@ -72,7 +74,8 @@ void main() { ...@@ -72,7 +74,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -94,6 +97,7 @@ void main() { ...@@ -94,6 +97,7 @@ void main() {
}, },
), ),
), ),
),
); );
// No thumbs get select when tapping between the thumbs outside the touch // No thumbs get select when tapping between the thumbs outside the touch
...@@ -127,7 +131,8 @@ void main() { ...@@ -127,7 +131,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -152,6 +157,7 @@ void main() { ...@@ -152,6 +157,7 @@ void main() {
}, },
), ),
), ),
),
); );
// No thumbs get select when tapping between the thumbs outside the touch // No thumbs get select when tapping between the thumbs outside the touch
...@@ -187,7 +193,8 @@ void main() { ...@@ -187,7 +193,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -212,6 +219,7 @@ void main() { ...@@ -212,6 +219,7 @@ void main() {
}, },
), ),
), ),
),
); );
// No thumbs get select when tapping between the thumbs outside the touch // No thumbs get select when tapping between the thumbs outside the touch
...@@ -247,7 +255,8 @@ void main() { ...@@ -247,7 +255,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -269,6 +278,7 @@ void main() { ...@@ -269,6 +278,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -292,7 +302,8 @@ void main() { ...@@ -292,7 +302,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -314,6 +325,7 @@ void main() { ...@@ -314,6 +325,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -337,7 +349,8 @@ void main() { ...@@ -337,7 +349,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -362,6 +375,7 @@ void main() { ...@@ -362,6 +375,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -385,7 +399,8 @@ void main() { ...@@ -385,7 +399,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -410,6 +425,7 @@ void main() { ...@@ -410,6 +425,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -433,7 +449,8 @@ void main() { ...@@ -433,7 +449,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -455,6 +472,7 @@ void main() { ...@@ -455,6 +472,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -484,7 +502,8 @@ void main() { ...@@ -484,7 +502,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -506,6 +525,7 @@ void main() { ...@@ -506,6 +525,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -535,7 +555,8 @@ void main() { ...@@ -535,7 +555,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -560,6 +581,7 @@ void main() { ...@@ -560,6 +581,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -589,7 +611,8 @@ void main() { ...@@ -589,7 +611,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -614,6 +637,7 @@ void main() { ...@@ -614,6 +637,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -643,7 +667,8 @@ void main() { ...@@ -643,7 +667,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -665,6 +690,7 @@ void main() { ...@@ -665,6 +690,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -694,7 +720,8 @@ void main() { ...@@ -694,7 +720,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -716,6 +743,7 @@ void main() { ...@@ -716,6 +743,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -745,7 +773,8 @@ void main() { ...@@ -745,7 +773,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -770,6 +799,7 @@ void main() { ...@@ -770,6 +799,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -799,7 +829,8 @@ void main() { ...@@ -799,7 +829,8 @@ void main() {
RangeValues values = const RangeValues(30, 70); RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -824,6 +855,7 @@ void main() { ...@@ -824,6 +855,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -855,7 +887,8 @@ void main() { ...@@ -855,7 +887,8 @@ void main() {
RangeValues endValues; RangeValues endValues;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -885,6 +918,7 @@ void main() { ...@@ -885,6 +918,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -911,7 +945,8 @@ void main() { ...@@ -911,7 +945,8 @@ void main() {
RangeValues endValues; RangeValues endValues;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -941,6 +976,7 @@ void main() { ...@@ -941,6 +976,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
...@@ -995,12 +1031,14 @@ void main() { ...@@ -995,12 +1031,14 @@ void main() {
Color inactiveColor, Color inactiveColor,
int divisions, int divisions,
bool enabled = true, bool enabled = true,
bool useV2Slider = false,
}) { }) {
RangeValues values = const RangeValues(0.5, 0.75); RangeValues values = const RangeValues(0.5, 0.75);
final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) { final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues; values = newValues;
}; };
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1015,6 +1053,8 @@ void main() { ...@@ -1015,6 +1053,8 @@ void main() {
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
useV2Slider: useV2Slider,
),
), ),
), ),
), ),
...@@ -1023,6 +1063,31 @@ void main() { ...@@ -1023,6 +1063,31 @@ void main() {
); );
} }
testWidgets('Range Slider V2 uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(theme: theme, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
// Check default theme for enabled widget.
expect(sliderBox, paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: sliderTheme.activeTrackColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme(); final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
...@@ -1046,6 +1111,34 @@ void main() { ...@@ -1046,6 +1111,34 @@ void main() {
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes when setting the active color', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(theme: theme, activeColor: activeColor, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: activeColor)
..rrect(color: sliderTheme.inactiveTrackColor));
expect(
sliderBox,
paints
..circle(color: activeColor)
..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 active color', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes when setting the active color', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed); const Color activeColor = Color(0xcafefeed);
final ThemeData theme = _buildTheme(); final ThemeData theme = _buildTheme();
...@@ -1072,6 +1165,33 @@ void main() { ...@@ -1072,6 +1165,33 @@ void main() {
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async {
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(theme: theme, inactiveColor: inactiveColor, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: sliderTheme.activeTrackColor)
..rrect(color: inactiveColor));
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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async {
const Color inactiveColor = Color(0xdeadbeef); const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme(); final ThemeData theme = _buildTheme();
...@@ -1097,6 +1217,38 @@ void main() { ...@@ -1097,6 +1217,38 @@ void main() {
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
useV2Slider: true,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: activeColor)
..rrect(color: inactiveColor));
expect(
sliderBox,
paints
..circle(color: activeColor)
..circle(color: activeColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
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 { testWidgets('Range Slider uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed); const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef); const Color inactiveColor = Color(0xdeadbeef);
...@@ -1128,6 +1280,36 @@ void main() { ...@@ -1128,6 +1280,36 @@ void main() {
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(theme: theme, divisions: 3, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: sliderTheme.activeTrackColor)
..rrect(color: sliderTheme.inactiveTrackColor));
expect(
sliderBox,
paints
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme(); final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
...@@ -1156,6 +1338,48 @@ void main() { ...@@ -1156,6 +1338,48 @@ void main() {
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
divisions: 3,
useV2Slider: true,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: activeColor)
..rrect(color: inactiveColor));
expect(
sliderBox,
paints
..circle(color: activeColor)
..circle(color: activeColor)
..circle(color: inactiveColor)
..circle(color: activeColor)
..circle(color: activeColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed); const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef); const Color inactiveColor = Color(0xdeadbeef);
...@@ -1195,6 +1419,27 @@ void main() { ...@@ -1195,6 +1419,27 @@ void main() {
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes for a default disabled slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(theme: theme, enabled: false, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.disabledInactiveTrackColor)
..rect(color: sliderTheme.disabledActiveTrackColor)
..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 for a default disabled slider', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes for a default disabled slider', (WidgetTester tester) async {
final ThemeData theme = _buildTheme(); final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
...@@ -1215,6 +1460,35 @@ void main() { ...@@ -1215,6 +1460,35 @@ void main() {
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes for a disabled slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
enabled: false,
useV2Slider: true,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.disabledInactiveTrackColor)
..rect(color: sliderTheme.disabledActiveTrackColor)
..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 for a disabled slider with active and inactive colors', (WidgetTester tester) async { testWidgets('Range Slider uses the right theme colors for the right shapes for a disabled slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed); const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef); const Color inactiveColor = Color(0xdeadbeef);
...@@ -1241,6 +1515,66 @@ void main() { ...@@ -1241,6 +1515,66 @@ void main() {
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor)));
}); });
testWidgets('Range Slider V2 uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
RangeValues values = const RangeValues(0.5, 0.75);
Widget buildApp({
Color activeColor,
Color inactiveColor,
int divisions,
bool enabled = true,
}) {
final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
divisions: divisions,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
useV2Slider: true,
),
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp(divisions: 3));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0);
final TestGesture gesture = await tester.startGesture(topRight);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(values.end, equals(1));
expect(
valueIndicatorBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.valueIndicatorColor),
);
await gesture.up();
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
});
testWidgets('Range Slider uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async { 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 customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef); const Color customColor2 = Color(0xdeadbeef);
...@@ -1257,7 +1591,8 @@ void main() { ...@@ -1257,7 +1591,8 @@ void main() {
final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) { final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues; values = newValues;
}; };
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1277,12 +1612,13 @@ void main() { ...@@ -1277,12 +1612,13 @@ void main() {
), ),
), ),
), ),
),
); );
} }
await tester.pumpWidget(buildApp(divisions: 3)); await tester.pumpWidget(buildApp(divisions: 3));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0); final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0);
TestGesture gesture = await tester.startGesture(topRight); TestGesture gesture = await tester.startGesture(topRight);
...@@ -1290,7 +1626,7 @@ void main() { ...@@ -1290,7 +1626,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(values.end, equals(1)); expect(values.end, equals(1));
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path(color: sliderTheme.valueIndicatorColor) ..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.valueIndicatorColor), ..path(color: sliderTheme.valueIndicatorColor),
...@@ -1310,7 +1646,7 @@ void main() { ...@@ -1310,7 +1646,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(values.end, equals(1)); expect(values.end, equals(1));
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path(color: customColor1) ..path(color: customColor1)
..path(color: customColor1), ..path(color: customColor1),
...@@ -1318,6 +1654,77 @@ void main() { ...@@ -1318,6 +1654,77 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('Range Slider V2 top thumb gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
thumbColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
useV2Slider: true,
),
),
),
),
);
},
),
),
),
);
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, closeTo(0.5, 0.03));
expect(values.end, closeTo(0.5, 0.03));
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints
..circle(color: sliderTheme.thumbColor)
..circle(color: sliderTheme.overlappingShapeStrokeColor)
..circle(color: sliderTheme.thumbColor),
);
});
testWidgets('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async { testWidgets('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
...@@ -1332,7 +1739,8 @@ void main() { ...@@ -1332,7 +1739,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1357,6 +1765,7 @@ void main() { ...@@ -1357,6 +1765,7 @@ void main() {
}, },
), ),
), ),
),
); );
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
...@@ -1386,6 +1795,83 @@ void main() { ...@@ -1386,6 +1795,83 @@ void main() {
); );
}); });
testWidgets('Range Slider V2 top value indicator gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
showValueIndicator: ShowValueIndicator.always,
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
useV2Slider: true,
),
),
),
),
);
},
),
),
),
);
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, closeTo(0.5, 0.03));
expect(values.end, closeTo(0.5, 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor)
..path(color: sliderTheme.valueIndicatorColor),
);
await gesture.up();
});
testWidgets('Range Slider top value indicator gets stroked when overlapping', (WidgetTester tester) async { testWidgets('Range Slider top value indicator gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7); RangeValues values = const RangeValues(0.3, 0.7);
...@@ -1401,7 +1887,8 @@ void main() { ...@@ -1401,7 +1887,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1427,9 +1914,10 @@ void main() { ...@@ -1427,9 +1914,10 @@ void main() {
}, },
), ),
), ),
),
); );
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius. // inwards by the overlay radius.
...@@ -1450,7 +1938,84 @@ void main() { ...@@ -1450,7 +1938,84 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor)
..path(color: sliderTheme.valueIndicatorColor),
);
await gesture.up();
});
testWidgets('Range Slider V2 top value indicator gets stroked when overlapping with large text scale', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
showValueIndicator: ShowValueIndicator.always,
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: 2.0),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
useV2Slider: true,
),
),
),
),
);
},
),
),
),
);
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, closeTo(0.5, 0.03));
expect(values.end, closeTo(0.5, 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints paints
..path(color: sliderTheme.valueIndicatorColor) ..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor) ..path(color: sliderTheme.overlappingShapeStrokeColor)
...@@ -1475,7 +2040,8 @@ void main() { ...@@ -1475,7 +2040,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1501,9 +2067,10 @@ void main() { ...@@ -1501,9 +2067,10 @@ void main() {
}, },
), ),
), ),
),
); );
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating // Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius. // inwards by the overlay radius.
...@@ -1524,7 +2091,7 @@ void main() { ...@@ -1524,7 +2091,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path(color: sliderTheme.valueIndicatorColor) ..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor) ..path(color: sliderTheme.overlappingShapeStrokeColor)
...@@ -1565,7 +2132,8 @@ void main() { ...@@ -1565,7 +2132,8 @@ void main() {
'labelStart: "lowerValue"', 'labelStart: "lowerValue"',
'labelEnd: "upperValue"', 'labelEnd: "upperValue"',
'activeColor: MaterialColor(primary value: Color(0xff2196f3))', 'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))' 'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
'useV1Slider',
]); ]);
}); });
} }
...@@ -39,6 +39,8 @@ class LoggingThumbShape extends SliderComponentShape { ...@@ -39,6 +39,8 @@ class LoggingThumbShape extends SliderComponentShape {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
log.add(thumbCenter); log.add(thumbCenter);
final Paint thumbPaint = Paint()..color = Colors.red; final Paint thumbPaint = Paint()..color = Colors.red;
...@@ -76,7 +78,8 @@ void main() { ...@@ -76,7 +78,8 @@ void main() {
double endValue; double endValue;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -105,6 +108,7 @@ void main() { ...@@ -105,6 +108,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -134,7 +138,8 @@ void main() { ...@@ -134,7 +138,8 @@ void main() {
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -157,6 +162,7 @@ void main() { ...@@ -157,6 +162,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -184,8 +190,10 @@ void main() { ...@@ -184,8 +190,10 @@ void main() {
int startValueUpdates = 0; int startValueUpdates = 0;
int endValueUpdates = 0; int endValueUpdates = 0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -217,6 +225,7 @@ void main() { ...@@ -217,6 +225,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -238,7 +247,8 @@ void main() { ...@@ -238,7 +247,8 @@ void main() {
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -262,6 +272,7 @@ void main() { ...@@ -262,6 +272,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -290,7 +301,8 @@ void main() { ...@@ -290,7 +301,8 @@ void main() {
final List<Offset> log = <Offset>[]; final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log); final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -318,6 +330,7 @@ void main() { ...@@ -318,6 +330,7 @@ void main() {
}, },
), ),
), ),
),
); );
final List<Offset> expectedLog = <Offset>[ final List<Offset> expectedLog = <Offset>[
...@@ -359,7 +372,8 @@ void main() { ...@@ -359,7 +372,8 @@ void main() {
int updates = 0; int updates = 0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -383,6 +397,7 @@ void main() { ...@@ -383,6 +397,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -401,7 +416,8 @@ void main() { ...@@ -401,7 +416,8 @@ void main() {
final List<Offset> log = <Offset>[]; final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log); final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -429,6 +445,7 @@ void main() { ...@@ -429,6 +445,7 @@ void main() {
}, },
), ),
), ),
),
); );
final List<Offset> expectedLog = <Offset>[ final List<Offset> expectedLog = <Offset>[
...@@ -469,7 +486,8 @@ void main() { ...@@ -469,7 +486,8 @@ void main() {
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -498,6 +516,7 @@ void main() { ...@@ -498,6 +516,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, equals(0.0)); expect(value, equals(0.0));
...@@ -520,7 +539,9 @@ void main() { ...@@ -520,7 +539,9 @@ void main() {
testWidgets('Slider can be given zero values', (WidgetTester tester) async { testWidgets('Slider can be given zero values', (WidgetTester tester) async {
final List<double> log = <double>[]; final List<double> log = <double>[];
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -535,13 +556,17 @@ void main() { ...@@ -535,13 +556,17 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
await tester.tap(find.byType(Slider)); await tester.tap(find.byType(Slider));
expect(log, <double>[0.5]); expect(log, <double>[0.5]);
log.clear(); log.clear();
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -556,13 +581,237 @@ void main() { ...@@ -556,13 +581,237 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
await tester.tap(find.byType(Slider)); await tester.tap(find.byType(Slider));
expect(log, <double>[]); expect(log, <double>[]);
log.clear(); log.clear();
}); });
testWidgets('Slider V2 uses the right theme colors for the right components', (WidgetTester tester) async {
const Color customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
disabledThumbColor: Color(0xff000001),
disabledActiveTickMarkColor: Color(0xff000002),
disabledActiveTrackColor: Color(0xff000003),
disabledInactiveTickMarkColor: Color(0xff000004),
disabledInactiveTrackColor: Color(0xff000005),
activeTrackColor: Color(0xff000006),
activeTickMarkColor: Color(0xff000007),
inactiveTrackColor: Color(0xff000008),
inactiveTickMarkColor: Color(0xff000009),
overlayColor: Color(0xff000010),
thumbColor: Color(0xff000011),
valueIndicatorColor: Color(0xff000012),
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
double value = 0.45;
Widget buildApp({
Color activeColor,
Color inactiveColor,
int divisions,
bool enabled = true,
}) {
final ValueChanged<double> onChanged = !enabled
? null
: (double d) {
value = d;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: Slider(
value: value,
label: '$value',
divisions: divisions,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
useV2Slider: true,
),
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Check default theme for enabled widget.
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..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..rrect(color: customColor1)..rrect(color: sliderTheme.inactiveTrackColor));
expect(sliderBox, paints..shadow(color: Colors.black));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..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..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..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..rrect(color: customColor1)..rrect(color: customColor2));
expect(sliderBox, paints..shadow(color: Colors.black));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..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..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor));
expect(
sliderBox,
paints
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..shadow(color: Colors.black)
..circle(color: sliderTheme.thumbColor)
);
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
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(
activeColor: customColor1,
inactiveColor: customColor2,
divisions: 3,
));
expect(sliderBox, paints..rrect(color: customColor1)..rrect(color: customColor2));
expect(
sliderBox,
paints
..circle(color: customColor2)
..circle(color: customColor2)
..circle(color: customColor1)
..circle(color: customColor1)
..shadow(color: Colors.black)
..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
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 default theme for disabled widget.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..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..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
..rrect(color: sliderTheme.disabledActiveTrackColor)
..rrect(color: sliderTheme.disabledInactiveTrackColor));
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
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));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
valueIndicatorBox,
paints
..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)
..shadow(color: Colors.black)
..circle(color: sliderTheme.thumbColor)
..path(color: sliderTheme.valueIndicatorColor),
);
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,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
valueIndicatorBox,
paints
..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
..shadow(color: Colors.black)
..circle(color: customColor1) // thumb
..path(color: sliderTheme.valueIndicatorColor), // indicator
);
await gesture.up();
});
testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async { testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async {
const Color customColor1 = Color(0xcafefeed); const Color customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef); const Color customColor2 = Color(0xdeadbeef);
...@@ -597,7 +846,8 @@ void main() { ...@@ -597,7 +846,8 @@ void main() {
: (double d) { : (double d) {
value = d; value = d;
}; };
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -617,12 +867,14 @@ void main() { ...@@ -617,12 +867,14 @@ void main() {
), ),
), ),
), ),
),
); );
} }
await tester.pumpWidget(buildApp()); await tester.pumpWidget(buildApp());
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider)); final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Check default theme for enabled widget. // Check default theme for enabled widget.
expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor)); expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor));
...@@ -729,7 +981,7 @@ void main() { ...@@ -729,7 +981,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0)); expect(value, equals(2.0 / 3.0));
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..rect(color: sliderTheme.activeTrackColor) ..rect(color: sliderTheme.activeTrackColor)
..rect(color: sliderTheme.inactiveTrackColor) ..rect(color: sliderTheme.inactiveTrackColor)
...@@ -738,8 +990,8 @@ void main() { ...@@ -738,8 +990,8 @@ void main() {
..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor)
..path(color: sliderTheme.valueIndicatorColor) ..circle(color: sliderTheme.thumbColor)
..circle(color: sliderTheme.thumbColor), ..path(color: sliderTheme.valueIndicatorColor),
); );
await gesture.up(); await gesture.up();
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
...@@ -757,7 +1009,7 @@ void main() { ...@@ -757,7 +1009,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0)); expect(value, equals(2.0 / 3.0));
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..rect(color: customColor1) // active track ..rect(color: customColor1) // active track
..rect(color: customColor2) // inactive track ..rect(color: customColor2) // inactive track
...@@ -766,15 +1018,17 @@ void main() { ...@@ -766,15 +1018,17 @@ void main() {
..circle(color: customColor2) // 2nd tick mark ..circle(color: customColor2) // 2nd tick mark
..circle(color: customColor2) // 3rd tick mark ..circle(color: customColor2) // 3rd tick mark
..circle(color: customColor1) // 4th tick mark ..circle(color: customColor1) // 4th tick mark
..path(color: customColor1) // indicator ..circle(color: customColor1) // thumb
..circle(color: customColor1), // thumb ..path(color: customColor1), // indicator
); );
await gesture.up(); await gesture.up();
}); });
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async { testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -794,7 +1048,9 @@ void main() { ...@@ -794,7 +1048,9 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
await tester.tap(find.byType(Slider)); await tester.tap(find.byType(Slider));
expect(value, equals(0.5)); expect(value, equals(0.5));
...@@ -802,7 +1058,9 @@ void main() { ...@@ -802,7 +1058,9 @@ void main() {
testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async { testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -817,7 +1075,9 @@ void main() { ...@@ -817,7 +1075,9 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
final Offset center = tester.getCenter(find.byType(Slider)); final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center); final TestGesture gesture = await tester.startGesture(center);
...@@ -833,7 +1093,9 @@ void main() { ...@@ -833,7 +1093,9 @@ void main() {
testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async { testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -848,7 +1110,9 @@ void main() { ...@@ -848,7 +1110,9 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
final Offset center = tester.getCenter(find.byType(Slider)); final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center); final TestGesture gesture = await tester.startGesture(center);
...@@ -863,7 +1127,9 @@ void main() { ...@@ -863,7 +1127,9 @@ void main() {
}); });
testWidgets('Slider sizing', (WidgetTester tester) async { testWidgets('Slider sizing', (WidgetTester tester) async {
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -876,10 +1142,14 @@ void main() { ...@@ -876,10 +1142,14 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0));
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -894,10 +1164,14 @@ void main() { ...@@ -894,10 +1164,14 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 600.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 600.0));
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -914,10 +1188,159 @@ void main() { ...@@ -914,10 +1188,159 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0));
}); });
testWidgets('Slider V2 respects textScaleFactor', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
Widget buildSlider({
double textScaleFactor,
bool isDiscrete = true,
ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete,
}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Material(
child: Theme(
data: Theme.of(context).copyWith(
sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show),
),
child: Center(
child: OverflowBox(
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Slider(
key: sliderKey,
min: 0.0,
max: 100.0,
divisions: isDiscrete ? 10 : null,
label: '${value.round()}',
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
useV2Slider: true,
),
),
),
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(
tester.firstRenderObject(find.byType(Overlay)),
paints
..path(
includes: const <Offset>[
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();
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(
tester.firstRenderObject(find.byType(Overlay)),
paints
..path(
includes: const <Offset>[
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();
// Check continuous
await tester.pumpWidget(buildSlider(
textScaleFactor: 1.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.firstRenderObject(find.byType(Overlay)),
paints
..path(
includes: const <Offset>[
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();
await tester.pumpWidget(buildSlider(
textScaleFactor: 2.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(
tester.firstRenderObject(find.byType(Overlay)),
paints
..path(
includes: const <Offset>[
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();
}, skip: isBrowser);
testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async { testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
final Key sliderKey = UniqueKey(); final Key sliderKey = UniqueKey();
double value = 0.0; double value = 0.0;
...@@ -927,7 +1350,8 @@ void main() { ...@@ -927,7 +1350,8 @@ void main() {
bool isDiscrete = true, bool isDiscrete = true,
ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete, ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete,
}) { }) {
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -962,6 +1386,7 @@ void main() { ...@@ -962,6 +1386,7 @@ void main() {
); );
}, },
), ),
),
); );
} }
...@@ -970,7 +1395,7 @@ void main() { ...@@ -970,7 +1395,7 @@ void main() {
TestGesture gesture = await tester.startGesture(center); TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0)); expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 1.0, y: 1.0));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -980,7 +1405,7 @@ void main() { ...@@ -980,7 +1405,7 @@ void main() {
gesture = await tester.startGesture(center); gesture = await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0)); expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 2.0, y: 2.0));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -995,7 +1420,7 @@ void main() { ...@@ -995,7 +1420,7 @@ void main() {
gesture = await tester.startGesture(center); gesture = await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0)); expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 1.0, y: 1.0));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1009,7 +1434,7 @@ void main() { ...@@ -1009,7 +1434,7 @@ void main() {
gesture = await tester.startGesture(center); gesture = await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0)); expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 2.0, y: 2.0));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1019,7 +1444,8 @@ void main() { ...@@ -1019,7 +1444,8 @@ void main() {
Widget buildSlider({ Widget buildSlider({
int divisions, int divisions,
}) { }) {
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1035,6 +1461,7 @@ void main() { ...@@ -1035,6 +1461,7 @@ void main() {
), ),
), ),
), ),
),
); );
} }
...@@ -1064,6 +1491,139 @@ void main() { ...@@ -1064,6 +1491,139 @@ void main() {
expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 1)); expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 1));
}); });
testWidgets('Slider V2 has correct animations when reparented', (WidgetTester tester) async {
final Key sliderKey = GlobalKey(debugLabel: 'A');
double value = 0.0;
Widget buildSlider(int parents) {
Widget createParents(int parents, StateSetter setState) {
Widget slider = Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
useV2Slider: true,
);
for (int i = 0; i < parents; ++i) {
slider = Column(children: <Widget>[slider]);
}
return slider;
}
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: createParents(parents, setState),
),
);
},
),
),
);
}
Future<void> testReparenting(bool reparent) async {
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
final Offset center = tester.getCenter(find.byType(Slider));
// Move to 0.0.
TestGesture gesture = await tester.startGesture(Offset.zero);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..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.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),
);
gesture = await tester.startGesture(center);
await tester.pump();
// Wait for animations to start.
await tester.pump(const Duration(milliseconds: 25));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
expect(
sliderBox,
paints
..circle(x: 111.20703125, y: 24.0, radius: 5.687664985656738)
..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.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),
);
// Reparenting in the middle of an animation should do nothing.
if (reparent) {
await tester.pumpWidget(buildSlider(2));
}
// Move a little further in the animations.
await tester.pump(const Duration(milliseconds: 10));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
expect(
sliderBox,
paints
..circle(x: 190.0135726928711, y: 24.0, radius: 12.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.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.
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..circle(x: 400.0, y: 24.0, radius: 24.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.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();
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..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.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 tester.pumpWidget(buildSlider(1));
// Do it once without reparenting in the middle of an animation
await testReparenting(false);
// Now do it again with reparenting in the middle of an animation.
await testReparenting(true);
});
testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async { testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async {
final Key sliderKey = GlobalKey(debugLabel: 'A'); final Key sliderKey = GlobalKey(debugLabel: 'A');
double value = 0.0; double value = 0.0;
...@@ -1087,7 +1647,8 @@ void main() { ...@@ -1087,7 +1647,8 @@ void main() {
return slider; return slider;
} }
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1099,6 +1660,7 @@ void main() { ...@@ -1099,6 +1660,7 @@ void main() {
); );
}, },
), ),
),
); );
} }
...@@ -1194,10 +1756,12 @@ void main() { ...@@ -1194,10 +1756,12 @@ void main() {
await testReparenting(true); await testReparenting(true);
}); });
testWidgets('Slider Semantics', (WidgetTester tester) async { testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality( await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1208,6 +1772,7 @@ void main() { ...@@ -1208,6 +1772,7 @@ void main() {
), ),
), ),
), ),
),
)); ));
expect( expect(
...@@ -1216,11 +1781,23 @@ void main() { ...@@ -1216,11 +1781,23 @@ void main() {
TestSemantics.root(children: <TestSemantics>[ TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 1, id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 3,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
value: '50%', value: '50%',
increasedValue: '55%', increasedValue: '55%',
decreasedValue: '45%', decreasedValue: '45%',
textDirection: TextDirection.ltr, ),
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index, ],
),
],
), ),
]), ]),
ignoreRect: true, ignoreRect: true,
...@@ -1228,7 +1805,8 @@ void main() { ...@@ -1228,7 +1805,8 @@ void main() {
)); ));
// Disable slider // Disable slider
await tester.pumpWidget(Directionality( await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1239,12 +1817,24 @@ void main() { ...@@ -1239,12 +1817,24 @@ void main() {
), ),
), ),
), ),
),
)); ));
expect( expect(
semantics, semantics,
hasSemantics( hasSemantics(
TestSemantics.root(), TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
),
],
),
]),
ignoreRect: true, ignoreRect: true,
ignoreTransform: true, ignoreTransform: true,
)); ));
...@@ -1252,11 +1842,12 @@ void main() { ...@@ -1252,11 +1842,12 @@ void main() {
semantics.dispose(); semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('Slider Semantics', (WidgetTester tester) async { testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
Theme( MaterialApp(
home: Theme(
data: ThemeData.light(), data: ThemeData.light(),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -1273,6 +1864,7 @@ void main() { ...@@ -1273,6 +1864,7 @@ void main() {
), ),
), ),
), ),
),
); );
expect( expect(
...@@ -1281,23 +1873,35 @@ void main() { ...@@ -1281,23 +1873,35 @@ void main() {
TestSemantics.root(children: <TestSemantics>[ TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 1, id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 3,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
value: '50%', value: '50%',
increasedValue: '60%', increasedValue: '60%',
decreasedValue: '40%', decreasedValue: '40%',
textDirection: TextDirection.ltr, ),
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index, ],
),
],
), ),
]), ]),
ignoreRect: true, ignoreRect: true,
ignoreTransform: true, ignoreTransform: true,
)); ));
semantics.dispose(); semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async { testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality( await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1312,6 +1916,7 @@ void main() { ...@@ -1312,6 +1916,7 @@ void main() {
), ),
), ),
), ),
),
)); ));
expect( expect(
...@@ -1320,11 +1925,22 @@ void main() { ...@@ -1320,11 +1925,22 @@ void main() {
TestSemantics.root(children: <TestSemantics>[ TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 1, id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 3,
actions: SemanticsAction.increase.index | SemanticsAction.decrease.index,
value: '40', value: '40',
increasedValue: '60', increasedValue: '60',
decreasedValue: '20', decreasedValue: '20',
textDirection: TextDirection.ltr, ),
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index, ],
),
],
), ),
]), ]),
ignoreRect: true, ignoreRect: true,
...@@ -1342,7 +1958,8 @@ void main() { ...@@ -1342,7 +1958,8 @@ void main() {
double value = 0.45; double value = 0.45;
Widget buildApp({ SliderThemeData sliderTheme, int divisions, bool enabled = true }) { Widget buildApp({ SliderThemeData sliderTheme, int divisions, bool enabled = true }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null; final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window), data: MediaQueryData.fromWindow(window),
...@@ -1363,6 +1980,7 @@ void main() { ...@@ -1363,6 +1980,7 @@ void main() {
), ),
), ),
), ),
),
); );
} }
...@@ -1379,9 +1997,9 @@ void main() { ...@@ -1379,9 +1997,9 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
expect( expect(
sliderBox, valueIndicatorBox,
isVisible isVisible
? (paints..path(color: theme.valueIndicatorColor)) ? (paints..path(color: theme.valueIndicatorColor))
: isNot(paints..path(color: theme.valueIndicatorColor)), : isNot(paints..path(color: theme.valueIndicatorColor)),
...@@ -1421,7 +2039,8 @@ void main() { ...@@ -1421,7 +2039,8 @@ void main() {
final Key sliderKey = UniqueKey(); final Key sliderKey = UniqueKey();
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1445,6 +2064,7 @@ void main() { ...@@ -1445,6 +2064,7 @@ void main() {
}, },
), ),
), ),
),
); );
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
...@@ -1517,7 +2137,8 @@ void main() { ...@@ -1517,7 +2137,8 @@ void main() {
final Key sliderKey = UniqueKey(); final Key sliderKey = UniqueKey();
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1547,6 +2168,7 @@ void main() { ...@@ -1547,6 +2168,7 @@ void main() {
}, },
), ),
), ),
),
); );
final RenderBox renderObject = tester.renderObject<RenderBox>(find.byType(Slider)); final RenderBox renderObject = tester.renderObject<RenderBox>(find.byType(Slider));
...@@ -1583,6 +2205,7 @@ void main() { ...@@ -1583,6 +2205,7 @@ void main() {
'label: "Set a value"', 'label: "Set a value"',
'activeColor: MaterialColor(primary value: Color(0xff2196f3))', 'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))', 'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
'useV1Slider',
]); ]);
}); });
} }
...@@ -94,6 +94,24 @@ void main() { ...@@ -94,6 +94,24 @@ void main() {
]); ]);
}); });
testWidgets('Slider V2 uses ThemeData slider theme if present', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.red,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.disabledActiveTrackColor)
..rrect(color: sliderTheme.disabledInactiveTrackColor),
);
});
testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async { testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async {
final ThemeData theme = ThemeData( final ThemeData theme = ThemeData(
platform: TargetPlatform.android, platform: TargetPlatform.android,
...@@ -112,6 +130,28 @@ void main() { ...@@ -112,6 +130,28 @@ void main() {
); );
}); });
testWidgets('Slider V2 overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.red,
);
final SliderThemeData sliderTheme = theme.sliderTheme;
final SliderThemeData customTheme = sliderTheme.copyWith(
activeTrackColor: Colors.purple,
inactiveTrackColor: Colors.purple.withAlpha(0x3d),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..rrect(color: customTheme.disabledActiveTrackColor)
..rrect(color: customTheme.disabledInactiveTrackColor),
);
});
testWidgets('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async { testWidgets('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async {
final ThemeData theme = ThemeData( final ThemeData theme = ThemeData(
platform: TargetPlatform.android, platform: TargetPlatform.android,
...@@ -218,6 +258,40 @@ void main() { ...@@ -218,6 +258,40 @@ void main() {
expect(lerp.valueIndicatorTextStyle.color, equals(middleGrey.withAlpha(0xff))); expect(lerp.valueIndicatorTextStyle.color, equals(middleGrey.withAlpha(0xff)));
}); });
testWidgets('Slider V2 track 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);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
const Radius radius = Radius.circular(2);
const Radius activatedRadius = Radius.circular(3);
// The enabled slider thumb has track segments that extend to and from
// the center of the thumb.
expect(
sliderBox,
paints
..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), 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, useV2Slider: true));
await tester.pumpAndSettle(); // wait for disable animation
// The disabled slider thumb is the same size as the enabled thumb.
expect(
sliderBox,
paints
..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor)
..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor),
);
});
testWidgets('Default slider track draws correctly', (WidgetTester tester) async { testWidgets('Default slider track draws correctly', (WidgetTester tester) async {
final ThemeData theme = ThemeData( final ThemeData theme = ThemeData(
platform: TargetPlatform.android, platform: TargetPlatform.android,
...@@ -240,12 +314,7 @@ void main() { ...@@ -240,12 +314,7 @@ void main() {
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation await tester.pumpAndSettle(); // wait for disable animation
// The disabled slider thumb has a horizontal gap between itself and the // The disabled slider thumb is the same size as the enabled thumb.
// track segments. Therefore, the track segments are shorter since they do
// not extend to the center of the thumb, but rather the outer edge of th
// gap. As a result, the `right` value of the first segment is less than it
// is above, and the `left` value of the second segment is more than it is
// above.
expect( expect(
sliderBox, sliderBox,
paints paints
...@@ -359,14 +428,18 @@ void main() { ...@@ -359,14 +428,18 @@ void main() {
); );
}); });
testWidgets('Default slider value indicator shape draws correctly', (WidgetTester tester) async { testWidgets('Slider V2 value indicator shape draws correctly', (WidgetTester tester) async {
final ThemeData theme = ThemeData( final ThemeData theme = ThemeData(
platform: TargetPlatform.android, platform: TargetPlatform.android,
primarySwatch: Colors.blue, 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 }) { Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) {
return Directionality( return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale), data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale),
...@@ -381,6 +454,7 @@ void main() { ...@@ -381,6 +454,7 @@ void main() {
label: value, label: value,
divisions: 3, divisions: 3,
onChanged: (double d) { }, onChanged: (double d) { },
useV2Slider: true,
), ),
), ),
), ),
...@@ -388,19 +462,201 @@ void main() { ...@@ -388,19 +462,201 @@ void main() {
), ),
), ),
), ),
),
); );
} }
await tester.pumpWidget(buildApp('1')); await tester.pumpWidget(buildApp('1'));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
Offset center = tester.getCenter(find.byType(Slider)); Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center); TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints
..path(
includes: const <Offset>[
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(
valueIndicatorBox,
paints
..rrect()
..rrect()
..path(
includes: const <Offset>[
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(
valueIndicatorBox,
paints
..rrect()
..rrect()
..path(
includes: const <Offset>[
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(
valueIndicatorBox,
paints
..rrect()
..rrect()
..path(
includes: const <Offset>[
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(
valueIndicatorBox,
paints
..rrect()
..rrect()
..path(
includes: const <Offset>[
Offset(0.0, 0.0),
Offset(-12.0, -12.0),
Offset(61.0, -16.0),
Offset(0.0, -20.0),
],
excludes: const <Offset>[
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(
valueIndicatorBox,
paints
..rrect()
..rrect()
..path(
includes: const <Offset>[
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('Default 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 MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale),
child: Material(
child: Row(
children: <Widget>[
Expanded(
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: sliderValue,
label: value,
divisions: 3,
onChanged: (double d) { },
),
),
),
],
),
),
),
),
);
}
await tester.pumpWidget(buildApp('1'));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -422,7 +678,7 @@ void main() { ...@@ -422,7 +678,7 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -443,7 +699,7 @@ void main() { ...@@ -443,7 +699,7 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -464,7 +720,7 @@ void main() { ...@@ -464,7 +720,7 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -485,7 +741,7 @@ void main() { ...@@ -485,7 +741,7 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -511,7 +767,7 @@ void main() { ...@@ -511,7 +767,7 @@ void main() {
// Wait for value indicator animation to finish. // Wait for value indicator animation to finish.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
sliderBox, valueIndicatorBox,
paints paints
..path( ..path(
color: sliderTheme.valueIndicatorColor, color: sliderTheme.valueIndicatorColor,
...@@ -558,6 +814,36 @@ void main() { ...@@ -558,6 +814,36 @@ void main() {
); );
}); });
testWidgets('The slider V2 track height can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(trackHeight: 16);
const Radius radius = Radius.circular(8);
const Radius activatedRadius = Radius.circular(9);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// Top and bottom are centerY (300) + and - trackRadius (8).
expect(
sliderBox,
paints
..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), 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, useV2Slider: true));
await tester.pumpAndSettle(); // wait for disable animation
// The disabled thumb is smaller so the active track has to paint longer to
// get to the edge.
expect(
sliderBox,
paints
..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor)
..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor),
);
});
testWidgets('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async { testWidgets('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
thumbShape: const RoundSliderThumbShape( thumbShape: const RoundSliderThumbShape(
...@@ -606,7 +892,6 @@ void main() { ...@@ -606,7 +892,6 @@ void main() {
); );
}); });
testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async { testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5), tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5),
...@@ -640,6 +925,39 @@ void main() { ...@@ -640,6 +925,39 @@ void main() {
); );
}); });
testWidgets('The default slider V2 tick mark shape size can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5, useV2Slider: true),
activeTickMarkColor: const Color(0xfadedead),
inactiveTickMarkColor: const Color(0xfadebeef),
disabledActiveTickMarkColor: const Color(0xfadecafe),
disabledInactiveTickMarkColor: const Color(0xfadeface),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, useV2Slider: true));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..circle(x: 26, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor)
..circle(x: 400, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor)
..circle(x: 774, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false, useV2Slider: true));
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..circle(x: 26, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor)
..circle(x: 400, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor)
..circle(x: 774, y: 300, radius: 5, color: sliderTheme.disabledInactiveTickMarkColor),
);
});
testWidgets('The default slider overlay shape size can be overridden', (WidgetTester tester) async { testWidgets('The default slider overlay shape size can be overridden', (WidgetTester tester) async {
const double uniqueOverlayRadius = 23; const double uniqueOverlayRadius = 23;
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
...@@ -800,7 +1118,7 @@ void main() { ...@@ -800,7 +1118,7 @@ void main() {
divisions: 4, divisions: 4,
)); ));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Tap the center of the track and wait for animations to finish. // Tap the center of the track and wait for animations to finish.
final Offset center = tester.getCenter(find.byType(Slider)); final Offset center = tester.getCenter(find.byType(Slider));
...@@ -808,14 +1126,14 @@ void main() { ...@@ -808,14 +1126,14 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Only 1 value indicator. // Only 1 value indicator.
expect(sliderBox, paintsExactlyCountTimes(#drawRect, 0)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawRect, 0));
expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 0)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawCircle, 0));
expect(sliderBox, paintsExactlyCountTimes(#drawPath, 1)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1));
await gesture.up(); 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. // Pump a slider with just a value indicator.
await tester.pumpWidget(_buildApp( await tester.pumpWidget(_buildApp(
ThemeData().sliderTheme.copyWith( ThemeData().sliderTheme.copyWith(
...@@ -824,13 +1142,13 @@ void main() { ...@@ -824,13 +1142,13 @@ void main() {
thumbShape: SliderComponentShape.noThumb, thumbShape: SliderComponentShape.noThumb,
tickMarkShape: SliderTickMarkShape.noTickMark, tickMarkShape: SliderTickMarkShape.noTickMark,
showValueIndicator: ShowValueIndicator.always, showValueIndicator: ShowValueIndicator.always,
rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(), valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
), ),
value: 0.5, value: 0.5,
divisions: 4, divisions: 4,
)); ));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider)); final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// Tap the center of the track to kick off the animation of the value indicator. // Tap the center of the track to kick off the animation of the value indicator.
final Offset center = tester.getCenter(find.byType(Slider)); final Offset center = tester.getCenter(find.byType(Slider));
...@@ -838,11 +1156,102 @@ void main() { ...@@ -838,11 +1156,102 @@ void main() {
// Nothing to paint at scale 0. // Nothing to paint at scale 0.
await tester.pump(); await tester.pump();
expect(sliderBox, paintsNothing); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0));
// Painting a path for the value indicator.
await tester.pump(const Duration(milliseconds: 16));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1));
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 valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// 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(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0));
// Painting a path for the value indicator. // Painting a path for the value indicator.
await tester.pump(const Duration(milliseconds: 16)); await tester.pump(const Duration(milliseconds: 16));
expect(sliderBox, paintsExactlyCountTimes(#drawPath, 1)); expect(valueIndicatorBox, 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<RenderBox>(find.byType(RangeSlider));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// 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(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0));
// Painting a path for each value indicator.
await tester.pump(const Duration(milliseconds: 16));
expect(valueIndicatorBox, 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 valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
// 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(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0));
// Painting a path for each value indicator.
await tester.pump(const Duration(milliseconds: 16));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2));
await gesture.up(); await gesture.up();
}); });
...@@ -853,6 +1262,7 @@ Widget _buildApp( ...@@ -853,6 +1262,7 @@ Widget _buildApp(
double value = 0.0, double value = 0.0,
bool enabled = true, bool enabled = true,
int divisions, int divisions,
bool useV2Slider = false,
}) { }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null; final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return MaterialApp( return MaterialApp(
...@@ -865,6 +1275,31 @@ Widget _buildApp( ...@@ -865,6 +1275,31 @@ Widget _buildApp(
label: '$value', label: '$value',
onChanged: onChanged, onChanged: onChanged,
divisions: divisions, divisions: divisions,
useV2Slider: useV2Slider
),
),
),
),
);
}
Widget _buildRangeApp(
SliderThemeData sliderTheme, {
RangeValues values = const RangeValues(0, 0),
bool enabled = true,
int divisions,
}) {
final ValueChanged<RangeValues> 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,
), ),
), ),
), ),
......
...@@ -169,7 +169,10 @@ void main() { ...@@ -169,7 +169,10 @@ void main() {
(ByteData data) { }, (ByteData data) { },
); );
final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider)); final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider));
expect(renderObject.debugNeedsLayout, isTrue);
bool sliderBoxNeedsLayout;
renderObject.visitChildren((RenderObject child) {sliderBoxNeedsLayout = child.debugNeedsLayout;});
expect(sliderBoxNeedsLayout, isTrue);
}); });
testWidgets('Slider relayout upon system fonts changes', (WidgetTester tester) async { testWidgets('Slider relayout upon system fonts changes', (WidgetTester tester) async {
...@@ -192,7 +195,10 @@ void main() { ...@@ -192,7 +195,10 @@ void main() {
(ByteData data) { }, (ByteData data) { },
); );
final RenderObject renderObject = tester.renderObject(find.byType(Slider)); final RenderObject renderObject = tester.renderObject(find.byType(Slider));
expect(renderObject.debugNeedsLayout, isTrue);
bool sliderBoxNeedsLayout;
renderObject.visitChildren((RenderObject child) {sliderBoxNeedsLayout = child.debugNeedsLayout;});
expect(sliderBoxNeedsLayout, isTrue);
}); });
testWidgets('TimePicker relayout upon system fonts changes', (WidgetTester tester) async { testWidgets('TimePicker relayout upon system fonts changes', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment