(创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,还请三连支持一波哇ヾ(@^∇^@)ノ)
目录
拖拽事件
概述
拖拽流程
手势拖拽
鼠标拖拽
拖拽背板图
开发步骤
通用拖拽适配
多选拖拽适配
手势事件
gesture(常规手势绑定方法)
priorityGesture(带优先级的手势绑定方法)
parallelGesture(并行手势绑定方法)
单一手势
点击手势(TapGesture)
长按手势(LongPressGesture)
拖动手势(PanGesture)
拖拽事件
概述
拖拽框架提供了一种通过鼠标或手势触屏的方式传递数据,即从一个组件位置拖出数据,并拖入到另一个组件位置上进行响应,拖出一方提供数据,拖入一方接收和处理数据。该操作可以让用户方便地移动、复制或删除指定内容。
- 拖拽操作:在某个能够响应拖出的组件上长按并滑动触发的拖拽行为,当用户释放时,拖拽操作结束;
- 拖拽背景(背板):用户所拖动数据的形象化表示,开发者可通过onDragStart的CustomerBuilder或DragItemInfo设置,也可以通过dragPreview通用属性设置;
- 拖拽内容:拖动的数据,使用UDMF统一API UnifiedData 进行封装;
- 拖出对象:触发拖拽操作并提供数据的组件;
- 拖入目标:可接收并处理拖动数据的组件;
- 拖拽点:鼠标或手指等与屏幕的接触位置,是否进入组件范围的判定是以接触点是否进入范围进行判断。
拖拽流程
手势拖拽
对于手势长按触发拖拽的场景,发起拖拽前框架侧会对当前组件是否可拖拽进行校验,针对默认可拖出的组件(Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink)需要判断是否设置了draggable属性为true(若系统使能分层参数,则draggable默认为true),其他组件需要额外判断是否设置了onDragStart回调函数,在满足上述可拖拽条件下,长按大于等于500ms可触发拖拽,长按800ms开始做预览图的浮起动效。
手势拖拽(手指/手写笔)触发拖拽流程:
鼠标拖拽
鼠标拖拽属于即拖即走,只要鼠标左键在可拖拽的组件上按下并移动大于1vp就可触发拖拽。
当前支持应用内和跨应用拖拽,提供了多个回调事件供开发者感知拖拽状态并干预系统默认拖拽行为,具体如下:
回调事件 | 说明 |
---|---|
onDragStart | 支持拖出的组件产生拖出动作时触发。 该回调可以感知拖拽行为的发起,开发者可通过在 onDragStart 方法中设置拖拽所传递的数据以及自定义拖拽背板图。推荐开发者使用pixelmap的方式返回背板图,不推荐使用customBuilder的方式,会有额外的性能开销。 |
onDragEnter | 当拖拽活动的拖拽点进入组件范围内时触发,只有该组件监听了onDrop事件时,此回调才会被触发。 |
onDragMove | 拖拽点在组件范围内移动时触发;只有该组件监听了onDrop事件时,此回调才会被触发。 在此过程中可通过DragEvent中的setResult方法影响系统部分场景下的外观 1. 设置DragResult.DROP_ENABLED; 2. 设置DragResult.DROP_DISABLED。 |
onDragLeave | 拖拽点离开组件范围时触发;只有该组件监听了onDrop事件时,此回调才会被触发。 针对以下两种情况默认不会发送onDragLeave事件: 1. 父组件移动到子组件; 2. 目标组件与当前组件布局有重叠; API version 12开始可通过UIContext中的setDragEventStrictReportingEnabled方法严格触发onDragLeave事件。 |
onDrop | 当用户在组件范围内释放时触发,需在此回调中通过DragEvent中的setResult方法设置拖拽结果,否则在拖出方组件的onDragEnd方法中通过getResult方法只能拿到默认的处理结果DragResult.DRAG_FAILED。 该回调也是开发者干预系统默认拖入处理行为的地方,系统会优先执行开发者的onDrop回调,通过在回调中执行setResult方法来告知系统该如何处理所拖拽的数据; 1. 设置 DragResult.DRAG_SUCCESSFUL,数据完全由开发者自己处理,系统不进行处理; 2. 设置DragResult.DRAG_FAILED,数据不再由系统继续处理; 3. 设置DragResult.DRAG_CANCELED,系统也不需要进行数据处理; 4. 设置DragResult.DROP_ENABLED或DragResult.DROP_DISABLED会被忽略,同设置DragResult.DRAG_FAILED; |
onDragEnd | 当用户释放拖拽时,拖拽活动结束,发起拖出动作的组件会触发该回调。 |
onPreDrag | 绑定此事件的组件,当触发拖拽发起前的不同阶段时,触发回调。 开发者可以使用该方法监听PreDragStatus中的枚举在发起拖拽前的不同阶段准备数据。 1. ACTION_DETECTING_STATUS:拖拽手势启动阶段。(按下50ms时触发); 2. READY_TO_TRIGGER_DRAG_ACTION:拖拽准备完成,可发起拖拽阶段。(按下500ms时触发); 3. PREVIEW_LIFT_STARTED:拖拽浮起动效发起阶段。(按下800ms时触发); 4. PREVIEW_LIFT_FINISHED:拖拽浮起动效结束阶段。(浮起动效完全结束时触发); 5. PREVIEW_LANDING_STARTED:拖拽落回动效发起阶段。(落回动效发起时触发); 6. PREVIEW_LANDING_FINISHED:拖拽落回动效结束阶段。(落回动效结束时触发); 7. ACTION_CANCELED_BEFORE_DRAG:拖拽浮起落位动效中断。(已满足READY_TO_TRIGGER_DRAG_ACTION状态后,未达到动效阶段,手指抬手时触发)。 |
拖拽背板图
拖拽移动过程中显示的拖拽背板图,并非是组件本身,其是用户拖动数据的表示,开发者可以将其设置为任意可显示的图像。其中onDragStart 回调返回的customBuilder或pixelmap可以设置拖拽移动过程中的背板图,浮起图默认使用组件本身的截图;dragpreview属性设置的customBuilder或pixelmap可以设置浮起和拖拽过程的背板图;如果开发者没有配置背板图,则系统会默认取组件本身的截图作为浮起及拖拽过程中的背板图。
拖拽背板图当前支持设置透明度、圆角、阴影和模糊,具体用法见:拖拽控制
约束:
- 对于容器组件,如果内部内容通过position,offset等手段使得绘制区域超出了容器组件范围,则系统截图无法截取到范围之外的内容,此种情况下,如果一定要浮起及拖拽背板能够包含范围之外的内容,则可考虑通过扩大容器范围或自定义方式进行;
- 不管是使用自定义builder或是系统默认截图方式,截图都暂时无法应用scale、rotate等图形变换效果。
开发步骤
通用拖拽适配
如下以Image组件为例,介绍组件拖拽开发的基本步骤,以及开发中需要注意的事项。
组件使能拖拽
设置draggable属性为true,并设置onDragStart回调,回调中可以通过UDMF设置拖拽的数据,并返回自定义拖拽背板图;
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; Image($r('app.media.app_icon')) .width(100) .height(100) .draggable(true) .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: "this is extraInfo", }; // onDragStart回调函数中返回自定义拖拽背板图 return dragItemInfo; })
手势场景触发拖拽抵赖底层绑定的长按手势,若开发者在被拖拽组件上也绑定长按手势,则会与底层的长按手势发生竞争,导致拖拽失败。可以用并行手势解决解决此类问题,如下:
.parallelGesture(LongPressGesture().onAction(() => { promptAction.showToast({ duration: 100, message: 'Long press gesture trigger' }); }))
自定义拖拽背板图
自定义拖拽背板图的pixmap可以通过设置onPreDrag函数在长按50ms时触发的回调中前提准备;
.onPreDrag((status: PreDragStatus) => { if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) { this.getComponentSnapshot(); } })
具体pixmap的生成可以调用componentSnapshot.createFromBuilder函数;
@Builder pixelMapBuilder() { Column() { Image($r('app.media.startIcon')) .width(120) .height(120) .backgroundColor(Color.Yellow) } } private getComponentSnapshot(): void { componentSnapshot.createFromBuilder(()=>{this.pixelMapBuilder()}, (error: Error, pixmap: image.PixelMap) => { if(error){ console.log("error: " + JSON.stringify(error)) return; } this.pixmap = pixmap; }) }
多选拖拽适配
API version 12开始Grid组件和List组件中的GridItem和ListItem组件支持多选拖拽,当前只支持onDragStart的方式。如下以Grid为例,介绍多选拖拽的基本步骤,以及开发中的注意事项。
组件多选拖拽使能
创建GridItem子组件并绑定onDragStart函数。同时设置GridItem组件的状态是可选中的;
Grid() { ForEach(this.numbers, (idx: number) => { GridItem() { Column() .backgroundColor(this.colors[idx % 9]) .width(50) .height(50) .opacity(1.0) .id('grid'+idx) } .onDragStart(()=>{}) .selectable(true) }, (idx: string) => idx) }
多选拖拽功能默认为关闭状态,使用多选拖拽需要在dragPreviewOptions接口中的DragInteractionOptions参数中设置isMultiSelectionEnabled为true,表示当前组件是否多选。DragInteractionOptions也有支持组件浮起前默认效果的参数defaultAnimationBeforeLifting,设置该参数为true后组件在浮起前会有一个默认缩小动效。
.dragPreviewOptions({isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true})
为了保证选中状态,需要设置GridItem子组件selected状态为true。例如,可以通过onClick的调用去设置特定的组件为选中的状态。
.selected(this.isSelectedGrid[idx]) .onClick(()=>{ this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] })
多选拖拽性能优化
多选拖拽情况下,多选会有聚拢的动画效果,聚拢效果触发时,会给当前屏幕内显示的选中组件截图,当选中组件过多,会导致很高的性能开销。多选拖拽支持从dragPreview中获取截图来作为聚拢动效的截图,以节省性能。
.dragPreview({ pixelMap:this.pixmap })
截图的获取可以在选中组件时通过调用componentSnapshot中的get方法获取。如下通过获取组件对应id的方法进行截图。
@State previewData: DragItemInfo[] = [] @State isSelectedGrid: boolean[] = [] .onClick(()=>{ this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] if (this.isSelectedGrid[idx]) { let gridItemName = 'grid' + idx componentSnapshot.get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ this.pixmap = pixmap this.previewData[idx] = { pixelMap:this.pixmap } }) } })
数量角标适配
多选拖拽的数量角标当前需要应用使用dragPreviewOptions中的numberBadge参数设置,开发者需要根据当前选中的节点数量来设置数量角标。
@State numberBadge: number = 0; .onClick(()=>{ this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] if (this.isSelectedGrid[idx]) { this.numberBadge++; } else { this.numberBadge--; } }) // 多选场景右上角数量角标需要应用设置numberBadge参数 .dragPreviewOptions({numberBadge: this.numberBadge})
手势事件
通过给各个组件绑定不同的手势事件,并设计事件的响应方式,当手势识别成功时,ArkUI框架将通过事件回调通知组件手势识别的结果。
gesture(常规手势绑定方法)
.gesture(gesture: GestureType, mask?: GestureMask)
gesture为通用的一种手势绑定方法,可以将手势绑定到对应的组件上。
例如,可以将点击手势TapGesture通过gesture手势将方法绑定到Text组件上。
// xxx.ets @Entry @Component struct Index { build() { Column() { Text('Gesture').fontSize(28) // 采用gesture手势绑定方法绑定TapGesture .gesture( TapGesture() .onAction(() => { console.info('TapGesture is onAction'); })) } .height(200) .width(250) } }
priorityGesture(带优先级的手势绑定方法)
.priorityGesture(gesture: GestureType, mask?: GestureMask)
priorityGesture是带优先级的手势绑定方法,可以在组件上绑定优先识别的手势。
在默认情况下,当父组件和子组件使用gesture绑定同类型的手势时,子组件优先识别通过gesture绑定的手势。当父组件使用priorityGesture绑定与子组件同类型的手势时,父组件优先识别通过priorityGesture绑定的手势。
长按手势时,设置触发长按的最短时间小的组件会优先响应,会忽略priorityGesture设置。
例如,当父组件Column和子组件Text同时绑定TapGesture手势时,父组件以带优先级手势priorityGesture的形式进行绑定时,优先响应父组件绑定的TapGesture。
// xxx.ets @Entry @Component struct Index { build() { Column() { Text('Gesture').fontSize(28) .gesture( TapGesture() .onAction(() => { console.info('Text TapGesture is onAction'); })) } .height(200) .width(250) // 设置为priorityGesture时,点击文本区域会忽略Text组件的TapGesture手势事件,优先响应父组件Column的TapGesture手势事件 .priorityGesture( TapGesture() .onAction(() => { console.info('Column TapGesture is onAction'); }), GestureMask.IgnoreInternal) } }
parallelGesture(并行手势绑定方法)
.parallelGesture(gesture: GestureType, mask?: GestureMask)
parallelGesture是并行的手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。
在默认情况下,手势事件为非冒泡事件,当父子组件绑定相同的手势时,父子组件绑定的手势事件会发生竞争,最多只有一个组件的手势事件能够获得响应。而当父组件绑定了并行手势parallelGesture时,父子组件相同的手势事件都可以触发,实现类似冒泡效果。
// xxx.ets @Entry @Component struct Index { build() { Column() { Text('Gesture').fontSize(28) .gesture( TapGesture() .onAction(() => { console.info('Text TapGesture is onAction'); })) } .height(200) .width(250) // 设置为parallelGesture时,点击文本区域会同时响应父组件Column和子组件Text的TapGesture手势事件 .parallelGesture( TapGesture() .onAction(() => { console.info('Column TapGesture is onAction'); }), GestureMask.Normal) } }
单一手势
点击手势(TapGesture)
TapGesture(value?:{count?:number, fingers?:number})
点击手势支持单次点击和多次点击,拥有两个可选参数:
count:声明该点击手势识别的连续点击次数。默认值为1,若设置小于1的非法值会被转化为默认值。如果配置多次点击,上一次抬起和下一次按下的超时时间为300毫秒。
fingers:用于声明触发点击的手指数量,最小值为1,最大值为10,默认值为1。当配置多指时,若第一根手指按下300毫秒内未有足够的手指数按下则手势识别失败。
以在Text组件上绑定双击手势(count值为2的点击手势)为例:
// xxx.ets @Entry @Component struct Index { @State value: string = ""; build() { Column() { Text('Click twice').fontSize(28) .gesture( // 绑定count为2的TapGesture TapGesture({ count: 2 }) .onAction((event: GestureEvent|undefined) => { if(event){ this.value = JSON.stringify(event.fingerList[0]); } })) Text(this.value) } .height(200) .width(250) .padding(20) .border({ width: 3 }) .margin(30) } }
长按手势(LongPressGesture)
LongPressGesture(value?:{fingers?:number, repeat?:boolean, duration?:number})
长按手势用于触发长按手势事件,拥有三个可选参数:
fingers:用于声明触发长按手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
repeat:用于声明是否连续触发事件回调,默认值为false。
duration:用于声明触发长按所需的最短时间,单位为毫秒,默认值为500。
以在Text组件上绑定可以重复触发的长按手势为例:
// xxx.ets @Entry @Component struct Index { @State count: number = 0; build() { Column() { Text('LongPress OnAction:' + this.count).fontSize(28) .gesture( // 绑定可以重复触发的LongPressGesture LongPressGesture({ repeat: true }) .onAction((event: GestureEvent|undefined) => { if(event){ if (event.repeat) { this.count++; } } }) .onActionEnd(() => { this.count = 0; }) ) } .height(200) .width(250) .padding(20) .border({ width: 3 }) .margin(30) } }
拖动手势(PanGesture)
PanGesture(value?:{ fingers?:number, direction?:PanDirection, distance?:number})
拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:
fingers:用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。
distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。
以在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动
// xxx.ets @Entry @Component struct Index { @State offsetX: number = 0; @State offsetY: number = 0; @State positionX: number = 0; @State positionY: number = 0; build() { Column() { Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY) .fontSize(28) .height(200) .width(300) .padding(20) .border({ width: 3 }) // 在组件上绑定布局位置信息 .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) .gesture( // 绑定拖动手势 PanGesture() .onActionStart((event: GestureEvent|undefined) => { console.info('Pan start'); }) // 当触发拖动手势时,根据回调函数修改组件的布局位置信息 .onActionUpdate((event: GestureEvent|undefined) => { if(event){ this.offsetX = this.positionX + event.offsetX; this.offsetY = this.positionY + event.offsetY; } }) .onActionEnd(() => { this.positionX = this.offsetX; this.positionY = this.offsetY; }) ) } .height(200) .width(250) } }
说明
大部分可滑动组件,如List、Grid、Scroll、Tab等组件是通过PanGesture实现滑动,在组件内部的子组件绑定拖动手势(PanGesture)或者滑动手势(SwipeGesture)会导致手势竞争。
当在子组件绑定PanGesture时,在子组件区域进行滑动仅触发子组件的PanGesture。如果需要父组件响应,需要通过修改手势绑定方法或者子组件向父组件传递消息进行实现,或者通过修改父子组件的PanGesture参数distance使得拖动更灵敏。当子组件绑定SwipeGesture时,由于PanGesture和SwipeGesture触发条件不同,需要修改PanGesture和SwipeGesture的参数以达到所需效果。
不合理的阈值设置会导致滑动不跟手(响应时延慢)的问题。