From 8a4db32bb804c6d02157a1608697e7a3f33b1ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Ara=C3=BCjo?= Date: Fri, 18 May 2018 23:52:46 +0200 Subject: [PATCH] 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. --- .../flutter/lib/src/cupertino/slider.dart | 139 ++++++++++++++++-- .../flutter/test/cupertino/slider_test.dart | 127 +++++++++++++++- 2 files changed, 251 insertions(+), 15 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/slider.dart b/packages/flutter/lib/src/cupertino/slider.dart index 29c743f73a..bbcb198b2f 100644 --- a/packages/flutter/lib/src/cupertino/slider.dart +++ b/packages/flutter/lib/src/cupertino/slider.dart @@ -12,6 +12,9 @@ import 'package:flutter/widgets.dart'; 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 @@ class CupertinoSlider extends StatefulWidget { /// /// * [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 @@ class CupertinoSlider extends StatefulWidget { /// /// ```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 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 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 onChangeEnd; + /// The minimum value the user can select. /// /// Defaults to 0.0. @@ -121,7 +202,20 @@ class CupertinoSlider extends StatefulWidget { class _CupertinoSliderState extends State 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 @@ class _CupertinoSliderState extends State with TickerProviderSt 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 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { this.divisions, this.activeColor, this.onChanged, + this.onChangeStart, + this.onChangeEnd, this.vsync, }) : super(key: key); @@ -150,6 +248,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { final int divisions; final Color activeColor; final ValueChanged onChanged; + final ValueChanged onChangeStart; + final ValueChanged onChangeEnd; final TickerProvider vsync; @override @@ -159,6 +259,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { divisions: divisions, activeColor: activeColor, onChanged: onChanged, + onChangeStart: onChangeStart, + onChangeEnd: onChangeEnd, vsync: vsync, textDirection: Directionality.of(context), ); @@ -171,6 +273,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { ..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 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { int divisions, Color activeColor, ValueChanged onChanged, + this.onChangeStart, + this.onChangeEnd, TickerProvider vsync, @required TextDirection textDirection, }) : assert(value != null && value >= 0.0 && value <= 1.0), @@ -254,6 +360,9 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { markNeedsSemanticsUpdate(); } + ValueChanged onChangeStart; + ValueChanged onChangeEnd; + TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { @@ -293,12 +402,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { 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 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { } } - 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 595c7e29f4..d2fa176139 100644 --- a/packages/flutter/test/cupertino/slider_test.dart +++ b/packages/flutter/test/cupertino/slider_test.dart @@ -11,6 +11,14 @@ import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { + + Future _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 @@ void main() { 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 @@ void main() { 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 @@ void main() { )); 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 @@ void main() { 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 @@ void main() { value = newValue; }); }, + onChangeStart: (double value) { + setState(() { + startValue = value; + }); + }, + onChangeEnd: (double value) { + setState(() { + endValue = value; + }); + } ), ), ); @@ -144,12 +255,18 @@ void main() { )); 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.