mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[Slider] Rebase. (#52663)
This commit is contained in:
parent
a2d62df3ee
commit
e71cf1cdbe
@ -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,15 +273,21 @@ class _SlidersState extends State<_Sliders> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Slider.adaptive(
|
||||
value: _continuousValue,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_continuousValue = value;
|
||||
});
|
||||
},
|
||||
SliderTheme(
|
||||
data: const SliderThemeData(
|
||||
showValueIndicator: ShowValueIndicator.always,
|
||||
),
|
||||
child: Slider.adaptive(
|
||||
label: _continuousValue.toStringAsFixed(6).toString(),
|
||||
value: _continuousValue,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_continuousValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Continuous with Editable Numerical Value'),
|
||||
],
|
||||
@ -314,7 +325,7 @@ class _SlidersState extends State<_Sliders> {
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
activeTickMarkColor: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
|
||||
inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
|
||||
overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
|
||||
thumbColor: Colors.deepPurple,
|
||||
valueIndicatorColor: Colors.deepPurpleAccent,
|
||||
|
||||
@ -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(
|
||||
values: _unlerpRangeValues(widget.values),
|
||||
divisions: widget.divisions,
|
||||
labels: widget.labels,
|
||||
sliderTheme: sliderTheme,
|
||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||
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,
|
||||
// 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,22 +1376,27 @@ 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) {
|
||||
_sliderTheme.rangeValueIndicatorShape.paint(
|
||||
context,
|
||||
bottomThumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
isOnTop: false,
|
||||
labelPainter: bottomLabelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
textDirection: _textDirection,
|
||||
thumb: bottomThumb,
|
||||
value: bottomValue,
|
||||
);
|
||||
_state.paintBottomValueIndicator = (PaintingContext context, Offset offset) {
|
||||
_sliderTheme.rangeValueIndicatorShape.paint(
|
||||
context,
|
||||
bottomThumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
isOnTop: false,
|
||||
labelPainter: bottomLabelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
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,32 +1453,37 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
|
||||
break;
|
||||
}
|
||||
|
||||
_sliderTheme.rangeValueIndicatorShape.paint(
|
||||
context,
|
||||
topThumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
isOnTop: thumbDelta < innerOverflow,
|
||||
labelPainter: topLabelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
textDirection: _textDirection,
|
||||
thumb: topThumb,
|
||||
value: topValue,
|
||||
);
|
||||
_state.paintTopValueIndicator = (PaintingContext context, Offset offset) {
|
||||
_sliderTheme.rangeValueIndicatorShape.paint(
|
||||
context,
|
||||
topThumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
isOnTop: thumbDelta < innerOverflow,
|
||||
labelPainter: topLabelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
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(
|
||||
value: _unlerp(widget.value),
|
||||
divisions: widget.divisions,
|
||||
label: widget.label,
|
||||
sliderTheme: sliderTheme,
|
||||
mediaQueryData: MediaQuery.of(context),
|
||||
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,
|
||||
// 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,
|
||||
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,19 +739,22 @@ 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),
|
||||
_platform = platform,
|
||||
_semanticFormatterCallback = semanticFormatterCallback,
|
||||
_label = label,
|
||||
_value = value,
|
||||
_divisions = divisions,
|
||||
_sliderTheme = sliderTheme,
|
||||
_mediaQueryData = mediaQueryData,
|
||||
_onChanged = onChanged,
|
||||
_state = state,
|
||||
_textDirection = textDirection {
|
||||
assert(state != null),
|
||||
assert(textDirection != null),
|
||||
_platform = platform,
|
||||
_semanticFormatterCallback = semanticFormatterCallback,
|
||||
_label = label,
|
||||
_value = value,
|
||||
_divisions = divisions,
|
||||
_sliderTheme = sliderTheme,
|
||||
_textScaleFactor = textScaleFactor,
|
||||
_screenSize = screenSize,
|
||||
_onChanged = onChanged,
|
||||
_state = state,
|
||||
_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,31 +1254,36 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
|
||||
if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
|
||||
if (showValueIndicator) {
|
||||
_sliderTheme.valueIndicatorShape.paint(
|
||||
context,
|
||||
thumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
labelPainter: _labelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
textDirection: _textDirection,
|
||||
value: _value,
|
||||
);
|
||||
_state.paintValueIndicator = (PaintingContext context, Offset offset) {
|
||||
_sliderTheme.valueIndicatorShape.paint(
|
||||
context,
|
||||
offset + thumbCenter,
|
||||
activationAnimation: _valueIndicatorAnimation,
|
||||
enableAnimation: _enableAnimation,
|
||||
isDiscrete: isDiscrete,
|
||||
labelPainter: _labelPainter,
|
||||
parentBox: this,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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,32 +428,38 @@ 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(
|
||||
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) { },
|
||||
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) { },
|
||||
useV2Slider: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -393,14 +468,195 @@ 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),
|
||||
@ -628,7 +913,7 @@ void main() {
|
||||
..circle(x: 771, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false));
|
||||
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@ -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(sliderBox, paintsExactlyCountTimes(#drawPath, 1));
|
||||
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(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 {
|
||||
@ -191,8 +194,11 @@ void main() {
|
||||
SystemChannels.system.codec.encodeMessage(data),
|
||||
(ByteData data) { },
|
||||
);
|
||||
final RenderObject renderObject = tester.renderObject(find.byType(Slider));
|
||||
expect(renderObject.debugNeedsLayout, isTrue);
|
||||
final RenderObject renderObject = tester.renderObject(find.byType(Slider));
|
||||
|
||||
bool sliderBoxNeedsLayout;
|
||||
renderObject.visitChildren((RenderObject child) {sliderBoxNeedsLayout = child.debugNeedsLayout;});
|
||||
expect(sliderBoxNeedsLayout, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('TimePicker relayout upon system fonts changes', (WidgetTester tester) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user