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 通用拖曳适配效果
ICP备案:
公安联网备案: