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> {
}
void handleDemoAction(LeaveBehindDemoAction action) {
switch (action) {
case LeaveBehindDemoAction.reset:
initListItems();
break;
case LeaveBehindDemoAction.horizontalSwipe:
_dismissDirection = DismissDirection.horizontal;
break;
case LeaveBehindDemoAction.leftSwipe:
_dismissDirection = DismissDirection.endToStart;
break;
case LeaveBehindDemoAction.rightSwipe:
_dismissDirection = DismissDirection.startToEnd;
break;
}
setState(() {
switch (action) {
case LeaveBehindDemoAction.reset:
initListItems();
break;
case LeaveBehindDemoAction.horizontalSwipe:
_dismissDirection = DismissDirection.horizontal;
break;
case LeaveBehindDemoAction.leftSwipe:
_dismissDirection = DismissDirection.endToStart;
break;
case LeaveBehindDemoAction.rightSwipe:
_dismissDirection = DismissDirection.startToEnd;
break;
}
});
}
void handleUndo(LeaveBehindItem item) {
......@@ -161,9 +163,16 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
)
]
),
body: new ListView(
children: leaveBehindItems.map(buildItem).toList()
)
body: leaveBehindItems.isEmpty
? 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';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'ticker_provider.dart';
......@@ -114,12 +115,21 @@ class Dismissible extends StatefulWidget {
/// immediately after the the widget is dismissed.
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
/// 40% towards one direction to be considered dismissed. Clients can define different
/// thresholds for each dismiss direction. This allows for use cases where item can be
/// dismissed to end but not to start.
/// Represented as a fraction, e.g. if it is 0.4 (the default), then the item
/// has to be dragged at least 40% towards one direction to be considered
/// dismissed. Clients can define different thresholds for each dismiss
/// 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;
@override
......@@ -165,6 +175,8 @@ class _DismissibleClipper extends CustomClipper<Rect> {
}
}
enum _FlingGestureKind { none, forward, reverse }
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override
void initState() {
......@@ -200,15 +212,23 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|| widget.direction == DismissDirection.startToEnd;
}
DismissDirection get _dismissDirection {
if (_directionIsXAxis)
return _dragExtent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
return _dragExtent > 0 ? DismissDirection.down : DismissDirection.up;
DismissDirection _extentToDirection(double extent) {
if (extent == 0.0)
return null;
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 {
return widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold;
}
DismissDirection get _dismissDirection => _extentToDirection(_dragExtent);
bool get _isActive {
return _dragUnderway || _moveController.isAnimating;
......@@ -246,16 +266,40 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
break;
case DismissDirection.up:
case DismissDirection.endToStart:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
case DismissDirection.down:
case DismissDirection.startToEnd:
if (_dragExtent + delta > 0)
_dragExtent += delta;
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) {
setState(() {
......@@ -275,35 +319,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
).animate(_moveController);
}
bool _isFlingGesture(Velocity velocity) {
// Cannot fling an item if it cannot be dismissed by drag.
if (_dismissThreshold >= 1.0)
return false;
_FlingGestureKind _describeFlingGesture(Velocity velocity) {
assert(widget.direction != null);
if (_dragExtent == 0.0) {
// 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 vy = velocity.pixelsPerSecond.dy;
DismissDirection flingDirection;
// Verify that the fling is in the generally right direction and fast enough.
if (_directionIsXAxis) {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
return false;
switch (widget.direction) {
case DismissDirection.horizontal:
return vx.abs() > _kMinFlingVelocity;
case DismissDirection.endToStart:
return -vx > _kMinFlingVelocity;
default:
return vx > _kMinFlingVelocity;
}
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity)
return _FlingGestureKind.none;
assert(vx != 0.0);
flingDirection = _extentToDirection(vx);
} else {
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta)
return false;
switch (widget.direction) {
case DismissDirection.vertical:
return vy.abs() > _kMinFlingVelocity;
case DismissDirection.up:
return -vy > _kMinFlingVelocity;
default:
return vy > _kMinFlingVelocity;
}
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity)
return _FlingGestureKind.none;
assert(vy != 0.0);
flingDirection = _extentToDirection(vy);
}
assert(_dismissDirection != null);
if (flingDirection == _dismissDirection)
return _FlingGestureKind.forward;
return _FlingGestureKind.reverse;
}
void _handleDragEnd(DragEndDetails details) {
......@@ -312,14 +356,35 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
_dragUnderway = false;
if (_moveController.isCompleted) {
_startResizeAnimation();
} else if (_isFlingGesture(details.velocity)) {
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
_dragExtent = flingVelocity.sign;
_moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
} else if (_moveController.value > _dismissThreshold) {
_moveController.forward();
} else {
_moveController.reverse();
return;
}
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
switch (_describeFlingGesture(details.velocity)) {
case _FlingGestureKind.forward:
assert(_dragExtent != 0.0);
assert(!_moveController.isDismissed);
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
assert(_resizeController == null);
assert(_sizePriorToCollapse == null);
if (widget.resizeDuration == null) {
if (widget.onDismissed != null)
widget.onDismissed(_dismissDirection);
if (widget.onDismissed != null) {
final DismissDirection direction = _dismissDirection;
assert(direction != null);
widget.onDismissed(direction);
}
} else {
_resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged)
......@@ -357,8 +425,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
void _handleResizeProgressChanged() {
if (_resizeController.isCompleted) {
if (widget.onDismissed != null)
widget.onDismissed(_dismissDirection);
if (widget.onDismissed != null) {
final DismissDirection direction = _dismissDirection;
assert(direction != null);
widget.onDismissed(direction);
}
} else {
if (widget.onResize != null)
widget.onResize();
......@@ -368,6 +439,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
@override
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
assert(!_directionIsXAxis || debugCheckHasDirectionality(context));
Widget background = widget.background;
if (widget.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection;
......
......@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
const double itemExtent = 100.0;
Axis scrollDirection = Axis.vertical;
......@@ -13,9 +14,9 @@ DismissDirection reportedDismissDirection;
List<int> dismissedItems = <int>[];
Widget background;
Widget buildTest({ double startToEndThreshold }) {
Widget buildTest({ double startToEndThreshold, TextDirection textDirection: TextDirection.ltr }) {
return new Directionality(
textDirection: TextDirection.ltr,
textDirection: textDirection,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
Widget buildDismissibleItem(int item) {
......@@ -44,17 +45,14 @@ Widget buildTest({ double startToEndThreshold }) {
);
}
return new Directionality(
textDirection: TextDirection.ltr,
child: new Container(
padding: const EdgeInsets.all(10.0),
child: new ListView(
scrollDirection: scrollDirection,
itemExtent: itemExtent,
children: <int>[0, 1, 2, 3, 4]
.where((int i) => !dismissedItems.contains(i))
.map(buildDismissibleItem).toList(),
),
return new Container(
padding: const EdgeInsets.all(10.0),
child: new ListView(
scrollDirection: scrollDirection,
itemExtent: itemExtent,
children: <int>[0, 1, 2, 3, 4]
.where((int i) => !dismissedItems.contains(i))
.map(buildDismissibleItem).toList(),
),
);
},
......@@ -62,32 +60,30 @@ Widget buildTest({ double startToEndThreshold }) {
);
}
Future<Null> dismissElement(WidgetTester tester, Finder finder, { DismissDirection gestureDirection }) async {
assert(tester.any(finder));
assert(gestureDirection != DismissDirection.horizontal);
assert(gestureDirection != DismissDirection.vertical);
typedef Future<Null> DismissMethod(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection });
Future<Null> dismissElement(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection }) async {
Offset downLocation;
Offset upLocation;
switch (gestureDirection) {
case DismissDirection.endToStart:
case AxisDirection.left:
// getTopRight() returns a point that's just beyond itemWidget's right
// edge and outside the Dismissible event listener's bounds.
downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0);
upLocation = tester.getTopLeft(finder);
break;
case DismissDirection.startToEnd:
case AxisDirection.right:
// we do the same thing here to keep the test symmetric
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
upLocation = tester.getTopRight(finder);
break;
case DismissDirection.up:
case AxisDirection.up:
// getBottomLeft() returns a point that's just below itemWidget's bottom
// edge and outside the Dismissible event listener's bounds.
downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1);
upLocation = tester.getTopLeft(finder);
break;
case DismissDirection.down:
case AxisDirection.down:
// again with doing the same here for symmetry
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
upLocation = tester.getBottomLeft(finder);
......@@ -96,19 +92,49 @@ Future<Null> dismissElement(WidgetTester tester, Finder finder, { DismissDirecti
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.up();
}
Future<Null> dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirection }) async {
assert(gestureDirection != DismissDirection.horizontal);
assert(gestureDirection != DismissDirection.vertical);
Future<Null> flingElement(WidgetTester tester, Finder finder, { @required AxisDirection gestureDirection, double initialOffsetFactor: 0.0 }) async {
Offset delta;
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());
expect(itemFinder, findsOneWidget);
await dismissElement(tester, itemFinder, gestureDirection: gestureDirection);
await mechanism(tester, itemFinder, gestureDirection: gestureDirection);
await tester.pump(); // start the slide
await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking...
......@@ -147,12 +173,56 @@ void main() {
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
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(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.endToStart);
......@@ -165,49 +235,151 @@ void main() {
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.up);
await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.up);
await dismissItem(tester, 1, gestureDirection: DismissDirection.down);
await dismissItem(tester, 1, gestureDirection: AxisDirection.down);
expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1]));
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;
dismissDirection = DismissDirection.endToStart;
await tester.pumpWidget(buildTest());
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, 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(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;
dismissDirection = DismissDirection.startToEnd;
await tester.pumpWidget(buildTest());
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(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(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 {
......@@ -217,11 +389,11 @@ void main() {
await tester.pumpWidget(buildTest());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.down);
await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.up);
await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
......@@ -233,11 +405,43 @@ void main() {
await tester.pumpWidget(buildTest());
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(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.down);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
......@@ -249,11 +453,27 @@ void main() {
await tester.pumpWidget(buildTest(startToEndThreshold: 1.0));
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(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart);
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
});
......@@ -309,12 +529,12 @@ void main() {
);
expect(find.text('1'), 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(const Duration(seconds: 1)); // finish the slide away
expect(find.text('1'), findsOneWidget);
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(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);
......@@ -331,7 +551,7 @@ void main() {
final Finder itemFinder = find.text('0');
expect(itemFinder, findsOneWidget);
await dismissElement(tester, itemFinder, gestureDirection: DismissDirection.startToEnd);
await dismissElement(tester, itemFinder, gestureDirection: AxisDirection.right);
await tester.pump();
expect(find.text('background'), findsOneWidget); // The other four have been culled.
......
......@@ -299,11 +299,28 @@ class WidgetController {
///
/// 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].
///
/// 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, {
int pointer,
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
......@@ -324,7 +341,19 @@ class WidgetController {
///
/// 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].
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(speed > 0.0); // speed is pixels/second
return TestAsyncUtils.guard(() async {
......@@ -335,8 +364,13 @@ class WidgetController {
double timeStamp = 0.0;
double lastTimeStamp = timeStamp;
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) {
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);
timeStamp += timeStampDelta;
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