Add RTL support to Slider and CupertinoSlider (#11822)

Fixes #11358
This commit is contained in:
Adam Barth 2017-08-29 20:34:17 -07:00 committed by GitHub
parent 3437b77007
commit 7b549def56
5 changed files with 489 additions and 163 deletions

View File

@ -161,6 +161,7 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor, activeColor: activeColor,
onChanged: onChanged, onChanged: onChanged,
vsync: vsync, vsync: vsync,
textDirection: Directionality.of(context),
); );
} }
@ -170,7 +171,8 @@ class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget {
..value = value ..value = value
..divisions = divisions ..divisions = divisions
..activeColor = activeColor ..activeColor = activeColor
..onChanged = onChanged; ..onChanged = onChanged
..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.
} }
@ -192,11 +194,14 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
Color activeColor, Color activeColor,
ValueChanged<double> onChanged, ValueChanged<double> onChanged,
TickerProvider vsync, TickerProvider vsync,
@required TextDirection textDirection,
}) : assert(value != null && value >= 0.0 && value <= 1.0), }) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(textDirection != null),
_value = value, _value = value,
_divisions = divisions, _divisions = divisions,
_activeColor = activeColor, _activeColor = activeColor,
_onChanged = onChanged, _onChanged = onChanged,
_textDirection = textDirection,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSliderWidth, height: _kSliderHeight)) { super(additionalConstraints: const BoxConstraints.tightFor(width: _kSliderWidth, height: _kSliderHeight)) {
_drag = new HorizontalDragGestureRecognizer() _drag = new HorizontalDragGestureRecognizer()
..onStart = _handleDragStart ..onStart = _handleDragStart
@ -251,6 +256,16 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
markNeedsSemanticsUpdate(noGeometry: true); markNeedsSemanticsUpdate(noGeometry: true);
} }
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value)
return;
_textDirection = value;
markNeedsPaint();
}
AnimationController _position; AnimationController _position;
HorizontalDragGestureRecognizer _drag; HorizontalDragGestureRecognizer _drag;
@ -265,7 +280,18 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
double get _trackLeft => _kPadding; double get _trackLeft => _kPadding;
double get _trackRight => size.width - _kPadding; double get _trackRight => size.width - _kPadding;
double get _thumbCenter => lerpDouble(_trackLeft + CupertinoThumbPainter.radius, _trackRight - CupertinoThumbPainter.radius, _value); double get _thumbCenter {
double visualPosition;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - _value;
break;
case TextDirection.ltr:
visualPosition = _value;
break;
}
return lerpDouble(_trackLeft + CupertinoThumbPainter.radius, _trackRight - CupertinoThumbPainter.radius, visualPosition);
}
bool get isInteractive => onChanged != null; bool get isInteractive => onChanged != null;
@ -279,7 +305,15 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) { if (isInteractive) {
final double extent = math.max(_kPadding, size.width - 2.0 * (_kPadding + CupertinoThumbPainter.radius)); final double extent = math.max(_kPadding, size.width - 2.0 * (_kPadding + CupertinoThumbPainter.radius));
_currentDragValue += details.primaryDelta / extent; final double valueDelta = details.primaryDelta / extent;
switch (textDirection) {
case TextDirection.rtl:
_currentDragValue -= valueDelta;
break;
case TextDirection.ltr:
_currentDragValue += valueDelta;
break;
}
onChanged(_discretizedCurrentDragValue); onChanged(_discretizedCurrentDragValue);
} }
} }
@ -304,9 +338,21 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; double visualPosition;
Color leftColor;
final double value = _position.value; Color rightColor;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - _position.value;
leftColor = _kTrackColor;
rightColor = _activeColor;
break;
case TextDirection.ltr:
visualPosition = _position.value;
leftColor = _activeColor;
rightColor = _kTrackColor;
break;
}
final double trackCenter = offset.dy + size.height / 2.0; final double trackCenter = offset.dy + size.height / 2.0;
final double trackLeft = offset.dx + _trackLeft; final double trackLeft = offset.dx + _trackLeft;
@ -315,15 +361,16 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc
final double trackRight = offset.dx + _trackRight; final double trackRight = offset.dx + _trackRight;
final double trackActive = offset.dx + _thumbCenter; final double trackActive = offset.dx + _thumbCenter;
final Canvas canvas = context.canvas;
final Paint paint = new Paint(); final Paint paint = new Paint();
if (value > 0.0) { if (visualPosition > 0.0) {
paint.color = _activeColor; paint.color = rightColor;
canvas.drawRRect(new RRect.fromLTRBXY(trackLeft, trackTop, trackActive, trackBottom, 1.0, 1.0), paint); canvas.drawRRect(new RRect.fromLTRBXY(trackLeft, trackTop, trackActive, trackBottom, 1.0, 1.0), paint);
} }
if (value < 1.0) { if (visualPosition < 1.0) {
paint.color = _kTrackColor; paint.color = leftColor;
canvas.drawRRect(new RRect.fromLTRBXY(trackActive, trackTop, trackRight, trackBottom, 1.0, 1.0), paint); canvas.drawRRect(new RRect.fromLTRBXY(trackActive, trackTop, trackRight, trackBottom, 1.0, 1.0), paint);
} }

View File

@ -229,6 +229,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
textTheme: textTheme, textTheme: textTheme,
onChanged: onChanged, onChanged: onChanged,
vsync: vsync, vsync: vsync,
textDirection: Directionality.of(context),
); );
} }
@ -242,7 +243,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
..thumbOpenAtMin = thumbOpenAtMin ..thumbOpenAtMin = thumbOpenAtMin
..textTheme = textTheme ..textTheme = textTheme
..onChanged = onChanged; ..onChanged = onChanged
..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.
} }
@ -290,14 +292,17 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
TextTheme textTheme, TextTheme textTheme,
ValueChanged<double> onChanged, ValueChanged<double> onChanged,
TickerProvider vsync, TickerProvider vsync,
@required TextDirection textDirection,
}) : assert(value != null && value >= 0.0 && value <= 1.0), }) : assert(value != null && value >= 0.0 && value <= 1.0),
assert(textDirection != null),
_value = value, _value = value,
_divisions = divisions, _divisions = divisions,
_activeColor = activeColor, _activeColor = activeColor,
_inactiveColor = inactiveColor, _inactiveColor = inactiveColor,
_thumbOpenAtMin = thumbOpenAtMin, _thumbOpenAtMin = thumbOpenAtMin,
_textTheme = textTheme, _textTheme = textTheme,
_onChanged = onChanged { _onChanged = onChanged,
_textDirection = textDirection {
this.label = label; this.label = label;
final GestureArenaTeam team = new GestureArenaTeam(); final GestureArenaTeam team = new GestureArenaTeam();
_drag = new HorizontalDragGestureRecognizer() _drag = new HorizontalDragGestureRecognizer()
@ -415,6 +420,17 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
} }
} }
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value)
return;
_textDirection = value;
// TODO(abarth): Update _labelPainter's text direction.
markNeedsPaint();
}
double get _trackLength => size.width - 2.0 * _kReactionRadius; double get _trackLength => size.width - 2.0 * _kReactionRadius;
Animation<double> _reaction; Animation<double> _reaction;
@ -430,8 +446,19 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
bool get isInteractive => onChanged != null; bool get isInteractive => onChanged != null;
double _getValueFromVisualPosition(double visualPosition) {
switch (textDirection) {
case TextDirection.rtl:
return 1.0 - visualPosition;
case TextDirection.ltr:
return visualPosition;
}
return null;
}
double _getValueFromGlobalPosition(Offset globalPosition) { double _getValueFromGlobalPosition(Offset globalPosition) {
return (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength; final double visualPosition = (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength;
return _getValueFromVisualPosition(visualPosition);
} }
double _discretize(double value) { double _discretize(double value) {
@ -452,7 +479,15 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) { if (isInteractive) {
_currentDragValue += details.primaryDelta / _trackLength; final double valueDelta = details.primaryDelta / _trackLength;
switch (textDirection) {
case TextDirection.rtl:
_currentDragValue -= valueDelta;
break;
case TextDirection.ltr:
_currentDragValue += valueDelta;
break;
}
onChanged(_discretize(_currentDragValue)); onChanged(_discretize(_currentDragValue));
} }
} }
@ -523,6 +558,26 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
final double trackLength = size.width - 2 * _kReactionRadius; final double trackLength = size.width - 2 * _kReactionRadius;
final bool enabled = isInteractive; final bool enabled = isInteractive;
final double value = _position.value; final double value = _position.value;
final bool thumbAtMin = value == 0.0;
final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor;
final Paint trackPaint = new Paint()..color = _inactiveColor;
double visualPosition;
Paint leftPaint;
Paint rightPaint;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - value;
leftPaint = trackPaint;
rightPaint = primaryPaint;
break;
case TextDirection.ltr:
visualPosition = value;
leftPaint = primaryPaint;
rightPaint = trackPaint;
break;
}
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label); final double additionalHeightForLabel = _getAdditionalHeightForLabel(label);
final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel; final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel;
@ -530,26 +585,23 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
final double trackTop = trackCenter - 1.0; final double trackTop = trackCenter - 1.0;
final double trackBottom = trackCenter + 1.0; final double trackBottom = trackCenter + 1.0;
final double trackRight = trackLeft + trackLength; final double trackRight = trackLeft + trackLength;
final double trackActive = trackLeft + trackLength * value; final double trackActive = trackLeft + trackLength * visualPosition;
final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor;
final Paint trackPaint = new Paint()..color = _inactiveColor;
final Offset thumbCenter = new Offset(trackActive, trackCenter); final Offset thumbCenter = new Offset(trackActive, trackCenter);
final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius; final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius;
if (enabled) { if (enabled) {
if (value > 0.0) if (visualPosition > 0.0)
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), primaryPaint); canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), leftPaint);
if (value < 1.0) { if (visualPosition < 1.0) {
final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null; final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null;
final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0; final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0;
canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), trackPaint); canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), rightPaint);
} }
} else { } else {
if (value > 0.0) if (visualPosition > 0.0)
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint); canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint);
if (value < 1.0) if (visualPosition < 1.0)
canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint); canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint);
} }
@ -589,7 +641,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
_labelPainter.paint(canvas, labelOffset); _labelPainter.paint(canvas, labelOffset);
return; return;
} else { } else {
final Color reactionBaseColor = value == 0.0 ? _kActiveTrackColor : _activeColor; final Color reactionBaseColor = thumbAtMin ? _kActiveTrackColor : _activeColor;
final Paint reactionPaint = new Paint()..color = reactionBaseColor.withAlpha(kRadialReactionAlpha); final Paint reactionPaint = new Paint()..color = reactionBaseColor.withAlpha(kRadialReactionAlpha);
canvas.drawCircle(thumbCenter, _kReactionRadiusTween.evaluate(_reaction), reactionPaint); canvas.drawCircle(thumbCenter, _kReactionRadiusTween.evaluate(_reaction), reactionPaint);
} }
@ -597,7 +649,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler {
Paint thumbPaint = primaryPaint; Paint thumbPaint = primaryPaint;
double thumbRadiusDelta = 0.0; double thumbRadiusDelta = 0.0;
if (value == 0.0 && thumbOpenAtMin) { if (thumbAtMin && thumbOpenAtMin) {
thumbPaint = trackPaint; thumbPaint = trackPaint;
// This is destructive to trackPaint. // This is destructive to trackPaint.
thumbPaint thumbPaint

View File

@ -11,12 +11,13 @@ import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
testWidgets('Slider does not move when tapped', (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;
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new StatefulBuilder( textDirection: TextDirection.ltr,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return new Material( return new Material(
child: new Center( child: new Center(
@ -33,7 +34,7 @@ void main() {
); );
}, },
), ),
); ));
expect(value, equals(0.0)); expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey)); await tester.tap(find.byKey(sliderKey));
@ -44,12 +45,13 @@ void main() {
expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
}); });
testWidgets('Slider moves when dragged', (WidgetTester tester) async { testWidgets('Slider does not move when tapped (RTL)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey(); final Key sliderKey = new UniqueKey();
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new StatefulBuilder( textDirection: TextDirection.rtl,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return new Material( return new Material(
child: new Center( child: new Center(
@ -66,7 +68,42 @@ void main() {
); );
}, },
), ),
); ));
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.0));
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;
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;
});
},
),
),
);
},
),
));
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));
@ -81,15 +118,54 @@ void main() {
expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
}); });
testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.rtl,
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;
});
},
),
),
);
},
),
));
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))));
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 Semantics', (WidgetTester tester) async { testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new CupertinoSlider( textDirection: TextDirection.ltr,
child: new CupertinoSlider(
value: 0.5, value: 0.5,
onChanged: (double v) {}, onChanged: (double v) {},
), ),
); ));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
@ -105,12 +181,13 @@ void main() {
)); ));
// Disable slider // Disable slider
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new CupertinoSlider( textDirection: TextDirection.ltr,
child: new CupertinoSlider(
value: 0.5, value: 0.5,
onChanged: null, onChanged: null,
), ),
); ));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root(), new TestSemantics.root(),

View File

@ -13,27 +13,30 @@ import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
testWidgets('Slider can move when tapped', (WidgetTester tester) async { testWidgets('Slider can 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;
await tester.pumpWidget( await tester.pumpWidget(
new StatefulBuilder( new Directionality(
builder: (BuildContext context, StateSetter setState) { textDirection: TextDirection.ltr,
return new Material( child: new StatefulBuilder(
child: new Center( builder: (BuildContext context, StateSetter setState) {
child: new Slider( return new Material(
key: sliderKey, child: new Center(
value: value, child: new Slider(
onChanged: (double newValue) { key: sliderKey,
setState(() { value: value,
value = newValue; onChanged: (double newValue) {
}); setState(() {
}, value = newValue;
});
},
),
), ),
), );
); },
}, ),
), ),
); );
@ -42,6 +45,58 @@ void main() {
expect(value, equals(0.5)); expect(value, equals(0.5));
await tester.pump(); // No animation should start. await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
await tester.tapAt(target);
expect(value, closeTo(0.25, 0.05));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider can move when tapped (RTL)', (WidgetTester tester) async {
final Key sliderKey = new UniqueKey();
double value = 0.0;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Material(
child: new Center(
child: new Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
await tester.tapAt(target);
expect(value, closeTo(0.75, 0.05));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
}); });
testWidgets('Slider take on discrete values', (WidgetTester tester) async { testWidgets('Slider take on discrete values', (WidgetTester tester) async {
@ -49,28 +104,31 @@ void main() {
double value = 0.0; double value = 0.0;
await tester.pumpWidget( await tester.pumpWidget(
new StatefulBuilder( new Directionality(
builder: (BuildContext context, StateSetter setState) { textDirection: TextDirection.ltr,
return new Material( child: new StatefulBuilder(
child: new Center( builder: (BuildContext context, StateSetter setState) {
child: new SizedBox( return new Material(
width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth child: new Center(
child: new Slider( child: new SizedBox(
key: sliderKey, width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth
min: 0.0, child: new Slider(
max: 100.0, key: sliderKey,
divisions: 10, min: 0.0,
value: value, max: 100.0,
onChanged: (double newValue) { divisions: 10,
setState(() { value: value,
value = newValue; onChanged: (double newValue) {
}); setState(() {
}, value = newValue;
});
},
),
), ),
), ),
), );
); },
}, ),
), ),
); );
@ -95,12 +153,15 @@ void main() {
testWidgets('Slider can be given zero values', testWidgets('Slider can be given zero values',
(WidgetTester tester) async { (WidgetTester tester) async {
final List<double> log = <double>[]; final List<double> log = <double>[];
await tester.pumpWidget(new Material( await tester.pumpWidget(new Directionality(
child: new Slider( textDirection: TextDirection.ltr,
value: 0.0, child: new Material(
min: 0.0, child: new Slider(
max: 1.0, value: 0.0,
onChanged: (double newValue) { log.add(newValue); }, min: 0.0,
max: 1.0,
onChanged: (double newValue) { log.add(newValue); },
),
), ),
)); ));
@ -108,12 +169,15 @@ void main() {
expect(log, <double>[0.5]); expect(log, <double>[0.5]);
log.clear(); log.clear();
await tester.pumpWidget(new Material( await tester.pumpWidget(new Directionality(
child: new Slider( textDirection: TextDirection.ltr,
value: 0.0, child: new Material(
min: 0.0, child: new Slider(
max: 0.0, value: 0.0,
onChanged: (double newValue) { log.add(newValue); }, min: 0.0,
max: 0.0,
onChanged: (double newValue) { log.add(newValue); },
),
), ),
)); ));
@ -127,14 +191,17 @@ void main() {
final Color customColor = const Color(0xFF4CD964); final Color customColor = const Color(0xFF4CD964);
final ThemeData theme = new ThemeData(platform: TargetPlatform.android); final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
Widget buildApp(Color activeColor) { Widget buildApp(Color activeColor) {
return new Material( return new Directionality(
child: new Center( textDirection: TextDirection.ltr,
child: new Theme( child: new Material(
data: theme, child: new Center(
child: new Slider( child: new Theme(
value: 0.5, data: theme,
activeColor: activeColor, child: new Slider(
onChanged: (double newValue) {}, value: 0.5,
activeColor: activeColor,
onChanged: (double newValue) {},
),
), ),
), ),
), ),
@ -162,14 +229,17 @@ void main() {
final Color customColor = const Color(0xFF4CD964); final Color customColor = const Color(0xFF4CD964);
final ThemeData theme = new ThemeData(platform: TargetPlatform.android); final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
Widget buildApp(Color inactiveColor) { Widget buildApp(Color inactiveColor) {
return new Material( return new Directionality(
child: new Center( textDirection: TextDirection.ltr,
child: new Theme( child: new Material(
data: theme, child: new Center(
child: new Slider( child: new Theme(
value: 0.5, data: theme,
inactiveColor: inactiveColor, child: new Slider(
onChanged: (double newValue) {}, value: 0.5,
inactiveColor: inactiveColor,
onChanged: (double newValue) {},
),
), ),
), ),
), ),
@ -188,15 +258,47 @@ void main() {
expect(sliderBox, paints..circle(color: theme.accentColor)); expect(sliderBox, paints..circle(color: theme.accentColor));
}); });
testWidgets('Slider can draw an open thumb at min', testWidgets('Slider can draw an open thumb at min (LTR)',
(WidgetTester tester) async { (WidgetTester tester) async {
Widget buildApp(bool thumbOpenAtMin) { Widget buildApp(bool thumbOpenAtMin) {
return new Material( return new Directionality(
child: new Center( textDirection: TextDirection.ltr,
child: new Slider( child: new Material(
value: 0.0, child: new Center(
thumbOpenAtMin: thumbOpenAtMin, child: new Slider(
onChanged: (double newValue) {}, value: 0.0,
thumbOpenAtMin: thumbOpenAtMin,
onChanged: (double newValue) {},
),
),
),
);
}
await tester.pumpWidget(buildApp(false));
final RenderBox sliderBox =
tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(style: PaintingStyle.fill));
expect(sliderBox, isNot(paints..circle()..circle()));
await tester.pumpWidget(buildApp(true));
expect(sliderBox, paints..circle(style: PaintingStyle.stroke));
expect(sliderBox, isNot(paints..circle()..circle()));
});
testWidgets('Slider can draw an open thumb at min (RTL)',
(WidgetTester tester) async {
Widget buildApp(bool thumbOpenAtMin) {
return new Directionality(
textDirection: TextDirection.rtl,
child: new Material(
child: new Center(
child: new Slider(
value: 0.0,
thumbOpenAtMin: thumbOpenAtMin,
onChanged: (double newValue) {},
),
), ),
), ),
); );
@ -217,19 +319,22 @@ void main() {
testWidgets('Slider can tap in vertical scroller', testWidgets('Slider can tap in vertical scroller',
(WidgetTester tester) async { (WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(new Material( await tester.pumpWidget(new Directionality(
child: new ListView( textDirection: TextDirection.ltr,
children: <Widget>[ child: new Material(
new Slider( child: new ListView(
value: value, children: <Widget>[
onChanged: (double newValue) { new Slider(
value = newValue; value: value,
}, onChanged: (double newValue) {
), value = newValue;
new Container( },
height: 2000.0, ),
), new Container(
], height: 2000.0,
),
],
),
), ),
)); ));
@ -237,15 +342,18 @@ void main() {
expect(value, equals(0.5)); expect(value, equals(0.5));
}); });
testWidgets('Slider drags immediately', (WidgetTester tester) async { testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async {
double value = 0.0; double value = 0.0;
await tester.pumpWidget(new Material( await tester.pumpWidget(new Directionality(
child: new Center( textDirection: TextDirection.ltr,
child: new Slider( child: new Material(
value: value, child: new Center(
onChanged: (double newValue) { child: new Slider(
value = newValue; value: value,
}, onChanged: (double newValue) {
value = newValue;
},
),
), ),
), ),
)); ));
@ -262,20 +370,39 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('Slider sizing', (WidgetTester tester) async { testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(const Material( double value = 0.0;
child: const Center( await tester.pumpWidget(new Directionality(
child: const Slider( textDirection: TextDirection.rtl,
value: 0.5, child: new Material(
onChanged: null, child: new Center(
child: new Slider(
value: value,
onChanged: (double newValue) {
value = newValue;
},
),
), ),
), ),
)); ));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0));
await tester.pumpWidget(const Material( final Offset center = tester.getCenter(find.byType(Slider));
child: const Center( final TestGesture gesture = await tester.startGesture(center);
child: const IntrinsicWidth(
expect(value, equals(0.5));
await gesture.moveBy(const Offset(1.0, 0.0));
expect(value, lessThan(0.5));
await gesture.up();
});
testWidgets('Slider sizing', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: const Material(
child: const Center(
child: const Slider( child: const Slider(
value: 0.5, value: 0.5,
onChanged: null, onChanged: null,
@ -283,16 +410,34 @@ void main() {
), ),
), ),
)); ));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0));
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: const Material(
child: const Center(
child: const IntrinsicWidth(
child: const Slider(
value: 0.5,
onChanged: null,
),
),
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0)); expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0));
await tester.pumpWidget(const Material( await tester.pumpWidget(const Directionality(
child: const Center( textDirection: TextDirection.ltr,
child: const OverflowBox( child: const Material(
maxWidth: double.INFINITY, child: const Center(
maxHeight: double.INFINITY, child: const OverflowBox(
child: const Slider( maxWidth: double.INFINITY,
value: 0.5, maxHeight: double.INFINITY,
onChanged: null, child: const Slider(
value: 0.5,
onChanged: null,
),
), ),
), ),
), ),
@ -303,14 +448,15 @@ void main() {
testWidgets('Slider Semantics', (WidgetTester tester) async { testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new Material( textDirection: TextDirection.ltr,
child: new Material(
child: new Slider( child: new Slider(
value: 0.5, value: 0.5,
onChanged: (double v) {}, onChanged: (double v) {},
), ),
), ),
); ));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root( new TestSemantics.root(
@ -326,14 +472,15 @@ void main() {
)); ));
// Disable slider // Disable slider
await tester.pumpWidget( await tester.pumpWidget(new Directionality(
new Material( textDirection: TextDirection.ltr,
child: new Material(
child: new Slider( child: new Slider(
value: 0.5, value: 0.5,
onChanged: null, onChanged: null,
), ),
), ),
); ));
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics.root(), new TestSemantics.root(),

View File

@ -219,13 +219,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new SemanticsDebugger( new SemanticsDebugger(
child: new Material( child: new Directionality(
child: new Center( textDirection: TextDirection.ltr,
child: new Slider( child: new Material(
value: value, child: new Center(
onChanged: (double newValue) { child: new Slider(
value = newValue; value: value,
}, onChanged: (double newValue) {
value = newValue;
},
),
), ),
), ),
), ),