Added onChangeStart and onChangeEnd to CupertinoSlider (#17535)
This is a follow up on issue #17169 and the pull request #17298
This pull request adds the onChangeStart and onChangeEnd callbacks for CupertinoSlider. These are called when a user starts and ends a change respectively.
Pushing for @dcaraujo0872, the PR author.
diff --git a/packages/flutter/lib/src/cupertino/slider.dart b/packages/flutter/lib/src/cupertino/slider.dart
index 29c743f..bbcb198 100644
--- a/packages/flutter/lib/src/cupertino/slider.dart
+++ b/packages/flutter/lib/src/cupertino/slider.dart
@@ -12,6 +12,9 @@
import 'colors.dart';
import 'thumb_painter.dart';
+// Examples can assume:
+// int _cupertinoSliderValue = 1;
+
/// An iOS-style slider.
///
/// Used to select from a range of values.
@@ -41,10 +44,16 @@
///
/// * [value] determines currently selected value for this slider.
/// * [onChanged] is called when the user selects a new value for the slider.
+ /// * [onChangeStart] is called when the user starts to select a new value for
+ /// the slider.
+ /// * [onChangeEnd] is called when the user is done selecting a new value for
+ /// the slider.
const CupertinoSlider({
Key key,
@required this.value,
@required this.onChanged,
+ this.onChangeStart,
+ this.onChangeEnd,
this.min: 0.0,
this.max: 1.0,
this.divisions,
@@ -75,19 +84,91 @@
///
/// ```dart
/// new CupertinoSlider(
- /// value: _duelCommandment.toDouble(),
+ /// value: _cupertinoSliderValue.toDouble(),
/// min: 1.0,
/// max: 10.0,
/// divisions: 10,
/// onChanged: (double newValue) {
/// setState(() {
- /// _duelCommandment = newValue.round();
+ /// _cupertinoSliderValue = newValue.round();
/// });
/// },
/// )
/// ```
+ ///
+ /// See also:
+ ///
+ /// * [onChangeStart] for a callback that is called when the user starts
+ /// changing the value.
+ /// * [onChangeEnd] for a callback that is called when the user stops
+ /// changing the value.
final ValueChanged<double> onChanged;
+ /// Called when the user starts selecting a new value for the slider.
+ ///
+ /// This callback shouldn't be used to update the slider [value] (use
+ /// [onChanged] for that), but rather to be notified when the user has started
+ /// selecting a new value by starting a drag.
+ ///
+ /// The value passed will be the last [value] that the slider had before the
+ /// change began.
+ ///
+ /// ## Sample code
+ ///
+ /// ```dart
+ /// new CupertinoSlider(
+ /// value: _cupertinoSliderValue.toDouble(),
+ /// min: 1.0,
+ /// max: 10.0,
+ /// divisions: 10,
+ /// onChanged: (double newValue) {
+ /// setState(() {
+ /// _cupertinoSliderValue = newValue.round();
+ /// });
+ /// },
+ /// onChangeStart: (double startValue) {
+ /// print('Started change at $startValue');
+ /// },
+ /// )
+ /// ```
+ ///
+ /// See also:
+ ///
+ /// * [onChangeEnd] for a callback that is called when the value change is
+ /// complete.
+ final ValueChanged<double> onChangeStart;
+
+ /// Called when the user is done selecting a new value for the slider.
+ ///
+ /// This callback shouldn't be used to update the slider [value] (use
+ /// [onChanged] for that), but rather to know when the user has completed
+ /// selecting a new [value] by ending a drag.
+ ///
+ /// ## Sample code
+ ///
+ /// ```dart
+ /// new CupertinoSlider(
+ /// value: _cupertinoSliderValue.toDouble(),
+ /// min: 1.0,
+ /// max: 10.0,
+ /// divisions: 10,
+ /// onChanged: (double newValue) {
+ /// setState(() {
+ /// _cupertinoSliderValue = newValue.round();
+ /// });
+ /// },
+ /// onChangeEnd: (double newValue) {
+ /// print('Ended change on $newValue');
+ /// },
+ /// )
+ /// ```
+ ///
+ /// See also:
+ ///
+ /// * [onChangeStart] for a callback that is called when a value change
+ /// begins.
+ final ValueChanged<double> onChangeEnd;
+
/// The minimum value the user can select.
///
/// Defaults to 0.0.
@@ -121,7 +202,20 @@
class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderStateMixin {
void _handleChanged(double value) {
assert(widget.onChanged != null);
- widget.onChanged(value * (widget.max - widget.min) + widget.min);
+ final double lerpValue = lerpDouble(widget.min, widget.max, value);
+ if (lerpValue != widget.value) {
+ widget.onChanged(lerpValue);
+ }
+ }
+
+ void _handleDragStart(double value) {
+ assert(widget.onChangeStart != null);
+ widget.onChangeStart(lerpDouble(widget.min, widget.max, value));
+ }
+
+ void _handleDragEnd(double value) {
+ assert(widget.onChangeEnd != null);
+ widget.onChangeEnd(lerpDouble(widget.min, widget.max, value));
}
@override
@@ -131,6 +225,8 @@
divisions: widget.divisions,
activeColor: widget.activeColor,
onChanged: widget.onChanged != null ? _handleChanged : null,
+ onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
+ onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
vsync: this,
);
}
@@ -143,6 +239,8 @@
this.divisions,
this.activeColor,
this.onChanged,
+ this.onChangeStart,
+ this.onChangeEnd,
this.vsync,
}) : super(key: key);
@@ -150,6 +248,8 @@
final int divisions;
final Color activeColor;
final ValueChanged<double> onChanged;
+ final ValueChanged<double> onChangeStart;
+ final ValueChanged<double> onChangeEnd;
final TickerProvider vsync;
@override
@@ -159,6 +259,8 @@
divisions: divisions,
activeColor: activeColor,
onChanged: onChanged,
+ onChangeStart: onChangeStart,
+ onChangeEnd: onChangeEnd,
vsync: vsync,
textDirection: Directionality.of(context),
);
@@ -171,6 +273,8 @@
..divisions = divisions
..activeColor = activeColor
..onChanged = onChanged
+ ..onChangeStart = onChangeStart
+ ..onChangeEnd = onChangeEnd
..textDirection = Directionality.of(context);
// Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object.
@@ -191,6 +295,8 @@
int divisions,
Color activeColor,
ValueChanged<double> onChanged,
+ this.onChangeStart,
+ this.onChangeEnd,
TickerProvider vsync,
@required TextDirection textDirection,
}) : assert(value != null && value >= 0.0 && value <= 1.0),
@@ -254,6 +360,9 @@
markNeedsSemanticsUpdate();
}
+ ValueChanged<double> onChangeStart;
+ ValueChanged<double> onChangeEnd;
+
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
@@ -293,12 +402,7 @@
bool get isInteractive => onChanged != null;
- void _handleDragStart(DragStartDetails details) {
- if (isInteractive) {
- _currentDragValue = _value;
- onChanged(_discretizedCurrentDragValue);
- }
- }
+ void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
@@ -316,7 +420,22 @@
}
}
- void _handleDragEnd(DragEndDetails details) {
+ void _handleDragEnd(DragEndDetails details) => _endInteraction();
+
+ void _startInteraction(Offset globalPosition) {
+ if (isInteractive) {
+ if (onChangeStart != null) {
+ onChangeStart(_discretizedCurrentDragValue);
+ }
+ _currentDragValue = _value;
+ onChanged(_discretizedCurrentDragValue);
+ }
+ }
+
+ void _endInteraction() {
+ if (onChangeEnd != null) {
+ onChangeEnd(_discretizedCurrentDragValue);
+ }
_currentDragValue = 0.0;
}
diff --git a/packages/flutter/test/cupertino/slider_test.dart b/packages/flutter/test/cupertino/slider_test.dart
index 595c7e2..d2fa176 100644
--- a/packages/flutter/test/cupertino/slider_test.dart
+++ b/packages/flutter/test/cupertino/slider_test.dart
@@ -11,6 +11,14 @@
import '../widgets/semantics_tester.dart';
void main() {
+
+ Future<Null> _dragSlider(WidgetTester tester, Key sliderKey) {
+ final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
+ const double unit = CupertinoThumbPainter.radius;
+ const double delta = 3.0 * unit;
+ return tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
+ }
+
testWidgets('Slider does not move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
@@ -79,10 +87,10 @@
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
-
- testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async {
+ testWidgets('Slider calls onChangeStart once when interaction begins', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
+ int numberOfTimesOnChangeStartIsCalled = 0;
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
@@ -98,6 +106,91 @@
value = newValue;
});
},
+ onChangeStart: (double value) {
+ numberOfTimesOnChangeStartIsCalled++;
+ }
+ ),
+ ),
+ );
+ },
+ ),
+ ));
+
+ await _dragSlider(tester, sliderKey);
+
+ expect(numberOfTimesOnChangeStartIsCalled, equals(1));
+
+ await tester.pump(); // No animation should start.
+ // Check the transientCallbackCount before tearing down the widget to ensure
+ // that no animation is running.
+ expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
+ });
+
+ testWidgets('Slider calls onChangeEnd once after interaction has ended', (WidgetTester tester) async {
+ final Key sliderKey = new UniqueKey();
+ double value = 0.0;
+ int numberOfTimesOnChangeEndIsCalled = 0;
+
+ await tester.pumpWidget(new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return new Material(
+ child: new Center(
+ child: new CupertinoSlider(
+ key: sliderKey,
+ value: value,
+ onChanged: (double newValue) {
+ setState(() {
+ value = newValue;
+ });
+ },
+ onChangeEnd: (double value) {
+ numberOfTimesOnChangeEndIsCalled++;
+ }
+ ),
+ ),
+ );
+ },
+ ),
+ ));
+
+ await _dragSlider(tester, sliderKey);
+
+ expect(numberOfTimesOnChangeEndIsCalled, equals(1));
+
+ await tester.pump(); // No animation should start.
+ // Check the transientCallbackCount before tearing down the widget to ensure
+ // that no animation is running.
+ expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
+ });
+
+ testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async {
+ final Key sliderKey = new UniqueKey();
+ double value = 0.0;
+ double startValue;
+ double endValue;
+
+ await tester.pumpWidget(new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return new Material(
+ child: new Center(
+ child: new CupertinoSlider(
+ key: sliderKey,
+ value: value,
+ onChanged: (double newValue) {
+ setState(() {
+ value = newValue;
+ });
+ },
+ onChangeStart: (double value) {
+ startValue = value;
+ },
+ onChangeEnd: (double value) {
+ endValue = value;
+ }
),
),
);
@@ -106,12 +199,18 @@
));
expect(value, equals(0.0));
+
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
await tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
+
final Size size = tester.getSize(find.byKey(sliderKey));
- expect(value, equals(delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius))));
+ final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius));
+ expect(startValue, equals(0.0));
+ expect(value, equals(finalValue));
+ expect(endValue, equals(finalValue));
+
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
@@ -121,7 +220,9 @@
testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
-
+ double startValue;
+ double endValue;
+
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.rtl,
child: new StatefulBuilder(
@@ -136,6 +237,16 @@
value = newValue;
});
},
+ onChangeStart: (double value) {
+ setState(() {
+ startValue = value;
+ });
+ },
+ onChangeEnd: (double value) {
+ setState(() {
+ endValue = value;
+ });
+ }
),
),
);
@@ -144,12 +255,18 @@
));
expect(value, equals(0.0));
+
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
await tester.dragFrom(bottomRight - const Offset(unit, unit), const Offset(-delta, 0.0));
+
final Size size = tester.getSize(find.byKey(sliderKey));
- expect(value, equals(delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius))));
+ final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius));
+ expect(startValue, equals(0.0));
+ expect(value, equals(finalValue));
+ expect(endValue, equals(finalValue));
+
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.