今天的開發重點是讓使用者可以拖曳行程項目快速調整順序,提升操作直覺性與流暢度。從需求出發,分析不同原生實作方式,並分享我最終選擇 LongPressDraggable + DragTarget 的原因。
在日常行程管理中,使用者常需要:
如果僅靠上下移動按鈕,操作過程繁瑣且耗時。拖曳操作則可以更直覺地重排行程順序。
在 Flutter 中,有三種純原生方式可考慮:
實作方式 | 優勢 | 缺點 | 適用場景 |
---|---|---|---|
ReorderableListView | 內建支援拖曳排序,簡單易用;自動提供動畫與拖曳反饋;資料更新簡單 | 難以自訂拖曳外觀;對多層子行程或分組列表彈性低 | 單層列表行程排序,UI 為簡單列表 |
LongPressDraggable + DragTarget | 支援自訂拖曳 feedback;可控制拖入區域與高亮;可實作多層拖曳或分組拖曳 | 需要自行管理 DragTarget 邏輯;實作比 ReorderableListView 複雜 | 多層子行程或分組行程,需自訂拖曳動畫與互動 |
GestureDetector + 自行管理位置 | 完全自訂拖曳邏輯與動畫;可跨 Widget 拖曳;最自由的互動設計 | 實作最複雜;需管理拖曳位置、碰撞判斷與資料更新;動畫需額外處理 | 非列表型 UI 或跨頁面、跨元件拖曳,需自訂交互體驗 |
考量到行程有 多層子行程需求,以及希望提供 自訂拖曳動畫與高亮反饋,最終決定採用 LongPressDraggable 搭配 DragTarget:
假設我們有一個行程列表 activities
,每個項目都是 LongPressDraggable,而其他行程或空區域作為 DragTarget:
Column(
children: List.generate(
activitiesForDay.length, // 逐一建立當天的活動列表
(index) {
final activity = activitiesForDay[index]; // 取得當前活動
return DragTarget<Activity>(
builder: (context, candidateData, rejectedData) {
// DragTarget:當有其他活動拖曳到此位置時會觸發
return LongPressDraggable<Activity>(
data: activity, // 拖曳時攜帶的資料
feedback: Material(
// 拖曳過程中顯示的 Widget
child: Container(
width: 300,
padding: const EdgeInsets.all(8),
color: Colors.blueAccent,
child: Text(
activity.name,
style: const TextStyle(color: Colors.white),
),
),
),
childWhenDragging: Opacity(
// 當自己被拖曳時,原本位置顯示的 Widget(通常半透明)
opacity: 0.5,
child: Text(activity.name),
),
child: Text(activity.name), // 平常狀態顯示的 Widget
);
},
onWillAcceptWithDetails: (details) =>
details.data.id != activity.id, // 判斷拖入的資料是否可接受
onAcceptWithDetails: (details) {
final draggedActivity = details.data;
// 當拖曳完成放下時觸發
// TODO: 在這裡實作重新排序邏輯
debugPrint('Activity ${draggedActivity.id} dropped on ${activity.id}');
},
);
},
),
)
小補充:
onWillAcceptWithDetails
用來預判拖入的資料是否可以接受,回傳 true/false
,例如避免拖到自己身上。onAcceptWithDetails
在拖放完成後觸發,真正執行資料更新或重新排序等邏輯。這些細節讓拖曳操作自然、直覺。
主行程拖曳 | 子行程拖曳 |
---|---|
![]() |
![]() |