add support to customize Slider interacivity (#121483)
design doc: https://docs.flutter.dev/go/permissible-slider-interaction closes #113370 open questions: - No, as `SliderInteraction.none` doesn't exist anymore. - Yes (done) - Yes. - SliderInteraction - SliderAction - Slider.allowedInteraction - Slider.permissibleInteraction - Slider.interaction - Slider.allowedAction - Slider.permittedAction
This commit is contained in:
parent
d9ea36ccb7
commit
293715a6a4
@ -34,6 +34,36 @@ typedef PaintValueIndicator = void Function(PaintingContext context, Offset offs
|
|||||||
|
|
||||||
enum _SliderType { material, adaptive }
|
enum _SliderType { material, adaptive }
|
||||||
|
|
||||||
|
/// Possible ways for a user to interact with a [Slider].
|
||||||
|
enum SliderInteraction {
|
||||||
|
/// Allows the user to interact with a [Slider] by tapping or sliding anywhere
|
||||||
|
/// on the track.
|
||||||
|
///
|
||||||
|
/// Essentially all possible interactions are allowed.
|
||||||
|
///
|
||||||
|
/// This is different from [SliderInteraction.slideOnly] as when you try
|
||||||
|
/// to slide anywhere other than the thumb, the thumb will move to the first
|
||||||
|
/// point of contact.
|
||||||
|
tapAndSlide,
|
||||||
|
|
||||||
|
/// Allows the user to interact with a [Slider] by only tapping anywhere on
|
||||||
|
/// the track.
|
||||||
|
///
|
||||||
|
/// Sliding interaction is ignored.
|
||||||
|
tapOnly,
|
||||||
|
|
||||||
|
/// Allows the user to interact with a [Slider] only by sliding anywhere on
|
||||||
|
/// the track.
|
||||||
|
///
|
||||||
|
/// Tapping interaction is ignored.
|
||||||
|
slideOnly,
|
||||||
|
|
||||||
|
/// Allows the user to interact with a [Slider] only by sliding the thumb.
|
||||||
|
///
|
||||||
|
/// Tapping and sliding interactions on the track are ignored.
|
||||||
|
slideThumb;
|
||||||
|
}
|
||||||
|
|
||||||
/// A Material Design slider.
|
/// A Material Design slider.
|
||||||
///
|
///
|
||||||
/// Used to select from a range of values.
|
/// Used to select from a range of values.
|
||||||
@ -158,6 +188,7 @@ class Slider extends StatefulWidget {
|
|||||||
this.semanticFormatterCallback,
|
this.semanticFormatterCallback,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.allowedInteraction,
|
||||||
}) : _sliderType = _SliderType.material,
|
}) : _sliderType = _SliderType.material,
|
||||||
assert(min <= max),
|
assert(min <= max),
|
||||||
assert(value >= min && value <= max,
|
assert(value >= min && value <= max,
|
||||||
@ -198,6 +229,7 @@ class Slider extends StatefulWidget {
|
|||||||
this.semanticFormatterCallback,
|
this.semanticFormatterCallback,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.allowedInteraction,
|
||||||
}) : _sliderType = _SliderType.adaptive,
|
}) : _sliderType = _SliderType.adaptive,
|
||||||
assert(min <= max),
|
assert(min <= max),
|
||||||
assert(value >= min && value <= max,
|
assert(value >= min && value <= max,
|
||||||
@ -502,6 +534,15 @@ class Slider extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.Focus.autofocus}
|
/// {@macro flutter.widgets.Focus.autofocus}
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Allowed way for the user to interact with the [Slider].
|
||||||
|
///
|
||||||
|
/// For example, if this is set to [SliderInteraction.tapOnly], the user can
|
||||||
|
/// interact with the slider only by tapping anywhere on the track. Sliding
|
||||||
|
/// will have no effect.
|
||||||
|
///
|
||||||
|
/// Defaults to [SliderInteraction.tapAndSlide].
|
||||||
|
final SliderInteraction? allowedInteraction;
|
||||||
|
|
||||||
final _SliderType _sliderType ;
|
final _SliderType _sliderType ;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -753,6 +794,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
|
const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
|
||||||
final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
|
final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
|
||||||
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
|
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
|
||||||
|
const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide;
|
||||||
|
|
||||||
final Set<MaterialState> states = <MaterialState>{
|
final Set<MaterialState> states = <MaterialState>{
|
||||||
if (!_enabled) MaterialState.disabled,
|
if (!_enabled) MaterialState.disabled,
|
||||||
@ -807,6 +849,9 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
|
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
|
||||||
?? sliderTheme.mouseCursor?.resolve(states)
|
?? sliderTheme.mouseCursor?.resolve(states)
|
||||||
?? MaterialStateMouseCursor.clickable.resolve(states);
|
?? MaterialStateMouseCursor.clickable.resolve(states);
|
||||||
|
final SliderInteraction effectiveAllowedInteraction = widget.allowedInteraction
|
||||||
|
?? sliderTheme.allowedInteraction
|
||||||
|
?? defaultAllowedInteraction;
|
||||||
|
|
||||||
// This size is used as the max bounds for the painting of the value
|
// 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
|
// indicators It must be kept in sync with the function with the same name
|
||||||
@ -877,6 +922,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
semanticFormatterCallback: widget.semanticFormatterCallback,
|
semanticFormatterCallback: widget.semanticFormatterCallback,
|
||||||
hasFocus: _focused,
|
hasFocus: _focused,
|
||||||
hovering: _hovering,
|
hovering: _hovering,
|
||||||
|
allowedInteraction: effectiveAllowedInteraction,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -940,6 +986,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
required this.semanticFormatterCallback,
|
required this.semanticFormatterCallback,
|
||||||
required this.hasFocus,
|
required this.hasFocus,
|
||||||
required this.hovering,
|
required this.hovering,
|
||||||
|
required this.allowedInteraction,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double value;
|
final double value;
|
||||||
@ -956,6 +1003,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
final _SliderState state;
|
final _SliderState state;
|
||||||
final bool hasFocus;
|
final bool hasFocus;
|
||||||
final bool hovering;
|
final bool hovering;
|
||||||
|
final SliderInteraction allowedInteraction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RenderSlider createRenderObject(BuildContext context) {
|
_RenderSlider createRenderObject(BuildContext context) {
|
||||||
@ -977,6 +1025,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
hasFocus: hasFocus,
|
hasFocus: hasFocus,
|
||||||
hovering: hovering,
|
hovering: hovering,
|
||||||
gestureSettings: MediaQuery.gestureSettingsOf(context),
|
gestureSettings: MediaQuery.gestureSettingsOf(context),
|
||||||
|
allowedInteraction: allowedInteraction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1000,7 +1049,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
..platform = Theme.of(context).platform
|
..platform = Theme.of(context).platform
|
||||||
..hasFocus = hasFocus
|
..hasFocus = hasFocus
|
||||||
..hovering = hovering
|
..hovering = hovering
|
||||||
..gestureSettings = MediaQuery.gestureSettingsOf(context);
|
..gestureSettings = MediaQuery.gestureSettingsOf(context)
|
||||||
|
..allowedInteraction = allowedInteraction;
|
||||||
// Ticker provider cannot change since there's a 1:1 relationship between
|
// Ticker provider cannot change since there's a 1:1 relationship between
|
||||||
// the _SliderRenderObjectWidget object and the _SliderState object.
|
// the _SliderRenderObjectWidget object and the _SliderState object.
|
||||||
}
|
}
|
||||||
@ -1025,6 +1075,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
required bool hasFocus,
|
required bool hasFocus,
|
||||||
required bool hovering,
|
required bool hovering,
|
||||||
required DeviceGestureSettings gestureSettings,
|
required DeviceGestureSettings gestureSettings,
|
||||||
|
required SliderInteraction allowedInteraction,
|
||||||
}) : assert(value >= 0.0 && value <= 1.0),
|
}) : assert(value >= 0.0 && value <= 1.0),
|
||||||
assert(secondaryTrackValue == null || (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0)),
|
assert(secondaryTrackValue == null || (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0)),
|
||||||
_platform = platform,
|
_platform = platform,
|
||||||
@ -1040,7 +1091,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
_state = state,
|
_state = state,
|
||||||
_textDirection = textDirection,
|
_textDirection = textDirection,
|
||||||
_hasFocus = hasFocus,
|
_hasFocus = hasFocus,
|
||||||
_hovering = hovering {
|
_hovering = hovering,
|
||||||
|
_allowedInteraction = allowedInteraction {
|
||||||
_updateLabelPainter();
|
_updateLabelPainter();
|
||||||
final GestureArenaTeam team = GestureArenaTeam();
|
final GestureArenaTeam team = GestureArenaTeam();
|
||||||
_drag = HorizontalDragGestureRecognizer()
|
_drag = HorizontalDragGestureRecognizer()
|
||||||
@ -1294,6 +1346,16 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
_updateForHover(_hovering);
|
_updateForHover(_hovering);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SliderInteraction _allowedInteraction;
|
||||||
|
SliderInteraction get allowedInteraction => _allowedInteraction;
|
||||||
|
set allowedInteraction(SliderInteraction value) {
|
||||||
|
if (value == _allowedInteraction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_allowedInteraction = value;
|
||||||
|
markNeedsSemanticsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void _updateForFocus(bool focused) {
|
void _updateForFocus(bool focused) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
_state.overlayController.forward();
|
_state.overlayController.forward();
|
||||||
@ -1423,13 +1485,26 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
void _startInteraction(Offset globalPosition) {
|
void _startInteraction(Offset globalPosition) {
|
||||||
_state.showValueIndicator();
|
_state.showValueIndicator();
|
||||||
if (!_active && isInteractive) {
|
if (!_active && isInteractive) {
|
||||||
|
switch (allowedInteraction) {
|
||||||
|
case SliderInteraction.tapAndSlide:
|
||||||
|
case SliderInteraction.tapOnly:
|
||||||
_active = true;
|
_active = true;
|
||||||
|
_currentDragValue = _getValueFromGlobalPosition(globalPosition);
|
||||||
|
onChanged!(_discretize(_currentDragValue));
|
||||||
|
case SliderInteraction.slideThumb:
|
||||||
|
if (_isPointerOnOverlay(globalPosition)) {
|
||||||
|
_active = true;
|
||||||
|
_currentDragValue = value;
|
||||||
|
}
|
||||||
|
case SliderInteraction.slideOnly:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_active) {
|
||||||
// We supply the *current* value as the start location, so that if we have
|
// We supply the *current* value as the start location, so that if we have
|
||||||
// a tap, it consists of a call to onChangeStart with the previous value and
|
// a tap, it consists of a call to onChangeStart with the previous value and
|
||||||
// a call to onChangeEnd with the new value.
|
// a call to onChangeEnd with the new value.
|
||||||
onChangeStart?.call(_discretize(value));
|
onChangeStart?.call(_discretize(value));
|
||||||
_currentDragValue = _getValueFromGlobalPosition(globalPosition);
|
|
||||||
onChanged!(_discretize(_currentDragValue));
|
|
||||||
_state.overlayController.forward();
|
_state.overlayController.forward();
|
||||||
if (showValueIndicator) {
|
if (showValueIndicator) {
|
||||||
_state.valueIndicatorController.forward();
|
_state.valueIndicatorController.forward();
|
||||||
@ -1444,6 +1519,7 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _endInteraction() {
|
void _endInteraction() {
|
||||||
if (!_state.mounted) {
|
if (!_state.mounted) {
|
||||||
@ -1473,7 +1549,18 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInteractive) {
|
// for slide only, there is no start interaction trigger, so _active
|
||||||
|
// will be false and needs to be made true.
|
||||||
|
if (!_active && allowedInteraction == SliderInteraction.slideOnly) {
|
||||||
|
_active = true;
|
||||||
|
_currentDragValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (allowedInteraction) {
|
||||||
|
case SliderInteraction.tapAndSlide:
|
||||||
|
case SliderInteraction.slideOnly:
|
||||||
|
case SliderInteraction.slideThumb:
|
||||||
|
if (_active && isInteractive) {
|
||||||
final double valueDelta = details.primaryDelta! / _trackRect.width;
|
final double valueDelta = details.primaryDelta! / _trackRect.width;
|
||||||
switch (textDirection) {
|
switch (textDirection) {
|
||||||
case TextDirection.rtl:
|
case TextDirection.rtl:
|
||||||
@ -1483,6 +1570,10 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
}
|
}
|
||||||
onChanged!(_discretize(_currentDragValue));
|
onChanged!(_discretize(_currentDragValue));
|
||||||
}
|
}
|
||||||
|
case SliderInteraction.tapOnly:
|
||||||
|
// cannot slide (drag) as its tapOnly.
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragEnd(DragEndDetails details) {
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
@ -1497,6 +1588,10 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
_endInteraction();
|
_endInteraction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isPointerOnOverlay(Offset globalPosition) {
|
||||||
|
return overlayRect!.contains(globalToLocal(globalPosition));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hitTestSelf(Offset position) => true;
|
bool hitTestSelf(Offset position) => true;
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'material_state.dart';
|
import 'material_state.dart';
|
||||||
|
import 'slider.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
/// Applies a slider theme to descendant [Slider] widgets.
|
/// Applies a slider theme to descendant [Slider] widgets.
|
||||||
@ -292,6 +293,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
this.minThumbSeparation,
|
this.minThumbSeparation,
|
||||||
this.thumbSelector,
|
this.thumbSelector,
|
||||||
this.mouseCursor,
|
this.mouseCursor,
|
||||||
|
this.allowedInteraction,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Generates a SliderThemeData from three main colors.
|
/// Generates a SliderThemeData from three main colors.
|
||||||
@ -576,6 +578,11 @@ class SliderThemeData with Diagnosticable {
|
|||||||
/// If specified, overrides the default value of [Slider.mouseCursor].
|
/// If specified, overrides the default value of [Slider.mouseCursor].
|
||||||
final MaterialStateProperty<MouseCursor?>? mouseCursor;
|
final MaterialStateProperty<MouseCursor?>? mouseCursor;
|
||||||
|
|
||||||
|
/// Allowed way for the user to interact with the [Slider].
|
||||||
|
///
|
||||||
|
/// If specified, overrides the default value of [Slider.allowedInteraction].
|
||||||
|
final SliderInteraction? allowedInteraction;
|
||||||
|
|
||||||
/// Creates a copy of this object but with the given fields replaced with the
|
/// Creates a copy of this object but with the given fields replaced with the
|
||||||
/// new values.
|
/// new values.
|
||||||
SliderThemeData copyWith({
|
SliderThemeData copyWith({
|
||||||
@ -609,6 +616,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
double? minThumbSeparation,
|
double? minThumbSeparation,
|
||||||
RangeThumbSelector? thumbSelector,
|
RangeThumbSelector? thumbSelector,
|
||||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||||
|
SliderInteraction? allowedInteraction,
|
||||||
}) {
|
}) {
|
||||||
return SliderThemeData(
|
return SliderThemeData(
|
||||||
trackHeight: trackHeight ?? this.trackHeight,
|
trackHeight: trackHeight ?? this.trackHeight,
|
||||||
@ -641,6 +649,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
|
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
|
||||||
thumbSelector: thumbSelector ?? this.thumbSelector,
|
thumbSelector: thumbSelector ?? this.thumbSelector,
|
||||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||||
|
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -684,6 +693,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
|
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
|
||||||
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
|
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
|
||||||
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
|
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
|
||||||
|
allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -720,6 +730,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
minThumbSeparation,
|
minThumbSeparation,
|
||||||
thumbSelector,
|
thumbSelector,
|
||||||
mouseCursor,
|
mouseCursor,
|
||||||
|
allowedInteraction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -761,7 +772,8 @@ class SliderThemeData with Diagnosticable {
|
|||||||
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
|
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
|
||||||
&& other.minThumbSeparation == minThumbSeparation
|
&& other.minThumbSeparation == minThumbSeparation
|
||||||
&& other.thumbSelector == thumbSelector
|
&& other.thumbSelector == thumbSelector
|
||||||
&& other.mouseCursor == mouseCursor;
|
&& other.mouseCursor == mouseCursor
|
||||||
|
&& other.allowedInteraction == allowedInteraction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -798,6 +810,7 @@ class SliderThemeData with Diagnosticable {
|
|||||||
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
|
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
|
||||||
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
|
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
|
||||||
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
|
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
|
||||||
|
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3703,4 +3703,209 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Slider.allowedInteraction', () {
|
||||||
|
testWidgets('SliderInteraction.tapOnly', (WidgetTester tester) async {
|
||||||
|
double value = 1.0;
|
||||||
|
final Key sliderKey = UniqueKey();
|
||||||
|
// (slider's left padding (overlayRadius), windowHeight / 2)
|
||||||
|
const Offset startOfTheSliderTrack = Offset(24, 300);
|
||||||
|
const Offset centerOfTheSlideTrack = Offset(400, 300);
|
||||||
|
|
||||||
|
Widget buildWidget() => MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(builder: (BuildContext _, StateSetter setState) {
|
||||||
|
return Slider(
|
||||||
|
value: value,
|
||||||
|
key: sliderKey,
|
||||||
|
allowedInteraction: SliderInteraction.tapOnly,
|
||||||
|
onChanged: (double newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// allow tap only
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
// test tap
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfTheSlideTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 1.0 -> 0.5
|
||||||
|
expect(value, 0.5);
|
||||||
|
|
||||||
|
// test slide
|
||||||
|
await gesture.moveTo(startOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect, remains 0.5
|
||||||
|
expect(value, 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliderInteraction.tapAndSlide', (WidgetTester tester) async {
|
||||||
|
double value = 1.0;
|
||||||
|
final Key sliderKey = UniqueKey();
|
||||||
|
// (slider's left padding (overlayRadius), windowHeight / 2)
|
||||||
|
const Offset startOfTheSliderTrack = Offset(24, 300);
|
||||||
|
const Offset centerOfTheSlideTrack = Offset(400, 300);
|
||||||
|
const Offset endOfTheSliderTrack = Offset(800 - 24, 300);
|
||||||
|
|
||||||
|
Widget buildWidget() => MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(builder: (BuildContext _, StateSetter setState) {
|
||||||
|
return Slider(
|
||||||
|
value: value,
|
||||||
|
key: sliderKey,
|
||||||
|
// allowedInteraction: SliderInteraction.tapAndSlide, // default
|
||||||
|
onChanged: (double newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
// Test tap.
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfTheSlideTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 1.0 -> 0.5
|
||||||
|
expect(value, 0.5);
|
||||||
|
|
||||||
|
// test slide
|
||||||
|
await gesture.moveTo(startOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 0.5 -> 0.0
|
||||||
|
expect(value, 0.0);
|
||||||
|
await gesture.moveTo(endOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 0.0 -> 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliderInteraction.slideOnly', (WidgetTester tester) async {
|
||||||
|
double value = 1.0;
|
||||||
|
final Key sliderKey = UniqueKey();
|
||||||
|
// (slider's left padding (overlayRadius), windowHeight / 2)
|
||||||
|
const Offset startOfTheSliderTrack = Offset(24, 300);
|
||||||
|
const Offset centerOfTheSlideTrack = Offset(400, 300);
|
||||||
|
const Offset endOfTheSliderTrack = Offset(800 - 24, 300);
|
||||||
|
|
||||||
|
Widget buildApp() {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(builder: (BuildContext _, StateSetter setState) {
|
||||||
|
return Slider(
|
||||||
|
value: value,
|
||||||
|
key: sliderKey,
|
||||||
|
allowedInteraction: SliderInteraction.slideOnly,
|
||||||
|
onChanged: (double newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildApp());
|
||||||
|
|
||||||
|
// test tap
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfTheSlideTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect as tap is disabled, remains 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
|
||||||
|
// test slide
|
||||||
|
await gesture.moveTo(startOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 1.0 -> 0.5
|
||||||
|
expect(value, 0.5);
|
||||||
|
await gesture.moveTo(endOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 0.0 -> 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliderInteraction.slideThumb', (WidgetTester tester) async {
|
||||||
|
double value = 1.0;
|
||||||
|
final Key sliderKey = UniqueKey();
|
||||||
|
// (slider's left padding (overlayRadius), windowHeight / 2)
|
||||||
|
const Offset startOfTheSliderTrack = Offset(24, 300);
|
||||||
|
const Offset centerOfTheSliderTrack = Offset(400, 300);
|
||||||
|
const Offset endOfTheSliderTrack = Offset(800 - 24, 300);
|
||||||
|
|
||||||
|
Widget buildApp() {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(builder: (BuildContext _, StateSetter setState) {
|
||||||
|
return Slider(
|
||||||
|
value: value,
|
||||||
|
key: sliderKey,
|
||||||
|
allowedInteraction: SliderInteraction.slideThumb,
|
||||||
|
onChanged: (double newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildApp());
|
||||||
|
|
||||||
|
// test tap
|
||||||
|
final TestGesture gesture = await tester.startGesture(centerOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect, remains 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
|
||||||
|
// test slide
|
||||||
|
await gesture.moveTo(startOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect, remains 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
|
||||||
|
// test slide thumb
|
||||||
|
await gesture.up();
|
||||||
|
await gesture.down(endOfTheSliderTrack); // where the thumb is
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect, remains 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
await gesture.moveTo(centerOfTheSliderTrack);
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 1.0 -> 0.5
|
||||||
|
expect(value, 0.5);
|
||||||
|
|
||||||
|
// test tap inside overlay but not on thumb, then slide
|
||||||
|
await gesture.up();
|
||||||
|
// default overlay radius is 12, so 10 is inside the overlay
|
||||||
|
await gesture.down(centerOfTheSliderTrack.translate(-10, 0));
|
||||||
|
await tester.pump();
|
||||||
|
// has no effect, remains 1.0
|
||||||
|
expect(value, 0.5);
|
||||||
|
await gesture.moveTo(endOfTheSliderTrack.translate(-10, 0));
|
||||||
|
await tester.pump();
|
||||||
|
// changes from 0.5 -> 1.0
|
||||||
|
expect(value, 1.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ void main() {
|
|||||||
showValueIndicator: ShowValueIndicator.always,
|
showValueIndicator: ShowValueIndicator.always,
|
||||||
valueIndicatorTextStyle: TextStyle(color: Colors.black),
|
valueIndicatorTextStyle: TextStyle(color: Colors.black),
|
||||||
mouseCursor: MaterialStateMouseCursor.clickable,
|
mouseCursor: MaterialStateMouseCursor.clickable,
|
||||||
|
allowedInteraction: SliderInteraction.tapOnly,
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
|
|
||||||
final List<String> description = builder.properties
|
final List<String> description = builder.properties
|
||||||
@ -99,6 +100,7 @@ void main() {
|
|||||||
'showValueIndicator: always',
|
'showValueIndicator: always',
|
||||||
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
|
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
|
||||||
'mouseCursor: MaterialStateMouseCursor(clickable)',
|
'mouseCursor: MaterialStateMouseCursor(clickable)',
|
||||||
|
'allowedInteraction: tapOnly'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1907,6 +1909,113 @@ void main() {
|
|||||||
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
|
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('SliderTheme.allowedInteraction is themeable', (WidgetTester tester) async {
|
||||||
|
double value = 0.0;
|
||||||
|
|
||||||
|
Widget buildApp({
|
||||||
|
bool isAllowedInteractionInThemeNull = false,
|
||||||
|
bool isAllowedInteractionInSliderNull = false,
|
||||||
|
}) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SliderTheme(
|
||||||
|
data: ThemeData().sliderTheme.copyWith(
|
||||||
|
allowedInteraction: isAllowedInteractionInThemeNull
|
||||||
|
? null
|
||||||
|
: SliderInteraction.slideOnly,
|
||||||
|
),
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (_, void Function(void Function()) setState) {
|
||||||
|
return Slider(
|
||||||
|
value: value,
|
||||||
|
allowedInteraction: isAllowedInteractionInSliderNull
|
||||||
|
? null
|
||||||
|
: SliderInteraction.tapOnly,
|
||||||
|
onChanged: (double newValue) {
|
||||||
|
setState(() {
|
||||||
|
value = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.createGesture();
|
||||||
|
|
||||||
|
// when theme and parameter are specified, parameter is used [tapOnly].
|
||||||
|
await tester.pumpWidget(buildApp());
|
||||||
|
// tap is allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(value, equals(0.5)); // changes
|
||||||
|
await gesture.up();
|
||||||
|
// slide isn't allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveBy(const Offset(50, 0));
|
||||||
|
expect(value, equals(0.0)); // no change
|
||||||
|
await gesture.up();
|
||||||
|
|
||||||
|
// when only parameter is specified, parameter is used [tapOnly].
|
||||||
|
await tester.pumpWidget(buildApp(isAllowedInteractionInThemeNull: true));
|
||||||
|
// tap is allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(value, equals(0.5)); // changes
|
||||||
|
await gesture.up();
|
||||||
|
// slide isn't allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveBy(const Offset(50, 0));
|
||||||
|
expect(value, equals(0.0)); // no change
|
||||||
|
await gesture.up();
|
||||||
|
|
||||||
|
// when theme is specified but parameter is null, theme is used [slideOnly].
|
||||||
|
await tester.pumpWidget(buildApp(isAllowedInteractionInSliderNull: true));
|
||||||
|
// tap isn't allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(value, equals(0.0)); // no change
|
||||||
|
await gesture.up();
|
||||||
|
// slide isn't allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveBy(const Offset(50, 0));
|
||||||
|
expect(value, greaterThan(0.0)); // changes
|
||||||
|
await gesture.up();
|
||||||
|
|
||||||
|
// when both theme and parameter are null, default is used [tapAndSlide].
|
||||||
|
await tester.pumpWidget(buildApp(
|
||||||
|
isAllowedInteractionInSliderNull: true,
|
||||||
|
isAllowedInteractionInThemeNull: true,
|
||||||
|
));
|
||||||
|
// tap is allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(value, equals(0.5));
|
||||||
|
await gesture.up();
|
||||||
|
// slide is allowed.
|
||||||
|
value = 0.0;
|
||||||
|
await gesture.down(tester.getCenter(find.byType(Slider)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveBy(const Offset(50, 0));
|
||||||
|
expect(value, greaterThan(0.0)); // changes
|
||||||
|
await gesture.up();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Default value indicator color', (WidgetTester tester) async {
|
testWidgets('Default value indicator color', (WidgetTester tester) async {
|
||||||
debugDisableShadows = false;
|
debugDisableShadows = false;
|
||||||
try {
|
try {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user