Commit 8a4db32b authored by Christopher Araüjo's avatar Christopher Araüjo Committed by Greg Spencer

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.
parent ef25052c
...@@ -12,6 +12,9 @@ import 'package:flutter/widgets.dart'; ...@@ -12,6 +12,9 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'thumb_painter.dart'; import 'thumb_painter.dart';
// Examples can assume:
// int _cupertinoSliderValue = 1;
/// An iOS-style slider. /// An iOS-style slider.
/// ///
/// Used to select from a range of values. /// Used to select from a range of values.
...@@ -41,10 +44,16 @@ class CupertinoSlider extends StatefulWidget { ...@@ -41,10 +44,16 @@ class CupertinoSlider extends StatefulWidget {
/// ///
/// * [value] determines currently selected value for this slider. /// * [value] determines currently selected value for this slider.
/// * [onChanged] is called when the user selects a new value for the 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({ const CupertinoSlider({
Key key, Key key,
@required this.value, @required this.value,
@required this.onChanged, @required this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.min: 0.0, this.min: 0.0,
this.max: 1.0, this.max: 1.0,
this.divisions, this.divisions,
...@@ -75,19 +84,91 @@ class CupertinoSlider extends StatefulWidget { ...@@ -75,19 +84,91 @@ class CupertinoSlider extends StatefulWidget {
/// ///
/// ```dart /// ```dart
/// new CupertinoSlider( /// new CupertinoSlider(
/// value: _duelCommandment.toDouble(), /// value: _cupertinoSliderValue.toDouble(),
/// min: 1.0, /// min: 1.0,
/// max: 10.0, /// max: 10.0,
/// divisions: 10, /// divisions: 10,
/// onChanged: (double newValue) { /// onChanged: (double newValue) {
/// setState(() { /// 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; 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. /// The minimum value the user can select.
/// ///
/// Defaults to 0.0. /// Defaults to 0.0.
...@@ -121,7 +202,20 @@ class CupertinoSlider extends StatefulWidget { ...@@ -121,7 +202,20 @@ class CupertinoSlider extends StatefulWidget {
class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderStateMixin { class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderStateMixin {
void _handleChanged(double value) { void _handleChanged(double value) {
assert(widget.onChanged != null); 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 @override
...@@ -131,6 +225,8 @@ class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderSt ...@@ -131,6 +225,8 @@ class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderSt
divisions: widget.divisions, divisions: widget.divisions,
activeColor: widget.activeColor, activeColor: widget.activeColor,
onChanged: widget.onChanged != null ? _handleChanged : null, onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
vsync: this, vsync: this,
); );
} }
...@@ -143,6 +239,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -143,6 +239,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
this.divisions, this.divisions,
this.activeColor, this.activeColor,
this.onChanged, this.onChanged,
this.onChangeStart,
this.onChangeEnd,
this.vsync, this.vsync,
}) : super(key: key); }) : super(key: key);
...@@ -150,6 +248,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -150,6 +248,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
final int divisions; final int divisions;
final Color activeColor; final Color activeColor;
final ValueChanged<double> onChanged; final ValueChanged<double> onChanged;
final ValueChanged<double> onChangeStart;
final ValueChanged<double> onChangeEnd;
final TickerProvider vsync; final TickerProvider vsync;
@override @override
...@@ -159,6 +259,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -159,6 +259,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
divisions: divisions, divisions: divisions,
activeColor: activeColor, activeColor: activeColor,
onChanged: onChanged, onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
vsync: vsync, vsync: vsync,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
); );
...@@ -171,6 +273,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -171,6 +273,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
..divisions = divisions ..divisions = divisions
..activeColor = activeColor ..activeColor = activeColor
..onChanged = onChanged ..onChanged = onChanged
..onChangeStart = onChangeStart
..onChangeEnd = onChangeEnd
..textDirection = Directionality.of(context); ..textDirection = Directionality.of(context);
// 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.
...@@ -191,6 +295,8 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { ...@@ -191,6 +295,8 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
int divisions, int divisions,
Color activeColor, Color activeColor,
ValueChanged<double> onChanged, ValueChanged<double> onChanged,
this.onChangeStart,
this.onChangeEnd,
TickerProvider vsync, TickerProvider vsync,
@required TextDirection textDirection, @required TextDirection textDirection,
}) : assert(value != null && value >= 0.0 && value <= 1.0), }) : assert(value != null && value >= 0.0 && value <= 1.0),
...@@ -254,6 +360,9 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { ...@@ -254,6 +360,9 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
ValueChanged<double> onChangeStart;
ValueChanged<double> onChangeEnd;
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
...@@ -293,12 +402,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { ...@@ -293,12 +402,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
bool get isInteractive => onChanged != null; bool get isInteractive => onChanged != null;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);
if (isInteractive) {
_currentDragValue = _value;
onChanged(_discretizedCurrentDragValue);
}
}
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) { if (isInteractive) {
...@@ -316,7 +420,22 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { ...@@ -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; _currentDragValue = 0.0;
} }
......
...@@ -11,6 +11,14 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -11,6 +11,14 @@ import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { 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 { testWidgets('Slider does not move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey(); final Key sliderKey = new UniqueKey();
double value = 0.0; double value = 0.0;
...@@ -79,10 +87,89 @@ void main() { ...@@ -79,10 +87,89 @@ void main() {
expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
}); });
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,
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) {
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 { testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey(); final Key sliderKey = new UniqueKey();
double value = 0.0; double value = 0.0;
double startValue;
double endValue;
await tester.pumpWidget(new Directionality( await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -98,6 +185,12 @@ void main() { ...@@ -98,6 +185,12 @@ void main() {
value = newValue; value = newValue;
}); });
}, },
onChangeStart: (double value) {
startValue = value;
},
onChangeEnd: (double value) {
endValue = value;
}
), ),
), ),
); );
...@@ -106,12 +199,18 @@ void main() { ...@@ -106,12 +199,18 @@ void main() {
)); ));
expect(value, equals(0.0)); expect(value, equals(0.0));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius; const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit; const double delta = 3.0 * unit;
await tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0)); await tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
final Size size = tester.getSize(find.byKey(sliderKey)); 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. await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure // Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running. // that no animation is running.
...@@ -121,6 +220,8 @@ void main() { ...@@ -121,6 +220,8 @@ void main() {
testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async { testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey(); final Key sliderKey = new UniqueKey();
double value = 0.0; double value = 0.0;
double startValue;
double endValue;
await tester.pumpWidget(new Directionality( await tester.pumpWidget(new Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
...@@ -136,6 +237,16 @@ void main() { ...@@ -136,6 +237,16 @@ void main() {
value = newValue; value = newValue;
}); });
}, },
onChangeStart: (double value) {
setState(() {
startValue = value;
});
},
onChangeEnd: (double value) {
setState(() {
endValue = value;
});
}
), ),
), ),
); );
...@@ -144,12 +255,18 @@ void main() { ...@@ -144,12 +255,18 @@ void main() {
)); ));
expect(value, equals(0.0)); expect(value, equals(0.0));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey)); final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius; const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit; const double delta = 3.0 * unit;
await tester.dragFrom(bottomRight - const Offset(unit, unit), const Offset(-delta, 0.0)); await tester.dragFrom(bottomRight - const Offset(unit, unit), const Offset(-delta, 0.0));
final Size size = tester.getSize(find.byKey(sliderKey)); 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. await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure // Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running. // that no animation is running.
......
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