Unverified Commit 2db0c25f authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Dismissible RTL (#13137)

Fix the dismissible demo in the gallery (make it actuall update when you pick something from its menu; give it a better affordance for resetting once you've dismissed everything).

Improve some docs.

Fix various flinging bugs with dismissible. Add tests for those cases.

Add a feature to flutter_test to support a drag-then-fling gesture (used by the flinging tests).
parent e6119282
...@@ -60,20 +60,22 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> { ...@@ -60,20 +60,22 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
} }
void handleDemoAction(LeaveBehindDemoAction action) { void handleDemoAction(LeaveBehindDemoAction action) {
switch (action) { setState(() {
case LeaveBehindDemoAction.reset: switch (action) {
initListItems(); case LeaveBehindDemoAction.reset:
break; initListItems();
case LeaveBehindDemoAction.horizontalSwipe: break;
_dismissDirection = DismissDirection.horizontal; case LeaveBehindDemoAction.horizontalSwipe:
break; _dismissDirection = DismissDirection.horizontal;
case LeaveBehindDemoAction.leftSwipe: break;
_dismissDirection = DismissDirection.endToStart; case LeaveBehindDemoAction.leftSwipe:
break; _dismissDirection = DismissDirection.endToStart;
case LeaveBehindDemoAction.rightSwipe: break;
_dismissDirection = DismissDirection.startToEnd; case LeaveBehindDemoAction.rightSwipe:
break; _dismissDirection = DismissDirection.startToEnd;
} break;
}
});
} }
void handleUndo(LeaveBehindItem item) { void handleUndo(LeaveBehindItem item) {
...@@ -161,9 +163,16 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> { ...@@ -161,9 +163,16 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
) )
] ]
), ),
body: new ListView( body: leaveBehindItems.isEmpty
children: leaveBehindItems.map(buildItem).toList() ? new Center(
) child: new RaisedButton(
onPressed: () => handleDemoAction(LeaveBehindDemoAction.reset),
child: const Text('Reset the list'),
),
)
: new ListView(
children: leaveBehindItems.map(buildItem).toList()
),
); );
} }
} }
...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'automatic_keep_alive.dart'; import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -114,12 +115,21 @@ class Dismissible extends StatefulWidget { ...@@ -114,12 +115,21 @@ class Dismissible extends StatefulWidget {
/// immediately after the the widget is dismissed. /// immediately after the the widget is dismissed.
final Duration resizeDuration; final Duration resizeDuration;
/// The offset threshold the item has to be dragged in order to be considered dismissed. /// The offset threshold the item has to be dragged in order to be considered
/// dismissed.
/// ///
/// Represented as a fraction, e.g. if it is 0.4, then the item has to be dragged at least /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item
/// 40% towards one direction to be considered dismissed. Clients can define different /// has to be dragged at least 40% towards one direction to be considered
/// thresholds for each dismiss direction. This allows for use cases where item can be /// dismissed. Clients can define different thresholds for each dismiss
/// dismissed to end but not to start. /// direction.
///
/// Flinging is treated as being equivalent to dragging almost to 1.0, so
/// flinging can dismiss an item past any threshold less than 1.0.
///
/// See also [direction], which controls the directions in which the items can
/// be dismissed. Setting a threshold of 1.0 (or greater) prevents a drag in
/// the given [DismissDirection] even if it would be allowed by the
/// [direction] property.
final Map<DismissDirection, double> dismissThresholds; final Map<DismissDirection, double> dismissThresholds;
@override @override
...@@ -165,6 +175,8 @@ class _DismissibleClipper extends CustomClipper<Rect> { ...@@ -165,6 +175,8 @@ class _DismissibleClipper extends CustomClipper<Rect> {
} }
} }
enum _FlingGestureKind { none, forward, reverse }
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override @override
void initState() { void initState() {
...@@ -200,15 +212,23 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -200,15 +212,23 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|| widget.direction == DismissDirection.startToEnd; || widget.direction == DismissDirection.startToEnd;
} }
DismissDirection get _dismissDirection { DismissDirection _extentToDirection(double extent) {
if (_directionIsXAxis) if (extent == 0.0)
return _dragExtent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart; return null;
return _dragExtent > 0 ? DismissDirection.down : DismissDirection.up; if (_directionIsXAxis) {
switch (Directionality.of(context)) {
case TextDirection.rtl:
return extent < 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
case TextDirection.ltr:
return extent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
}
assert(false);
return null;
}
return extent > 0 ? DismissDirection.down : DismissDirection.up;
} }
double get _dismissThreshold { DismissDirection get _dismissDirection => _extentToDirection(_dragExtent);
return widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold;
}
bool get _isActive { bool get _isActive {
return _dragUnderway || _moveController.isAnimating; return _dragUnderway || _moveController.isAnimating;
...@@ -246,16 +266,40 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -246,16 +266,40 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
break; break;
case DismissDirection.up: case DismissDirection.up:
case DismissDirection.endToStart:
if (_dragExtent + delta < 0) if (_dragExtent + delta < 0)
_dragExtent += delta; _dragExtent += delta;
break; break;
case DismissDirection.down: case DismissDirection.down:
case DismissDirection.startToEnd:
if (_dragExtent + delta > 0) if (_dragExtent + delta > 0)
_dragExtent += delta; _dragExtent += delta;
break; break;
case DismissDirection.endToStart:
switch (Directionality.of(context)) {
case TextDirection.rtl:
if (_dragExtent + delta > 0)
_dragExtent += delta;
break;
case TextDirection.ltr:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
}
break;
case DismissDirection.startToEnd:
switch (Directionality.of(context)) {
case TextDirection.rtl:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
case TextDirection.ltr:
if (_dragExtent + delta > 0)
_dragExtent += delta;
break;
}
break;
} }
if (oldDragExtent.sign != _dragExtent.sign) { if (oldDragExtent.sign != _dragExtent.sign) {
setState(() { setState(() {
...@@ -275,35 +319,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -275,35 +319,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
).animate(_moveController); ).animate(_moveController);
} }
bool _isFlingGesture(Velocity velocity) { _FlingGestureKind _describeFlingGesture(Velocity velocity) {
// Cannot fling an item if it cannot be dismissed by drag. assert(widget.direction != null);
if (_dismissThreshold >= 1.0) if (_dragExtent == 0.0) {
return false; // If it was a fling, then it was a fling that was let loose at the exact
// middle of the range (i.e. when there's no displacement). In that case,
// we assume that the user meant to fling it back to the center, as
// opposed to having wanted to drag it out one way, then fling it past the
// center and into and out the other side.
return _FlingGestureKind.none;
}
final double vx = velocity.pixelsPerSecond.dx; final double vx = velocity.pixelsPerSecond.dx;
final double vy = velocity.pixelsPerSecond.dy; final double vy = velocity.pixelsPerSecond.dy;
DismissDirection flingDirection;
// Verify that the fling is in the generally right direction and fast enough.
if (_directionIsXAxis) { if (_directionIsXAxis) {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta) if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity)
return false; return _FlingGestureKind.none;
switch (widget.direction) { assert(vx != 0.0);
case DismissDirection.horizontal: flingDirection = _extentToDirection(vx);
return vx.abs() > _kMinFlingVelocity;
case DismissDirection.endToStart:
return -vx > _kMinFlingVelocity;
default:
return vx > _kMinFlingVelocity;
}
} else { } else {
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta) if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity)
return false; return _FlingGestureKind.none;
switch (widget.direction) { assert(vy != 0.0);
case DismissDirection.vertical: flingDirection = _extentToDirection(vy);
return vy.abs() > _kMinFlingVelocity;
case DismissDirection.up:
return -vy > _kMinFlingVelocity;
default:
return vy > _kMinFlingVelocity;
}
} }
assert(_dismissDirection != null);
if (flingDirection == _dismissDirection)
return _FlingGestureKind.forward;
return _FlingGestureKind.reverse;
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
...@@ -312,14 +356,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -312,14 +356,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
_dragUnderway = false; _dragUnderway = false;
if (_moveController.isCompleted) { if (_moveController.isCompleted) {
_startResizeAnimation(); _startResizeAnimation();
} else if (_isFlingGesture(details.velocity)) { return;
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy; }
_dragExtent = flingVelocity.sign; final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
_moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); switch (_describeFlingGesture(details.velocity)) {
} else if (_moveController.value > _dismissThreshold) { case _FlingGestureKind.forward:
_moveController.forward(); assert(_dragExtent != 0.0);
} else { assert(!_moveController.isDismissed);
_moveController.reverse(); if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
_moveController.reverse();
break;
}
_dragExtent = flingVelocity.sign;
_moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
break;
case _FlingGestureKind.reverse:
assert(_dragExtent != 0.0);
assert(!_moveController.isDismissed);
_dragExtent = flingVelocity.sign;
_moveController.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale);
break;
case _FlingGestureKind.none:
if (!_moveController.isDismissed) { // we already know it's not completed, we check that above
if (_moveController.value > (widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold)) {
_moveController.forward();
} else {
_moveController.reverse();
}
}
break;
} }
} }
...@@ -335,8 +400,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -335,8 +400,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
assert(_resizeController == null); assert(_resizeController == null);
assert(_sizePriorToCollapse == null); assert(_sizePriorToCollapse == null);
if (widget.resizeDuration == null) { if (widget.resizeDuration == null) {
if (widget.onDismissed != null) if (widget.onDismissed != null) {
widget.onDismissed(_dismissDirection); final DismissDirection direction = _dismissDirection;
assert(direction != null);
widget.onDismissed(direction);
}
} else { } else {
_resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this) _resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged) ..addListener(_handleResizeProgressChanged)
...@@ -357,8 +425,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -357,8 +425,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
void _handleResizeProgressChanged() { void _handleResizeProgressChanged() {
if (_resizeController.isCompleted) { if (_resizeController.isCompleted) {
if (widget.onDismissed != null) if (widget.onDismissed != null) {
widget.onDismissed(_dismissDirection); final DismissDirection direction = _dismissDirection;
assert(direction != null);
widget.onDismissed(direction);
}
} else { } else {
if (widget.onResize != null) if (widget.onResize != null)
widget.onResize(); widget.onResize();
...@@ -368,6 +439,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -368,6 +439,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
assert(!_directionIsXAxis || debugCheckHasDirectionality(context));
Widget background = widget.background; Widget background = widget.background;
if (widget.secondaryBackground != null) { if (widget.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection; final DismissDirection direction = _dismissDirection;
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
const double itemExtent = 100.0; const double itemExtent = 100.0;
Axis scrollDirection = Axis.vertical; Axis scrollDirection = Axis.vertical;
...@@ -13,9 +14,9 @@ DismissDirection reportedDismissDirection; ...@@ -13,9 +14,9 @@ DismissDirection reportedDismissDirection;
List<int> dismissedItems = <int>[]; List<int> dismissedItems = <int>[];
Widget background; Widget background;
Widget buildTest({ double startToEndThreshold }) { Widget buildTest({ double startToEndThreshold, TextDirection textDirection: TextDirection.ltr }) {
return new Directionality( return new Directionality(
textDirection: TextDirection.ltr, textDirection: textDirection,
child: new StatefulBuilder( child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
Widget buildDismissibleItem(int item) { Widget buildDismissibleItem(int item) {
...@@ -44,17 +45,14 @@ Widget buildTest({ double startToEndThreshold }) { ...@@ -44,17 +45,14 @@ Widget buildTest({ double startToEndThreshold }) {
); );
} }
return new Directionality( return new Container(
textDirection: TextDirection.ltr, padding: const EdgeInsets.all(10.0),
child: new Container( child: new ListView(
padding: const EdgeInsets.all(10.0), scrollDirection: scrollDirection,
child: new ListView( itemExtent: itemExtent,
scrollDirection: scrollDirection, children: <int>[0, 1, 2, 3, 4]
itemExtent: itemExtent, .where((int i) => !dismissedItems.contains(i))
children: <int>[0, 1, 2, 3, 4] .map(buildDismissibleItem).toList(),
.where((int i) => !dismissedItems.contains(i))
.map(buildDismissibleItem).toList(),
),
), ),
); );
}, },
...@@ -62,32 +60,30 @@ Widget buildTest({ double startToEndThreshold }) { ...@@ -62,32 +60,30 @@ Widget buildTest({ double startToEndThreshold }) {
); );
} }
Future<Null> dismissElement(WidgetTester tester, Finder finder, { DismissDirection gestureDirection }) async { typedef Future<Null> DismissMethod(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection });
assert(tester.any(finder));
assert(gestureDirection != DismissDirection.horizontal);
assert(gestureDirection != DismissDirection.vertical);
Future<Null> dismissElement(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection }) async {
Offset downLocation; Offset downLocation;
Offset upLocation; Offset upLocation;
switch (gestureDirection) { switch (gestureDirection) {
case DismissDirection.endToStart: case AxisDirection.left:
// getTopRight() returns a point that's just beyond itemWidget's right // getTopRight() returns a point that's just beyond itemWidget's right
// edge and outside the Dismissible event listener's bounds. // edge and outside the Dismissible event listener's bounds.
downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0); downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0);
upLocation = tester.getTopLeft(finder); upLocation = tester.getTopLeft(finder);
break; break;
case DismissDirection.startToEnd: case AxisDirection.right:
// we do the same thing here to keep the test symmetric // we do the same thing here to keep the test symmetric
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0); downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
upLocation = tester.getTopRight(finder); upLocation = tester.getTopRight(finder);
break; break;
case DismissDirection.up: case AxisDirection.up:
// getBottomLeft() returns a point that's just below itemWidget's bottom // getBottomLeft() returns a point that's just below itemWidget's bottom
// edge and outside the Dismissible event listener's bounds. // edge and outside the Dismissible event listener's bounds.
downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1); downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1);
upLocation = tester.getTopLeft(finder); upLocation = tester.getTopLeft(finder);
break; break;
case DismissDirection.down: case AxisDirection.down:
// again with doing the same here for symmetry // again with doing the same here for symmetry
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0); downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
upLocation = tester.getBottomLeft(finder); upLocation = tester.getBottomLeft(finder);
...@@ -96,19 +92,49 @@ Future<Null> dismissElement(WidgetTester tester, Finder finder, { DismissDirecti ...@@ -96,19 +92,49 @@ Future<Null> dismissElement(WidgetTester tester, Finder finder, { DismissDirecti
fail('unsupported gestureDirection'); fail('unsupported gestureDirection');
} }
final TestGesture gesture = await tester.startGesture(downLocation, pointer: 5); final TestGesture gesture = await tester.startGesture(downLocation);
await gesture.moveTo(upLocation); await gesture.moveTo(upLocation);
await gesture.up(); await gesture.up();
} }
Future<Null> dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirection }) async { Future<Null> flingElement(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection, double initialOffsetFactor: 0.0 }) async {
assert(gestureDirection != DismissDirection.horizontal); Offset delta;
assert(gestureDirection != DismissDirection.vertical); switch (gestureDirection) {
case AxisDirection.left:
delta = const Offset(-300.0, 0.0);
break;
case AxisDirection.right:
delta = const Offset(300.0, 0.0);
break;
case AxisDirection.up:
delta = const Offset(0.0, -300.0);
break;
case AxisDirection.down:
delta = const Offset(0.0, 300.0);
break;
default:
fail('unsupported gestureDirection');
}
await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor);
}
Future<Null> flingElementFromZero(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection }) async {
// This is a special case where we drag in one direction, then fling back so
// that at the point of release, we're at exactly the point at which we
// started, but with velocity. This is needed to check a boundary coundition
// in the flinging behavior.
await flingElement(tester, finder, gestureDirection: gestureDirection, initialOffsetFactor: -1.0);
}
Future<Null> dismissItem(WidgetTester tester, int item, {
@required AxisDirection gestureDirection,
DismissMethod mechanism: dismissElement,
}) async {
assert(gestureDirection != null);
final Finder itemFinder = find.text(item.toString()); final Finder itemFinder = find.text(item.toString());
expect(itemFinder, findsOneWidget); expect(itemFinder, findsOneWidget);
await dismissElement(tester, itemFinder, gestureDirection: gestureDirection); await mechanism(tester, itemFinder, gestureDirection: gestureDirection);
await tester.pump(); // start the slide await tester.pump(); // start the slide
await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking... await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking...
...@@ -147,12 +173,56 @@ void main() { ...@@ -147,12 +173,56 @@ void main() {
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.startToEnd); expect(reportedDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: DismissDirection.endToStart); await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.endToStart);
});
testWidgets('Horizontal fling triggers dismiss scrollDirection=vertical', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.horizontal;
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.endToStart);
});
testWidgets('Horizontal fling does not trigger at zero offset, but does otherwise', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.horizontal;
await tester.pumpWidget(buildTest(startToEndThreshold: 0.95));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElementFromZero);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, equals(<int>[]));
await dismissItem(tester, 0, gestureDirection: AxisDirection.left, mechanism: flingElementFromZero);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, equals(<int>[]));
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
expect(find.text('1'), findsNothing); expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1])); expect(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.endToStart); expect(reportedDismissDirection, DismissDirection.endToStart);
...@@ -165,49 +235,151 @@ void main() { ...@@ -165,49 +235,151 @@ void main() {
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.up); await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.up); expect(reportedDismissDirection, DismissDirection.up);
await dismissItem(tester, 1, gestureDirection: DismissDirection.down); await dismissItem(tester, 1, gestureDirection: AxisDirection.down);
expect(find.text('1'), findsNothing); expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1])); expect(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.down); expect(reportedDismissDirection, DismissDirection.down);
}); });
testWidgets('drag-left with DismissDirection.left triggers dismiss', (WidgetTester tester) async { testWidgets('drag-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async {
scrollDirection = Axis.vertical; scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.endToStart; dismissDirection = DismissDirection.endToStart;
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 1, gestureDirection: DismissDirection.startToEnd); await dismissItem(tester, 1, gestureDirection: AxisDirection.right);
await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart); await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
await dismissItem(tester, 1, gestureDirection: DismissDirection.endToStart); await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
}); });
testWidgets('drag-right with DismissDirection.right triggers dismiss', (WidgetTester tester) async { testWidgets('drag-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async {
scrollDirection = Axis.vertical; scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.startToEnd; dismissDirection = DismissDirection.startToEnd;
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart); await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('drag-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.endToStart;
await tester.pumpWidget(buildTest(textDirection: TextDirection.rtl));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('drag-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.startToEnd;
await tester.pumpWidget(buildTest(textDirection: TextDirection.rtl));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 1, gestureDirection: AxisDirection.right);
await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
});
testWidgets('fling-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.endToStart;
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 1, gestureDirection: AxisDirection.right);
await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
});
testWidgets('fling-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.startToEnd;
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('fling-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.endToStart;
await tester.pumpWidget(buildTest(textDirection: TextDirection.rtl));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('fling-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.startToEnd;
await tester.pumpWidget(buildTest(textDirection: TextDirection.rtl));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.left);
}); });
testWidgets('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async { testWidgets('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async {
...@@ -217,11 +389,11 @@ void main() { ...@@ -217,11 +389,11 @@ void main() {
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.down); await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.up); await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
}); });
...@@ -233,11 +405,43 @@ void main() { ...@@ -233,11 +405,43 @@ void main() {
await tester.pumpWidget(buildTest()); await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.up); await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('fling-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async {
scrollDirection = Axis.horizontal;
dismissDirection = DismissDirection.up;
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('fling-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async {
scrollDirection = Axis.horizontal;
dismissDirection = DismissDirection.down;
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.down); await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
}); });
...@@ -249,11 +453,27 @@ void main() { ...@@ -249,11 +453,27 @@ void main() {
await tester.pumpWidget(buildTest(startToEndThreshold: 1.0)); await tester.pumpWidget(buildTest(startToEndThreshold: 1.0));
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
testWidgets('fling-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.horizontal;
await tester.pumpWidget(buildTest(startToEndThreshold: 1.0));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty); expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart); await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0])); expect(dismissedItems, equals(<int>[0]));
}); });
...@@ -309,12 +529,12 @@ void main() { ...@@ -309,12 +529,12 @@ void main() {
); );
expect(find.text('1'), findsOneWidget); expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget); expect(find.text('2'), findsOneWidget);
await dismissElement(tester, find.text('2'), gestureDirection: DismissDirection.startToEnd); await dismissElement(tester, find.text('2'), gestureDirection: AxisDirection.right);
await tester.pump(); // start the slide away await tester.pump(); // start the slide away
await tester.pump(const Duration(seconds: 1)); // finish the slide away await tester.pump(const Duration(seconds: 1)); // finish the slide away
expect(find.text('1'), findsOneWidget); expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsNothing); expect(find.text('2'), findsNothing);
await dismissElement(tester, find.text('1'), gestureDirection: DismissDirection.startToEnd); await dismissElement(tester, find.text('1'), gestureDirection: AxisDirection.right);
await tester.pump(); // start the slide away await tester.pump(); // start the slide away
await tester.pump(const Duration(seconds: 1)); // finish the slide away (at which point the child is no longer included in the tree) await tester.pump(const Duration(seconds: 1)); // finish the slide away (at which point the child is no longer included in the tree)
expect(find.text('1'), findsNothing); expect(find.text('1'), findsNothing);
...@@ -331,7 +551,7 @@ void main() { ...@@ -331,7 +551,7 @@ void main() {
final Finder itemFinder = find.text('0'); final Finder itemFinder = find.text('0');
expect(itemFinder, findsOneWidget); expect(itemFinder, findsOneWidget);
await dismissElement(tester, itemFinder, gestureDirection: DismissDirection.startToEnd); await dismissElement(tester, itemFinder, gestureDirection: AxisDirection.right);
await tester.pump(); await tester.pump();
expect(find.text('background'), findsOneWidget); // The other four have been culled. expect(find.text('background'), findsOneWidget); // The other four have been culled.
......
...@@ -299,11 +299,28 @@ class WidgetController { ...@@ -299,11 +299,28 @@ class WidgetController {
/// ///
/// A fling is essentially a drag that ends at a particular speed. If you /// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [drag]. /// just want to drag and end without a fling, use [drag].
///
/// The `initialOffset` argument, if non-zero, causes the pointer to first
/// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
/// used to simulate a drag followed by a fling, including dragging in the
/// opposite direction of the fling (e.g. dragging 200 pixels to the right,
/// then fling to the left over 200 pixels, ending at the exact point that the
/// drag started).
Future<Null> fling(Finder finder, Offset offset, double speed, { Future<Null> fling(Finder finder, Offset offset, double speed, {
int pointer, int pointer,
Duration frameInterval: const Duration(milliseconds: 16), Duration frameInterval: const Duration(milliseconds: 16),
Offset initialOffset: Offset.zero,
Duration initialOffsetDelay: const Duration(seconds: 1),
}) { }) {
return flingFrom(getCenter(finder), offset, speed, pointer: pointer, frameInterval: frameInterval); return flingFrom(
getCenter(finder),
offset,
speed,
pointer: pointer,
frameInterval: frameInterval,
initialOffset: initialOffset,
initialOffsetDelay: initialOffsetDelay,
);
} }
/// Attempts a fling gesture starting from the given location, moving the /// Attempts a fling gesture starting from the given location, moving the
...@@ -324,7 +341,19 @@ class WidgetController { ...@@ -324,7 +341,19 @@ class WidgetController {
/// ///
/// A fling is essentially a drag that ends at a particular speed. If you /// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [dragFrom]. /// just want to drag and end without a fling, use [dragFrom].
Future<Null> flingFrom(Offset startLocation, Offset offset, double speed, { int pointer, Duration frameInterval: const Duration(milliseconds: 16) }) { ///
/// The `initialOffset` argument, if non-zero, causes the pointer to first
/// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
/// used to simulate a drag followed by a fling, including dragging in the
/// opposite direction of the fling (e.g. dragging 200 pixels to the right,
/// then fling to the left over 200 pixels, ending at the exact point that the
/// drag started).
Future<Null> flingFrom(Offset startLocation, Offset offset, double speed, {
int pointer,
Duration frameInterval: const Duration(milliseconds: 16),
Offset initialOffset: Offset.zero,
Duration initialOffsetDelay: const Duration(seconds: 1),
}) {
assert(offset.distance > 0.0); assert(offset.distance > 0.0);
assert(speed > 0.0); // speed is pixels/second assert(speed > 0.0); // speed is pixels/second
return TestAsyncUtils.guard(() async { return TestAsyncUtils.guard(() async {
...@@ -335,8 +364,13 @@ class WidgetController { ...@@ -335,8 +364,13 @@ class WidgetController {
double timeStamp = 0.0; double timeStamp = 0.0;
double lastTimeStamp = timeStamp; double lastTimeStamp = timeStamp;
await sendEventToBinding(testPointer.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result); await sendEventToBinding(testPointer.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
if (initialOffset.distance > 0.0) {
await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += initialOffsetDelay.inMilliseconds;
await pump(initialOffsetDelay);
}
for (int i = 0; i <= kMoveCount; i += 1) { for (int i = 0; i <= kMoveCount; i += 1) {
final Offset location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount); final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount);
await sendEventToBinding(testPointer.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result); await sendEventToBinding(testPointer.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta; timeStamp += timeStampDelta;
if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) { if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
......
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