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 {
@required SliderThemeData sliderTheme,
TextDirection textDirection,
Thumb thumb,
bool isPressed,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
......@@ -130,6 +131,8 @@ class _CustomThumbShape extends SliderComponentShape {
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
......@@ -169,6 +172,8 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final ColorTween enableColor = ColorTween(
......@@ -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,
min: 0.0,
max: 100.0,
......@@ -278,6 +288,7 @@ class _SlidersState extends State<_Sliders> {
});
},
),
),
const Text('Continuous with Editable Numerical Value'),
],
),
......
......@@ -21,10 +21,12 @@ ThemeData _buildDarkTheme() {
final ColorScheme colorScheme = const ColorScheme.dark().copyWith(
primary: primaryColor,
secondary: secondaryColor,
onPrimary: secondaryColor,
);
final ThemeData base = ThemeData(
brightness: Brightness.dark,
accentColorBrightness: Brightness.dark,
colorScheme: colorScheme,
primaryColor: primaryColor,
primaryColorDark: const Color(0xFF0050a0),
primaryColorLight: secondaryColor,
......
......@@ -22,6 +22,11 @@ import 'theme.dart';
// RangeValues _dollarsRange = RangeValues(50, 100);
// 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.
///
/// Used to select a range from a range of values.
......@@ -127,6 +132,7 @@ class RangeSlider extends StatefulWidget {
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
this.useV2Slider = false,
}) : assert(values != null),
assert(min != null),
assert(max != null),
......@@ -135,6 +141,7 @@ class RangeSlider extends StatefulWidget {
assert(values.start >= min && values.start <= max),
assert(values.end >= min && values.end <= max),
assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key);
/// The currently selected values for this range slider.
......@@ -333,6 +340,19 @@ class RangeSlider extends StatefulWidget {
/// {@end-tool}
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.
static const double _minTouchTargetWidth = kMinInteractiveDimension;
......@@ -354,6 +374,7 @@ class RangeSlider extends StatefulWidget {
properties.add(StringProperty('labelEnd', labels?.end));
properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('inactiveColor', inactiveColor));
properties.add(FlagProperty('useV2Slider', value: useV2Slider, ifFalse: 'useV1Slider'));
properties.add(ObjectFlagProperty<ValueChanged<RangeValues>>.has('semanticFormatterCallback', semanticFormatterCallback));
}
}
......@@ -377,6 +398,10 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
AnimationController startPositionController;
AnimationController endPositionController;
Timer interactionTimer;
// Value Indicator paint Animation that appears on the Overlay.
PaintRangeValueIndicator paintTopValueIndicator;
PaintRangeValueIndicator paintBottomValueIndicator;
@override
void initState() {
......@@ -520,14 +545,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
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
Widget build(BuildContext context) {
......@@ -543,6 +561,29 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
// colors come from the ThemeData.colorScheme. These colors, along with
// the default shapes and text styles are aligned to the Material
// 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(
trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
......@@ -555,14 +596,14 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
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),
valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary,
valueIndicatorColor: valueIndicatorColor,
rangeTrackShape: sliderTheme.rangeTrackShape ?? _defaultTrackShape,
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? _defaultTickMarkShape,
rangeThumbShape: sliderTheme.rangeThumbShape ?? _defaultThumbShape,
overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape,
rangeValueIndicatorShape: sliderTheme.rangeValueIndicatorShape ?? _defaultValueIndicatorShape,
rangeValueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator,
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith(
color: theme.colorScheme.onPrimary,
......@@ -571,19 +612,49 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
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),
divisions: widget.divisions,
labels: widget.labels,
sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this,
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 {
......@@ -594,11 +665,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
this.labels,
this.sliderTheme,
this.textScaleFactor,
this.screenSize,
this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.state,
this.semanticFormatterCallback,
this.useV2Slider,
}) : super(key: key);
final RangeValues values;
......@@ -606,11 +679,13 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
final RangeLabels labels;
final SliderThemeData sliderTheme;
final double textScaleFactor;
final Size screenSize;
final ValueChanged<RangeValues> onChanged;
final ValueChanged<RangeValues> onChangeStart;
final ValueChanged<RangeValues> onChangeEnd;
final RangeSemanticFormatterCallback semanticFormatterCallback;
final _RangeSliderState state;
final bool useV2Slider;
@override
_RenderRangeSlider createRenderObject(BuildContext context) {
......@@ -621,6 +696,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
sliderTheme: sliderTheme,
theme: Theme.of(context),
textScaleFactor: textScaleFactor,
screenSize: screenSize,
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
......@@ -628,6 +704,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform,
useV2Slider: useV2Slider,
);
}
......@@ -640,6 +717,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
..sliderTheme = sliderTheme
..theme = Theme.of(context)
..textScaleFactor = textScaleFactor
..screenSize = screenSize
..onChanged = onChanged
..onChangeStart = onChangeStart
..onChangeEnd = onChangeEnd
......@@ -657,6 +735,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
SliderThemeData sliderTheme,
ThemeData theme,
double textScaleFactor,
Size screenSize,
TargetPlatform platform,
ValueChanged<RangeValues> onChanged,
RangeSemanticFormatterCallback semanticFormatterCallback,
......@@ -664,6 +743,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
this.onChangeEnd,
@required _RangeSliderState state,
@required TextDirection textDirection,
bool useV2Slider,
}) : assert(values != null),
assert(values.start >= 0.0 && values.start <= 1.0),
assert(values.end >= 0.0 && values.end <= 1.0),
......@@ -677,9 +757,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_sliderTheme = sliderTheme,
_theme = theme,
_textScaleFactor = textScaleFactor,
_screenSize = screenSize,
_onChanged = onChanged,
_state = state,
_textDirection = textDirection {
_textDirection = textDirection,
_useV2Slider = useV2Slider {
_updateLabelPainters();
final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer()
......@@ -700,7 +782,12 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn,
);
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed && _state.overlayEntry != null) {
_state.overlayEntry.remove();
_state.overlayEntry = null;
}
});
_enableAnimation = CurvedAnimation(
parent: _state.enableController,
curve: Curves.easeInOut,
......@@ -849,6 +936,15 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_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> _onChanged;
set onChanged(ValueChanged<RangeValues> value) {
......@@ -913,6 +1009,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
return 0.05;
}
final bool _useV2Slider;
void _updateLabelPainters() {
_updateLabelPainter(Thumb.start);
_updateLabelPainter(Thumb.end);
......@@ -1009,6 +1107,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
}
void _startInteraction(Offset globalPosition) {
_state.showValueIndicator();
final double tapValue = _getValueFromGlobalPosition(globalPosition).clamp(0.0, 1.0) as double;
_lastThumbSelection = sliderTheme.thumbSelector(textDirection, values, tapValue, _thumbSize, size, 0);
......@@ -1200,8 +1299,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
isEnabled: isEnabled,
);
final bool startThumbSelected = _lastThumbSelection == Thumb.start;
final bool endThumbSelected = _lastThumbSelection == Thumb.end;
if (!_overlayAnimation.isDismissed) {
if (_lastThumbSelection == Thumb.start) {
if (startThumbSelected) {
_sliderTheme.overlayShape.paint(
context,
startThumbCenter,
......@@ -1215,7 +1317,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
value: startValue,
);
}
if (_lastThumbSelection == Thumb.end) {
if (endThumbSelected) {
_sliderTheme.overlayShape.paint(
context,
endThumbCenter,
......@@ -1236,7 +1338,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
isEnabled: isEnabled,
sliderTheme: _sliderTheme,
).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 (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) {
final double dy = trackRect.center.dy;
......@@ -1244,7 +1347,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final double value = i / divisions;
// The ticks are mapped to be within the track, so the tick mark width
// must be subtracted from the track width.
final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2;
final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
final Offset tickMarkOffset = Offset(dx, dy);
_sliderTheme.rangeTickMarkShape.paint(
context,
......@@ -1273,8 +1376,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final double bottomValue = isLastThumbStart ? endValue : startValue;
final double topValue = isLastThumbStart ? startValue : endValue;
final bool shouldPaintValueIndicators = isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator;
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
if (shouldPaintValueIndicators) {
_state.paintBottomValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.rangeValueIndicatorShape.paint(
context,
bottomThumbCenter,
......@@ -1288,7 +1393,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: _textDirection,
thumb: bottomThumb,
value: bottomValue,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
);
};
}
_sliderTheme.rangeThumbShape.paint(
......@@ -1301,6 +1409,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: textDirection,
sliderTheme: _sliderTheme,
thumb: bottomThumb,
isPressed: bottomThumb == Thumb.start ? startThumbSelected : endThumbSelected,
);
if (shouldPaintValueIndicators) {
......@@ -1309,15 +1418,29 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
center: startThumbCenter,
labelPainter: _startLabelPainter,
activationAnimation: _valueIndicatorAnimation,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
);
final double endOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift(
parentBox: this,
center: endThumbCenter,
labelPainter: _endLabelPainter,
activationAnimation: _valueIndicatorAnimation,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
);
final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _startLabelPainter).width / 2;
final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _endLabelPainter).width / 2;
final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(
isEnabled,
isDiscrete,
labelPainter: _startLabelPainter,
textScaleFactor: textScaleFactor,
).width / 2;
final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(
isEnabled,
isDiscrete,
labelPainter: _endLabelPainter,
textScaleFactor: textScaleFactor,
).width / 2;
double innerOverflow = startHalfWidth + endHalfWidth;
switch (textDirection) {
case TextDirection.ltr:
......@@ -1330,6 +1453,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
break;
}
_state.paintTopValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.rangeValueIndicatorShape.paint(
context,
topThumbCenter,
......@@ -1343,19 +1467,23 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
textDirection: _textDirection,
thumb: topThumb,
value: topValue,
textScaleFactor: textScaleFactor,
sizeWithOverflow: resolvedscreenSize,
);
};
}
_sliderTheme.rangeThumbShape.paint(
context,
topThumbCenter,
activationAnimation: _valueIndicatorAnimation,
activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
isOnTop: thumbDelta < sliderTheme.rangeThumbShape.getPreferredSize(isEnabled, isDiscrete).width,
textDirection: textDirection,
sliderTheme: _sliderTheme,
thumb: topThumb,
isPressed: topThumb == Thumb.start ? startThumbSelected : endThumbSelected,
);
}
......@@ -1419,3 +1547,66 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
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';
/// * [Slider.semanticFormatterCallback], which shows an example use case.
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 }
/// A Material Design slider.
......@@ -124,6 +129,7 @@ class Slider extends StatefulWidget {
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
this.useV2Slider = false,
}) : _sliderType = _SliderType.material,
assert(value != null),
assert(min != null),
......@@ -131,6 +137,7 @@ class Slider extends StatefulWidget {
assert(min <= max),
assert(value >= min && value <= max),
assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key);
/// Creates a [CupertinoSlider] if the target platform is iOS, creates a
......@@ -153,6 +160,7 @@ class Slider extends StatefulWidget {
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
this.useV2Slider = false,
}) : _sliderType = _SliderType.adaptive,
assert(value != null),
assert(min != null),
......@@ -160,6 +168,7 @@ class Slider extends StatefulWidget {
assert(min <= max),
assert(value >= min && value <= max),
assert(divisions == null || divisions > 0),
assert(useV2Slider != null),
super(key: key);
/// The currently selected value for this slider.
......@@ -374,6 +383,19 @@ class Slider extends StatefulWidget {
/// Ignored if this slider is created with [Slider.adaptive]
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 ;
@override
......@@ -392,6 +414,7 @@ class Slider extends StatefulWidget {
properties.add(StringProperty('label', label));
properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('inactiveColor', inactiveColor));
properties.add(FlagProperty('useV2Slider', value: useV2Slider, ifFalse: 'useV1Slider'));
properties.add(ObjectFlagProperty<ValueChanged<double>>.has('semanticFormatterCallback', semanticFormatterCallback));
}
}
......@@ -412,6 +435,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// and the next on a discrete slider.
AnimationController positionController;
Timer interactionTimer;
// Value Indicator Animation that appears on the Overlay.
PaintValueIndicator paintValueIndicator;
@override
void initState() {
......@@ -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;
}
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
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
......@@ -525,6 +542,28 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// colors come from the ThemeData.colorScheme. These colors, along with
// the default shapes and text styles are aligned to the Material
// 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(
trackHeight: sliderTheme.trackHeight ?? _defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
......@@ -536,31 +575,41 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? theme.colorScheme.onPrimary.withOpacity(0.12),
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
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),
valueIndicatorColor: widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary,
valueIndicatorColor: valueIndicatorColor,
trackShape: sliderTheme.trackShape ?? _defaultTrackShape,
tickMarkShape: sliderTheme.tickMarkShape ?? _defaultTickMarkShape,
thumbShape: sliderTheme.thumbShape ?? _defaultThumbShape,
overlayShape: sliderTheme.overlayShape ?? _defaultOverlayShape,
valueIndicatorShape: sliderTheme.valueIndicatorShape ?? _defaultValueIndicatorShape,
valueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? _defaultShowValueIndicator,
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyText1.copyWith(
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),
divisions: widget.divisions,
label: widget.label,
sliderTheme: sliderTheme,
mediaQueryData: MediaQuery.of(context),
textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this,
semanticFormatterCallback: widget.semanticFormatterCallback,
useV2Slider: widget.useV2Slider,
),
);
}
......@@ -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 {
const _SliderRenderObjectWidget({
Key key,
......@@ -591,24 +660,28 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
this.divisions,
this.label,
this.sliderTheme,
this.mediaQueryData,
this.textScaleFactor,
this.screenSize,
this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.state,
this.semanticFormatterCallback,
this.useV2Slider,
}) : super(key: key);
final double value;
final int divisions;
final String label;
final SliderThemeData sliderTheme;
final MediaQueryData mediaQueryData;
final double textScaleFactor;
final Size screenSize;
final ValueChanged<double> onChanged;
final ValueChanged<double> onChangeStart;
final ValueChanged<double> onChangeEnd;
final SemanticFormatterCallback semanticFormatterCallback;
final _SliderState state;
final bool useV2Slider;
@override
_RenderSlider createRenderObject(BuildContext context) {
......@@ -617,7 +690,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
divisions: divisions,
label: label,
sliderTheme: sliderTheme,
mediaQueryData: mediaQueryData,
textScaleFactor: textScaleFactor,
screenSize: screenSize,
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
......@@ -625,6 +699,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform,
useV2Slider: useV2Slider,
);
}
......@@ -636,7 +711,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..label = label
..sliderTheme = sliderTheme
..theme = Theme.of(context)
..mediaQueryData = mediaQueryData
..textScaleFactor = textScaleFactor
..screenSize = screenSize
..onChanged = onChanged
..onChangeStart = onChangeStart
..onChangeEnd = onChangeEnd
......@@ -654,7 +730,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
int divisions,
String label,
SliderThemeData sliderTheme,
MediaQueryData mediaQueryData,
double textScaleFactor,
Size screenSize,
TargetPlatform platform,
ValueChanged<double> onChanged,
SemanticFormatterCallback semanticFormatterCallback,
......@@ -662,6 +739,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
this.onChangeEnd,
@required _SliderState state,
@required TextDirection textDirection,
bool useV2Slider,
}) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(state != null),
assert(textDirection != null),
......@@ -671,10 +749,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_value = value,
_divisions = divisions,
_sliderTheme = sliderTheme,
_mediaQueryData = mediaQueryData,
_textScaleFactor = textScaleFactor,
_screenSize = screenSize,
_onChanged = onChanged,
_state = state,
_textDirection = textDirection {
_textDirection = textDirection,
_useV2Slider = useV2Slider {
_updateLabelPainter();
final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer()
......@@ -695,7 +775,12 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_valueIndicatorAnimation = CurvedAnimation(
parent: _state.valueIndicatorController,
curve: Curves.fastOutSlowIn,
);
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed && _state.overlayEntry != null) {
_state.overlayEntry.remove();
_state.overlayEntry = null;
}
});
_enableAnimation = CurvedAnimation(
parent: _state.enableController,
curve: Curves.easeInOut,
......@@ -826,18 +911,26 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
markNeedsPaint();
}
MediaQueryData get mediaQueryData => _mediaQueryData;
MediaQueryData _mediaQueryData;
set mediaQueryData(MediaQueryData value) {
if (value == _mediaQueryData) {
double get textScaleFactor => _textScaleFactor;
double _textScaleFactor;
set textScaleFactor(double value) {
if (value == _textScaleFactor) {
return;
}
_mediaQueryData = value;
// Media query data includes the textScaleFactor, so we need to update the
// label painter.
_textScaleFactor = value;
_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> _onChanged;
set onChanged(ValueChanged<double> value) {
......@@ -871,6 +964,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_updateLabelPainter();
}
final bool _useV2Slider;
bool get showValueIndicator {
bool showValueIndicator;
switch (_sliderTheme.showValueIndicator) {
......@@ -915,7 +1010,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
text: label,
)
..textDirection = textDirection
..textScaleFactor = _mediaQueryData.textScaleFactor
..textScaleFactor = textScaleFactor
..layout();
} else {
_labelPainter.text = null;
......@@ -975,6 +1070,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
void _startInteraction(Offset globalPosition) {
_state.showValueIndicator();
if (isInteractive) {
_active = true;
// We supply the *current* value as the start location, so that if we have
......@@ -1008,6 +1104,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_active = false;
_currentDragValue = 0.0;
_state.overlayController.reverse();
if (showValueIndicator && _state.interactionTimer == null) {
_state.valueIndicatorController.reverse();
}
......@@ -1130,7 +1227,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
isEnabled: isInteractive,
sliderTheme: _sliderTheme,
).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 (adjustedTrackWidth / divisions >= 3.0 * tickMarkWidth) {
final double dy = trackRect.center.dy;
......@@ -1138,7 +1236,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final double value = i / divisions;
// The ticks are mapped to be within the track, so the tick mark width
// must be subtracted from the track width.
final double dx = trackRect.left + value * adjustedTrackWidth + tickMarkWidth / 2;
final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
final Offset tickMarkOffset = Offset(dx, dy);
_sliderTheme.tickMarkShape.paint(
context,
......@@ -1156,9 +1254,10 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
if (showValueIndicator) {
_state.paintValueIndicator = (PaintingContext context, Offset offset) {
_sliderTheme.valueIndicatorShape.paint(
context,
thumbCenter,
offset + thumbCenter,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
......@@ -1167,20 +1266,24 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
);
};
}
}
_sliderTheme.thumbShape.paint(
context,
thumbCenter,
activationAnimation: _valueIndicatorAnimation,
activationAnimation: _overlayAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
value: _value,
);
}
......@@ -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';
/// 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.
/// {@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.
///
......@@ -278,7 +300,7 @@ enum Thumb {
/// by creating subclasses of [SliderTrackShape],
/// [SliderComponentShape], and/or [SliderTickMarkShape]. See
/// [RoundSliderThumbShape], [RectangularSliderTrackShape],
/// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and
/// [RoundSliderTickMarkShape], [RectangularSliderValueIndicatorShape], and
/// [RoundSliderOverlayShape] for examples.
///
/// The track painting can be skipped by specifying 0 for [trackHeight].
......@@ -490,6 +512,7 @@ class SliderThemeData with Diagnosticable {
/// The color given to the [valueIndicatorShape] to draw itself with.
final Color valueIndicatorColor;
/// The shape that will be used to draw the [Slider]'s overlay.
///
/// Both the [overlayColor] and a non default [overlayShape] may be specified.
......@@ -976,6 +999,10 @@ abstract class SliderComponentShape {
/// [labelPainter] already has the [textDirection] set.
///
/// [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(
PaintingContext context,
Offset center, {
......@@ -987,6 +1014,8 @@ abstract class SliderComponentShape {
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
});
/// Special instance of [SliderComponentShape] to skip the thumb drawing.
......@@ -1202,6 +1231,9 @@ abstract class RangeSliderThumbShape {
/// left and right thumb.
///
/// {@macro flutter.material.rangeSlider.shape.thumb}
///
/// [isPressed] can be used to give the selected thumb additional selected
/// or pressed state visual feedback, such as a larger shadow.
void paint(
PaintingContext context,
Offset center, {
......@@ -1213,6 +1245,7 @@ abstract class RangeSliderThumbShape {
TextDirection textDirection,
SliderThemeData sliderTheme,
Thumb thumb,
bool isPressed,
});
}
......@@ -1238,7 +1271,14 @@ abstract class RangeSliderValueIndicatorShape {
///
/// [labelPainter] helps determine the width of the shape. It is variable
/// width because it is derived from a formatted string.
Size getPreferredSize(bool isEnabled, bool isDiscrete, { TextPainter labelPainter });
///
/// {@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.
///
......@@ -1249,6 +1289,8 @@ abstract class RangeSliderValueIndicatorShape {
Offset center,
TextPainter labelPainter,
Animation<double> activationAnimation,
double textScaleFactor,
Size sizeWithOverflow,
}) {
return 0;
}
......@@ -1270,6 +1312,12 @@ abstract class RangeSliderValueIndicatorShape {
/// the default case, this is used to paint a stroke around the top indicator
/// 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}
///
/// [textDirection] can be used to determine how any extra text or graphics,
......@@ -1287,6 +1335,8 @@ abstract class RangeSliderValueIndicatorShape {
bool isDiscrete,
bool isOnTop,
TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
......@@ -1552,17 +1602,17 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
assert(thumbCenter != null);
assert(isEnabled != null);
assert(isDiscrete != null);
// If the slider track height is less than or equal to 0, then it makes no
// difference whether the track is painted or not, therefore the painting
// can be a no-op.
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight <= 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor);
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
Paint leftTrackPaint;
......@@ -1586,11 +1636,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
isDiscrete: isDiscrete,
);
final Size thumbSize = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx - thumbSize.width / 2, trackRect.bottom);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left + trackRect.height / 2, trackRect.top, thumbCenter.dx, trackRect.bottom);
if (!leftTrackSegment.isEmpty)
context.canvas.drawRect(leftTrackSegment, leftTrackPaint);
final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + thumbSize.width / 2, trackRect.top, trackRect.right, trackRect.bottom);
final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx, trackRect.top, trackRect.right, trackRect.bottom);
if (!rightTrackSegment.isEmpty)
context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
}
......@@ -1620,7 +1669,10 @@ class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
/// * [RectangularSliderTrackShape], for a similar track with sharp edges.
class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
/// 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
void paint(
......@@ -1633,6 +1685,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
@required Offset thumbCenter,
bool isDiscrete = false,
bool isEnabled = false,
double additionalActiveTrackHeight = 2,
}) {
assert(context != null);
assert(offset != null);
......@@ -1646,9 +1699,9 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
assert(enableAnimation != null);
assert(textDirection != null);
assert(thumbCenter != null);
// If the slider track height is less than or equal to 0, then it makes no
// difference whether the track is painted or not, therefore the painting
// can be a no-op.
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight <= 0) {
return;
}
......@@ -1679,7 +1732,33 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
isEnabled: isEnabled,
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.
final Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height);
if (!leftTrackArcRect.isEmpty)
......@@ -1696,6 +1775,7 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS
if (!rightTrackSegment.isEmpty)
context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
}
}
}
/// A [RangeSlider] track that's a simple rectangle.
......@@ -1726,7 +1806,10 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape {
///
/// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive.
const RectangularRangeSliderTrackShape();
const RectangularRangeSliderTrackShape({this.useV2Slider});
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override
Rect getPreferredRect({
......@@ -1785,8 +1868,8 @@ class RectangularRangeSliderTrackShape extends RangeSliderTrackShape {
assert(textDirection != null);
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor);
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
......@@ -1852,7 +1935,10 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
///
/// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive.
const RoundedRectRangeSliderTrackShape();
const RoundedRectRangeSliderTrackShape({ this.useV2Slider });
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override
Rect getPreferredRect({
......@@ -1894,6 +1980,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
bool isEnabled = false,
bool isDiscrete = false,
@required TextDirection textDirection,
double additionalActiveTrackHeight = 2,
}) {
assert(context != null);
assert(offset != null);
......@@ -1910,12 +1997,23 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
assert(isEnabled != null);
assert(isDiscrete != null);
assert(textDirection != null);
if (sliderTheme.trackHeight <= 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
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);
final ColorTween activeTrackColorTween = ColorTween(
begin: sliderTheme.disabledActiveTrackColor,
end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(
begin: sliderTheme.disabledInactiveTrackColor,
end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()
..color = activeTrackColorTween.evaluate(enableAnimation);
final Paint inactivePaint = Paint()
..color = inactiveTrackColorTween.evaluate(enableAnimation);
Offset leftThumbOffset;
Offset rightThumbOffset;
......@@ -1940,6 +2038,42 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
isEnabled: isEnabled,
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 Rect leftTrackArcRect = Rect.fromLTWH(trackRect.left, trackRect.top, trackRect.height, trackRect.height);
......@@ -1960,6 +2094,7 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
if (!rightTrackArcRect.isEmpty)
context.canvas.drawArc(rightTrackArcRect, -math.pi / 2, math.pi, false, inactivePaint);
}
}
}
/// The default shape of each [Slider] tick mark.
......@@ -1982,13 +2117,21 @@ class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape {
/// sliders in a widget subtree.
class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// 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.
///
/// 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;
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override
Size getPreferredSize({
@required SliderThemeData sliderTheme,
......@@ -1997,9 +2140,11 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null);
assert(isEnabled != null);
// The tick marks are tiny circles. If no radius is provided, then they are
// defaulted to be the same height as the track.
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2);
// The tick marks are tiny circles. If no radius is provided, then the
// radius is defaulted to be a fraction of the
// [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
......@@ -2074,13 +2219,21 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// sliders in a widget subtree.
class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape {
/// 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.
///
/// 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;
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
@override
Size getPreferredSize({
@required SliderThemeData sliderTheme,
......@@ -2089,7 +2242,7 @@ class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape {
assert(sliderTheme != null);
assert(sliderTheme.trackHeight != null);
assert(isEnabled != null);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / (useV2Slider ? 4 : 2));
}
@override
......@@ -2198,6 +2351,8 @@ class _EmptySliderComponentShape extends SliderComponentShape {
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
// no-op.
}
......@@ -2205,6 +2360,9 @@ class _EmptySliderComponentShape extends SliderComponentShape {
/// 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:
///
/// * [Slider], which includes a thumb defined by this shape.
......@@ -2215,6 +2373,9 @@ class RoundSliderThumbShape extends SliderComponentShape {
const RoundSliderThumbShape({
this.enabledThumbRadius = 10.0,
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.
......@@ -2229,6 +2390,30 @@ class RoundSliderThumbShape extends SliderComponentShape {
final double disabledThumbRadius;
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
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
......@@ -2246,6 +2431,8 @@ class RoundSliderThumbShape extends SliderComponentShape {
@required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
......@@ -2253,6 +2440,7 @@ class RoundSliderThumbShape extends SliderComponentShape {
assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
assert(!sizeWithOverflow.isEmpty);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
......@@ -2263,16 +2451,35 @@ class RoundSliderThumbShape extends SliderComponentShape {
begin: sliderTheme.disabledThumbColor,
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(
center,
radiusTween.evaluate(enableAnimation),
Paint()..color = colorTween.evaluate(enableAnimation),
radius,
Paint()..color = color,
);
}
}
/// 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:
///
/// * [RangeSlider], which includes thumbs defined by this shape.
......@@ -2283,8 +2490,14 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
const RoundRangeSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
this.useV2Slider = false,
}) : assert(enabledThumbRadius != null);
/// {@macro flutter.material.slider.useV2Slider}
final bool useV2Slider;
/// 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.
......@@ -2297,6 +2510,16 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// The default is 1.
final double elevation;
/// The pressed elevation adds shadow to the pressed thumb.
///
/// The default is 6.
final double pressedElevation;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
......@@ -2314,6 +2537,7 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
@required SliderThemeData sliderTheme,
TextDirection textDirection,
Thumb thumb,
bool isPressed,
}) {
assert(context != null);
assert(center != null);
......@@ -2332,6 +2556,10 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
end: sliderTheme.thumbColor,
);
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
// the other thumb.
......@@ -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(
center,
radius,
Paint()..color = colorTween.evaluate(enableAnimation),
Paint()..color = color,
);
}
}
......@@ -2390,7 +2627,8 @@ class RoundSliderOverlayShape extends SliderComponentShape {
/// The preferred radius of the round thumb shape when enabled.
///
/// If it is not provided, then half of the track height is used.
/// If it is not provided, then half of the [SliderThemeData.trackHeight] is
/// used.
final double overlayRadius;
@override
......@@ -2410,6 +2648,8 @@ class RoundSliderOverlayShape extends SliderComponentShape {
@required SliderThemeData sliderTheme,
@required TextDirection textDirection,
@required double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
......@@ -2442,16 +2682,275 @@ class RoundSliderOverlayShape extends SliderComponentShape {
/// * [Slider], which includes a value indicator defined by this shape.
/// * [SliderTheme], which can be used to configure the slider value indicator
/// of all sliders in a widget subtree.
class 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 {
/// Create a slider value indicator in the shape of an upside-down pear.
const PaddleSliderValueIndicatorShape();
static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter();
static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete, { @required TextPainter labelPainter }) {
Size getPreferredSize(bool isEnabled, bool isDiscrete, {@required TextPainter labelPainter, @required double textScaleFactor,}) {
assert(labelPainter != null);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter);
assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor);
}
@override
......@@ -2466,6 +2965,8 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
@required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
......@@ -2474,23 +2975,27 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
assert(labelPainter != null);
assert(parentBox != null);
assert(sliderTheme != null);
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
_pathPainter.drawValueIndicator(
_pathPainter.paint(
parentBox,
context.canvas,
center,
Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value,
labelPainter,
textScaleFactor,
sizeWithOverflow,
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:
///
......@@ -2501,12 +3006,18 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
/// Create a slider value indicator in the shape of an upside-down pear.
const PaddleRangeSliderValueIndicatorShape();
static const _PaddleSliderTrackShapePathPainter _pathPainter = _PaddleSliderTrackShapePathPainter();
static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete, { @required TextPainter labelPainter }) {
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
@required TextPainter labelPainter,
@required double textScaleFactor,
}) {
assert(labelPainter != null);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter);
assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter, textScaleFactor);
}
@override
......@@ -2515,12 +3026,16 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
Offset center,
TextPainter labelPainter,
Animation<double> activationAnimation,
double textScaleFactor,
Size sizeWithOverflow,
}) {
return _pathPainter.getHorizontalShift(
parentBox: parentBox,
center: center,
labelPainter: labelPainter,
scale: activationAnimation.value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
);
}
......@@ -2538,6 +3053,8 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
TextDirection textDirection,
Thumb thumb,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
assert(context != null);
assert(center != null);
......@@ -2546,25 +3063,28 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
assert(labelPainter != null);
assert(parentBox != null);
assert(sliderTheme != null);
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
// Add a stroke of 1dp around the top paddle.
_pathPainter.drawValueIndicator(
_pathPainter.paint(
parentBox,
context.canvas,
center,
Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value,
labelPainter,
textScaleFactor,
sizeWithOverflow,
isOnTop ? sliderTheme.overlappingShapeStrokeColor : null,
);
}
}
class _PaddleSliderTrackShapePathPainter {
const _PaddleSliderTrackShapePathPainter();
class _PaddleSliderValueIndicatorPathPainter {
const _PaddleSliderValueIndicatorPathPainter();
// These constants define the shape of the default value indicator.
// The value indicator changes shape based on the size of
......@@ -2574,14 +3094,12 @@ class _PaddleSliderTrackShapePathPainter {
// Radius of the top lobe of the value indicator.
static const double _topLobeRadius = 16.0;
// Designed size of the label text. This is the size that the value indicator
// was designed to contain. We scale it from here to fit other sizes.
static const double _labelTextDesignSize = 14.0;
static const double _minLabelWidth = 16.0;
// Radius of the bottom lobe of the value indicator.
static const double _bottomLobeRadius = 10.0;
static const double _labelPadding = 8.0;
static const double _distanceBetweenTopBottomCenters = 40.0;
static const double _middleNeckWidth = 2.0;
static const double _middleNeckWidth = 3.0;
static const double _bottomNeckRadius = 4.5;
// The base of the triangle between the top lobe center and the centers of
// the two top neck arcs.
......@@ -2609,10 +3127,12 @@ class _PaddleSliderTrackShapePathPainter {
bool isEnabled,
bool isDiscrete,
TextPainter labelPainter,
double textScaleFactor,
) {
assert(labelPainter != null);
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
return Size(labelPainter.width + 2 * _labelPadding * textScaleFactor, _preferredHeight * textScaleFactor);
assert(textScaleFactor != null && textScaleFactor >= 0);
final double width = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
return Size(width, _preferredHeight * textScaleFactor);
}
// Adds an arc to the path that has the attributes passed in. This is
......@@ -2628,15 +3148,17 @@ class _PaddleSliderTrackShapePathPainter {
Offset center,
TextPainter labelPainter,
double scale,
double textScaleFactor,
Size sizeWithOverflow,
}) {
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
assert(!sizeWithOverflow.isEmpty);
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
final double halfWidthNeeded = math.max(
0.0,
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
);
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, textScaleFactor * scale, center);
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, textScaleFactor * scale, center, sizeWithOverflow.width);
return shift * textScaleFactor;
}
......@@ -2647,8 +3169,9 @@ class _PaddleSliderTrackShapePathPainter {
double halfWidthNeeded,
double scale,
Offset center,
double widthWithOverflow,
) {
const double edgeMargin = 4.0;
const double edgeMargin = 8.0;
final Rect topLobeRect = Rect.fromLTWH(
-_topLobeRadius - halfWidthNeeded,
-_topLobeRadius - _distanceBetweenTopBottomCenters,
......@@ -2661,12 +3184,11 @@ class _PaddleSliderTrackShapePathPainter {
final Offset bottomRight = (topLobeRect.bottomRight * scale) + center;
double shift = 0.0;
final double startGlobal = parentBox.localToGlobal(Offset.zero).dx;
if (topLeft.dx < startGlobal + edgeMargin) {
shift = startGlobal + edgeMargin - topLeft.dx;
if (topLeft.dx < edgeMargin) {
shift = edgeMargin - topLeft.dx;
}
final double endGlobal = parentBox.localToGlobal(Offset(parentBox.size.width, parentBox.size.height)).dx;
final double endGlobal = widthWithOverflow;
if (bottomRight.dx > endGlobal - edgeMargin) {
shift = endGlobal - edgeMargin - bottomRight.dx;
}
......@@ -2682,13 +3204,15 @@ class _PaddleSliderTrackShapePathPainter {
return shift;
}
void drawValueIndicator(
void paint(
RenderBox parentBox,
Canvas canvas,
Offset center,
Paint paint,
double scale,
TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
Color strokePaintColor,
) {
if (scale == 0.0) {
......@@ -2696,10 +3220,10 @@ class _PaddleSliderTrackShapePathPainter {
// our math below will attempt to divide by zero and send needless NaNs to the engine.
return;
}
assert(!sizeWithOverflow.isEmpty);
// The entire value indicator should scale with the size of the label,
// to keep it large enough to encompass the label text.
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
final double overallScale = scale * textScaleFactor;
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
......@@ -2741,7 +3265,7 @@ class _PaddleSliderTrackShapePathPainter {
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
);
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center, sizeWithOverflow.width);
final double leftWidthNeeded = halfWidthNeeded - shift;
final double rightWidthNeeded = halfWidthNeeded + shift;
......@@ -2750,7 +3274,8 @@ class _PaddleSliderTrackShapePathPainter {
final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / _neckTriangleBase));
final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / _neckTriangleBase));
// The angle between the top neck arc's center and the top lobe's center
// and vertical.
// and vertical. The base amount is chosen so that the neck is smooth,
// even when the lobe is shifted due to its size.
final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
// The center of the top left neck arc.
......
......@@ -17,7 +17,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -39,6 +40,7 @@ void main() {
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
......@@ -72,7 +74,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -94,6 +97,7 @@ void main() {
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
......@@ -127,7 +131,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -152,6 +157,7 @@ void main() {
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
......@@ -187,7 +193,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -212,6 +219,7 @@ void main() {
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
......@@ -247,7 +255,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -269,6 +278,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -292,7 +302,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -314,6 +325,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -337,7 +349,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -362,6 +375,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -385,7 +399,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -410,6 +425,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -433,7 +449,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -455,6 +472,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -484,7 +502,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -506,6 +525,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -535,7 +555,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -560,6 +581,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -589,7 +611,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -614,6 +637,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -643,7 +667,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -665,6 +690,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -694,7 +720,8 @@ void main() {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -716,6 +743,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -745,7 +773,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -770,6 +799,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -799,7 +829,8 @@ void main() {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -824,6 +855,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -855,7 +887,8 @@ void main() {
RangeValues endValues;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -885,6 +918,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -911,7 +945,8 @@ void main() {
RangeValues endValues;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -941,6 +976,7 @@ void main() {
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
......@@ -995,12 +1031,14 @@ void main() {
Color inactiveColor,
int divisions,
bool enabled = true,
bool useV2Slider = false,
}) {
RangeValues values = const RangeValues(0.5, 0.75);
final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues;
};
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1015,6 +1053,8 @@ void main() {
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
useV2Slider: useV2Slider,
),
),
),
),
......@@ -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 {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
......@@ -1046,6 +1111,34 @@ void main() {
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 {
const Color activeColor = Color(0xcafefeed);
final ThemeData theme = _buildTheme();
......@@ -1072,6 +1165,33 @@ void main() {
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 {
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = _buildTheme();
......@@ -1097,6 +1217,38 @@ void main() {
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 {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
......@@ -1128,6 +1280,36 @@ void main() {
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 {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
......@@ -1156,6 +1338,48 @@ void main() {
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 {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
......@@ -1195,6 +1419,27 @@ void main() {
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 {
final ThemeData theme = _buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
......@@ -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 {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
......@@ -1241,6 +1515,66 @@ void main() {
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 {
const Color customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef);
......@@ -1257,7 +1591,8 @@ void main() {
final ValueChanged<RangeValues> onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues;
};
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1277,12 +1612,13 @@ void main() {
),
),
),
),
);
}
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);
TestGesture gesture = await tester.startGesture(topRight);
......@@ -1290,7 +1626,7 @@ void main() {
await tester.pumpAndSettle();
expect(values.end, equals(1));
expect(
sliderBox,
valueIndicatorBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.valueIndicatorColor),
......@@ -1310,7 +1646,7 @@ void main() {
await tester.pumpAndSettle();
expect(values.end, equals(1));
expect(
sliderBox,
valueIndicatorBox,
paints
..path(color: customColor1)
..path(color: customColor1),
......@@ -1318,6 +1654,77 @@ void main() {
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 {
RangeValues values = const RangeValues(0.3, 0.7);
......@@ -1332,7 +1739,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -1357,6 +1765,7 @@ void main() {
},
),
),
),
);
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
......@@ -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 {
RangeValues values = const RangeValues(0.3, 0.7);
......@@ -1401,7 +1887,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -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
// inwards by the overlay radius.
......@@ -1450,7 +1938,84 @@ void main() {
await tester.pumpAndSettle();
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
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor)
......@@ -1475,7 +2040,8 @@ void main() {
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -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
// inwards by the overlay radius.
......@@ -1524,7 +2091,7 @@ void main() {
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor)
......@@ -1565,7 +2132,8 @@ void main() {
'labelStart: "lowerValue"',
'labelEnd: "upperValue"',
'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 {
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
double textScaleFactor,
Size sizeWithOverflow,
}) {
log.add(thumbCenter);
final Paint thumbPaint = Paint()..color = Colors.red;
......@@ -76,7 +78,8 @@ void main() {
double endValue;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -105,6 +108,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -134,7 +138,8 @@ void main() {
double value = 0.0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -157,6 +162,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -184,8 +190,10 @@ void main() {
int startValueUpdates = 0;
int endValueUpdates = 0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -217,6 +225,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -238,7 +247,8 @@ void main() {
double value = 0.0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -262,6 +272,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -290,7 +301,8 @@ void main() {
final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -318,6 +330,7 @@ void main() {
},
),
),
),
);
final List<Offset> expectedLog = <Offset>[
......@@ -359,7 +372,8 @@ void main() {
int updates = 0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -383,6 +397,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -401,7 +416,8 @@ void main() {
final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -429,6 +445,7 @@ void main() {
},
),
),
),
);
final List<Offset> expectedLog = <Offset>[
......@@ -469,7 +486,8 @@ void main() {
double value = 0.0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -498,6 +516,7 @@ void main() {
},
),
),
),
);
expect(value, equals(0.0));
......@@ -520,7 +539,9 @@ void main() {
testWidgets('Slider can be given zero values', (WidgetTester tester) async {
final List<double> log = <double>[];
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -535,13 +556,17 @@ void main() {
),
),
),
));
),
),
);
await tester.tap(find.byType(Slider));
expect(log, <double>[0.5]);
log.clear();
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -556,13 +581,237 @@ void main() {
),
),
),
));
),
),
);
await tester.tap(find.byType(Slider));
expect(log, <double>[]);
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 {
const Color customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef);
......@@ -597,7 +846,8 @@ void main() {
: (double d) {
value = d;
};
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -617,12 +867,14 @@ void main() {
),
),
),
),
);
}
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..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor));
......@@ -729,7 +981,7 @@ void main() {
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
valueIndicatorBox,
paints
..rect(color: sliderTheme.activeTrackColor)
..rect(color: sliderTheme.inactiveTrackColor)
......@@ -738,8 +990,8 @@ void main() {
..circle(color: sliderTheme.activeTickMarkColor)
..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();
// Wait for value indicator animation to finish.
......@@ -757,7 +1009,7 @@ void main() {
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
valueIndicatorBox,
paints
..rect(color: customColor1) // active track
..rect(color: customColor2) // inactive track
......@@ -766,15 +1018,17 @@ void main() {
..circle(color: customColor2) // 2nd tick mark
..circle(color: customColor2) // 3rd 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();
});
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -794,7 +1048,9 @@ void main() {
),
),
),
));
),
),
);
await tester.tap(find.byType(Slider));
expect(value, equals(0.5));
......@@ -802,7 +1058,9 @@ void main() {
testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -817,7 +1075,9 @@ void main() {
),
),
),
));
),
),
);
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
......@@ -833,7 +1093,9 @@ void main() {
testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -848,7 +1110,9 @@ void main() {
),
),
),
));
),
),
);
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
......@@ -863,7 +1127,9 @@ void main() {
});
testWidgets('Slider sizing', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -876,10 +1142,14 @@ void main() {
),
),
),
));
),
),
);
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,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -894,10 +1164,14 @@ void main() {
),
),
),
));
),
),
);
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,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -914,10 +1188,159 @@ void main() {
),
),
),
));
),
),
);
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 {
final Key sliderKey = UniqueKey();
double value = 0.0;
......@@ -927,7 +1350,8 @@ void main() {
bool isDiscrete = true,
ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete,
}) {
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -962,6 +1386,7 @@ void main() {
);
},
),
),
);
}
......@@ -970,7 +1395,7 @@ void main() {
TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pumpAndSettle();
......@@ -980,7 +1405,7 @@ void main() {
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pumpAndSettle();
......@@ -995,7 +1420,7 @@ void main() {
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pumpAndSettle();
......@@ -1009,7 +1434,7 @@ void main() {
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
expect(tester.firstRenderObject(find.byType(Overlay)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pumpAndSettle();
......@@ -1019,7 +1444,8 @@ void main() {
Widget buildSlider({
int divisions,
}) {
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1035,6 +1461,7 @@ void main() {
),
),
),
),
);
}
......@@ -1064,6 +1491,139 @@ void main() {
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 {
final Key sliderKey = GlobalKey(debugLabel: 'A');
double value = 0.0;
......@@ -1087,7 +1647,8 @@ void main() {
return slider;
}
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -1099,6 +1660,7 @@ void main() {
);
},
),
),
);
}
......@@ -1194,10 +1756,12 @@ void main() {
await testReparenting(true);
});
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1208,6 +1772,7 @@ void main() {
),
),
),
),
));
expect(
......@@ -1216,11 +1781,23 @@ void main() {
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
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%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
],
),
],
),
]),
ignoreRect: true,
......@@ -1228,7 +1805,8 @@ void main() {
));
// Disable slider
await tester.pumpWidget(Directionality(
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1239,12 +1817,24 @@ void main() {
),
),
),
),
));
expect(
semantics,
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,
ignoreTransform: true,
));
......@@ -1252,11 +1842,12 @@ void main() {
semantics.dispose();
}, 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);
await tester.pumpWidget(
Theme(
MaterialApp(
home: Theme(
data: ThemeData.light(),
child: Directionality(
textDirection: TextDirection.ltr,
......@@ -1273,6 +1864,7 @@ void main() {
),
),
),
),
);
expect(
......@@ -1281,23 +1873,35 @@ void main() {
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
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%',
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
],
),
],
),
]),
ignoreRect: true,
ignoreTransform: true,
));
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 {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1312,6 +1916,7 @@ void main() {
),
),
),
),
));
expect(
......@@ -1320,11 +1925,22 @@ void main() {
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
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',
increasedValue: '60',
decreasedValue: '20',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
],
),
],
),
]),
ignoreRect: true,
......@@ -1342,7 +1958,8 @@ void main() {
double value = 0.45;
Widget buildApp({ SliderThemeData sliderTheme, int divisions, bool enabled = true }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
......@@ -1363,6 +1980,7 @@ void main() {
),
),
),
),
);
}
......@@ -1379,9 +1997,9 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
final RenderBox valueIndicatorBox = tester.firstRenderObject(find.byType(Overlay));
expect(
sliderBox,
valueIndicatorBox,
isVisible
? (paints..path(color: theme.valueIndicatorColor))
: isNot(paints..path(color: theme.valueIndicatorColor)),
......@@ -1421,7 +2039,8 @@ void main() {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -1445,6 +2064,7 @@ void main() {
},
),
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
......@@ -1517,7 +2137,8 @@ void main() {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -1547,6 +2168,7 @@ void main() {
},
),
),
),
);
final RenderBox renderObject = tester.renderObject<RenderBox>(find.byType(Slider));
......@@ -1583,6 +2205,7 @@ void main() {
'label: "Set a value"',
'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
'useV1Slider',
]);
});
}
......@@ -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 {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
......@@ -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 {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
......@@ -218,6 +258,40 @@ void main() {
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 {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
......@@ -240,12 +314,7 @@ void main() {
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
// The disabled slider thumb has a horizontal gap between itself and the
// 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.
// The disabled slider thumb is the same size as the enabled thumb.
expect(
sliderBox,
paints
......@@ -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(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(
thumbColor: Colors.red.shade500,
showValueIndicator: ShowValueIndicator.always,
);
Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) {
return Directionality(
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale),
......@@ -381,6 +454,7 @@ void main() {
label: value,
divisions: 3,
onChanged: (double d) { },
useV2Slider: true,
),
),
),
......@@ -388,19 +462,201 @@ void main() {
),
),
),
),
);
}
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));
TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
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
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -422,7 +678,7 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -443,7 +699,7 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -464,7 +720,7 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -485,7 +741,7 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -511,7 +767,7 @@ void main() {
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
sliderBox,
valueIndicatorBox,
paints
..path(
color: sliderTheme.valueIndicatorColor,
......@@ -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 {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
thumbShape: const RoundSliderThumbShape(
......@@ -606,7 +892,6 @@ void main() {
);
});
testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5),
......@@ -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 {
const double uniqueOverlayRadius = 23;
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
......@@ -800,7 +1118,7 @@ void main() {
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.
final Offset center = tester.getCenter(find.byType(Slider));
......@@ -808,14 +1126,14 @@ void main() {
await tester.pumpAndSettle();
// Only 1 value indicator.
expect(sliderBox, paintsExactlyCountTimes(#drawRect, 0));
expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 0));
expect(sliderBox, paintsExactlyCountTimes(#drawPath, 1));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawRect, 0));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawCircle, 0));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1));
await gesture.up();
});
testWidgets('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async {
testWidgets('PaddleSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async {
// Pump a slider with just a value indicator.
await tester.pumpWidget(_buildApp(
ThemeData().sliderTheme.copyWith(
......@@ -824,13 +1142,13 @@ void main() {
thumbShape: SliderComponentShape.noThumb,
tickMarkShape: SliderTickMarkShape.noTickMark,
showValueIndicator: ShowValueIndicator.always,
rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(),
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
),
value: 0.5,
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.
final Offset center = tester.getCenter(find.byType(Slider));
......@@ -838,11 +1156,102 @@ void main() {
// Nothing to paint at scale 0.
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.
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();
});
......@@ -853,6 +1262,7 @@ Widget _buildApp(
double value = 0.0,
bool enabled = true,
int divisions,
bool useV2Slider = false,
}) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return MaterialApp(
......@@ -865,6 +1275,31 @@ Widget _buildApp(
label: '$value',
onChanged: onChanged,
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() {
(ByteData data) { },
);
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 {
......@@ -192,7 +195,10 @@ void main() {
(ByteData data) { },
);
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 {
......
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