Flutter Draggable Widget Class 源碼解析
本文主要解析 Flutter Draggable
的程式碼,解釋 Draggable
如何透過 Overlay
跟 OverLayEntry
在渲染階段時控制 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
透過 Listenable
將 PointerDown
事件傳遞到 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 有 childDragAnchorStrategy
跟 pointerDragAnchorStrategy
兩種,並取代原本的 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
DragTargetState
是 DragTarget
的 Stat,用來管理拖曳物件是否被放到 DragTarget
, 需要考慮的負責狀態有:
- Enter :: 物件覆蓋元件
- Leave :: 物件離開元件
- Drop :: 取消
- Move :: 移動
拖曳的元件同時覆蓋兩個 DragTarget。
Recognizer 之前提過有三種,接下來只解釋最常用的 ImmediateMultiDragGestureRecognizer
, 另外兩個原理是差不多的,只差是否有實作拖曳方向限制。
ImmediateMultiDragGestureRecognizer
ImmediateMultiDragGestureRecognizer
是一個繼承抽象類別 MultiDragPointerState
的 StatefulWidget
細節實作都在 _ImmediatePointerState
, 並在 _DraggableState.initState
階段初始化:
@override
void initState() {
super.initState();
_recognizer = widget.createRecognizer(_startDrag);
}
這邊可以注意到,_DraggableState
傳遞了自己的 _startDrag
給 ImmediateMultiDragGestureRecognizer
, _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,這樣判斷不在拖曳的狀態的時機點比較準確。
小結
綜上述,_DragAvatar
跟 MultiDragGestureRecognizer
是細節所在,前者負責管理應用層邏輯、後者則管理硬體裝置事件到應用層邏輯的轉譯。當我們要了解如何在 flutter 中實作可拖曳的元件時,這兩個類別便是讀者須詳加理解參考的部分。