Commit a710676d authored by Adam Barth's avatar Adam Barth

Tapping drawer during animation causes it to stick

The problem was we were using a tap gesture to stop the motion of the
drawer and a drag gesture to settle it. That can cause a broken
lifecycle. Now we use a single drag recognizer to drive the whole
lifecycle.

Fixes #775
Fixes #1276
parent ea19aca9
......@@ -14,13 +14,17 @@ enum DragState {
accepted
}
typedef void GestureDragDownCallback(Point globalPosition);
typedef void GestureDragStartCallback(Point globalPosition);
typedef void GestureDragUpdateCallback(double delta);
typedef void GestureDragEndCallback(Velocity velocity);
typedef void GestureDragCancelCallback();
typedef void GesturePanDownCallback(Point globalPosition);
typedef void GesturePanStartCallback(Point globalPosition);
typedef void GesturePanUpdateCallback(Offset delta);
typedef void GesturePanEndCallback(Velocity velocity);
typedef void GesturePanCancelCallback();
typedef void _GesturePolymorphicUpdateCallback<T>(T delta);
......@@ -32,9 +36,11 @@ bool _isFlingGesture(Velocity velocity) {
}
abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGestureRecognizer {
GestureDragDownCallback onDown;
GestureDragStartCallback onStart;
_GesturePolymorphicUpdateCallback<T> onUpdate;
GestureDragEndCallback onEnd;
GestureDragCancelCallback onCancel;
DragState _state = DragState.ready;
Point _initialPosition;
......@@ -54,6 +60,8 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest
_state = DragState.possible;
_initialPosition = event.position;
_pendingDragDelta = _initialPendingDragDelta;
if (onDown != null)
onDown(_initialPosition);
}
}
......@@ -90,11 +98,18 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest
}
}
@override
void rejectGesture(int pointer) {
ensureNotTrackingPointer(pointer);
}
@override
void didStopTrackingLastPointer(int pointer) {
if (_state == DragState.possible) {
resolve(GestureDisposition.rejected);
_state = DragState.ready;
if (onCancel != null)
onCancel();
return;
}
bool wasAccepted = (_state == DragState.accepted);
......
......@@ -99,6 +99,11 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
didStopTrackingLastPointer(pointer);
}
void ensureNotTrackingPointer(int pointer) {
if (_trackedPointers.contains(pointer))
stopTrackingPointer(pointer);
}
void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
if (event is PointerUpEvent || event is PointerCancelEvent)
stopTrackingPointer(event.pointer);
......
......@@ -122,11 +122,21 @@ class DrawerControllerState extends State<DrawerController> {
AnimationController _controller;
void _handleTapDown(Point position) {
void _handleDragDown(Point position) {
_controller.stop();
_ensureHistoryEntry();
}
void _handleDragCancel() {
if (_controller.isDismissed || _controller.isAnimating)
return;
if (_controller.value < 0.5) {
close();
} else {
open();
}
}
double get _width {
assert(!Scheduler.debugInFrame); // we should never try to read the tree state while building or laying out
RenderBox drawerBox = _drawerKey.currentContext?.findRenderObject();
......@@ -179,8 +189,10 @@ class DrawerControllerState extends State<DrawerController> {
} else {
return new GestureDetector(
key: _gestureDetectorKey,
onHorizontalDragDown: _handleDragDown,
onHorizontalDragUpdate: _move,
onHorizontalDragEnd: _settle,
onHorizontalDragCancel: _handleDragCancel,
child: new RepaintBoundary(
child: new Stack(
children: <Widget>[
......@@ -195,16 +207,13 @@ class DrawerControllerState extends State<DrawerController> {
),
new Align(
alignment: const FractionalOffset(0.0, 0.5),
child: new GestureDetector(
onTapDown: _handleTapDown,
child: new Align(
alignment: const FractionalOffset(1.0, 0.5),
widthFactor: _controller.value,
child: new RepaintBoundary(
child: new Focus(
key: _drawerKey,
child: config.child
)
child: new Align(
alignment: const FractionalOffset(1.0, 0.5),
widthFactor: _controller.value,
child: new RepaintBoundary(
child: new Focus(
key: _drawerKey,
child: config.child
)
)
)
......
......@@ -14,15 +14,16 @@ export 'package:flutter/gestures.dart' show
GestureTapCallback,
GestureTapCancelCallback,
GestureLongPressCallback,
GestureDragDownCallback,
GestureDragStartCallback,
GestureDragUpdateCallback,
GestureDragEndCallback,
GestureDragStartCallback,
GestureDragUpdateCallback,
GestureDragEndCallback,
GestureDragCancelCallback,
GesturePanDownCallback,
GesturePanStartCallback,
GesturePanUpdateCallback,
GesturePanEndCallback,
GesturePanCancelCallback,
GestureScaleStartCallback,
GestureScaleUpdateCallback,
GestureScaleEndCallback,
......@@ -49,15 +50,21 @@ class GestureDetector extends StatelessWidget {
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onVerticalDragCancel,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
......@@ -116,6 +123,9 @@ class GestureDetector extends StatelessWidget {
final GestureLongPressCallback onLongPress;
/// A pointer has contacted the screen and might begin to move vertically.
final GestureDragDownCallback onVerticalDragDown;
/// A pointer has contacted the screen and has begun to move vertically.
final GestureDragStartCallback onVerticalDragStart;
/// A pointer that is in contact with the screen and moving vertically has
......@@ -127,7 +137,14 @@ class GestureDetector extends StatelessWidget {
/// specific velocity when it stopped contacting the screen.
final GestureDragEndCallback onVerticalDragEnd;
/// The pointer that previously triggered the [onVerticalDragDown] did not
/// end up moving vertically.
final GestureDragCancelCallback onVerticalDragCancel;
/// A pointer has contacted the screen and might begin to move horizontally.
final GestureDragDownCallback onHorizontalDragDown;
/// A pointer has contacted the screen and has begun to move horizontally.
final GestureDragStartCallback onHorizontalDragStart;
/// A pointer that is in contact with the screen and moving horizontally has
......@@ -139,9 +156,15 @@ class GestureDetector extends StatelessWidget {
/// specific velocity when it stopped contacting the screen.
final GestureDragEndCallback onHorizontalDragEnd;
/// The pointer that previously triggered the [onHorizontalDragDown] did not
/// end up moving horizontally.
final GestureDragCancelCallback onHorizontalDragCancel;
final GesturePanDownCallback onPanDown;
final GesturePanStartCallback onPanStart;
final GesturePanUpdateCallback onPanUpdate;
final GesturePanEndCallback onPanEnd;
final GesturePanCancelCallback onPanCancel;
final GestureScaleStartCallback onScaleStart;
final GestureScaleUpdateCallback onScaleUpdate;
......@@ -185,30 +208,48 @@ class GestureDetector extends StatelessWidget {
};
}
if (onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null) {
if (onVerticalDragDown != null ||
onVerticalDragStart != null ||
onVerticalDragUpdate != null ||
onVerticalDragEnd != null ||
onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = (VerticalDragGestureRecognizer recognizer) {
return (recognizer ??= new VerticalDragGestureRecognizer())
..onDown = onVerticalDragDown
..onStart = onVerticalDragStart
..onUpdate = onVerticalDragUpdate
..onEnd = onVerticalDragEnd;
..onEnd = onVerticalDragEnd
..onCancel = onVerticalDragCancel;
};
}
if (onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null) {
if (onHorizontalDragDown != null ||
onHorizontalDragStart != null ||
onHorizontalDragUpdate != null ||
onHorizontalDragEnd != null ||
onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = (HorizontalDragGestureRecognizer recognizer) {
return (recognizer ??= new HorizontalDragGestureRecognizer())
..onDown = onHorizontalDragDown
..onStart = onHorizontalDragStart
..onUpdate = onHorizontalDragUpdate
..onEnd = onHorizontalDragEnd;
..onEnd = onHorizontalDragEnd
..onCancel = onHorizontalDragCancel;
};
}
if (onPanStart != null || onPanUpdate != null || onPanEnd != null) {
if (onPanDown != null ||
onPanStart != null ||
onPanUpdate != null ||
onPanEnd != null ||
onPanCancel != null) {
gestures[PanGestureRecognizer] = (PanGestureRecognizer recognizer) {
return (recognizer ??= new PanGestureRecognizer())
..onDown = onPanDown
..onStart = onPanStart
..onUpdate = onPanUpdate
..onEnd = onPanEnd;
..onEnd = onPanEnd
..onCancel = onPanCancel;
};
}
......
......@@ -470,7 +470,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
config.onScroll(_scrollOffset);
}
void _handlePointerDown(_) {
void _handleDragDown(_) {
_controller.stop();
}
......@@ -527,10 +527,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
key: _gestureDetectorKey,
gestures: buildGestureDetectors(),
behavior: HitTestBehavior.opaque,
child: new Listener(
child: buildContent(context),
onPointerDown: _handlePointerDown
)
child: buildContent(context)
);
}
......@@ -559,6 +556,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) {
return (recognizer ??= new VerticalDragGestureRecognizer())
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
......@@ -568,6 +566,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) {
return (recognizer ??= new HorizontalDragGestureRecognizer())
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
......
......@@ -45,7 +45,6 @@ void main() {
test('Drawer tap test', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
tester.pumpWidget(new Container()); // throw away the old App and its Navigator
tester.pumpWidget(
new MaterialApp(
routes: <String, WidgetBuilder>{
......@@ -81,4 +80,63 @@ void main() {
});
});
test('Drawer drag cancel resume', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
tester.pumpWidget(
new MaterialApp(
routes: <String, WidgetBuilder>{
'/': (BuildContext context) {
return new Scaffold(
key: scaffoldKey,
drawer: new Drawer(
child: new Block(
children: <Widget>[
new Text('drawer'),
new Container(
height: 1000.0,
decoration: new BoxDecoration(
backgroundColor: Colors.blue[500]
)
),
]
)
),
body: new Container()
);
}
}
)
);
expect(tester.findText('drawer'), isNull);
scaffoldKey.currentState.openDrawer();
tester.pump(); // drawer should be starting to animate in
expect(tester.findText('drawer'), isNotNull);
tester.pump(new Duration(seconds: 1)); // animation done
expect(tester.findText('drawer'), isNotNull);
tester.tapAt(const Point(750.0, 100.0)); // on the mask
tester.pump();
tester.pump(new Duration(milliseconds: 10));
// drawer should be starting to animate away
RenderBox textBox = tester.findText('drawer').renderObject;
double textLeft = textBox.localToGlobal(Point.origin).x;
expect(textLeft, lessThan(0.0));
TestGesture gesture = tester.startGesture(new Point(100.0, 100.0));
// drawer should be stopped.
tester.pump();
tester.pump(new Duration(milliseconds: 10));
expect(textBox.localToGlobal(Point.origin).x, equals(textLeft));
gesture.moveBy(new Offset(0.0, -50.0));
// drawer should be returning to visible
tester.pump();
tester.pump(new Duration(seconds: 1));
expect(textBox.localToGlobal(Point.origin).x, equals(0.0));
gesture.up();
});
});
}
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