mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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).
This commit is contained in:
parent
e6119282b7
commit
2db0c25f82
@ -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,51 +235,153 @@ 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: DismissDirection.startToEnd);
|
||||
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: 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 {
|
||||
scrollDirection = Axis.horizontal;
|
||||
dismissDirection = DismissDirection.up;
|
||||
@ -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: DismissDirection.down);
|
||||
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, 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: DismissDirection.endToStart);
|
||||
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, 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) {
|
||||
|
Loading…
Reference in New Issue
Block a user