// 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'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'basic.dart'; import 'binding.dart'; import 'framework.dart'; import 'overlay.dart'; typedef bool DragTargetWillAccept<T>(T data); typedef void DragTargetAccept<T>(T data); typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData); typedef void DragStartCallback(Point position, int pointer); /// Where the [Draggable] should be anchored during a drag. 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, } /// Subclass this component to customize the gesture used to start a drag. abstract class DraggableBase<T> extends StatefulComponent { DraggableBase({ Key key, this.data, this.child, this.feedback, this.feedbackOffset: Offset.zero, this.dragAnchor: DragAnchor.child }) : super(key: key) { assert(child != null); assert(feedback != null); } final T data; final Widget child; /// The widget to show when a drag is under way. final Widget feedback; /// 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; /// Where this widget should be anchored during a drag. final DragAnchor dragAnchor; /// 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>(); } /// Makes its child draggable starting from tap down. 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, gestureArena: Gesturer.instance.gestureArena, onTapDown: starter ); } } /// Makes its child draggable starting from long press. 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, gestureArena: Gesturer.instance.gestureArena, 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 => Gesturer.instance.pointerRouter; void initState() { super.initState(); _recognizer = config.createRecognizer(router, _startDrag); } GestureRecognizer _recognizer; Map<int, GestureArenaEntry> _activePointers = <int, GestureArenaEntry>{}; void _routePointer(PointerEvent event) { _activePointers[event.pointer] = Gesturer.instance.gestureArena.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; switch (config.dragAnchor) { case DragAnchor.child: final RenderBox renderObject = context.findRenderObject(); dragStartPoint = renderObject.globalToLocal(position); break; case DragAnchor.pointer: dragStartPoint = Point.origin; break; } new _DragAvatar<T>( pointer: pointer, router: router, overlay: Overlay.of(context), data: config.data, initialPosition: position, dragStartPoint: dragStartPoint, feedback: config.feedback, feedbackOffset: config.feedbackOffset ); } Widget build(BuildContext context) { return new Listener( onPointerDown: _routePointer, child: config.child ); } } /// Receives data when a [Draggable] widget is dropped. class DragTarget<T> extends StatefulComponent { const DragTarget({ Key key, this.builder, this.onWillAccept, this.onAccept }) : super(key: key); /// Called to build the contents of this widget. /// /// The builder can build different widgets depending on what is being dragged /// into this drag target. final DragTargetBuilder<T> builder; /// Called to determine whether this widget is interested in receiving a given /// piece of data being dragged over this drag target. final DragTargetWillAccept<T> onWillAccept; /// Called when an acceptable piece of data was dropped over this drag target. final DragTargetAccept<T> onAccept; _DragTargetState<T> createState() => new _DragTargetState<T>(); } class _DragTargetState<T> extends State<DragTarget<T>> { 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)); if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) { 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); }); if (config.onAccept != null) config.onAccept(data); } Widget build(BuildContext context) { return new MetaData( metaData: this, child: config.builder(context, new UnmodifiableListView<T>(_candidateData), new UnmodifiableListView<dynamic>(_rejectedData) ) ); } } enum _DragEndKind { dropped, canceled } // 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({ this.pointer, this.router, OverlayState overlay, this.data, Point initialPosition, this.dragStartPoint: Point.origin, this.feedback, this.feedbackOffset: Offset.zero }) { assert(pointer != null); assert(router != null); assert(overlay != null); assert(dragStartPoint != null); assert(feedbackOffset != null); router.addRoute(pointer, handleEvent); _entry = new OverlayEntry(builder: _build); overlay.insert(_entry); update(initialPosition); } final int pointer; final PointerRouter router; final T data; final Point dragStartPoint; final Widget feedback; final Offset feedbackOffset; _DragTargetState _activeTarget; bool _activeTargetWillAcceptDrop = false; Offset _lastOffset; OverlayEntry _entry; 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); } } void update(Point globalPosition) { _lastOffset = globalPosition - dragStartPoint; _entry.markNeedsBuild(); HitTestResult result = new HitTestResult(); WidgetFlutterBinding.instance.hitTest(result, globalPosition + feedbackOffset); _DragTargetState target = _getDragTarget(result.path); if (target == _activeTarget) return; if (_activeTarget != null) _activeTarget.didLeave(data); _activeTarget = target; _activeTargetWillAcceptDrop = _activeTarget != null && _activeTarget.didEnter(data); } _DragTargetState _getDragTarget(List<HitTestEntry> path) { // 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) { if (entry.target is RenderMetaData) { RenderMetaData renderMetaData = entry.target; if (renderMetaData.metaData is _DragTargetState) return renderMetaData.metaData; } } return null; } void finish(_DragEndKind endKind) { if (_activeTarget != null) { if (endKind == _DragEndKind.dropped && _activeTargetWillAcceptDrop) _activeTarget.didDrop(data); else _activeTarget.didLeave(data); } _activeTarget = null; _activeTargetWillAcceptDrop = false; _entry.remove(); _entry = null; router.removeRoute(pointer, handleEvent); } Widget _build(BuildContext context) { return new Positioned( left: _lastOffset.dx, top: _lastOffset.dy, child: new IgnorePointer( child: feedback ) ); } }