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
......@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material_state.dart';
import 'slider.dart';
import 'theme.dart';
/// Applies a slider theme to descendant [Slider] widgets.
......@@ -292,6 +293,7 @@ class SliderThemeData with Diagnosticable {
this.minThumbSeparation,
this.thumbSelector,
this.mouseCursor,
this.allowedInteraction,
});
/// Generates a SliderThemeData from three main colors.
......@@ -576,6 +578,11 @@ class SliderThemeData with Diagnosticable {
/// If specified, overrides the default value of [Slider.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
/// new values.
SliderThemeData copyWith({
......@@ -609,6 +616,7 @@ class SliderThemeData with Diagnosticable {
double? minThumbSeparation,
RangeThumbSelector? thumbSelector,
MaterialStateProperty<MouseCursor?>? mouseCursor,
SliderInteraction? allowedInteraction,
}) {
return SliderThemeData(
trackHeight: trackHeight ?? this.trackHeight,
......@@ -641,6 +649,7 @@ class SliderThemeData with Diagnosticable {
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
thumbSelector: thumbSelector ?? this.thumbSelector,
mouseCursor: mouseCursor ?? this.mouseCursor,
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
);
}
......@@ -684,6 +693,7 @@ class SliderThemeData with Diagnosticable {
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
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,
thumbSelector,
mouseCursor,
allowedInteraction,
),
);
......@@ -761,7 +772,8 @@ class SliderThemeData with Diagnosticable {
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
&& other.minThumbSeparation == minThumbSeparation
&& other.thumbSelector == thumbSelector
&& other.mouseCursor == mouseCursor;
&& other.mouseCursor == mouseCursor
&& other.allowedInteraction == allowedInteraction;
}
@override
......@@ -798,6 +810,7 @@ class SliderThemeData with Diagnosticable {
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
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,
valueIndicatorTextStyle: TextStyle(color: Colors.black),
mouseCursor: MaterialStateMouseCursor.clickable,
allowedInteraction: SliderInteraction.tapOnly,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -99,6 +100,7 @@ void main() {
'showValueIndicator: always',
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
'mouseCursor: MaterialStateMouseCursor(clickable)',
'allowedInteraction: tapOnly'
]);
});
......@@ -1907,6 +1909,113 @@ void main() {
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 {
debugDisableShadows = false;
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