diff --git a/packages/flutter/lib/src/widgets/dismissable.dart b/packages/flutter/lib/src/widgets/dismissable.dart index 3afd7cbf83..f2643f0cae 100644 --- a/packages/flutter/lib/src/widgets/dismissable.dart +++ b/packages/flutter/lib/src/widgets/dismissable.dart @@ -79,7 +79,8 @@ class Dismissable extends StatefulWidget { this.onResize, this.onDismissed, this.direction: DismissDirection.horizontal, - this.resizeDuration: const Duration(milliseconds: 300) + this.resizeDuration: const Duration(milliseconds: 300), + this.dismissThresholds: const {}, }) : super(key: key) { assert(key != null); assert(secondaryBackground != null ? background != null : true); @@ -113,6 +114,14 @@ class Dismissable 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. + /// + /// 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. + final Map dismissThresholds; + @override _DismissableState createState() => new _DismissableState(); } @@ -195,6 +204,10 @@ class _DismissableState extends State with TickerProviderStateMixin return _dragExtent > 0 ? DismissDirection.down : DismissDirection.up; } + double get _dismissThreshold { + return config.dismissThresholds[_dismissDirection] ?? _kDismissThreshold; + } + bool get _isActive { return _dragUnderway || _moveController.isAnimating; } @@ -262,6 +275,9 @@ class _DismissableState extends State with TickerProviderStateMixin } bool _isFlingGesture(Velocity velocity) { + // Cannot fling an item if it cannot be dismissed by drag. + if (_dismissThreshold >= 1.0) + return false; final double vx = velocity.pixelsPerSecond.dx; final double vy = velocity.pixelsPerSecond.dy; if (_directionIsXAxis) { @@ -299,7 +315,7 @@ class _DismissableState extends State with TickerProviderStateMixin 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 > _kDismissThreshold) { + } else if (_moveController.value > _dismissThreshold) { _moveController.forward(); } else { _moveController.reverse(); diff --git a/packages/flutter/test/widgets/dismissable_test.dart b/packages/flutter/test/widgets/dismissable_test.dart index b3d367c46c..05201c562e 100644 --- a/packages/flutter/test/widgets/dismissable_test.dart +++ b/packages/flutter/test/widgets/dismissable_test.dart @@ -23,31 +23,38 @@ void handleOnDismissed(DismissDirection direction, int item) { dismissedItems.add(item); } -Widget buildDismissableItem(int item) { - return new Dismissable( - key: new ValueKey(item), - direction: dismissDirection, - onDismissed: (DismissDirection direction) { handleOnDismissed(direction, item); }, - onResize: () { handleOnResize(item); }, - background: background, - child: new Container( - width: itemExtent, - height: itemExtent, - child: new Text(item.toString()) - ) - ); -} +Widget buildTest({ double startToEndThreshold }) { + Widget buildDismissableItem(int item) { + return new Dismissable( + key: new ValueKey(item), + direction: dismissDirection, + onDismissed: (DismissDirection direction) { + handleOnDismissed(direction, item); + }, + onResize: () { + handleOnResize(item); + }, + background: background, + dismissThresholds: startToEndThreshold == null + ? {} + : {DismissDirection.startToEnd: startToEndThreshold}, + child: new Container( + width: itemExtent, + height: itemExtent, + child: new Text(item.toString()) + ) + ); + } -Widget widgetBuilder() { return new Container( - padding: const EdgeInsets.all(10.0), - child: new ScrollableList( - scrollDirection: scrollDirection, - itemExtent: itemExtent, - children: [0, 1, 2, 3, 4].where( - (int i) => !dismissedItems.contains(i) - ).map(buildDismissableItem) - ) + padding: const EdgeInsets.all(10.0), + child: new ScrollableList( + scrollDirection: scrollDirection, + itemExtent: itemExtent, + children: [0, 1, 2, 3, 4] + .where((int i) => !dismissedItems.contains(i)) + .map(buildDismissableItem) + ) ); } @@ -58,7 +65,7 @@ Future dismissElement(WidgetTester tester, Finder finder, { DismissDirecti Point downLocation; Point upLocation; - switch(gestureDirection) { + switch (gestureDirection) { case DismissDirection.endToStart: // getTopRight() returns a point that's just beyond itemWidget's right // edge and outside the Dismissable event listener's bounds. @@ -99,11 +106,11 @@ Future dismissItem(WidgetTester tester, int item, { DismissDirection gestu await dismissElement(tester, itemFinder, gestureDirection: gestureDirection); - await tester.pumpWidget(widgetBuilder()); // start the slide - await tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the slide and start shrinking... - await tester.pumpWidget(widgetBuilder()); // first frame of shrinking animation - await tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the shrinking and call the callback... - await tester.pumpWidget(widgetBuilder()); // rebuild after the callback removes the entry + await tester.pumpWidget(buildTest()); // start the slide + await tester.pumpWidget(buildTest(), const Duration(seconds: 1)); // finish the slide and start shrinking... + await tester.pumpWidget(buildTest()); // first frame of shrinking animation + await tester.pumpWidget(buildTest(), const Duration(seconds: 1)); // finish the shrinking and call the callback... + await tester.pumpWidget(buildTest()); // rebuild after the callback removes the entry } class Test1215DismissableWidget extends StatelessWidget { @@ -133,7 +140,7 @@ void main() { scrollDirection = Axis.vertical; dismissDirection = DismissDirection.horizontal; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); @@ -151,7 +158,7 @@ void main() { scrollDirection = Axis.horizontal; dismissDirection = DismissDirection.vertical; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.up); @@ -169,7 +176,7 @@ void main() { scrollDirection = Axis.vertical; dismissDirection = DismissDirection.endToStart; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.startToEnd); @@ -187,7 +194,7 @@ void main() { scrollDirection = Axis.vertical; dismissDirection = DismissDirection.startToEnd; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart); @@ -203,7 +210,7 @@ void main() { scrollDirection = Axis.horizontal; dismissDirection = DismissDirection.up; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.down); @@ -219,7 +226,7 @@ void main() { scrollDirection = Axis.horizontal; dismissDirection = DismissDirection.down; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0, gestureDirection: DismissDirection.up); @@ -231,6 +238,22 @@ void main() { expect(dismissedItems, equals([0])); }); + testWidgets('drag-left has no effect on dismissable 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, gestureDirection: DismissDirection.startToEnd); + expect(find.text('0'), findsOneWidget); + expect(dismissedItems, isEmpty); + + await dismissItem(tester, 0, gestureDirection: DismissDirection.endToStart); + expect(find.text('0'), findsNothing); + expect(dismissedItems, equals([0])); + }); + // This is a regression test for an fn2 bug where dragging a card caused an // assert "'!_disqualifiedFromEverAppearingAgain' is not true". The old URL // was https://github.com/domokit/sky_engine/issues/1068 but that issue is 404 @@ -241,22 +264,22 @@ void main() { scrollDirection = Axis.horizontal; dismissDirection = DismissDirection.down; - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); Point location = tester.getTopLeft(find.text('0')); Offset offset = const Offset(0.0, 5.0); TestGesture gesture = await tester.startGesture(location, pointer: 5); await gesture.moveBy(offset); - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); await gesture.moveBy(offset); - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); await gesture.moveBy(offset); - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); await gesture.moveBy(offset); - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); await gesture.up(); }); - // This one is for a case where dssmissing a widget above a previously + // This one is for a case where dismissing a widget above a previously // dismissed widget threw an exception, which was documented at the // now-obsolete URL https://github.com/flutter/engine/issues/1215 (the URL // died in the migration to the new repo). Don't copy this test; it doesn't @@ -294,7 +317,7 @@ void main() { dismissDirection = DismissDirection.horizontal; background = new Text('background'); - await tester.pumpWidget(widgetBuilder()); + await tester.pumpWidget(buildTest()); expect(dismissedItems, isEmpty); Finder itemFinder = find.text('0');