Unverified Commit 293715a6 authored by Viren Khatri's avatar Viren Khatri Committed by GitHub

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
parent d9ea36cc
...@@ -34,6 +34,36 @@ typedef PaintValueIndicator = void Function(PaintingContext context, Offset offs ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -720,6 +730,7 @@ class SliderThemeData with Diagnosticable {
minThumbSeparation, minThumbSeparation,
thumbSelector, thumbSelector,
mouseCursor, mouseCursor,
allowedInteraction,
), ),
); );
...@@ -761,7 +772,8 @@ class SliderThemeData with Diagnosticable { ...@@ -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 { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment