本文主要解析 Flutter Draggable 的程式碼,解釋 Draggable 如何透過 OverlayOverLayEntry 在渲染階段時控制 feedback 出現跟消失,以及DragTarget 如何處理目標元件接受被拖曳元件 。 源碼在 lib/src/widgets/drag_target.dart

整個源碼裡面的類(class)比下圖多,這邊只顯示重要的。


classDiagram

direction TB

    class Draggable {
    }
    class _DraggableState {
    }
    class _DragAvatar {
    }
    class Drag {
    }
    class MultiDragGestureRecognizer {
    }
    class ImmediateMultiDragGestureRecognizer {
    }
    class HorizontalMultiDragGestureRecognizer {
    }
    class VerticalMultiDragGestureRecognizer {
    }
    class Overlay {
    }
    class OverlayEntry {
    }
    class DragTarget {
    }
    class _DragTargetState {
    }

    Draggable *-- _DraggableState
    _DraggableState --> _DragAvatar
    Drag <|-- _DragAvatar
    _DraggableState *-- MultiDragGestureRecognizer

    MultiDragGestureRecognizer <|-- ImmediateMultiDragGestureRecognizer
    MultiDragGestureRecognizer <|-- HorizontalMultiDragGestureRecognizer
    MultiDragGestureRecognizer <|-- VerticalMultiDragGestureRecognizer

    MultiDragGestureRecognizer --> Drag

    _DragAvatar --> Overlay
    _DragAvatar *-- OverlayEntry

    DragTarget --> _DragTargetState

    _DraggableState --> _DragTargetState

Draggable

三個比較重要的參數

  • child :: 尚未進入拖曳開始的子元件
  • feedback :: 拖曳中,跟著滑鼠游標移動元件
  • childWhenDragging :: 拖曳時顯示的子元件

DraggableState

此類別負責管理 DraggableWidget 的四種狀態,分別如下:

  • Drag Start :: point down
  • Drag Update :: point down update
  • Drag cancel :: 沒有 point down event
  • Drag Complete :: 拖曳的物件被目標接收

build 透過 ListenablePointerDown 事件傳遞到 children widgets 中。

該元件將判斷是否為 DragStart 的狀態的任務委由下面這三個 Recognizer 處理。

  • ImmediateMultiDragGestureRecognizer :: the most straight-forward variant of multi-pointer drag gesture recognizer.
  • HorizontalMultiDragGestureRecognizer :: which only recognizes drags that start horizontally.
  • VerticalMultiDragGestureRecognizer :: which only recognizes drags that start vertically.

_DragAvatar

_DragAvatar 繼承 src/gestures/drag.dart Drag,用於管理拖曳中跟拖曳完畢的狀態。拖曳開始會為 feedback 參數傳遞進來的 Widget 建立 OverlayEntry, 而拖曳中則會根據使用者的鼠標位置更新該 entry 的位置, 拖曳結束後則移除。 OverlayEntry 用於建立浮動 Widget 插入到 Overlay Widget 中,也就是一個特殊的 Stack

class _DragAvatar<T extends Object> extends Drag {}

這個 class 會被在 DraggableState._startDrag 建立,而 dragStartPoint 會因為 drag anchor strategy 有所調整,目前 strategy 有 childDragAnchorStrategypointerDragAnchorStrategy 兩種,並取代原本的 DragAnchor 設定。

_DragAvatar<T>? _startDrag(Offset position) {
  /// ....
}

接下來看重要的 public methods。

  • void update(DragUpdateDetails details)
  • void end(DragEndDetails details)
  • void cancel()
  • void updateDrag(Offset globalPosition)
  • void finishDrag(_DragEndKind endKind, [ Velocity? velocity ])

_getDragTargets 實作如何取得 drag targets,方式是透過 WidgetsBinding.instance.hitTest

final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
final List<_DragTargetState<Object>> targets = _getDragTargets(result.path).toList();

_lastOffset 的數字變動,決定了新的 OverlayEntry 是否要 build。在 OveryEntry 的註解可以看到 markNeedsBuild 如下解釋

/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
/// follows the user's finger across the screen after the drag begins. Using the
/// overlay to display the drag avatar lets the avatar float over the other
/// widgets in the app. As the user's finger moves, draggable calls
/// [markNeedsBuild] on the overlay entry to cause it to rebuild. It its build,
/// the entry includes a [Positioned] with its top and left property set to
/// position the drag avatar near the user's finger. When the drag is over,
/// [Draggable] removes the entry from the overlay to remove the drag avatar
/// from view.

也就是每次手指移動,都會呼叫 markNeedsBuild 更新 feedback 的位置。具體實作在 _DragAvatar._build_DragAvatar.updateDrag

 Widget _build(BuildContext context) {
    final RenderBox box = overlayState.context.findRenderObject()! as RenderBox;
    final Offset overlayTopLeft = box.localToGlobal(Offset.zero);
    return Positioned(
      left: _lastOffset!.dx - overlayTopLeft.dx,
      top: _lastOffset!.dy - overlayTopLeft.dy,
      child: IgnorePointer(
        ignoring: ignoringFeedbackPointer,
        ignoringSemantics: ignoringFeedbackSemantics,
        child: feedback,
      ),
    );

void updateDrag(Offset globalPosition) {
    _lastOffset = globalPosition - dragStartPoint;
    _entry!.markNeedsBuild();

    /// ....
}

DragTarget

DragTarget 的 constructor 有一個 builder 用於建立元件。

DragTargetState

DragTargetStateDragTarget 的 Stat,用來管理拖曳物件是否被放到 DragTarget, 需要考慮的負責狀態有:

  1. Enter :: 物件覆蓋元件
  2. Leave :: 物件離開元件
  3. Drop :: 取消
  4. Move :: 移動

拖曳的元件同時覆蓋兩個 DragTarget。

Recognizer 之前提過有三種,接下來只解釋最常用的 ImmediateMultiDragGestureRecognizer, 另外兩個原理是差不多的,只差是否有實作拖曳方向限制。

ImmediateMultiDragGestureRecognizer

ImmediateMultiDragGestureRecognizer 是一個繼承抽象類別 MultiDragPointerStateStatefulWidget 細節實作都在 _ImmediatePointerState , 並在 _DraggableState.initState 階段初始化:

  @override
  void initState() {
    super.initState();
    _recognizer = widget.createRecognizer(_startDrag);
  }

這邊可以注意到,_DraggableState 傳遞了自己的 _startDragImmediateMultiDragGestureRecognizer_startDrag(Drag client) 建立 DragUpdateDetails 然後呼叫 _DragAvatar.update(details)

Drag 建立的方式有兩種,一個是如 Draggable 透過 onStart 參數,傳遞一個回傳 Drag 的 callback 過去, 另一種是在預設狀況下,用 MultiDragPointerState 產生出來。

if (onStart != null)
  drag = invokeCallback<Drag?>('onStart', () => onStart!(initialPosition));
if (drag != null) {
  state._startDrag(drag);
 } else {
  _removeState(pointer);
}

所有的 gesture 事件處理均透過 _handleEvents 處理,利用 GestureBinding.instance.pointerRouter.addRoute 吃 event, 並且根據對應的狀態執行 _up, _cancel 。 recognizer dispose 的方式比較特殊, 作者希望 state 被 dispose 時,recognizer 會比較慢 dispose,這樣判斷不在拖曳的狀態的時機點比較準確。

小結

綜上述,_DragAvatarMultiDragGestureRecognizer 是細節所在,前者負責管理應用層邏輯、後者則管理硬體裝置事件到應用層邏輯的轉譯。當我們要了解如何在 flutter 中實作可拖曳的元件時,這兩個類別便是讀者須詳加理解參考的部分。