ArkUI拖曳事件详解(附带实例)
拖曳框架提供了一种通过鼠标或手势触屏传递数据的方式,即从一个组件位置拖出数据,并将它拖入另一个组件位置上进行响应。拖出一方提供数据,拖入一方接收和处理数据。该操作可以让用户方便地移动、复制或删除指定内容。
拖曳事件主要包括以下几个概念:
当满足上述可拖曳条件时,长按大于或等于 500ms 即可触发拖曳,长按 800ms 后开始进行预览图的浮起动效。
当拖曳操作与菜单功能一起使用,并通过 isShow 方式控制菜单显隐时,不建议在用户操作了 800ms 后再控制菜单显示,因为这可能会导致非预期的行为。
手势拖曳(手指/手写笔)触发拖曳的流程如下图所示:

图 1 手势拖曳触发拖曳流程
当前支持应用内和跨应用的鼠标拖曳,提供了多个回调事件供开发者感知拖曳状态,并干预系统默认的拖曳行为,具体如下表所示。
DragEvent 支持 get 方法获取拖曳行为的相关信息,下表列出了 get 方法在对应拖曳回调中是否能返回有效数据。
DragEvent 支持 set 方法向系统传递信息,这些信息部分会影响系统对 UI 或数据的处理方式。下表列出了 set 方法应该在回调的哪个阶段执行才会被系统接收并处理。
onDragStart 回调返回的 customBuilder 或 pixelmap 可以设置拖曳移动过程中的背板图,浮起图默认使用组件本身的截图;dragpreview 属性设置的 customBuilder 或 pixelmap 可以设置浮起和拖曳过程的背板图。如果开发者没有配置背板图,则系统会默认取组件本身的截图作为浮起及拖曳过程中的背板图。拖曳背板图当前支持设置透明度、圆角、阴影和模糊效果。
对于容器组件,如果内部内容通过 position、offset 等手段使得绘制区域超出了容器组件范围,那么系统无法截取到范围之外的内容。在此情况下,如果一定要让浮起及拖曳背板包含范围之外的内容,则可考虑通过扩大容器范围或使用自定义方式。不管是使用自定义 builder 还是系统默认的截图方式,截图都暂时无法应用 scale、rotate 等图形变换效果。
自定义拖曳背板图的 pixmap 可以通过 onPreDrag 回调函数来设置。该回调函数会在长按 50ms 时触发,开发者可以在此时准备拖曳时所需的背板图和其他相关数据。
如果开发者希望严格控制 onDragLeave 事件的触发,则可以通过调用 setDragEventStrictReportingEnabled 方法来启用此功能。启用后,onDragLeave 事件会在拖曳元素离开目标区域时准确触发。通过设置 allowDrop 属性,开发者可以定义组件允许接收的数据类型,并据此控制拖曳时的角标显示:
此外,在实现 onDrop 回调的情况下,还可以通过在 onDragMove 中设置 DragResult为DROP_ENABLED,并设置 DragBehavior 为 COPY 或 MOVE,来控制角标的显示。
要处理拖曳数据,开发者需要设置 onDrop 回调,并在该回调中处理接收到的拖曳数据,同时显示设置拖曳结果。
数据的传递是通过 UDMF 实现的,在数据较大时可能存在时延,因此在首次获取数据失败时建议加 1500ms 的延迟重试机制,拖曳发起方可以通过设置 onDragEnd 回调感知拖曳结果。
具体的代码实现如下:

图 2 通用拖曳适配效果
拖曳事件主要包括以下几个概念:
- 拖曳操作:在某个能够响应拖出的组件上长按并滑动触发的拖曳行为,当用户释放时,拖曳操作结束;
- 拖曳背景:用户所拖动数据的形象化表示,开发者可通过 onDragStart 的 CustomerBuilder 或 DragItemInfo 设置,也可以通过 dragPreview 通用属性设置;
- 拖曳内容:拖动的数据,使用 UDMF(Unified Data Management Framework,统一数据管理框架)统一 API UnifiedData 进行封装;
- 拖出对象:触发拖曳操作并提供数据的组件;
- 拖入目标:可接收并处理拖动数据的组件;
- 拖曳点:鼠标或手指等与屏幕的接触位置,是否进入组件范围的判定是以接触点是否进入范围来判断的。
手势拖曳
对于手势长按触发拖曳的场景,在发起拖曳前,框架会对当前组件是否可拖曳进行校验:- 针对默认可拖曳的组件(Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink),需要判断其是否已将 draggable 属性设置为 true(若系统启用分层参数,则 draggable 默认为 true);
- 其他组件则需额外判断是否设置了 onDragStart 回调函数。
当满足上述可拖曳条件时,长按大于或等于 500ms 即可触发拖曳,长按 800ms 后开始进行预览图的浮起动效。
当拖曳操作与菜单功能一起使用,并通过 isShow 方式控制菜单显隐时,不建议在用户操作了 800ms 后再控制菜单显示,因为这可能会导致非预期的行为。
手势拖曳(手指/手写笔)触发拖曳的流程如下图所示:

图 1 手势拖曳触发拖曳流程
鼠标拖曳
鼠标拖曳属于即拖即走,只要鼠标左键在可拖曳的组件上按下并移动大于 1vp,即可触发拖曳。当前支持应用内和跨应用的鼠标拖曳,提供了多个回调事件供开发者感知拖曳状态,并干预系统默认的拖曳行为,具体如下表所示。
回调事件 | 说明 |
---|---|
onDragStart | 当可拖拽的组件产生拖拽动作时触发。该回调可以感知拖拽行为的发起,开发者可在 onDragStart 方法中设置拖拽传递的数据以及自定义拖拽背板图。推荐开发者使用 pixelmap 的方式返回背板图,不推荐使用 customBuilder 的方式,因为后者会有额外的性能开销。 |
onDragEnter | 当拖拽活动的拖拽点进入组件范围内时触发,只有在该组件监听了 onDrop 事件时,此回调才会被触发。 |
onDragMove | 当拖拽点在组件范围内移动时触发;只有在该组件监听了 onDrop 事件时,此回调才会被触发。在此过程中可通过 DragEvent 中的 setResult 方法影响系统在部分场景下的外观。当在此事件中将 DragEvent 的结果设置为 DragResult.DROP_ENABLED 时,表示当前组件可以接收被拖拽的元素;将 DragEvent 的结果设置为 DragResult.DROP_DISABLED 时,表示当前组件不允许接收被拖拽的元素。 |
onDragLeave | 当拖拽点离开组件范围时触发;只有在该组件监听了 onDrop 事件时,此回调才会被触发。针对以下两种情况默认不会发送 onDragLeave 事件:①父组件移动到子组件;②目标组件与当前组件布局有重叠。从 API version 12 开始,可通过 UIContext 中的 setDragEventStrictReportingEnabled 方法严格触发 onDragLeave 事件。 |
onDrop |
当用户在组件范围内释放拖拽的内容时触发。需在此回调中通过 DragEvent 中的 setResult 方法设置拖拽结果,否则在拖出方组件的 onDragEnd 方法中通过 getResult 方法只能拿到默认的处理结果 DragResult.DRAG_FAILED。该回调也是开发者干预系统默认拖入处理行为的地方,系统会优先执行开发者的 onDrop 回调,并通过在该回调中执行 setResult 方法来告知系统如何处理所拖拽的数据:
|
onDragEnd | 当用户释放拖拽时,拖拽活动结束,发起拖出动作的组件会触发该回调。 |
onPreDrag |
绑定此事件的组件,在拖拽开始前的不同阶段,会触发该回调。开发者可以使用该回调监听 PreDragStatus 类型参数值,在发起拖拽前的不同阶段准备不同的数据:
|
DragEvent 支持 get 方法获取拖曳行为的相关信息,下表列出了 get 方法在对应拖曳回调中是否能返回有效数据。
回调事件 | onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop | onDragEnd |
---|---|---|---|---|---|---|
getData | - | - | - | - | 支持 | - |
getSummary | - | 支持 | 支持 | 支持 | 支持 | - |
getResult | - | - | - | - | - | 支持 |
getPreviewRect | 支持 | - | - | - | 支持 | - |
getVelocity/X/Y | - | 支持 | 支持 | 支持 | 支持 | - |
getWindowX/Y | 支持 | 支持 | 支持 | 支持 | 支持 | - |
getDisplayX/Y | 支持 | 支持 | 支持 | 支持 | 支持 | - |
getX/Y | 支持 | 支持 | 支持 | 支持 | 支持 | - |
behavior | - | - | - | - | - | 支持 |
DragEvent 支持 set 方法向系统传递信息,这些信息部分会影响系统对 UI 或数据的处理方式。下表列出了 set 方法应该在回调的哪个阶段执行才会被系统接收并处理。
回调事件 | onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop |
---|---|---|---|---|---|
useCustomDropAnimation | - | - | - | - | 支持 |
setData | 支持 | - | - | - | - |
setResult | 支持,可通过 set failed 或 cancel 来阻止拖曳的发起 | 支持,不作为最终结果传递给 onDragEnd | 支持,不作为最终结果传递给 onDragEnd | 支持,不作为最终结果传递给 onDragEnd | 支持,作为最终结果传递给 onDragEnd |
behavior | - | 支持 | 支持 | 支持 | 支持 |
拖曳背板图
拖曳移动过程中显示的拖曳背板图,并非组件本身,而是用户拖动数据的表示,开发者可以将其设置为任意可显示的图像。onDragStart 回调返回的 customBuilder 或 pixelmap 可以设置拖曳移动过程中的背板图,浮起图默认使用组件本身的截图;dragpreview 属性设置的 customBuilder 或 pixelmap 可以设置浮起和拖曳过程的背板图。如果开发者没有配置背板图,则系统会默认取组件本身的截图作为浮起及拖曳过程中的背板图。拖曳背板图当前支持设置透明度、圆角、阴影和模糊效果。
对于容器组件,如果内部内容通过 position、offset 等手段使得绘制区域超出了容器组件范围,那么系统无法截取到范围之外的内容。在此情况下,如果一定要让浮起及拖曳背板包含范围之外的内容,则可考虑通过扩大容器范围或使用自定义方式。不管是使用自定义 builder 还是系统默认的截图方式,截图都暂时无法应用 scale、rotate 等图形变换效果。
通用拖曳适配
要启用组件的拖曳功能,需把 draggable 属性设置为 true,并设置 onDragStart 回调,回调中可以通过 UDMF 设置拖曳的数据,并返回自定义拖曳背板图。手势场景触发拖曳依赖底层绑定的长按手势,若开发者在被拖曳组件上也绑定了长按手势,则会与底层的长按手势发生竞争,导致拖曳失败。开发者可以使用并行手势来解决此类问题。自定义拖曳背板图的 pixmap 可以通过 onPreDrag 回调函数来设置。该回调函数会在长按 50ms 时触发,开发者可以在此时准备拖曳时所需的背板图和其他相关数据。
如果开发者希望严格控制 onDragLeave 事件的触发,则可以通过调用 setDragEventStrictReportingEnabled 方法来启用此功能。启用后,onDragLeave 事件会在拖曳元素离开目标区域时准确触发。通过设置 allowDrop 属性,开发者可以定义组件允许接收的数据类型,并据此控制拖曳时的角标显示:
- 当拖曳数据是定义允许落入的数据类型时,显示 COPY 角标;
- 当拖曳数据不是定义允许落入的数据类型时,显示 FORBIDDEN 角标;
- 未设置 allowDrop 时,显示 MOVE 角标。
此外,在实现 onDrop 回调的情况下,还可以通过在 onDragMove 中设置 DragResult为DROP_ENABLED,并设置 DragBehavior 为 COPY 或 MOVE,来控制角标的显示。
要处理拖曳数据,开发者需要设置 onDrop 回调,并在该回调中处理接收到的拖曳数据,同时显示设置拖曳结果。
数据的传递是通过 UDMF 实现的,在数据较大时可能存在时延,因此在首次获取数据失败时建议加 1500ms 的延迟重试机制,拖曳发起方可以通过设置 onDragEnd 回调感知拖曳结果。
具体的代码实现如下:
// 入口组件 @Entry @Component struct Demo0601 { // 目标图片 URI @State targetImage: string = ''; // 图片显示大小(宽高相同) @State imageSize: number = 100; // 图片可见状态 @State imgState: Visibility = Visibility.Visible; // 拖拽时使用的自定义像素图 @State pixmap: image.PixelMap | undefined = undefined; /* ========= 自定义 Builder:用于生成拖拽背板图 ========= */ @Builder pixelMapBuilder() { Column() { Image($r('app.media.background')) .width(120) .height(120) .backgroundColor(Color.Yellow); } } /* ========= UDMF 数据获取:带 1500 ms 重试机制 ========= */ // 重试获取 UDMF 数据 getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void): boolean { try { let data: UnifiedData = event.getData(); if (!data) { return false; } let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords(); if (!records || records.length <= 0) { return false; } callback(event); return true; } catch (e) { console.log('获取数据异常:' + (e as BusinessError).message); return false; } } // 首次获取失败后延迟 1500 ms 再次尝试 getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) { if (this.getDataFromUdmfRetry(event, callback)) { return; } setTimeout(() => { this.getDataFromUdmfRetry(event, callback); }, 1500); } /* ========= 提前生成拖拽背板像素图 ========= */ // 调用 createFromBuilder 获取自定义 builder 的截图 private getComponentSnapshot(): void { this.getUIContext() .getComponentSnapshot() .createFromBuilder( () => this.pixelMapBuilder(), (error: Error, pixmap: image.PixelMap) => { if (error) { return; } this.pixmap = pixmap; } ); } // 长按 50 ms 时提前准备自定义截图 private PreDragChange(preDragStatus: PreDragStatus): void { if (preDragStatus === PreDragStatus.ACTION_DETECTING_STATUS) { this.getComponentSnapshot(); } } /* ========= UI 构建 ========= */ build() { Row() { Column() { Text('开始进行拖拽'); /* ------------ 拖拽源组件 ------------ */ Row() { Image($r('app.media.background')) .width(this.imageSize) .height(this.imageSize) .visibility(this.imgState) // 并行手势:允许同时响应系统拖拽与自定义长按 .parallelGesture( LongPressGesture() .onAction(() => { promptAction.showToast({ duration: 100, message: '长按手势进行拖拽' }); }) ) // 拖拽开始 .onDragStart((event) => { // 构造拖拽数据 let data: unifiedDataChannel.Image = new unifiedDataChannel.Image(); data.imageUri = 'common/harmonyos.jpg'; let unifiedData = new unifiedDataChannel.UnifiedData(data); event.setData(unifiedData); // 返回自定义拖拽背板 let dragItemInfo: DragItemInfo = { pixelMap: this.pixmap, extraInfo: '额外信息' }; return dragItemInfo; }) // 提前准备背板 .onPreDrag((status: PreDragStatus) => { this.PreDragChange(status); }) // 拖拽结束 .onDragEnd((event) => { // 接收方 onDrop 中设置的 result 会在此拿到 if (event.getResult() === DragResult.DRAG_SUCCESSFUL) { promptAction.showToast({ duration: 100, message: '拖拽成功' }); } else if (event.getResult() === DragResult.DRAG_FAILED) { promptAction.showToast({ duration: 100, message: '拖拽失败' }); } }); } Text('拖拽的目标区域'); /* ------------ 拖拽目标组件 ------------ */ Row() { Image(this.targetImage) .width(this.imageSize) .height(this.imageSize) .draggable(true) .margin({ left: 15 }) .border({ color: Color.Black, width: 1 }) // 拖拽进入目标区域时行为 .onDragMove((event) => { // 控制角标显示类型为 MOVE(即不额外显示角标) event.setResult(DragResult.DROP_ENABLED); event.dragBehavior = DragBehavior.MOVE; }) // 允许接收的数据类型 .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE]) // 接收并处理拖拽数据 .onDrop((dragEvent?: DragEvent) => { this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => { let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords(); let rect: Rectangle = event.getPreviewRect(); this.imageSize = Number(rect.width); this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri; this.imgState = Visibility.None; // 显式设置成功结果,回传给拖出方的 onDragEnd event.setResult(DragResult.DRAG_SUCCESSFUL); }); }); } }.width('100%') } } }代码运行效果如下图所示:

图 2 通用拖曳适配效果