【HarmonyOS】应用开发拖拽功能详解
【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 方法设置传递的统一数据:
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 源码
拖拽功能实现
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;@BuilderpixelMapBuilder() {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%')}
}