iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 13

Day 13 - 行程拖曳功能開發:LongPressDraggable 與 DragTarget 的選擇與實作

  • 分享至 

  • xImage
  •  

今天的開發重點是讓使用者可以拖曳行程項目快速調整順序,提升操作直覺性與流暢度。從需求出發,分析不同原生實作方式,並分享我最終選擇 LongPressDraggable + DragTarget 的原因。


為什麼需要拖曳行程?

在日常行程管理中,使用者常需要:

  • 調整景點順序
  • 移動子行程到不同主要行程
  • 重新安排交通段落或自由活動

如果僅靠上下移動按鈕,操作過程繁瑣且耗時。拖曳操作則可以更直覺地重排行程順序。


原生實作方式比較

在 Flutter 中,有三種純原生方式可考慮:

實作方式 優勢 缺點 適用場景
ReorderableListView 內建支援拖曳排序,簡單易用;自動提供動畫與拖曳反饋;資料更新簡單 難以自訂拖曳外觀;對多層子行程或分組列表彈性低 單層列表行程排序,UI 為簡單列表
LongPressDraggable + DragTarget 支援自訂拖曳 feedback;可控制拖入區域與高亮;可實作多層拖曳或分組拖曳 需要自行管理 DragTarget 邏輯;實作比 ReorderableListView 複雜 多層子行程或分組行程,需自訂拖曳動畫與互動
GestureDetector + 自行管理位置 完全自訂拖曳邏輯與動畫;可跨 Widget 拖曳;最自由的互動設計 實作最複雜;需管理拖曳位置、碰撞判斷與資料更新;動畫需額外處理 非列表型 UI 或跨頁面、跨元件拖曳,需自訂交互體驗

選擇 LongPressDraggable + DragTarget 的原因

考量到行程有 多層子行程需求,以及希望提供 自訂拖曳動畫與高亮反饋,最終決定採用 LongPressDraggable 搭配 DragTarget:

  • 可以長按拖曳每個行程項目
  • 拖曳時提供即時 visual feedback
  • 可在拖入目標時高亮顯示,提升使用者辨識度
  • 支援調整子行程順序

Flutter 實作範例

假設我們有一個行程列表 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}');
        },
      );
    },
  ),
)

小補充:

  • 使用 Material 包裝 feedback Widget,避免拖曳過程中文字顏色或陰影顯示異常。
  • childWhenDragging 可視化提示使用者此 activity 正在被拖曳。
  • onWillAcceptWithDetails 用來預判拖入的資料是否可以接受,回傳 true/false,例如避免拖到自己身上。
  • onAcceptWithDetails 在拖放完成後觸發,真正執行資料更新或重新排序等邏輯。

使用者體驗優化

  • 長按觸發拖曳:使用 LongPressDraggable,避免手指稍微滑動就誤觸拖曳。
  • 拖曳視覺提示:拖曳過程中提供清楚的 feedback(形狀與原項目一致),原位置半透明提示該項目正在被拖動;拖入目標區域時高亮顯示,讓使用者清楚知道放置位置。
  • 避免拖入自身:透過 onWillAcceptWithDetails 判斷,防止把項目拖到自己身上。
  • 順序即時更新:onAcceptWithDetails 重新排列行程,拖曳完成即時更新資料。

這些細節讓拖曳操作自然、直覺。


今日成果

  • 長按行程即可拖曳
  • 拖曳過程即時預覽位置
  • 放開即可重新排序
  • 支援子行程拖曳
主行程拖曳 子行程拖曳

上一篇
Day 12 - 資料模型與狀態管理串接:從 Model 到功能驗證
下一篇
Day 14 - 從 Prompt 到程式碼:API 串接前的測試與準備
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言