drag_target.dart 7.72 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';

7
import 'package:flutter/rendering.dart';
8 9 10 11 12

import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
13 14 15

typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
16 17 18
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
typedef void DragFinishedNotification();

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
class Draggable extends StatefulComponent {
39 40 41 42 43 44 45 46
  Draggable({
    Key key,
    this.navigator,
    this.data,
    this.child,
    this.feedback,
    this.feedbackOffset: Offset.zero,
    this.dragAnchor: DragAnchor.child
47
  }) : super(key: key) {
48
    assert(navigator != null);
49 50
    assert(child != null);
    assert(feedback != null);
51 52 53 54 55 56 57
  }

  final NavigatorState navigator;
  final dynamic data;
  final Widget child;
  final Widget feedback;

58 59 60 61 62 63
  /// 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;
  final DragAnchor dragAnchor;

64
  _DraggableState createState() => new _DraggableState();
65 66
}

67
class _DraggableState extends State<Draggable> {
68 69
  DragRoute _route;

70
  void _startDrag(PointerInputEvent event) {
71 72
    if (_route != null)
      return; // TODO(ianh): once we switch to using gestures, just hand the gesture to the route so it can do everything itself. then we can have multiple drags at the same time.
73 74 75 76 77 78 79 80 81 82 83 84
    final Point point = new Point(event.x, event.y);
    Point dragStartPoint;
    switch (config.dragAnchor) {
      case DragAnchor.child:
        final RenderBox renderObject = context.findRenderObject();
        dragStartPoint = renderObject.globalToLocal(point);
        break;
      case DragAnchor.pointer:
        dragStartPoint = Point.origin;
        break;
    }
    assert(dragStartPoint != null);
85 86
    _route = new DragRoute(
      data: config.data,
87
      dragStartPoint: dragStartPoint,
88
      feedback: config.feedback,
89
      feedbackOffset: config.feedbackOffset,
90 91 92 93 94 95 96 97
      onDragFinished: () {
        _route = null;
      }
    );
    _route.update(point);
    config.navigator.push(_route);
  }

98
  void _updateDrag(PointerInputEvent event) {
99 100 101 102 103 104 105
    if (_route != null) {
      config.navigator.setState(() {
        _route.update(new Point(event.x, event.y));
      });
    }
  }

106
  void _cancelDrag(PointerInputEvent event) {
107 108 109 110 111 112
    if (_route != null) {
      config.navigator.popRoute(_route, DragEndKind.canceled);
      assert(_route == null);
    }
  }

113
  void _drop(PointerInputEvent event) {
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
    if (_route != null) {
      _route.update(new Point(event.x, event.y));
      config.navigator.popRoute(_route, DragEndKind.dropped);
      assert(_route == null);
    }
  }

  Widget build(BuildContext context) {
    // TODO(abarth): We should be using a GestureDetector
    return new Listener(
      onPointerDown: _startDrag,
      onPointerMove: _updateDrag,
      onPointerCancel: _cancelDrag,
      onPointerUp: _drop,
      child: config.child
    );
  }
}

133 134

class DragTarget<T> extends StatefulComponent {
135
  const DragTarget({
136 137 138 139 140 141
    Key key,
    this.builder,
    this.onWillAccept,
    this.onAccept
  }) : super(key: key);

142 143 144 145 146 147
  final DragTargetBuilder<T> builder;
  final DragTargetWillAccept<T> onWillAccept;
  final DragTargetAccept<T> onAccept;

  DragTargetState<T> createState() => new DragTargetState<T>();
}
148

149
class DragTargetState<T> extends State<DragTarget<T>> {
150 151 152 153 154 155
  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));
156
    if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) {
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
      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);
    });
179 180
    if (config.onAccept != null)
      config.onAccept(data);
181 182
  }

183 184 185 186 187 188 189
  Widget build(BuildContext context) {
    return new MetaData(
      metaData: this,
      child: config.builder(context,
                            new UnmodifiableListView<T>(_candidateData),
                            new UnmodifiableListView<dynamic>(_rejectedData))
    );
190 191 192
  }
}

193 194 195 196

enum DragEndKind { dropped, canceled }

class DragRoute extends Route {
197 198 199 200 201 202 203 204 205
  DragRoute({
    this.data,
    this.dragStartPoint: Point.origin,
    this.feedback,
    this.feedbackOffset: Offset.zero,
    this.onDragFinished
  }) {
    assert(feedbackOffset != null);
  }
206 207

  final dynamic data;
208 209
  final Point dragStartPoint;
  final Widget feedback;
210
  final Offset feedbackOffset;
211
  final DragFinishedNotification onDragFinished;
212

213
  DragTargetState _activeTarget;
214
  bool _activeTargetWillAcceptDrop = false;
215
  Offset _lastOffset;
216 217

  void update(Point globalPosition) {
218
    _lastOffset = globalPosition - dragStartPoint;
219
    HitTestResult result = WidgetFlutterBinding.instance.hitTest(globalPosition + feedbackOffset);
220
    DragTargetState target = _getDragTarget(result.path);
221 222 223 224 225 226 227 228
    if (target == _activeTarget)
      return;
    if (_activeTarget != null)
      _activeTarget.didLeave(data);
    _activeTarget = target;
    _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data);
  }

229 230 231 232 233 234 235 236 237 238
  DragTargetState _getDragTarget(List<HitTestEntry> path) {
    // TODO(abarth): Why do we reverse the path here?
    for (HitTestEntry entry in path.reversed) {
      if (entry.target is RenderMetaData) {
        RenderMetaData renderMetaData = entry.target;
        if (renderMetaData.metaData is DragTargetState)
          return renderMetaData.metaData;
      }
    }
    return null;
239 240
  }

241 242 243 244 245 246 247
  void didPop([DragEndKind endKind]) {
    if (_activeTarget != null) {
      if (endKind == DragEndKind.dropped && _activeTargetWillAcceptDrop)
        _activeTarget.didDrop(data);
      else
        _activeTarget.didLeave(data);
    }
248 249
    _activeTarget = null;
    _activeTargetWillAcceptDrop = false;
250 251 252 253 254 255 256 257
    if (onDragFinished != null)
      onDragFinished();
    super.didPop(endKind);
  }

  bool get ephemeral => true;
  bool get modal => false;
  bool get opaque => false;
258

259
  Widget build(RouteArguments args) {
260 261 262 263
    return new Positioned(
      left: _lastOffset.dx,
      top: _lastOffset.dy,
      child: new IgnorePointer(
264
        child: feedback
265 266
      )
    );
267 268
  }
}