Commit 255ed0b9 authored by Hixie's avatar Hixie

Make Draggable use gestures

Draggable is now itself a gesture arena member. This means it won't
conflict with other gesture recognisers in the same path.

This also allows variants of Draggable that are triggered by other
gestures.

Also, some cleanup of DoubleTapGestureRecognizer, GestureDetector, and
PrimaryPointerGestureRecognizer.

Also, make MultiTapGestureRecognizer support a timeout for longpress.

Also, make Draggable data be typed.

Also, hide warnings about constructor warnings for now. Analyzer doesn't
support them yet. (Have to do this on a per-line basis)

Directions for future research:
 - animating the avatar (enter/exit transitions)
 - interaction with the navigator (canceling a drag on page navigation, etc)
 - double-tap draggable
parent 5516d12f
...@@ -6,29 +6,23 @@ import 'package:flutter/material.dart'; ...@@ -6,29 +6,23 @@ import 'package:flutter/material.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
class DragData {
DragData(this.text);
final String text;
}
class ExampleDragTarget extends StatefulComponent { class ExampleDragTarget extends StatefulComponent {
ExampleDragTargetState createState() => new ExampleDragTargetState(); ExampleDragTargetState createState() => new ExampleDragTargetState();
} }
class ExampleDragTargetState extends State<ExampleDragTarget> { class ExampleDragTargetState extends State<ExampleDragTarget> {
String _text = 'Drag Target'; Color _color = Colors.grey[500];
void _handleAccept(DragData data) { void _handleAccept(Color data) {
setState(() { setState(() {
_text = 'dropped: ${data.text}'; _color = data;
}); });
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new DragTarget<DragData>( return new DragTarget<Color>(
onAccept: _handleAccept, onAccept: _handleAccept,
builder: (BuildContext context, List<DragData> data, _) { builder: (BuildContext context, List<Color> data, _) {
return new Container( return new Container(
height: 100.0, height: 100.0,
margin: new EdgeDims.all(10.0), margin: new EdgeDims.all(10.0),
...@@ -37,10 +31,7 @@ class ExampleDragTargetState extends State<ExampleDragTarget> { ...@@ -37,10 +31,7 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
width: 3.0, width: 3.0,
color: data.isEmpty ? Colors.white : Colors.blue[500] color: data.isEmpty ? Colors.white : Colors.blue[500]
), ),
backgroundColor: data.isEmpty ? Colors.grey[500] : Colors.green[500] backgroundColor: data.isEmpty ? _color : Colors.grey[200]
),
child: new Center(
child: new Text(_text)
) )
); );
} }
...@@ -49,42 +40,84 @@ class ExampleDragTargetState extends State<ExampleDragTarget> { ...@@ -49,42 +40,84 @@ class ExampleDragTargetState extends State<ExampleDragTarget> {
} }
class Dot extends StatelessComponent { class Dot extends StatelessComponent {
Dot({ Key key, this.color, this.size }) : super(key: key); Dot({ Key key, this.color, this.size, this.child }) : super(key: key);
final Color color; final Color color;
final double size; final double size;
final Widget child;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Container(
width: size, width: size,
height: size, height: size,
decoration: new BoxDecoration( decoration: new BoxDecoration(
borderRadius: 10.0, backgroundColor: color,
backgroundColor: color shape: Shape.circle
) ),
child: child
); );
} }
} }
class ExampleDragSource extends StatelessComponent { class ExampleDragSource extends StatelessComponent {
ExampleDragSource({ Key key, this.name, this.color }) : super(key: key); ExampleDragSource({
final String name; Key key,
this.color,
this.heavy: false,
this.under: true,
this.child
}) : super(key: key);
final Color color; final Color color;
final bool heavy;
final bool under;
final Widget child;
static const kDotSize = 50.0; static const double kDotSize = 50.0;
static const kFingerSize = 50.0; static const double kHeavyMultiplier = 1.5;
static const double kFingerSize = 50.0;
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Draggable( double size = kDotSize;
data: new DragData(name), DraggableConstructor<Color> constructor = new Draggable<Color>#;
child: new Dot(color: color, size: kDotSize), if (heavy) {
feedback: new Transform( size *= kHeavyMultiplier;
transform: new Matrix4.identity()..translate(-kDotSize / 2.0, -(kDotSize / 2.0 + kFingerSize)), constructor = new LongPressDraggable<Color>#;
child: new Opacity( }
opacity: 0.75,
child: new Dot(color: color, size: kDotSize) Widget contents = new DefaultTextStyle(
style: Theme.of(context).text.body1.copyWith(textAlign: TextAlign.center),
child: new Dot(
color: color,
size: size,
child: new Center(child: child)
) )
), );
feedbackOffset: const Offset(0.0, -kFingerSize),
dragAnchor: DragAnchor.pointer Widget feedback = new Opacity(
opacity: 0.75,
child: contents
);
Offset feedbackOffset;
DragAnchor anchor;
if (!under) {
feedback = new Transform(
transform: new Matrix4.identity()
..translate(-size / 2.0, -(size / 2.0 + kFingerSize)),
child: feedback
);
feedbackOffset = const Offset(0.0, -kFingerSize);
anchor = DragAnchor.pointer;
} else {
feedbackOffset = Offset.zero;
anchor = DragAnchor.child;
}
return constructor(
data: color,
child: contents,
feedback: feedback,
feedbackOffset: feedbackOffset,
dragAnchor: anchor
); );
} }
} }
...@@ -95,13 +128,26 @@ class DragAndDropApp extends StatelessComponent { ...@@ -95,13 +128,26 @@ class DragAndDropApp extends StatelessComponent {
toolBar: new ToolBar( toolBar: new ToolBar(
center: new Text('Drag and Drop Flutter Demo') center: new Text('Drag and Drop Flutter Demo')
), ),
body: new DefaultTextStyle( body: new Column(<Widget>[
style: Theme.of(context).text.body1.copyWith(textAlign: TextAlign.center),
child: new Column(<Widget>[
new Flexible(child: new Row(<Widget>[ new Flexible(child: new Row(<Widget>[
new ExampleDragSource(name: 'Orange', color: const Color(0xFFFF9000)), new ExampleDragSource(
new ExampleDragSource(name: 'Teal', color: const Color(0xFF00FFFF)), color: const Color(0xFFFFF000),
new ExampleDragSource(name: 'Yellow', color: const Color(0xFFFFF000)), under: true,
heavy: false,
child: new Text('under')
),
new ExampleDragSource(
color: const Color(0xFF0FFF00),
under: false,
heavy: true,
child: new Text('long-press above')
),
new ExampleDragSource(
color: const Color(0xFF00FFF0),
under: false,
heavy: false,
child: new Text('above')
),
], ],
alignItems: FlexAlignItems.center, alignItems: FlexAlignItems.center,
justifyContent: FlexJustifyContent.spaceAround justifyContent: FlexJustifyContent.spaceAround
...@@ -113,7 +159,6 @@ class DragAndDropApp extends StatelessComponent { ...@@ -113,7 +159,6 @@ class DragAndDropApp extends StatelessComponent {
new Flexible(child: new ExampleDragTarget()), new Flexible(child: new ExampleDragTarget()),
])), ])),
]) ])
)
); );
} }
} }
......
...@@ -48,6 +48,12 @@ class _GestureArenaState { ...@@ -48,6 +48,12 @@ class _GestureArenaState {
bool isHeld = false; bool isHeld = false;
bool hasPendingSweep = false; bool hasPendingSweep = false;
/// If a gesture attempts to win while the arena is still open, it becomes the
/// "eager winnner". We look for an eager winner when closing the arena to new
/// participants, and if there is one, we resolve the arena it its favour at
/// that time.
GestureArenaMember eagerWinner;
void add(GestureArenaMember member) { void add(GestureArenaMember member) {
assert(isOpen); assert(isOpen);
members.add(member); members.add(member);
...@@ -122,6 +128,8 @@ class GestureArena { ...@@ -122,6 +128,8 @@ class GestureArena {
state.members.first.acceptGesture(key); state.members.first.acceptGesture(key);
} else if (state.members.isEmpty) { } else if (state.members.isEmpty) {
_arenas.remove(key); _arenas.remove(key);
} else if (state.eagerWinner != null) {
_resolveInFavorOf(key, state, state.eagerWinner);
} }
} }
...@@ -129,14 +137,28 @@ class GestureArena { ...@@ -129,14 +137,28 @@ class GestureArena {
_GestureArenaState state = _arenas[key]; _GestureArenaState state = _arenas[key];
if (state == null) if (state == null)
return; // This arena has already resolved. return; // This arena has already resolved.
assert(!state.isOpen);
assert(state.members.contains(member)); assert(state.members.contains(member));
if (disposition == GestureDisposition.rejected) { if (disposition == GestureDisposition.rejected) {
state.members.remove(member); state.members.remove(member);
member.rejectGesture(key); member.rejectGesture(key);
if (!state.isOpen)
_tryToResolveArena(key, state); _tryToResolveArena(key, state);
} else { } else {
assert(disposition == GestureDisposition.accepted); assert(disposition == GestureDisposition.accepted);
if (state.isOpen) {
if (state.eagerWinner == null)
state.eagerWinner = member;
} else {
_resolveInFavorOf(key, state, member);
}
}
}
void _resolveInFavorOf(Object key, _GestureArenaState state, GestureArenaMember member) {
assert(state == _arenas[key]);
assert(state != null);
assert(state.eagerWinner == null || state.eagerWinner == member);
assert(!state.isOpen);
_arenas.remove(key); _arenas.remove(key);
for (GestureArenaMember rejectedMember in state.members) { for (GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member) if (rejectedMember != member)
...@@ -144,5 +166,4 @@ class GestureArena { ...@@ -144,5 +166,4 @@ class GestureArena {
} }
member.acceptGesture(key); member.acceptGesture(key);
} }
}
} }
\ No newline at end of file
...@@ -10,7 +10,13 @@ import 'constants.dart'; ...@@ -10,7 +10,13 @@ import 'constants.dart';
import 'events.dart'; import 'events.dart';
import 'pointer_router.dart'; import 'pointer_router.dart';
import 'recognizer.dart'; import 'recognizer.dart';
import 'tap.dart' show GestureTapDownCallback, GestureTapDownCallback, GestureTapCallback, GestureTapCancelCallback;
typedef void GestureDoubleTapCallback();
typedef void GestureMultiTapDownCallback(Point globalPosition, int pointer);
typedef void GestureMultiTapUpCallback(Point globalPosition, int pointer);
typedef void GestureMultiTapCallback(int pointer);
typedef void GestureMultiTapCancelCallback(int pointer);
/// TapTracker helps track individual tap sequences as part of a /// TapTracker helps track individual tap sequences as part of a
/// larger gesture. /// larger gesture.
...@@ -52,7 +58,12 @@ class _TapTracker { ...@@ -52,7 +58,12 @@ class _TapTracker {
class DoubleTapGestureRecognizer extends GestureRecognizer { class DoubleTapGestureRecognizer extends GestureRecognizer {
DoubleTapGestureRecognizer({ this.router, this.onDoubleTap }); DoubleTapGestureRecognizer({
PointerRouter router,
this.onDoubleTap
}) : _router = router {
assert(router != null);
}
// Implementation notes: // Implementation notes:
// The double tap recognizer can be in one of four states. There's no // The double tap recognizer can be in one of four states. There's no
...@@ -74,8 +85,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -74,8 +85,8 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
// - The long timer between taps expires // - The long timer between taps expires
// - The gesture arena decides we have been rejected wholesale // - The gesture arena decides we have been rejected wholesale
PointerRouter router; PointerRouter _router;
GestureTapCallback onDoubleTap; GestureDoubleTapCallback onDoubleTap;
Timer _doubleTapTimer; Timer _doubleTapTimer;
_TapTracker _firstTap; _TapTracker _firstTap;
...@@ -92,22 +103,26 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -92,22 +103,26 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
entry: GestureArena.instance.add(event.pointer, this) entry: GestureArena.instance.add(event.pointer, this)
); );
_trackers[event.pointer] = tracker; _trackers[event.pointer] = tracker;
tracker.startTrackingPointer(router, handleEvent); tracker.startTrackingPointer(_router, handleEvent);
} }
void handleEvent(PointerInputEvent event) { void handleEvent(PointerInputEvent event) {
_TapTracker tracker = _trackers[event.pointer]; _TapTracker tracker = _trackers[event.pointer];
assert(tracker != null); assert(tracker != null);
if (event.type == 'pointerup') { switch (event.type) {
case 'pointerup':
if (_firstTap == null) if (_firstTap == null)
_registerFirstTap(tracker); _registerFirstTap(tracker);
else else
_registerSecondTap(tracker); _registerSecondTap(tracker);
} else if (event.type == 'pointermove' && break;
!tracker.isWithinTolerance(event, kDoubleTapTouchSlop)) { case 'pointermove':
if (!tracker.isWithinTolerance(event, kDoubleTapTouchSlop))
_reject(tracker); _reject(tracker);
} else if (event.type == 'pointercancel') { break;
case 'pointercancel':
_reject(tracker); _reject(tracker);
break;
} }
} }
...@@ -139,7 +154,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -139,7 +154,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
void dispose() { void dispose() {
_reset(); _reset();
router = null; _router = null;
} }
void _reset() { void _reset() {
...@@ -184,7 +199,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -184,7 +199,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
} }
void _freezeTracker(_TapTracker tracker) { void _freezeTracker(_TapTracker tracker) {
tracker.stopTrackingPointer(router, handleEvent); tracker.stopTrackingPointer(_router, handleEvent);
} }
void _startDoubleTapTimer() { void _startDoubleTapTimer() {
...@@ -213,21 +228,35 @@ class _TapGesture extends _TapTracker { ...@@ -213,21 +228,35 @@ class _TapGesture extends _TapTracker {
_TapGesture({ _TapGesture({
MultiTapGestureRecognizer gestureRecognizer, MultiTapGestureRecognizer gestureRecognizer,
PointerInputEvent event PointerInputEvent event,
Duration longTapDelay
}) : gestureRecognizer = gestureRecognizer, }) : gestureRecognizer = gestureRecognizer,
_lastPosition = event.position,
super(event: event, entry: GestureArena.instance.add(event.pointer, gestureRecognizer)) { super(event: event, entry: GestureArena.instance.add(event.pointer, gestureRecognizer)) {
startTrackingPointer(gestureRecognizer.router, handleEvent); startTrackingPointer(gestureRecognizer.router, handleEvent);
if (longTapDelay > Duration.ZERO) {
_timer = new Timer(longTapDelay, () {
_timer = null;
gestureRecognizer._handleLongTap(event.pointer, _lastPosition);
});
}
} }
final MultiTapGestureRecognizer gestureRecognizer; final MultiTapGestureRecognizer gestureRecognizer;
bool _wonArena = false; bool _wonArena = false;
Timer _timer;
Point _lastPosition;
Point _finalPosition; Point _finalPosition;
void handleEvent(PointerInputEvent event) { void handleEvent(PointerInputEvent event) {
assert(event.pointer == pointer); assert(event.pointer == pointer);
if (event.type == 'pointermove' && !isWithinTolerance(event, kTouchSlop)) { if (event.type == 'pointermove') {
if (!isWithinTolerance(event, kTouchSlop))
cancel(); cancel();
else
_lastPosition = event.position;
} else if (event.type == 'pointercancel') { } else if (event.type == 'pointercancel') {
cancel(); cancel();
} else if (event.type == 'pointerup') { } else if (event.type == 'pointerup') {
...@@ -237,6 +266,12 @@ class _TapGesture extends _TapTracker { ...@@ -237,6 +266,12 @@ class _TapGesture extends _TapTracker {
} }
} }
void stopTrackingPointer(PointerRouter router, PointerRoute route) {
_timer?.cancel();
_timer = null;
super.stopTrackingPointer(router, route);
}
void accept() { void accept() {
_wonArena = true; _wonArena = true;
_check(); _check();
...@@ -269,52 +304,68 @@ class _TapGesture extends _TapTracker { ...@@ -269,52 +304,68 @@ class _TapGesture extends _TapTracker {
/// taps, on up-1 and up-2. /// taps, on up-1 and up-2.
class MultiTapGestureRecognizer extends GestureRecognizer { class MultiTapGestureRecognizer extends GestureRecognizer {
MultiTapGestureRecognizer({ MultiTapGestureRecognizer({
this.router, PointerRouter router,
this.onTapDown, this.onTapDown,
this.onTapUp, this.onTapUp,
this.onTap, this.onTap,
this.onTapCancel this.onTapCancel,
}); this.longTapDelay: Duration.ZERO,
this.onLongTapDown
}) : _router = router {
assert(router != null);
}
PointerRouter router; PointerRouter get router => _router;
GestureTapDownCallback onTapDown; PointerRouter _router;
GestureTapDownCallback onTapUp; GestureMultiTapDownCallback onTapDown;
GestureTapCallback onTap; GestureMultiTapUpCallback onTapUp;
GestureTapCancelCallback onTapCancel; GestureMultiTapCallback onTap;
GestureMultiTapCancelCallback onTapCancel;
Duration longTapDelay;
GestureMultiTapDownCallback onLongTapDown;
Map<int, _TapGesture> _gestureMap = new Map<int, _TapGesture>(); final Map<int, _TapGesture> _gestureMap = new Map<int, _TapGesture>();
void addPointer(PointerInputEvent event) { void addPointer(PointerInputEvent event) {
assert(!_gestureMap.containsKey(event.pointer)); assert(!_gestureMap.containsKey(event.pointer));
_gestureMap[event.pointer] = new _TapGesture( _gestureMap[event.pointer] = new _TapGesture(
gestureRecognizer: this, gestureRecognizer: this,
event: event event: event,
longTapDelay: longTapDelay
); );
if (onTapDown != null) if (onTapDown != null)
onTapDown(event.position); onTapDown(event.position, event.pointer);
} }
void acceptGesture(int pointer) { void acceptGesture(int pointer) {
assert(_gestureMap.containsKey(pointer)); assert(_gestureMap.containsKey(pointer));
_gestureMap[pointer]?.accept(); _gestureMap[pointer]?.accept();
assert(!_gestureMap.containsKey(pointer));
} }
void rejectGesture(int pointer) { void rejectGesture(int pointer) {
assert(_gestureMap.containsKey(pointer)); assert(_gestureMap.containsKey(pointer));
_gestureMap[pointer]?.reject(); _gestureMap[pointer]?.reject();
assert(!_gestureMap.containsKey(pointer));
} }
void _resolveTap(int pointer, _TapResolution resolution, Point globalPosition) { void _resolveTap(int pointer, _TapResolution resolution, Point globalPosition) {
_gestureMap.remove(pointer); _gestureMap.remove(pointer);
if (resolution == _TapResolution.tap) { if (resolution == _TapResolution.tap) {
if (onTapUp != null) if (onTapUp != null)
onTapUp(globalPosition); onTapUp(globalPosition, pointer);
if (onTap != null) if (onTap != null)
onTap(); onTap(pointer);
} else { } else {
if (onTapCancel != null) if (onTapCancel != null)
onTapCancel(); onTapCancel(pointer);
}
} }
void _handleLongTap(int pointer, Point lastPosition) {
assert(_gestureMap.containsKey(pointer));
if (onLongTapDown != null)
onLongTapDown(lastPosition, pointer);
} }
void dispose() { void dispose() {
...@@ -323,7 +374,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer { ...@@ -323,7 +374,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
gesture.cancel(); gesture.cancel();
// Rejection of each gesture should cause it to be removed from our map // Rejection of each gesture should cause it to be removed from our map
assert(_gestureMap.isEmpty); assert(_gestureMap.isEmpty);
router = null; _router = null;
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' show Point, Offset;
import 'arena.dart'; import 'arena.dart';
import 'constants.dart'; import 'constants.dart';
...@@ -84,10 +84,6 @@ enum GestureRecognizerState { ...@@ -84,10 +84,6 @@ enum GestureRecognizerState {
defunct defunct
} }
ui.Point _getPoint(PointerInputEvent event) {
return new ui.Point(event.x, event.y);
}
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer { abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
PrimaryPointerGestureRecognizer({ PointerRouter router, this.deadline }) PrimaryPointerGestureRecognizer({ PointerRouter router, this.deadline })
: super(router: router); : super(router: router);
...@@ -96,7 +92,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -96,7 +92,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
GestureRecognizerState state = GestureRecognizerState.ready; GestureRecognizerState state = GestureRecognizerState.ready;
int primaryPointer; int primaryPointer;
ui.Point initialPosition; Point initialPosition;
Timer _timer; Timer _timer;
void addPointer(PointerInputEvent event) { void addPointer(PointerInputEvent event) {
...@@ -104,7 +100,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -104,7 +100,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
if (state == GestureRecognizerState.ready) { if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible; state = GestureRecognizerState.possible;
primaryPointer = event.pointer; primaryPointer = event.pointer;
initialPosition = _getPoint(event); initialPosition = event.position;
if (deadline != null) if (deadline != null)
_timer = new Timer(deadline, didExceedDeadline); _timer = new Timer(deadline, didExceedDeadline);
} }
...@@ -159,7 +155,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -159,7 +155,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
} }
double _getDistance(PointerInputEvent event) { double _getDistance(PointerInputEvent event) {
ui.Offset offset = _getPoint(event) - initialPosition; Offset offset = event.position - initialPosition;
return offset.distance; return offset.distance;
} }
......
...@@ -587,6 +587,7 @@ class Container extends StatelessComponent { ...@@ -587,6 +587,7 @@ class Container extends StatelessComponent {
}) : super(key: key) { }) : super(key: key) {
assert(margin == null || margin.isNonNegative); assert(margin == null || margin.isNonNegative);
assert(padding == null || padding.isNonNegative); assert(padding == null || padding.isNonNegative);
assert(decoration == null || decoration.shape != Shape.circle || decoration.borderRadius == null); // can't have a border radius if you're a circle
} }
final Widget child; final Widget child;
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
...@@ -15,6 +17,16 @@ import 'overlay.dart'; ...@@ -15,6 +17,16 @@ import 'overlay.dart';
typedef bool DragTargetWillAccept<T>(T data); typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data); typedef void DragTargetAccept<T>(T data);
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData); typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
typedef void DragStartCallback(Point position, int pointer);
typedef DraggableBase<T> DraggableConstructor<T>({
Key key,
T data,
Widget child,
Widget feedback,
Offset feedbackOffset,
DragAnchor dragAnchor
});
enum DragAnchor { enum DragAnchor {
/// Display the feedback anchored at the position of the original child. If /// Display the feedback anchored at the position of the original child. If
...@@ -35,8 +47,8 @@ enum DragAnchor { ...@@ -35,8 +47,8 @@ enum DragAnchor {
pointer, pointer,
} }
class Draggable extends StatefulComponent { abstract class DraggableBase<T> extends StatefulComponent {
Draggable({ DraggableBase({
Key key, Key key,
this.data, this.data,
this.child, this.child,
...@@ -48,7 +60,7 @@ class Draggable extends StatefulComponent { ...@@ -48,7 +60,7 @@ class Draggable extends StatefulComponent {
assert(feedback != null); assert(feedback != null);
} }
final dynamic data; final T data;
final Widget child; final Widget child;
final Widget feedback; final Widget feedback;
...@@ -58,69 +70,122 @@ class Draggable extends StatefulComponent { ...@@ -58,69 +70,122 @@ class Draggable extends StatefulComponent {
final Offset feedbackOffset; final Offset feedbackOffset;
final DragAnchor dragAnchor; final DragAnchor dragAnchor;
_DraggableState createState() => new _DraggableState(); /// Should return a GestureRecognizer instance that is configured to call the starter
/// argument when the drag is to begin. The arena for the pointer must not yet have
/// resolved at the time that the callback is invoked, because the draggable itself
/// is going to attempt to win the pointer's arena in that case.
GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter);
_DraggableState<T> createState() => new _DraggableState<T>();
} }
class _DraggableState extends State<Draggable> { class Draggable<T> extends DraggableBase<T> {
_DragAvatar _avatar; Draggable({
Key key,
T data,
Widget child,
Widget feedback,
Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child
}) : super(
key: key,
data: data,
child: child,
feedback: feedback,
feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor
);
void _startDrag(PointerInputEvent event) { GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter) {
if (_avatar != null) return new MultiTapGestureRecognizer(
return; // TODO(ianh): once we switch to using gestures, just hand the gesture to the avatar so it can do everything itself. then we can have multiple drags at the same time. router: router,
final Point point = new Point(event.x, event.y); onTapDown: starter
);
}
}
class LongPressDraggable<T> extends DraggableBase<T> {
LongPressDraggable({
Key key,
T data,
Widget child,
Widget feedback,
Offset feedbackOffset: Offset.zero,
DragAnchor dragAnchor: DragAnchor.child
}) : super(
key: key,
data: data,
child: child,
feedback: feedback,
feedbackOffset: feedbackOffset,
dragAnchor: dragAnchor
);
GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter) {
return new MultiTapGestureRecognizer(
router: router,
longTapDelay: kLongPressTimeout,
onLongTapDown: (Point position, int pointer) {
userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY);
starter(position, pointer);
}
);
}
}
class _DraggableState<T> extends State<DraggableBase<T>> implements GestureArenaMember {
PointerRouter get router => FlutterBinding.instance.pointerRouter;
void initState() {
super.initState();
_recognizer = config.createRecognizer(router, _startDrag);
}
GestureRecognizer _recognizer;
Map<int, GestureArenaEntry> _activePointers = <int, GestureArenaEntry>{};
void _routePointer(PointerInputEvent event) {
_activePointers[event.pointer] = GestureArena.instance.add(event.pointer, this);
_recognizer.addPointer(event);
}
void acceptGesture(int pointer) {
_activePointers.remove(pointer);
}
void rejectGesture(int pointer) {
_activePointers.remove(pointer);
}
void _startDrag(Point position, int pointer) {
assert(_activePointers.containsKey(pointer));
_activePointers[pointer].resolve(GestureDisposition.accepted);
Point dragStartPoint; Point dragStartPoint;
switch (config.dragAnchor) { switch (config.dragAnchor) {
case DragAnchor.child: case DragAnchor.child:
final RenderBox renderObject = context.findRenderObject(); final RenderBox renderObject = context.findRenderObject();
dragStartPoint = renderObject.globalToLocal(point); dragStartPoint = renderObject.globalToLocal(position);
break; break;
case DragAnchor.pointer: case DragAnchor.pointer:
dragStartPoint = Point.origin; dragStartPoint = Point.origin;
break; break;
} }
assert(dragStartPoint != null); new _DragAvatar<T>(
_avatar = new _DragAvatar( pointer: pointer,
router: router,
overlay: Navigator.of(context).overlay,
data: config.data, data: config.data,
initialPosition: position,
dragStartPoint: dragStartPoint, dragStartPoint: dragStartPoint,
feedback: config.feedback, feedback: config.feedback,
feedbackOffset: config.feedbackOffset, feedbackOffset: config.feedbackOffset
onDragFinished: () {
_avatar = null;
}
); );
_avatar.update(point);
_avatar.markNeedsBuild(context);
}
void _updateDrag(PointerInputEvent event) {
if (_avatar != null) {
_avatar.update(new Point(event.x, event.y));
_avatar.markNeedsBuild(context);
}
}
void _cancelDrag(PointerInputEvent event) {
if (_avatar != null) {
_avatar.finish(_DragEndKind.canceled);
assert(_avatar == null);
}
}
void _drop(PointerInputEvent event) {
if (_avatar != null) {
_avatar.update(new Point(event.x, event.y));
_avatar.finish(_DragEndKind.dropped);
assert(_avatar == null);
}
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(abarth): We should be using a GestureDetector
return new Listener( return new Listener(
onPointerDown: _startDrag, onPointerDown: _routePointer,
onPointerMove: _updateDrag,
onPointerCancel: _cancelDrag,
onPointerUp: _drop,
child: config.child child: config.child
); );
} }
...@@ -181,7 +246,8 @@ class DragTargetState<T> extends State<DragTarget<T>> { ...@@ -181,7 +246,8 @@ class DragTargetState<T> extends State<DragTarget<T>> {
metaData: this, metaData: this,
child: config.builder(context, child: config.builder(context,
new UnmodifiableListView<T>(_candidateData), new UnmodifiableListView<T>(_candidateData),
new UnmodifiableListView<dynamic>(_rejectedData)) new UnmodifiableListView<dynamic>(_rejectedData)
)
); );
} }
} }
...@@ -189,30 +255,63 @@ class DragTargetState<T> extends State<DragTarget<T>> { ...@@ -189,30 +255,63 @@ class DragTargetState<T> extends State<DragTarget<T>> {
enum _DragEndKind { dropped, canceled } enum _DragEndKind { dropped, canceled }
class _DragAvatar { // The lifetime of this object is a little dubious right now. Specifically, it
// lives as long as the pointer is down. Arguably it should self-immolate if the
// overlay goes away, or maybe even if the Draggable that created goes away.
// This will probably need to be changed once we have more experience with using
// this widget.
class _DragAvatar<T> {
_DragAvatar({ _DragAvatar({
this.pointer,
this.router,
OverlayState overlay,
this.data, this.data,
Point initialPosition,
this.dragStartPoint: Point.origin, this.dragStartPoint: Point.origin,
this.feedback, this.feedback,
this.feedbackOffset: Offset.zero, this.feedbackOffset: Offset.zero
this.onDragFinished
}) { }) {
assert(pointer != null);
assert(router != null);
assert(overlay != null);
assert(dragStartPoint != null);
assert(feedbackOffset != null); assert(feedbackOffset != null);
router.addRoute(pointer, handleEvent);
_entry = new OverlayEntry(builder: _build);
overlay.insert(_entry);
update(initialPosition);
} }
final dynamic data; final int pointer;
final PointerRouter router;
final T data;
final Point dragStartPoint; final Point dragStartPoint;
final Widget feedback; final Widget feedback;
final Offset feedbackOffset; final Offset feedbackOffset;
final VoidCallback onDragFinished;
DragTargetState _activeTarget; DragTargetState _activeTarget;
bool _activeTargetWillAcceptDrop = false; bool _activeTargetWillAcceptDrop = false;
Offset _lastOffset; Offset _lastOffset;
OverlayEntry _entry; OverlayEntry _entry;
void handleEvent(PointerInputEvent event) {
switch(event.type) {
case 'pointerup':
update(event.position);
finish(_DragEndKind.dropped);
break;
case 'pointercancel':
finish(_DragEndKind.canceled);
break;
case 'pointermove':
update(event.position);
break;
}
}
void update(Point globalPosition) { void update(Point globalPosition) {
_lastOffset = globalPosition - dragStartPoint; _lastOffset = globalPosition - dragStartPoint;
_entry.markNeedsBuild();
HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition + feedbackOffset); HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition + feedbackOffset);
DragTargetState target = _getDragTarget(result.path); DragTargetState target = _getDragTarget(result.path);
if (target == _activeTarget) if (target == _activeTarget)
...@@ -223,18 +322,10 @@ class _DragAvatar { ...@@ -223,18 +322,10 @@ class _DragAvatar {
_activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data); _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
} }
void markNeedsBuild(BuildContext context) {
if (_entry == null) {
_entry = new OverlayEntry(builder: _build);
Navigator.of(context).overlay.insert(_entry);
} else {
_entry.markNeedsBuild();
}
}
DragTargetState _getDragTarget(List<HitTestEntry> path) { DragTargetState _getDragTarget(List<HitTestEntry> path) {
// TODO(abarth): Why do we reverse the path here? // Look for the RenderBox that corresponds to the hit target (the hit target
for (HitTestEntry entry in path.reversed) { // widget builds a RenderMetadata box for us for this purpose).
for (HitTestEntry entry in path) {
if (entry.target is RenderMetaData) { if (entry.target is RenderMetaData) {
RenderMetaData renderMetaData = entry.target; RenderMetaData renderMetaData = entry.target;
if (renderMetaData.metaData is DragTargetState) if (renderMetaData.metaData is DragTargetState)
...@@ -255,8 +346,7 @@ class _DragAvatar { ...@@ -255,8 +346,7 @@ class _DragAvatar {
_activeTargetWillAcceptDrop = false; _activeTargetWillAcceptDrop = false;
_entry.remove(); _entry.remove();
_entry = null; _entry = null;
if (onDragFinished != null) router.removeRoute(pointer, handleEvent);
onDragFinished();
} }
Widget _build(BuildContext context) { Widget _build(BuildContext context) {
......
...@@ -84,7 +84,7 @@ class GestureDetector extends StatefulComponent { ...@@ -84,7 +84,7 @@ class GestureDetector extends StatefulComponent {
} }
class _GestureDetectorState extends State<GestureDetector> { class _GestureDetectorState extends State<GestureDetector> {
final PointerRouter _router = FlutterBinding.instance.pointerRouter; PointerRouter get _router => FlutterBinding.instance.pointerRouter;
TapGestureRecognizer _tap; TapGestureRecognizer _tap;
DoubleTapGestureRecognizer _doubleTap; DoubleTapGestureRecognizer _doubleTap;
......
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