drag_target.dart 13.3 KB
Newer Older
1 2 3 4
// 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.

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

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

typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
16
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
Hixie's avatar
Hixie committed
17

Adam Barth's avatar
Adam Barth committed
18
/// Where the [Draggable] should be anchored during a drag.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
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,
}

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

Hixie's avatar
Hixie committed
55
  final T data;
56 57

  /// The widget below this widget in the tree.
58
  final Widget child;
Adam Barth's avatar
Adam Barth committed
59

60 61 62 63 64 65 66 67
  /// The widget to show instead of [child] when a drag is under way.
  ///
  /// If this is null, then [child] will be used instead (and so the
  /// drag source representation will change while a drag is under
  /// way).
  final Widget childWhenDragging;

  /// The widget to show under the pointer when a drag is under way.
68 69
  final Widget feedback;

70 71 72 73
  /// 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
74 75

  /// Where this widget should be anchored during a drag.
76 77
  final DragAnchor dragAnchor;

78 79 80 81 82
  /// How many simultaneous drags to support. When null, no limit is applied.
  /// Set this to 1 if you want to only allow the drag source to have one item
  /// dragged at a time.
  final int maxSimultaneousDrags;

83 84
  /// Should return a new MultiDragGestureRecognizer instance
  /// constructed with the given arguments.
Hixie's avatar
Hixie committed
85
  MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart);
Hixie's avatar
Hixie committed
86

87
  @override
Hixie's avatar
Hixie committed
88
  _DraggableState<T> createState() => new _DraggableState<T>();
89 90
}

Adam Barth's avatar
Adam Barth committed
91
/// Makes its child draggable starting from tap down.
Hixie's avatar
Hixie committed
92 93 94 95 96
class Draggable<T> extends DraggableBase<T> {
  Draggable({
    Key key,
    T data,
    Widget child,
97
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
98 99
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
100 101
    DragAnchor dragAnchor: DragAnchor.child,
    int maxSimultaneousDrags
Hixie's avatar
Hixie committed
102 103 104 105
  }) : super(
    key: key,
    data: data,
    child: child,
106
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
107 108
    feedback: feedback,
    feedbackOffset: feedbackOffset,
109 110
    dragAnchor: dragAnchor,
    maxSimultaneousDrags: maxSimultaneousDrags
Hixie's avatar
Hixie committed
111 112
  );

113
  @override
114
  ImmediateMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
115
    return new ImmediateMultiDragGestureRecognizer()..onStart = onStart;
Hixie's avatar
Hixie committed
116 117 118
  }
}

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag horizontally.
class HorizontalDraggable<T> extends DraggableBase<T> {
  HorizontalDraggable({
    Key key,
    T data,
    Widget child,
    Widget childWhenDragging,
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
    int maxSimultaneousDrags
  }) : super(
    key: key,
    data: data,
    child: child,
    childWhenDragging: childWhenDragging,
    feedback: feedback,
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
    maxSimultaneousDrags: maxSimultaneousDrags
  );

142
  @override
143
  HorizontalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
144
    return new HorizontalMultiDragGestureRecognizer()..onStart = onStart;
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  }
}

/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag vertically.
class VerticalDraggable<T> extends DraggableBase<T> {
  VerticalDraggable({
    Key key,
    T data,
    Widget child,
    Widget childWhenDragging,
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
    int maxSimultaneousDrags
  }) : super(
    key: key,
    data: data,
    child: child,
    childWhenDragging: childWhenDragging,
    feedback: feedback,
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
    maxSimultaneousDrags: maxSimultaneousDrags
  );

171
  @override
172
  VerticalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
173
    return new VerticalMultiDragGestureRecognizer()..onStart = onStart;
174 175 176
  }
}

Adam Barth's avatar
Adam Barth committed
177
/// Makes its child draggable starting from long press.
Hixie's avatar
Hixie committed
178 179 180 181 182
class LongPressDraggable<T> extends DraggableBase<T> {
  LongPressDraggable({
    Key key,
    T data,
    Widget child,
183
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
184 185
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
186 187
    DragAnchor dragAnchor: DragAnchor.child,
    int maxSimultaneousDrags
Hixie's avatar
Hixie committed
188 189 190 191
  }) : super(
    key: key,
    data: data,
    child: child,
192
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
193 194
    feedback: feedback,
    feedbackOffset: feedbackOffset,
195 196
    dragAnchor: dragAnchor,
    maxSimultaneousDrags: maxSimultaneousDrags
Hixie's avatar
Hixie committed
197 198
  );

199
  @override
200
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
201 202 203
    return new DelayedMultiDragGestureRecognizer()
      ..onStart = (Point position) {
        Drag result = onStart(position);
204 205 206
        if (result != null)
          userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
        return result;
207
      };
Hixie's avatar
Hixie committed
208 209 210
  }
}

211
class _DraggableState<T> extends State<DraggableBase<T>> {
Hixie's avatar
Hixie committed
212

213
  @override
Hixie's avatar
Hixie committed
214 215
  void initState() {
    super.initState();
216
    _recognizer = config.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
217 218 219
  }

  GestureRecognizer _recognizer;
220
  int _activeCount = 0;
Hixie's avatar
Hixie committed
221

Ian Hickson's avatar
Ian Hickson committed
222
  void _routePointer(PointerEvent event) {
223 224
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
      return;
Hixie's avatar
Hixie committed
225 226 227
    _recognizer.addPointer(event);
  }

228
  _DragAvatar<T> _startDrag(Point position) {
229
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
230
      return null;
231 232 233 234
    Point dragStartPoint;
    switch (config.dragAnchor) {
      case DragAnchor.child:
        final RenderBox renderObject = context.findRenderObject();
Hixie's avatar
Hixie committed
235
        dragStartPoint = renderObject.globalToLocal(position);
236 237 238
        break;
      case DragAnchor.pointer:
        dragStartPoint = Point.origin;
Hixie's avatar
Hixie committed
239
      break;
240
    }
241 242 243
    setState(() {
      _activeCount += 1;
    });
244
    return new _DragAvatar<T>(
245
      overlay: Overlay.of(context, debugRequiredFor: config),
246
      data: config.data,
Hixie's avatar
Hixie committed
247
      initialPosition: position,
248
      dragStartPoint: dragStartPoint,
249
      feedback: config.feedback,
250 251
      feedbackOffset: config.feedbackOffset,
      onDragEnd: () {
Hixie's avatar
Hixie committed
252 253 254
        setState(() {
          _activeCount -= 1;
        });
255
      }
256 257 258
    );
  }

259
  @override
260
  Widget build(BuildContext context) {
261
    assert(Overlay.of(context, debugRequiredFor: config) != null);
262 263 264
    final bool canDrag = config.maxSimultaneousDrags == null ||
                         _activeCount < config.maxSimultaneousDrags;
    final bool showChild = _activeCount == 0 || config.childWhenDragging == null;
265
    return new Listener(
266 267
      onPointerDown: canDrag ? _routePointer : null,
      child: showChild ? config.child : config.childWhenDragging
268 269 270 271
    );
  }
}

Adam Barth's avatar
Adam Barth committed
272
/// Receives data when a [Draggable] widget is dropped.
273
class DragTarget<T> extends StatefulWidget {
274
  const DragTarget({
275 276 277 278 279 280
    Key key,
    this.builder,
    this.onWillAccept,
    this.onAccept
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
281 282 283 284
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
285
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
286 287 288

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

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

294
  @override
295
  _DragTargetState<T> createState() => new _DragTargetState<T>();
296
}
297

298
class _DragTargetState<T> extends State<DragTarget<T>> {
299 300 301 302 303 304
  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));
305
    if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) {
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
      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);
    });
328 329
    if (config.onAccept != null)
      config.onAccept(data);
330 331
  }

332
  @override
333 334 335
  Widget build(BuildContext context) {
    return new MetaData(
      metaData: this,
Hixie's avatar
Hixie committed
336
      behavior: HitTestBehavior.translucent,
Adam Barth's avatar
Adam Barth committed
337
      child: config.builder(context, _candidateData, _rejectedData)
338
    );
339 340 341
  }
}

342

Adam Barth's avatar
Adam Barth committed
343
enum _DragEndKind { dropped, canceled }
344

Hixie's avatar
Hixie committed
345 346 347 348 349
// 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.
350
class _DragAvatar<T> extends Drag {
Adam Barth's avatar
Adam Barth committed
351
  _DragAvatar({
Hixie's avatar
Hixie committed
352
    OverlayState overlay,
353
    this.data,
Hixie's avatar
Hixie committed
354
    Point initialPosition,
355 356
    this.dragStartPoint: Point.origin,
    this.feedback,
357 358
    this.feedbackOffset: Offset.zero,
    this.onDragEnd
359
  }) {
Hixie's avatar
Hixie committed
360 361
    assert(overlay != null);
    assert(dragStartPoint != null);
362
    assert(feedbackOffset != null);
Hixie's avatar
Hixie committed
363 364
    _entry = new OverlayEntry(builder: _build);
    overlay.insert(_entry);
365
    _position = initialPosition;
Hixie's avatar
Hixie committed
366
    update(initialPosition);
367
  }
368

Hixie's avatar
Hixie committed
369
  final T data;
370 371
  final Point dragStartPoint;
  final Widget feedback;
372
  final Offset feedbackOffset;
373
  final VoidCallback onDragEnd;
374

375
  _DragTargetState<T> _activeTarget;
376
  bool _activeTargetWillAcceptDrop = false;
377
  Point _position;
378
  Offset _lastOffset;
Adam Barth's avatar
Adam Barth committed
379
  OverlayEntry _entry;
380

381
  // Drag API
382
  @override
383 384 385 386
  void move(Offset offset) {
    _position += offset;
    update(_position);
  }
387 388

  @override
389
  void end(Velocity velocity) {
390 391
    finish(_DragEndKind.dropped);
  }
392 393

  @override
394 395
  void cancel() {
    finish(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
396 397
  }

398
  void update(Point globalPosition) {
399
    _lastOffset = globalPosition - dragStartPoint;
Hixie's avatar
Hixie committed
400
    _entry.markNeedsBuild();
Ian Hickson's avatar
Ian Hickson committed
401 402
    HitTestResult result = new HitTestResult();
    WidgetFlutterBinding.instance.hitTest(result, globalPosition + feedbackOffset);
403
    _DragTargetState<T> target = _getDragTarget(result.path);
404 405 406 407 408 409 410 411
    if (target == _activeTarget)
      return;
    if (_activeTarget != null)
      _activeTarget.didLeave(data);
    _activeTarget = target;
    _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
  }

412
  _DragTargetState<T> _getDragTarget(List<HitTestEntry> path) {
Hixie's avatar
Hixie committed
413 414 415
    // 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) {
416 417
      if (entry.target is RenderMetaData) {
        RenderMetaData renderMetaData = entry.target;
418
        if (renderMetaData.metaData is _DragTargetState<T>)
419 420 421 422
          return renderMetaData.metaData;
      }
    }
    return null;
423 424
  }

Adam Barth's avatar
Adam Barth committed
425
  void finish(_DragEndKind endKind) {
426
    if (_activeTarget != null) {
Adam Barth's avatar
Adam Barth committed
427
      if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop)
428 429 430 431
        _activeTarget.didDrop(data);
      else
        _activeTarget.didLeave(data);
    }
432 433
    _activeTarget = null;
    _activeTargetWillAcceptDrop = false;
Adam Barth's avatar
Adam Barth committed
434
    _entry.remove();
435
    _entry = null;
436 437
    if (onDragEnd != null)
      onDragEnd();
438 439
  }

Adam Barth's avatar
Adam Barth committed
440
  Widget _build(BuildContext context) {
441 442 443 444
    return new Positioned(
      left: _lastOffset.dx,
      top: _lastOffset.dy,
      child: new IgnorePointer(
445
        child: feedback
446 447
      )
    );
448 449
  }
}