drag_target.dart 11.1 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection';

Hixie's avatar
Hixie committed
7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
Hixie's avatar
Hixie committed
9
import 'package:flutter/services.dart';
10 11 12 13

import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
14
import 'overlay.dart';
15 16 17

typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
18
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
Hixie's avatar
Hixie committed
19 20
typedef void DragStartCallback(Point position, int pointer);

Adam Barth's avatar
Adam Barth committed
21
/// Where the [Draggable] should be anchored during a drag.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
enum DragAnchor {
  /// Display the feedback anchored at the position of the original child. If
  /// feedback is identical to the child, then this means the feedback will
  /// exactly overlap the original child when the drag starts.
  child,

  /// Display the feedback anchored at the position of the touch that started
  /// the drag. If feedback is identical to the child, then this means the top
  /// left of the feedback will be under the finger when the drag starts. This
  /// will likely not exactly overlap the original child, e.g. if the child is
  /// big and the touch was not centered. This mode is useful when the feedback
  /// is transformed so as to move the feedback to the left by half its width,
  /// and up by half its width plus the height of the finger, since then it
  /// appears as if putting the finger down makes the touch feedback appear
  /// above the finger. (It feels weird for it to appear offset from the
  /// original child if it's anchored to the child and not the finger.)
  pointer,
}

Adam Barth's avatar
Adam Barth committed
41
/// Subclass this component to customize the gesture used to start a drag.
Hixie's avatar
Hixie committed
42 43
abstract class DraggableBase<T> extends StatefulComponent {
  DraggableBase({
44 45 46 47 48 49
    Key key,
    this.data,
    this.child,
    this.feedback,
    this.feedbackOffset: Offset.zero,
    this.dragAnchor: DragAnchor.child
50
  }) : super(key: key) {
51 52
    assert(child != null);
    assert(feedback != null);
53 54
  }

Hixie's avatar
Hixie committed
55
  final T data;
56
  final Widget child;
Adam Barth's avatar
Adam Barth committed
57 58

  /// The widget to show when a drag is under way.
59 60
  final Widget feedback;

61 62 63 64
  /// The feedbackOffset can be used to set the hit test target point for the
  /// purposes of finding a drag target. It is especially useful if the feedback
  /// is transformed compared to the child.
  final Offset feedbackOffset;
Adam Barth's avatar
Adam Barth committed
65 66

  /// Where this widget should be anchored during a drag.
67 68
  final DragAnchor dragAnchor;

Hixie's avatar
Hixie committed
69 70 71 72 73 74 75
  /// 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>();
76 77
}

Adam Barth's avatar
Adam Barth committed
78
/// Makes its child draggable starting from tap down.
Hixie's avatar
Hixie committed
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
class Draggable<T> extends DraggableBase<T> {
  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
  );

  GestureRecognizer createRecognizer(PointerRouter router, DragStartCallback starter) {
    return new MultiTapGestureRecognizer(
      router: router,
99
      gestureArena: Gesturer.instance.gestureArena,
Hixie's avatar
Hixie committed
100 101 102 103 104
      onTapDown: starter
    );
  }
}

Adam Barth's avatar
Adam Barth committed
105
/// Makes its child draggable starting from long press.
Hixie's avatar
Hixie committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
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,
126
      gestureArena: Gesturer.instance.gestureArena,
Hixie's avatar
Hixie committed
127 128 129 130 131 132 133 134 135 136 137
      longTapDelay: kLongPressTimeout,
      onLongTapDown: (Point position, int pointer) {
        userFeedback.performHapticFeedback(HapticFeedbackType.VIRTUAL_KEY);
        starter(position, pointer);
      }
    );
  }
}

class _DraggableState<T> extends State<DraggableBase<T>> implements GestureArenaMember {

138
  PointerRouter get router => Gesturer.instance.pointerRouter;
Hixie's avatar
Hixie committed
139 140 141 142 143 144 145 146 147

  void initState() {
    super.initState();
    _recognizer = config.createRecognizer(router, _startDrag);
  }

  GestureRecognizer _recognizer;
  Map<int, GestureArenaEntry> _activePointers = <int, GestureArenaEntry>{};

Ian Hickson's avatar
Ian Hickson committed
148
  void _routePointer(PointerEvent event) {
149
    _activePointers[event.pointer] = Gesturer.instance.gestureArena.add(event.pointer, this);
Hixie's avatar
Hixie committed
150 151 152 153 154 155 156 157 158 159
    _recognizer.addPointer(event);
  }

  void acceptGesture(int pointer) {
    _activePointers.remove(pointer);
  }

  void rejectGesture(int pointer) {
    _activePointers.remove(pointer);
  }
160

Hixie's avatar
Hixie committed
161 162 163
  void _startDrag(Point position, int pointer) {
    assert(_activePointers.containsKey(pointer));
    _activePointers[pointer].resolve(GestureDisposition.accepted);
164 165 166 167
    Point dragStartPoint;
    switch (config.dragAnchor) {
      case DragAnchor.child:
        final RenderBox renderObject = context.findRenderObject();
Hixie's avatar
Hixie committed
168
        dragStartPoint = renderObject.globalToLocal(position);
169 170 171
        break;
      case DragAnchor.pointer:
        dragStartPoint = Point.origin;
Hixie's avatar
Hixie committed
172
      break;
173
    }
Hixie's avatar
Hixie committed
174 175 176
    new _DragAvatar<T>(
      pointer: pointer,
      router: router,
Hixie's avatar
Hixie committed
177
      overlay: Overlay.of(context),
178
      data: config.data,
Hixie's avatar
Hixie committed
179
      initialPosition: position,
180
      dragStartPoint: dragStartPoint,
181
      feedback: config.feedback,
Hixie's avatar
Hixie committed
182
      feedbackOffset: config.feedbackOffset
183 184 185 186 187
    );
  }

  Widget build(BuildContext context) {
    return new Listener(
Hixie's avatar
Hixie committed
188
      onPointerDown: _routePointer,
189 190 191 192 193
      child: config.child
    );
  }
}

Adam Barth's avatar
Adam Barth committed
194
/// Receives data when a [Draggable] widget is dropped.
195
class DragTarget<T> extends StatefulComponent {
196
  const DragTarget({
197 198 199 200 201 202
    Key key,
    this.builder,
    this.onWillAccept,
    this.onAccept
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
203 204 205 206
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
207
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
208 209 210

  /// Called to determine whether this widget is interested in receiving a given
  /// piece of data being dragged over this drag target.
211
  final DragTargetWillAccept<T> onWillAccept;
Adam Barth's avatar
Adam Barth committed
212 213

  /// Called when an acceptable piece of data was dropped over this drag target.
214 215
  final DragTargetAccept<T> onAccept;

216
  _DragTargetState<T> createState() => new _DragTargetState<T>();
217
}
218

219
class _DragTargetState<T> extends State<DragTarget<T>> {
220 221 222 223 224 225
  final List<T> _candidateData = new List<T>();
  final List<dynamic> _rejectedData = new List<dynamic>();

  bool didEnter(dynamic data) {
    assert(!_candidateData.contains(data));
    assert(!_rejectedData.contains(data));
226
    if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) {
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
      setState(() {
        _candidateData.add(data);
      });
      return true;
    }
    _rejectedData.add(data);
    return false;
  }

  void didLeave(dynamic data) {
    assert(_candidateData.contains(data) || _rejectedData.contains(data));
    setState(() {
      _candidateData.remove(data);
      _rejectedData.remove(data);
    });
  }

  void didDrop(dynamic data) {
    assert(_candidateData.contains(data));
    setState(() {
      _candidateData.remove(data);
    });
249 250
    if (config.onAccept != null)
      config.onAccept(data);
251 252
  }

253 254 255 256 257
  Widget build(BuildContext context) {
    return new MetaData(
      metaData: this,
      child: config.builder(context,
                            new UnmodifiableListView<T>(_candidateData),
Hixie's avatar
Hixie committed
258 259
                            new UnmodifiableListView<dynamic>(_rejectedData)
      )
260
    );
261 262 263
  }
}

264

Adam Barth's avatar
Adam Barth committed
265
enum _DragEndKind { dropped, canceled }
266

Hixie's avatar
Hixie committed
267 268 269 270 271 272
// 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> {
Adam Barth's avatar
Adam Barth committed
273
  _DragAvatar({
Hixie's avatar
Hixie committed
274 275 276
    this.pointer,
    this.router,
    OverlayState overlay,
277
    this.data,
Hixie's avatar
Hixie committed
278
    Point initialPosition,
279 280
    this.dragStartPoint: Point.origin,
    this.feedback,
Hixie's avatar
Hixie committed
281
    this.feedbackOffset: Offset.zero
282
  }) {
Hixie's avatar
Hixie committed
283 284 285 286
    assert(pointer != null);
    assert(router != null);
    assert(overlay != null);
    assert(dragStartPoint != null);
287
    assert(feedbackOffset != null);
Hixie's avatar
Hixie committed
288 289 290 291
    router.addRoute(pointer, handleEvent);
    _entry = new OverlayEntry(builder: _build);
    overlay.insert(_entry);
    update(initialPosition);
292
  }
293

Hixie's avatar
Hixie committed
294 295 296
  final int pointer;
  final PointerRouter router;
  final T data;
297 298
  final Point dragStartPoint;
  final Widget feedback;
299
  final Offset feedbackOffset;
300

301
  _DragTargetState _activeTarget;
302
  bool _activeTargetWillAcceptDrop = false;
303
  Offset _lastOffset;
Adam Barth's avatar
Adam Barth committed
304
  OverlayEntry _entry;
305

Ian Hickson's avatar
Ian Hickson committed
306 307 308 309 310 311 312 313
  void handleEvent(PointerEvent event) {
    if (event is PointerUpEvent) {
      update(event.position);
      finish(_DragEndKind.dropped);
    } else if (event is PointerCancelEvent) {
      finish(_DragEndKind.canceled);
    } else if (event is PointerMoveEvent) {
      update(event.position);
Hixie's avatar
Hixie committed
314 315 316
    }
  }

317
  void update(Point globalPosition) {
318
    _lastOffset = globalPosition - dragStartPoint;
Hixie's avatar
Hixie committed
319
    _entry.markNeedsBuild();
Ian Hickson's avatar
Ian Hickson committed
320 321
    HitTestResult result = new HitTestResult();
    WidgetFlutterBinding.instance.hitTest(result, globalPosition + feedbackOffset);
322
    _DragTargetState target = _getDragTarget(result.path);
323 324 325 326 327 328 329 330
    if (target == _activeTarget)
      return;
    if (_activeTarget != null)
      _activeTarget.didLeave(data);
    _activeTarget = target;
    _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
  }

331
  _DragTargetState _getDragTarget(List<HitTestEntry> path) {
Hixie's avatar
Hixie committed
332 333 334
    // Look for the RenderBox that corresponds to the hit target (the hit target
    // widget builds a RenderMetadata box for us for this purpose).
    for (HitTestEntry entry in path) {
335 336
      if (entry.target is RenderMetaData) {
        RenderMetaData renderMetaData = entry.target;
337
        if (renderMetaData.metaData is _DragTargetState)
338 339 340 341
          return renderMetaData.metaData;
      }
    }
    return null;
342 343
  }

Adam Barth's avatar
Adam Barth committed
344
  void finish(_DragEndKind endKind) {
345
    if (_activeTarget != null) {
Adam Barth's avatar
Adam Barth committed
346
      if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop)
347 348 349 350
        _activeTarget.didDrop(data);
      else
        _activeTarget.didLeave(data);
    }
351 352
    _activeTarget = null;
    _activeTargetWillAcceptDrop = false;
Adam Barth's avatar
Adam Barth committed
353
    _entry.remove();
354
    _entry = null;
Hixie's avatar
Hixie committed
355
    router.removeRoute(pointer, handleEvent);
356 357
  }

Adam Barth's avatar
Adam Barth committed
358
  Widget _build(BuildContext context) {
359 360 361 362
    return new Positioned(
      left: _lastOffset.dx,
      top: _lastOffset.dy,
      child: new IgnorePointer(
363
        child: feedback
364 365
      )
    );
366 367
  }
}