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.
This commit is contained in:
parent
ef25052c51
commit
8a4db32bb8
@ -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<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 CupertinoSlider extends StatefulWidget {
|
||||
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 @@ class _CupertinoSliderState extends State<CupertinoSlider> 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<double> onChanged;
|
||||
final ValueChanged<double> onChangeStart;
|
||||
final ValueChanged<double> 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<double> 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<double> onChangeStart;
|
||||
ValueChanged<double> 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;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
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 @@ 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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user