【HarmonyOS】应用开发拖拽功能详解
一、前言
拖拽交互本质上是一种通过鼠标或手势触屏传递数据的机制,用户可以从一个组件位置拖出数据并将其拖入到另一个组件位置,从而触发相应的响应。
在鸿蒙 中,ArkUI 框架对拖拽功能提供了完整的支持,从基础的单组件拖拽到复杂的多选拖拽、跨应用数据传递,再到自定义动效和悬停检测,形成了一套完善的解决方案。系统级别的支持,让我们对复杂的拖拽功能实现,非常容易就可完成。
二、拖拽事件流程与实现步骤
1、核心流程
因为鸿蒙是面向多设备的操作系统,拖拽流程主要包含手势拖拽 和鼠标拖拽 两种模式,两者在触发条件和交互细节上略有不同,但核心逻辑一致。 整个拖拽过程: (1)拖拽操作 :长按并滑动触发,释放时结束 对于手势操作,当用户在可拖拽组件上长按超过 500ms 时会触发拖拽,长按 800ms 时系统会执行预览图的浮起动效。
而鼠标拖拽则遵循"即拖即走"模式,当鼠标左键在可拖拽组件上按下并移动超过 1vp 时,即可触发拖拽功能。
(2)拖拽背板 :拖动数据时的可视化表示,可自定义 (3)拖拽内容 :使用 UDMF 统一数据框架封装,确保数据一致性和安全性 (4)拖出对象 :触发拖拽并提供数据的组件 (5)拖入目标:接收并处理拖拽数据的组件
2、回调事件
ArkUI 提供了一系列回调事件,帮助开发者感知拖拽状态并调整系统默认行为:
回调事件 | 说明 |
---|---|
onDragStart | 拖出动作开始时触发,可设置传递数据和自定义背板图 |
onDragEnter` | 拖拽点进入组件范围时触发(组件监听了onDrop) |
onDragMove | 拖拽点在组件范围内移动时触发 |
onDragLeave | 拖拽点移出组件范围时触发 |
onDrop | 用户在组件范围内释放拖拽时触发,需设置拖拽结果 |
onDragEnd | 拖拽活动终止时触发,可获取最终结果 |
onPreDrag | 拖拽开始前的不同阶段触发,可准备相关数据 |
3、数据传递与背板定制
拖拽数据通过 UDMF(用户数据管理框架)进行封装,确保跨组件和跨应用的数据一致性。在 onDragStart 回调中,可通过 setData 方法设置传递的统一数据:
typescript
onDragStart((event) => {
let data: unifiedDataChannel.Image = new unifiedDataChannel.Image();
data.imageUri = 'common/pic/img.png';
let unifiedData = new unifiedDataChannel.UnifiedData(data);
event.setData(unifiedData);
})
三、DEMO 源码
拖拽功能实现
typescript
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { promptAction } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
@Entry
@Component
struct DragAndDropDemo {
@State targetImage: string = '';
@State imageWidth: number = 100;
@State imageHeight: number = 100;
@State imgState: Visibility = Visibility.Visible;
@State pixmap: image.PixelMap | undefined = undefined;
@Builder
pixelMapBuilder() {
Column() {
Image($r('app.media.startIcon'))
.width(120)
.height(120)
.backgroundColor(Color.Yellow)
}
}
// 获取UDMF数据,包含重试机制
getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
try {
let data: unifiedDataChannel.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("getData failed, message: " + (e as Error).message);
return false;
}
}
getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
if (this.getDataFromUdmfRetry(event, callback)) {
return;
}
setTimeout(() => {
this.getDataFromUdmfRetry(event, callback);
}, 1500);
}
// 生成自定义背板图
private getComponentSnapshot(): void {
this.getUIContext().getComponentSnapshot().createFromBuilder(() => {
this.pixelMapBuilder();
}, (error: Error, pixmap: image.PixelMap) => {
if (error) {
console.log("error: " + JSON.stringify(error));
return;
}
this.pixmap = pixmap;
})
}
// 长按准备阶段处理
private preDragChange(preDragStatus: PreDragStatus): void {
if (preDragStatus === PreDragStatus.ACTION_DETECTING_STATUS) {
this.getComponentSnapshot();
}
}
build() {
Row() {
Column() {
Text('拖动源')
.fontSize(18)
.width('100%')
.height(40)
.margin(10)
.backgroundColor('#008888')
Row() {
Image($r('app.media.app_icon'))
.width(100)
.height(100)
.draggable(true)
.margin({ left: 15 })
.visibility(this.imgState)
// 平行手势处理长按冲突
.parallelGesture(LongPressGesture().onAction(() => {
this.getUIContext().getPromptAction().showToast({
duration: 100,
message: '长按手势触发'
});
}))
.onDragStart((event) => {
let data: unifiedDataChannel.Image = new unifiedDataChannel.Image();
data.imageUri = 'common/pic/img.png';
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) => {
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
this.getUIContext().getPromptAction().showToast({
duration: 100,
message: '拖拽成功'
});
} else if (event.getResult() === DragResult.DRAG_FAILED) {
this.getUIContext().getPromptAction().showToast({
duration: 100,
message: '拖拽失败'
});
}
})
}
}
Column() {
Text('目标区域')
.fontSize(20)
.width('100%')
.height(40)
.margin(10)
.backgroundColor('#008888')
Row() {
Image(this.targetImage)
.width(this.imageWidth)
.height(this.imageHeight)
.draggable(true)
.margin({ left: 15 })
.border({ color: Color.Black, width: 1 })
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDragMove((event) => {
event.setResult(DragResult.DROP_ENABLED);
event.dragBehavior = DragBehavior.MOVE;
})
.onDrop((dragEvent?: DragEvent) => {
this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();
let rect: Rectangle = event.getPreviewRect();
this.imageWidth = Number(rect.width);
this.imageHeight = Number(rect.height);
this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri;
this.imgState = Visibility.None;
event.setResult(DragResult.DRAG_SUCCESSFUL);
});
})
}
}
}
.height('100%')
}
}