HarmonyOS 鸿蒙Next中利用 drag 事件实现跨容器拖拽排序

HarmonyOS 鸿蒙Next中利用 drag 事件实现跨容器拖拽排序 想要实现一个经典的“两栏任务看板”拖拽排序功能。可以将任务卡片从“待办”列拖拽到“已完成”列,并实时更新数据源,应该如何使用drag 事件来实现呢?

3 回复

实现思路

首先定义 TaskData 类,包含任务的唯一 ID、标题和当前状态。

其次,我们使用 Row 容器并排展示两个 Column 容器(分别代表“待办”和“已完成”)。

再然后我们完成拖拽,在任务卡片上绑定 onDragStart。当长按并拖动时,记录被拖拽任务的信息(ID),并通过 dragPreview 设置拖拽时的缩略图样式。在目标 Column 容器上绑定 onDrop。当卡片被拖入并释放时,触发 onDrop。

最后在回调中获取 draggingId,修改对应数据的 status 属性,从而实现数据在不同列表间的移动。

应用场景

最常见的就是在任务看板视图中,任务在“待处理”、“进行中”、“已完成”之间流转。

实现效果

cke_4173.png

完整代码

import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';

class TaskData {
  id: string;
  title: string;
  status: string;

  constructor(title: string, status: string) {
    this.id = `task_${Math.random().toString(36).substr(2, 9)}`;
    this.title = title;
    this.status = status;
  }
}

@Builder
function DragPreviewBuilder(text: string) {
  Text(text)
    .padding(12)
    .backgroundColor('#0A59F7')
    .fontColor(Color.White)
    .borderRadius(8)
    .fontSize(14)
    .shadow({ radius: 10, color: '#40000000' })
}


interface DragStartEventTarget {
  startDrag(data: string | ESObject | undefined, callback: AsyncCallback<void>): void;
}

@Entry
@Component
struct DragDropFinalStrict {
  @State taskList: TaskData[] = [
    new TaskData("长按拖动我 1", "todo"),
    new TaskData("长按拖动我 2", "todo"),
    new TaskData("任务 3", "done")
  ];

  @State draggingId: string = '';

  build() {
    Column() {
      Text("拖拽演示")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      Row() {
        this.TaskColumn("待办", "todo", this.taskList.filter(t => t.status === 'todo'))
        this.TaskColumn("完成", "done", this.taskList.filter(t => t.status === 'done'))
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
    .padding(20)
  }

  @Builder
  TaskColumn(title: string, targetStatus: string, items: TaskData[]) {
    Column() {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 15 })

      Column() {
        ForEach(items, (item: TaskData) => {
          Row() {
            Text(item.title)
              .fontSize(16)
              .layoutWeight(1)
          }
          .width('100%')
          .padding(16)
          .margin({ bottom: 10 })
          .backgroundColor(this.draggingId === item.id ? '#E0E0E0' : Color.White)
          .borderRadius(8)
          .shadow({ radius: 4, color: '#1A000000', offsetX: 0, offsetY: 2 })
          .opacity(this.draggingId === item.id ? 0.4 : 1)
          .animation({ duration: 200 })
          .hitTestBehavior(HitTestMode.Transparent)

          .gesture(
            LongPressGesture({ repeat: false, duration: 200 })
              .onAction((event: GestureEvent) => {
                console.info("Long press detected, starting drag...");
                this.draggingId = item.id;

                try {
                  const target = event.target as object as DragStartEventTarget;

                  target.startDrag(
                    JSON.stringify({ id: item.id }),
                    (err: BusinessError) => {
                      if (err) {
                        console.error(`startDrag failed, code: ${err.code}, message: ${err.message}`);
                      } else {
                        console.info("startDrag success");
                      }
                    }
                  );
                } catch (e) {
                  console.error("startDrag exception: " + JSON.stringify(e));
                }
              })
          )

          .onDragStart((event: DragEvent) => {
            console.info(`onDragStart callback: ${item.title}`);
            return {
              builder: DragPreviewBuilder.bind(this, item.title)
            };
          })
          .onDragEnd(() => {
            this.draggingId = '';
          })
        }, (item: TaskData) => item.id)
      }
      .width('100%')
      .layoutWeight(1)
      .padding(10)
      .backgroundColor('#E0E0E0')
      .borderRadius(12)
      .onDrop((event: DragEvent) => {
        console.info("onDrop triggered");
        if (this.draggingId) {
          const targetIndex = this.taskList.findIndex(t => t.id === this.draggingId);
          if (targetIndex !== -1 && this.taskList[targetIndex].status !== targetStatus) {
            this.taskList[targetIndex].status = targetStatus;
          }
        }
      })
    }
    .width('45%')
  }
}

更多关于HarmonyOS 鸿蒙Next中利用 drag 事件实现跨容器拖拽排序的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,可通过@Drag装饰器实现跨容器拖拽排序。
拖拽组件需设置draggable(true),接收容器使用onDrop事件处理放置逻辑。
通过DragEvent获取拖拽数据,利用insertChildremoveChild动态调整组件位置。
关键点:统一管理拖拽数据标识,确保跨容器数据传递准确。

在HarmonyOS Next中,利用ArkTS的拖拽事件(DragEvent)实现跨容器拖拽排序,可以通过以下步骤完成:

1. 定义数据模型与状态

首先,定义任务卡片的数据结构,并使用@State装饰器管理两列的任务列表,确保UI能响应数据变化。

@State todoList: Task[] = [...];
@State doneList: Task[] = [...];

2. 为可拖拽元素添加手势

在任务卡片的组件上添加Gesture组件的DragGesture,并设置onDragStart来初始化拖拽数据。通过DragEventsetData方法传递任务信息。

.gesture(
  DragGesture()
    .onDragStart((event: DragEvent) => {
      event.setData('task', JSON.stringify(task));
    })
)

3. 为容器添加拖拽响应

在两列的容器上添加onDragDroponDragEnter等事件监听:

  • onDragEnter:当拖拽元素进入容器时,更新视觉反馈(如高亮)。
  • onDragDrop:当拖拽元素在容器内释放时,处理数据更新逻辑。

4. 实现跨列数据交换

在目标容器的onDragDrop回调中:

  • 通过event.getData获取拖拽任务的数据。
  • 根据拖拽起始列和目标列,从原列表移除任务,并添加到目标列表。
  • 使用数组操作更新@State变量,触发UI重新渲染。

5. 实时更新数据源

由于两列列表均使用@State装饰,任何修改都会自动同步到UI。若需持久化,可在数据变更后调用后端接口或本地存储方法。

示例代码片段

// 拖拽释放处理函数
onDragDrop(event: DragEvent) {
  let taskData = event.getData('task');
  let task: Task = JSON.parse(taskData);
  
  // 从原列表移除
  this.todoList = this.todoList.filter(item => item.id !== task.id);
  // 添加到目标列表
  this.doneList.push(task);
}

注意事项

  • 拖拽过程中可使用onDragEnter/onDragLeave优化视觉体验。
  • 跨容器拖拽时需注意事件冒泡和容器层级。
  • 复杂场景可结合@Provide/@Consume或全局状态管理共享数据。

以上方案通过ArkTS拖拽事件与状态管理,可实现高效的跨容器拖拽排序功能。

回到顶部