1# 支持统一拖拽 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiangtao92--> 5<!--Designer: @piggyguy--> 6<!--Tester: @songyanhong--> 7<!--Adviser: @HelloCrease--> 8 9统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出(drag)数据并将其拖入(drop)到另一个组件位置,以触发响应。在这一过程中,拖出方提供数据,而拖入方负责接收和处理数据。这一操作使用户能够便捷地移动、复制或删除指定内容。 10 11## 基本概念 12 13* 拖拽操作:在可响应拖出的组件上长按并滑动以触发拖拽行为,当用户释放手指或鼠标时,拖拽操作即告结束。 14* 拖拽背景(背板):用户拖动数据时的形象化表示。开发者可以通过[onDragStart](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragstart)的[CustomerBuilder](../reference/apis-arkui/arkui-ts/ts-types.md#custombuilder8)或[DragItemInfo](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragiteminfo)进行设置,也可以通过[dragPreview](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#dragpreview11)通用属性进行自定义。 15* 拖拽内容:被拖动的数据,使用UDMF统一API [UnifiedData](../reference/apis-arkdata/js-apis-data-unifiedDataChannel.md#unifieddata) 进行封装,确保数据的一致性和安全性。 16* 拖出对象:触发拖拽操作并提供数据的组件,通常具有响应拖拽的特性。 17* 拖入目标:可接收并处理拖动数据的组件,能够根据拖入的数据执行相应的操作。 18* 拖拽点:鼠标或手指与屏幕的接触位置,用于判断是否进入组件范围。判定依据是接触点是否位于组件的范围内。 19 20## 拖拽流程 21 22拖拽流程包含手势拖拽流程和鼠标拖拽流程,有助于开发者理解回调事件触发的时机。 23 24### 手势拖拽流程 25 26对于手势长按触发拖拽的场景,ArkUI在发起拖拽前会校验当前组件是否具备拖拽功能。对于具备默认可拖出能力的组件([Search](../reference/apis-arkui/arkui-ts/ts-basic-components-search.md)、[TextInput](../reference/apis-arkui/arkui-ts/ts-basic-components-textinput.md)、[TextArea](../reference/apis-arkui/arkui-ts/ts-basic-components-textarea.md)、[RichEditor](../reference/apis-arkui/arkui-ts/ts-basic-components-richeditor.md)、[Text](../reference/apis-arkui/arkui-ts/ts-basic-components-text.md)、[Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md)、[Hyperlink](../reference/apis-arkui/arkui-ts/ts-container-hyperlink.md))需要判断是否设置了[draggable](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#draggable)为true(系统通过[系统资源](../quick-start/resource-categories-and-access.md#系统资源)初始化具备默认可拖出能力的组件的[draggable](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#draggable)属性默认值)。其他组件则需额外确认是否已设置onDragStart回调函数。在满足上述条件后,长按时间达到或超过500ms即可触发拖拽,而长按800ms时,系统开始执行预览图的浮起动效。若与Menu功能结合使用,并通过isShow控制其显示与隐藏,建议避免在用户操作800ms后才控制菜单显示,此举可能引发非预期的行为。 27 28手势拖拽(手指/手写笔)触发拖拽流程: 29 30 31 32### 鼠标拖拽流程 33 34鼠标拖拽操作遵循即拖即走的模式,当鼠标左键在可拖拽的组件上按下并移动超过1vp时,即可触发拖拽功能。 35 36当前不仅支持应用内部的拖拽,还支持跨应用的拖拽操作。为了帮助开发者更好地感知拖拽状态并调整系统默认的拖拽行为,ArkUI提供了多个回调事件,具体详情如下: 37 38| **回调事件** | **说明**| 39| ---------------- | ------------------------| 40| [onDragStart](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragstart) | 拖出的组件产生拖出动作时,该回调触发。<br>该回调可以感知拖拽行为的发起,开发者可以在onDragStart方法中设置拖拽过程中传递的数据,并自定义拖拽的背板图像。建议开发者采用pixelmap的方式来返回背板图像,避免使用customBuilder,因为后者可能会带来额外的性能开销。| 41| [onDragEnter](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragenter) | 当拖拽操作的拖拽点进入组件的范围时,如果该组件监听了[onDrop](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondrop)事件,此回调将会被触发。| 42| [onDragMove](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragmove) | 当拖拽点在组件范围内移动时,如果该组件监听了onDrop事件,此回调将会被触发。<br>在这一过程中,可以通过调用[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7)中的setResult方法来影响系统在部分场景下的外观表现:<br>1. 设置DragResult.DROP\_ENABLED,组件允许落入。<br>2. 设置DragResult.DROP\_DISABLED,组件不允许落入。| 43| [onDragLeave](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragleave) | 当拖拽点移出组件范围时,如果该组件监听了onDrop事件,此回调将会被触发。<br>在以下两种情况下,系统默认不会触发onDragLeave事件:<br>1. 父组件移动到子组件。<br>2. 目标组件与当前组件布局有重叠。<br>API version 12开始可通过[UIContext](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md)中的[setDragEventStrictReportingEnabled](../reference/apis-arkui/arkts-apis-uicontext-dragcontroller.md#setdrageventstrictreportingenabled12)方法严格触发onDragLeave事件。| 44| [onDrop](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondrop) | 当用户在组件范围内释放拖拽操作时,此回调会被触发。开发者需在此回调中通过DragEvent的setResult方法来设置拖拽结果,否则在拖出方组件的onDragEnd方法中,通过getResult方法获取的将只是默认的处理结果DragResult.DRAG\_FAILED。<br>此回调是开发者干预系统默认拖入处理行为的关键点,系统会优先执行开发者定义的onDrop回调。通过在onDrop回调中调用setResult方法,开发者可以告知系统如何处理被拖拽的数据。<br>1. 设置 DragResult.DRAG\_SUCCESSFUL,数据完全由开发者自己处理,系统不进行处理。<br>2. 设置DragResult.DRAG\_FAILED,数据不再由系统继续处理。<br>3. 设置DragResult.DRAG\_CANCELED,系统也不需要进行数据处理。<br>4. 设置DragResult.DROP\_ENABLED或DragResult.DROP\_DISABLED会被忽略,等同于设置DragResult.DRAG\_SUCCESSFUL。| 45| [onDragEnd](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragend10) | 当用户释放拖拽时,拖拽活动终止,发起拖出动作的组件将触发该回调函数。| 46| [onPreDrag](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#onpredrag12) | 当触发拖拽事件的不同阶段时,绑定此事件的组件会触发该回调函数。<br>开发者可利用此方法,在拖拽开始前的不同阶段,根据[PreDragStatus](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#predragstatus12枚举说明)枚举准备相应数据。<br>1. ACTION\_DETECTING\_STATUS:拖拽手势启动阶段。按下50ms时触发。<br>2. READY\_TO\_TRIGGER\_DRAG\_ACTION:拖拽准备完成,可发起拖拽阶段。按下500ms时触发。<br>3. PREVIEW\_LIFT\_STARTED:拖拽浮起动效发起阶段。按下800ms时触发。<br>4. PREVIEW\_LIFT\_FINISHED:拖拽浮起动效结束阶段。浮起动效完全结束时触发。<br>5. PREVIEW\_LANDING\_STARTED:拖拽落回动效发起阶段。落回动效发起时触发。<br>6. PREVIEW\_LANDING\_FINISHED:拖拽落回动效结束阶段。落回动效结束时触发。<br>7. ACTION\_CANCELED\_BEFORE\_DRAG:拖拽浮起落位动效中断。已满足READY_TO_TRIGGER_DRAG_ACTION状态后,未达到动效阶段,手指抬起时触发。<br>8. PREPARING\_FOR_DRAG\_DETECTION<sup>18+</sup>:拖拽准备完成,可发起拖拽阶段。按下350ms时触发。| 47 48[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7)支持的get方法可用于获取拖拽行为的详细信息,下表展示了在相应的拖拽回调中,这些get方法是否能够返回有效数据。 49| 回调事件 | onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop | onDragEnd | 50| - | - | - | - | - | - | - | 51| getData |—|—|—|—| 支持 |—| 52| getSummary |—| 支持 | 支持 | 支持 | 支持 |—| 53| getResult |—|—|—|—|—| 支持 | 54| getPreviewRect |—|—|—|—| 支持 |—| 55| getVelocity/X/Y |—| 支持 | 支持 | 支持 | 支持 |—| 56| getWindowX/Y | 支持 | 支持 | 支持 | 支持 | 支持 |—| 57| getDisplayX/Y | 支持 | 支持 | 支持 | 支持 | 支持 |—| 58| getX/Y | 支持 | 支持 | 支持 | 支持 | 支持 |—| 59| behavior |—|—|—|—|—| 支持 | 60 61[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7)支持相关set方法向系统传递信息,这些信息部分会影响系统对UI或数据的处理方式。下表列出了set方法应该在回调的哪个阶段执行才会被系统接受并处理。 62| 回调事件 | onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop | 63| - | - | - | - | - | - | 64| useCustomDropAnimation |—|—|—|—| 支持 | 65| setData | 支持 |—|—|—|—| 66| setResult | 支持,可通过set failed或cancel来阻止拖拽发起 | 支持,不作为最终结果传递给onDragEnd | 支持,不作为最终结果传递给onDragEnd | 支持,不作为最终结果传递给onDragEnd | 支持,作为最终结果传递给onDragEnd | 67| behavior |—| 支持 | 支持 | 支持 | 支持 | 68 69## 拖拽背板图 70 71在拖拽移动过程中显示的背板图并非组件本身,而是表示用户拖动的数据,开发者可将其设定为任意可显示的图像。具体而言,onDragStart回调中返回的customBuilder或pixelmap可以用于设置拖拽移动过程中的背板图,而浮起图则默认采用组件本身的截图。dragpreview属性中设定的customBuilder或pixelmap可以用于配置浮起和拖拽过程的背板图。若开发者未配置背板图,系统将自动采用组件本身的截图作为拖拽和浮起时的背板图。 72 73拖拽背板图当前支持设置透明度、圆角、阴影和模糊,具体用法见[拖拽控制](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md)。 74 75 76 77**约束限制:** 78 79* 对于容器组件,如果内部内容通过position、offset等接口使得绘制区域超出了容器组件范围,则系统截图无法截取到范围之外的内容。此种情况下,如果一定要浮起,即拖拽背板能够包含范围之外的内容,则可考虑通过扩大容器范围或自定义方式实现。 80* 不论是使用自定义builder或是系统默认截图方式,截图都暂时无法应用[scale](../reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md#scale)、[rotate](../reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md#rotate)等图形变换效果。 81 82## 使用拖拽能力 83 84### 通用拖拽适配 85 86如下以[Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md)组件为例,介绍组件拖拽开发的基本步骤,以及开发中需要注意的事项。 87 881. 组件使能拖拽。 89 90 设置draggable属性为true,并配置onDragStart回调函数。在回调函数中,可通过UDMF(用户数据管理框架)设置拖拽的数据,并返回自定义的拖拽背景图像。 91 92 ```ts 93 import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; 94 95 Image($r('app.media.app_icon')) 96 .width(100) 97 .height(100) 98 .draggable(true) 99 .onDragStart((event) => { 100 let data: unifiedDataChannel.Image = new unifiedDataChannel.Image(); 101 data.imageUri = 'common/pic/img.png'; 102 let unifiedData = new unifiedDataChannel.UnifiedData(data); 103 event.setData(unifiedData); 104 105 let dragItemInfo: DragItemInfo = { 106 pixelMap: this.pixmap, 107 extraInfo: "this is extraInfo", 108 }; 109 // onDragStart回调函数中返回自定义拖拽背板图 110 return dragItemInfo; 111 }) 112 ``` 113 114 手势场景触发的拖拽功能依赖于底层绑定的长按手势。如果开发者在可拖拽组件上也绑定了长按手势,这将与底层的长按手势产生冲突,进而导致拖拽操作失败。为解决此类问题,可以采用并行手势的方案,具体如下。 115 116 ```ts 117 .parallelGesture(LongPressGesture().onAction(() => { 118 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Long press gesture trigger' }); 119 })) 120 ``` 121 1222. 自定义拖拽背板图。 123 124 可以通过在长按50ms时触发的回调中设置[onPreDrag](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#onpredrag12)回调函数,来提前准备自定义拖拽背板图的pixmap。 125 126 ```ts 127 .onPreDrag((status: PreDragStatus) => { 128 if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) { 129 this.getComponentSnapshot(); 130 } 131 }) 132 ``` 133 134 pixmap的生成可以调用[this.getUIContext().getComponentSnapshot().createFromBuilder()](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#createfrombuilder12)来实现。 135 136 ```ts 137 @Builder 138 pixelMapBuilder() { 139 Column() { 140 Image($r('app.media.startIcon')) 141 .width(120) 142 .height(120) 143 .backgroundColor(Color.Yellow) 144 } 145 } 146 private getComponentSnapshot(): void { 147 this.getUIContext().getComponentSnapshot().createFromBuilder(()=>{this.pixelMapBuilder()}, 148 (error: Error, pixmap: image.PixelMap) => { 149 if(error){ 150 console.error("error: " + JSON.stringify(error)) 151 return; 152 } 153 this.pixmap = pixmap; 154 }) 155 } 156 ``` 157 1583. 若开发者需确保触发[onDragLeave](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragleave)事件,应通过调用[setDragEventStrictReportingEnabled](../reference/apis-arkui/arkts-apis-uicontext-dragcontroller.md#setdrageventstrictreportingenabled12)方法进行设置。 159 160 ```ts 161 import { UIAbility } from '@kit.AbilityKit'; 162 import { window, UIContext } from '@kit.ArkUI'; 163 164 export default class EntryAbility extends UIAbility { 165 onWindowStageCreate(windowStage: window.WindowStage): void { 166 windowStage.loadContent('pages/Index', (err, data) => { 167 if (err.code) { 168 return; 169 } 170 windowStage.getMainWindow((err, data) => { 171 if (err.code) { 172 return; 173 } 174 let windowClass: window.Window = data; 175 let uiContext: UIContext = windowClass.getUIContext(); 176 uiContext.getDragController().setDragEventStrictReportingEnabled(true); 177 }); 178 }); 179 } 180 } 181 ``` 182 1834. 拖拽过程显示角标样式。 184 185 通过设置[allowDrop](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#allowdrop)来定义接收的数据类型,这将影响角标显示。当拖拽的数据符合定义的允许落入的数据类型时,显示“COPY”角标。当拖拽的数据类型不在允许范围内时,显示“FORBIDDEN”角标。若未设置allowDrop,则显示“MOVE”角标。以下代码示例表示仅接收UnifiedData中定义的HYPERLINK和PLAIN\_TEXT类型数据,其他类型数据将被禁止落入。 186 187 ```ts 188 .allowDrop([uniformTypeDescriptor.UniformDataType.HYPERLINK, uniformTypeDescriptor.UniformDataType.PLAIN_TEXT]) 189 ``` 190 191 在实现onDrop回调的情况下,还可以通过在onDragMove中设置[DragResult](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragresult10枚举说明)为DROP_ENABLED,并将[DragBehavior](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragbehavior10)设置为COPY或MOVE,以此来控制角标显示。如下代码将移动时的角标强制设置为“MOVE”。 192 193 ```ts 194 .onDragMove((event) => { 195 event.setResult(DragResult.DROP_ENABLED); 196 event.dragBehavior = DragBehavior.MOVE; 197 }) 198 ``` 199 2005. 拖拽数据的接收。 201 202 需要设置onDrop回调函数,并在回调函数中处理拖拽数据,显示设置拖拽结果。 203 204 ```ts 205 .onDrop((dragEvent?: DragEvent) => { 206 // 获取拖拽数据 207 this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => { 208 let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords(); 209 let rect: Rectangle = event.getPreviewRect(); 210 this.imageWidth = Number(rect.width); 211 this.imageHeight = Number(rect.height); 212 this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri; 213 this.imgState = Visibility.None; 214 // 显式设置result为successful,则将该值传递给拖出方的onDragEnd 215 event.setResult(DragResult.DRAG_SUCCESSFUL); 216 }) 217 ``` 218 219 数据的传递是通过UDMF实现的,在数据较大时可能存在时延,因此在首次获取数据失败时建议加1500ms的延迟重试机制。 220 221 ```ts 222 getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) { 223 try { 224 let data: UnifiedData = event.getData(); 225 if (!data) { 226 return false; 227 } 228 let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords(); 229 if (!records || records.length <= 0) { 230 return false; 231 } 232 callback(event); 233 return true; 234 } catch (e) { 235 console.error("getData failed, code: " + (e as BusinessError).code + ", message: " + (e as BusinessError).message); 236 return false; 237 } 238 } 239 240 getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) { 241 if (this.getDataFromUdmfRetry(event, callback)) { 242 return; 243 } 244 setTimeout(() => { 245 this.getDataFromUdmfRetry(event, callback); 246 }, 1500); 247 } 248 ``` 249 2506. 拖拽发起方可以通过设置onDragEnd回调感知拖拽结果。 251 252 ```ts 253 import { promptAction } from '@kit.ArkUI'; 254 255 .onDragEnd((event) => { 256 // onDragEnd里取到的result值在接收方onDrop设置 257 if (event.getResult() === DragResult.DRAG_SUCCESSFUL) { 258 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Drag Success' }); 259 } else if (event.getResult() === DragResult.DRAG_FAILED) { 260 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Drag failed' }); 261 } 262 }) 263 ``` 264 265**完整示例:** 266 267```ts 268import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; 269import { promptAction } from '@kit.ArkUI'; 270import { BusinessError } from '@kit.BasicServicesKit'; 271import { image } from '@kit.ImageKit'; 272 273@Entry 274@Component 275struct Index { 276 @State targetImage: string = ''; 277 @State imageWidth: number = 100; 278 @State imageHeight: number = 100; 279 @State imgState: Visibility = Visibility.Visible; 280 @State pixmap: image.PixelMap|undefined = undefined 281 282 @Builder 283 pixelMapBuilder() { 284 Column() { 285 Image($r('app.media.startIcon')) 286 .width(120) 287 .height(120) 288 .backgroundColor(Color.Yellow) 289 } 290 } 291 292 getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) { 293 try { 294 let data: UnifiedData = event.getData(); 295 if (!data) { 296 return false; 297 } 298 let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords(); 299 if (!records || records.length <= 0) { 300 return false; 301 } 302 callback(event); 303 return true; 304 } catch (e) { 305 console.error("getData failed, code: " + (e as BusinessError).code + ", message: " + (e as BusinessError).message); 306 return false; 307 } 308 } 309 // 获取UDMF数据,首次获取失败后添加1500ms延迟重试机制 310 getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) { 311 if (this.getDataFromUdmfRetry(event, callback)) { 312 return; 313 } 314 setTimeout(() => { 315 this.getDataFromUdmfRetry(event, callback); 316 }, 1500); 317 } 318 // 调用componentSnapshot中的createFromBuilder接口截取自定义builder的截图 319 private getComponentSnapshot(): void { 320 this.getUIContext().getComponentSnapshot().createFromBuilder(()=>{this.pixelMapBuilder()}, 321 (error: Error, pixmap: image.PixelMap) => { 322 if(error){ 323 console.error("error: " + JSON.stringify(error)) 324 return; 325 } 326 this.pixmap = pixmap; 327 }) 328 } 329 // 长按50ms时提前准备自定义截图的pixmap 330 private PreDragChange(preDragStatus: PreDragStatus): void { 331 if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) { 332 this.getComponentSnapshot(); 333 } 334 } 335 336 build() { 337 Row() { 338 Column() { 339 Text('start Drag') 340 .fontSize(18) 341 .width('100%') 342 .height(40) 343 .margin(10) 344 .backgroundColor('#008888') 345 Row() { 346 Image($r('app.media.app_icon')) 347 .width(100) 348 .height(100) 349 .draggable(true) 350 .margin({ left: 15 }) 351 .visibility(this.imgState) 352 // 绑定平行手势,可同时触发应用自定义长按手势 353 .parallelGesture(LongPressGesture().onAction(() => { 354 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Long press gesture trigger' }); 355 })) 356 .onDragStart((event) => { 357 let data: unifiedDataChannel.Image = new unifiedDataChannel.Image(); 358 data.imageUri = 'common/pic/img.png'; 359 let unifiedData = new unifiedDataChannel.UnifiedData(data); 360 event.setData(unifiedData); 361 362 let dragItemInfo: DragItemInfo = { 363 pixelMap: this.pixmap, 364 extraInfo: "this is extraInfo", 365 }; 366 return dragItemInfo; 367 }) 368 // 提前准备拖拽自定义背板图 369 .onPreDrag((status: PreDragStatus) => { 370 this.PreDragChange(status); 371 }) 372 .onDragEnd((event) => { 373 // onDragEnd里取到的result值在接收方onDrop设置 374 if (event.getResult() === DragResult.DRAG_SUCCESSFUL) { 375 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Drag Success' }); 376 } else if (event.getResult() === DragResult.DRAG_FAILED) { 377 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Drag failed' }); 378 } 379 }) 380 } 381 382 Text('Drag Target Area') 383 .fontSize(20) 384 .width('100%') 385 .height(40) 386 .margin(10) 387 .backgroundColor('#008888') 388 Row() { 389 Image(this.targetImage) 390 .width(this.imageWidth) 391 .height(this.imageHeight) 392 .draggable(true) 393 .margin({ left: 15 }) 394 .border({ color: Color.Black, width: 1 }) 395 // 控制角标显示类型为MOVE,即不显示角标 396 .onDragMove((event) => { 397 event.setResult(DragResult.DROP_ENABLED) 398 event.dragBehavior = DragBehavior.MOVE 399 }) 400 .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE]) 401 .onDrop((dragEvent?: DragEvent) => { 402 // 获取拖拽数据 403 this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => { 404 let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords(); 405 let rect: Rectangle = event.getPreviewRect(); 406 this.imageWidth = Number(rect.width); 407 this.imageHeight = Number(rect.height); 408 this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri; 409 this.imgState = Visibility.None; 410 // 显式设置result为successful,则将该值传递给拖出方的onDragEnd 411 event.setResult(DragResult.DRAG_SUCCESSFUL); 412 }) 413 }) 414 } 415 } 416 .width('100%') 417 .height('100%') 418 } 419 .height('100%') 420 } 421} 422 423``` 424 425 426### 多选拖拽适配 427 428从API version 12开始,[Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)组件和[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)组件中的GridItem和ListItem组件支持多选与拖拽功能。目前,仅支持onDragStart的触发方式。 429 430以下以Grid为例,详细介绍实现多选拖拽的基本步骤,以及在开发过程中需要注意的事项。 431 4321. 组件多选拖拽使能。 433 434 创建GridItem子组件并绑定onDragStart回调函数。同时设置GridItem组件的状态为可选中。 435 436 ```ts 437 Grid() { 438 ForEach(this.numbers, (idx: number) => { 439 GridItem() { 440 Column() 441 .backgroundColor(Color.Blue) 442 .width(50) 443 .height(50) 444 .opacity(1.0) 445 .id('grid'+idx) 446 } 447 .onDragStart(()=>{}) 448 .selectable(true) 449 }, (idx: string) => idx) 450 } 451 ``` 452 453 多选拖拽功能默认处于关闭状态。若要启用此功能,需在[dragPreviewOptions](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#dragpreviewoptions11)接口的DragInteractionOptions参数中,将isMultiSelectionEnabled设置为true,以表明当前组件支持多选。此外,DragInteractionOptions还包含defaultAnimationBeforeLifting参数,用于控制组件浮起前的默认效果。将该参数设置为true,组件在浮起前将展示一个默认的缩小动画效果。 454 455 ```ts 456 .dragPreviewOptions({isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true}) 457 ``` 458 459 为了确保选中状态,应将GridItem子组件的selected属性设置为true。例如,可以通过调用[onClick](../reference/apis-arkui/arkui-ts/ts-universal-events-click.md#onclick)来设置特定组件为选中状态。 460 461 ```ts 462 .selected(this.isSelectedGrid[idx]) 463 .onClick(()=>{ 464 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] 465 }) 466 ``` 467 4682. 优化多选拖拽性能。 469 470 在多选拖拽操作中,当多选触发聚拢动画效果时,系统会截取当前屏幕内显示的选中组件图像。如果选中组件数量过多,可能会造成较高的性能消耗。为了优化性能,多选拖拽功能支持从dragPreview中获取截图,用以实现聚拢动画效果,从而有效节省系统资源。 471 472 ```ts 473 .dragPreview({ 474 pixelMap:this.pixmap 475 }) 476 ``` 477 478 截图的获取可以在选中组件时通过调用[this.getUIContext().getComponentSnapshot().get()](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#get12)方法获取。以下示例通过获取组件对应id的方法进行截图。 479 480 ```ts 481 @State previewData: DragItemInfo[] = [] 482 @State isSelectedGrid: boolean[] = [] 483 .onClick(()=>{ 484 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] 485 if (this.isSelectedGrid[idx]) { 486 let gridItemName = 'grid' + idx 487 this.getUIContext().getComponentSnapshot().get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ 488 this.pixmap = pixmap 489 this.previewData[idx] = { 490 pixelMap:this.pixmap 491 } 492 }) 493 } 494 }) 495 ``` 496 4973. 多选显示效果。 498 499 通过[stateStyles](../reference/apis-arkui/arkui-ts/ts-universal-attributes-polymorphic-style.md#statestyles)可以设置选中态和非选中态的显示效果,方便区分。 500 501 ```ts 502 @Styles 503 normalStyles(): void{ 504 .opacity(1.0) 505 } 506 507 @Styles 508 selectStyles(): void{ 509 .opacity(0.4) 510 } 511 512 .stateStyles({ 513 normal : this.normalStyles, 514 selected: this.selectStyles 515 }) 516 ``` 517 5184. 适配数量角标。 519 520 多选拖拽的数量角标当前需要应用使用dragPreviewOptions中的numberBadge参数设置,开发者需要根据当前选中的节点数量来设置数量角标。 521 522 ```ts 523 @State numberBadge: number = 0; 524 525 .onClick(()=>{ 526 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] 527 if (this.isSelectedGrid[idx]) { 528 this.numberBadge++; 529 } else { 530 this.numberBadge--; 531 } 532 }) 533 // 多选场景右上角数量角标需要应用设置numberBadge参数 534 .dragPreviewOptions({numberBadge: this.numberBadge}) 535 ``` 536 537**完整示例:** 538 539```ts 540import { image } from '@kit.ImageKit'; 541 542@Entry 543@Component 544struct GridEts { 545 @State pixmap: image.PixelMap|undefined = undefined 546 @State numbers: number[] = [] 547 @State isSelectedGrid: boolean[] = [] 548 @State previewData: DragItemInfo[] = [] 549 @State numberBadge: number = 0; 550 551 @Styles 552 normalStyles(): void{ 553 .opacity(1.0) 554 } 555 556 @Styles 557 selectStyles(): void{ 558 .opacity(0.4) 559 } 560 561 onPageShow(): void { 562 let i: number = 0 563 for(i=0;i<100;i++){ 564 this.numbers.push(i) 565 this.isSelectedGrid.push(false) 566 this.previewData.push({}) 567 } 568 } 569 570 @Builder 571 RandomBuilder(idx: number) { 572 Column() 573 .backgroundColor(Color.Blue) 574 .width(50) 575 .height(50) 576 .opacity(1.0) 577 } 578 579 build() { 580 Column({ space: 5 }) { 581 Grid() { 582 ForEach(this.numbers, (idx: number) => { 583 GridItem() { 584 Column() 585 .backgroundColor(Color.Blue) 586 .width(50) 587 .height(50) 588 .opacity(1.0) 589 .id('grid'+idx) 590 } 591 .dragPreview(this.previewData[idx]) 592 .selectable(true) 593 .selected(this.isSelectedGrid[idx]) 594 // 设置多选显示效果 595 .stateStyles({ 596 normal : this.normalStyles, 597 selected: this.selectStyles 598 }) 599 .onClick(()=>{ 600 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] 601 if (this.isSelectedGrid[idx]) { 602 this.numberBadge++; 603 let gridItemName = 'grid' + idx 604 // 选中状态下提前调用componentSnapshot中的get接口获取pixmap 605 this.getUIContext().getComponentSnapshot().get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ 606 this.pixmap = pixmap 607 this.previewData[idx] = { 608 pixelMap:this.pixmap 609 } 610 }) 611 } else { 612 this.numberBadge--; 613 } 614 }) 615 // 使能多选拖拽,右上角数量角标需要应用设置numberBadge参数 616 .dragPreviewOptions({numberBadge: this.numberBadge},{isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true}) 617 .onDragStart(()=>{ 618 }) 619 }, (idx: string) => idx) 620 } 621 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 622 .columnsGap(5) 623 .rowsGap(10) 624 .backgroundColor(0xFAEEE0) 625 }.width('100%').margin({ top: 5 }) 626 } 627} 628``` 629 630 631### 适配自定义落位动效 632 633当开发者需要实现自定义落位动效时,可以禁用系统的默认动效。从API version 18开始,ArkUI提供了[executeDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#executedropanimation18)接口,用于自定义落位动效。以下以Image组件为例,详细介绍使用[executeDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#executedropanimation18)接口的基本步骤,以及开发过程中需要注意的事项。 634 6351. 组件拖拽设置。 636 设置draggable为true,并配置onDragStart,onDragEnd等回调函数。 637 ```ts 638 Image($r('app.media.app_icon')) 639 .width(100) 640 .height(100) 641 .draggable(true) 642 .margin({ left: 15 ,top: 40}) 643 .visibility(this.imgState) 644 .onDragStart((event) => {}) 645 .onDragEnd((event) => {}) 646 ``` 6472. 设置自定义动效。 648 649 自定义落位动效通过[animateTo](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md#animateto)接口设置动画相关的参数来实现。例如,可以改变组件的大小。 650 651 ```ts 652 customDropAnimation = () => { 653 this.getUIContext().animateTo({ duration: 1000, curve: Curve.EaseOut, playMode: PlayMode.Normal }, () => { 654 this.imageWidth = 200; 655 this.imageHeight = 200; 656 this.imgState = Visibility.None; 657 }) 658 } 659 ``` 660 6613. 拖拽落位适配动效。 662 663 设置onDrop回调函数,接收拖拽数据。拖拽落位动效通过[executeDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#executedropanimation18)函数执行,设置[useCustomDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7)为true禁用系统默认动效。 664 665 ```ts 666 Column() { 667 Image(this.targetImage) 668 .width(this.imageWidth) 669 .height(this.imageHeight) 670 } 671 .draggable(true) 672 .margin({ left: 15 }) 673 .border({ color: Color.Black, width: 1 }) 674 .allowDrop([udmfType.UniformDataType.IMAGE]) 675 .onDrop((dragEvent: DragEvent) => { 676 let records: Array<unifiedDataChannel.UnifiedRecord> = dragEvent.getData().getRecords(); 677 let rect: Rectangle = dragEvent.getPreviewRect(); 678 this.imageWidth = Number(rect.width); 679 this.imageHeight = Number(rect.height); 680 this.targetImage = (records[0] as udmf.Image).imageUri; 681 dragEvent.useCustomDropAnimation = true; 682 dragEvent.executeDropAnimation(this.customDropAnimation) 683 }) 684 ``` 685 686**完整示例:** 687 688```ts 689import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; 690import { promptAction } from '@kit.ArkUI'; 691 692 693@Entry 694@Component 695struct DropAnimationExample { 696 @State targetImage: string = ''; 697 @State imageWidth: number = 100; 698 @State imageHeight: number = 100; 699 @State imgState: Visibility = Visibility.Visible; 700 701 customDropAnimation = 702 () => { 703 this.getUIContext().animateTo({ duration: 1000, curve: Curve.EaseOut, playMode: PlayMode.Normal }, () => { 704 this.imageWidth = 200; 705 this.imageHeight = 200; 706 this.imgState = Visibility.None; 707 }) 708 } 709 710 build() { 711 Row() { 712 Column() { 713 Image($r('app.media.app_icon')) 714 .width(100) 715 .height(100) 716 .draggable(true) 717 .margin({ left: 15 ,top: 40}) 718 .visibility(this.imgState) 719 .onDragStart((event) => { 720 }) 721 .onDragEnd((event) => { 722 if (event.getResult() === DragResult.DRAG_SUCCESSFUL) { 723 console.info('Drag Success'); 724 } else if (event.getResult() === DragResult.DRAG_FAILED) { 725 console.info('Drag failed'); 726 } 727 }) 728 }.width('45%') 729 .height('100%') 730 Column() { 731 Text('Drag Target Area') 732 .fontSize(20) 733 .width(180) 734 .height(40) 735 .textAlign(TextAlign.Center) 736 .margin(10) 737 .backgroundColor('rgb(240,250,255)') 738 Column() { 739 Image(this.targetImage) 740 .width(this.imageWidth) 741 .height(this.imageHeight) 742 } 743 .draggable(true) 744 .margin({ left: 15 }) 745 .border({ color: Color.Black, width: 1 }) 746 .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE]) 747 .onDrop((dragEvent: DragEvent) => { 748 let records: Array<unifiedDataChannel.UnifiedRecord> = dragEvent.getData().getRecords(); 749 let rect: Rectangle = dragEvent.getPreviewRect(); 750 this.imageWidth = Number(rect.width); 751 this.imageHeight = Number(rect.height); 752 this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri; 753 dragEvent.useCustomDropAnimation = true; 754 dragEvent.executeDropAnimation(this.customDropAnimation) 755 }) 756 .width(this.imageWidth) 757 .height(this.imageHeight) 758 }.width('45%') 759 .height('100%') 760 .margin({ left: '5%' }) 761 } 762 .height('100%') 763 } 764} 765``` 766 767 768### 处理大批量数据 769 770当多选拖拽的数量较多或者拖拽数据量较大时,在拖拽过程中统一处理数据可能会影响拖拽功能的体验。以下以Grid组件为例,详细介绍在大批量数据拖拽过程中数据的推荐处理方式,以及在开发中需要注意的事项。 771 7721. 组件多选拖拽设置。 773 774 创建GridItem子组件,并设置其状态为可选中。再设置多选拖拽功能isMultiSelectionEnabled为true,最后设置选中状态用作区分是否选中。 775 776 ```ts 777 Grid() { 778 ForEach(this.numbers, (idx: number) => { 779 GridItem() { 780 Column() 781 .backgroundColor(Color.Blue) 782 .width(50) 783 .height(50) 784 .opacity(1.0) 785 .id('grid'+idx) 786 } 787 .dragPreview(this.previewData[idx]) 788 .dragPreviewOptions({numberBadge: this.numberBadge},{isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true}) 789 .selectable(true) 790 .selected(this.isSelectedGrid[idx]) 791 .stateStyles({ 792 normal : this.normalStyles, 793 selected: this.selectStyles 794 }) 795 .onClick(() => { 796 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx]; 797 }) 798 }, (idx: string) => idx) 799 } 800 ``` 801 802 多选拖拽的数据数量过多可能影响拖拽的体验,推荐多选拖拽最大多选数量为500。 803 804 ```ts 805 onPageShow(): void { 806 let i: number = 0 807 for(i=0;i<500;i++){ 808 this.numbers.push(i) 809 this.isSelectedGrid.push(false) 810 this.previewData.push({}) 811 } 812 } 813 ``` 8142. 多选拖拽选中时添加数据。 815 816 当数据量较大时,建议在选择数据时通过[addRecord](../reference/apis-arkdata/js-apis-data-unifiedDataChannel.md#addrecord)添加数据记录,以避免在拖拽过程中集中添加数据而导致显著的性能消耗。 817 818 ```ts 819 .onClick(()=>{ 820 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx]; 821 if (this.isSelectedGrid[idx]) { 822 let data: UDC.Image = new UDC.Image(); 823 data.uri = '/resource/image.jpeg'; 824 if (!this.unifiedData) { 825 this.unifiedData = new UDC.UnifiedData(data); 826 } 827 this.unifiedData.addRecord(data); 828 this.numberBadge++; 829 let gridItemName = 'grid' + idx; 830 // 选中状态下提前调用componentSnapshot中的get接口获取pixmap 831 this.getUIContext().getComponentSnapshot().get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ 832 this.pixmap = pixmap; 833 this.previewData[idx] = { 834 pixelMap:this.pixmap 835 } 836 }) 837 } else { 838 this.numberBadge--; 839 for (let i=0; i<this.isSelectedGrid.length; i++) { 840 if (this.isSelectedGrid[i] === true) { 841 let data: UDC.Image = new UDC.Image(); 842 data.uri = '/resource/image.jpeg'; 843 if (!this.unifiedData) { 844 this.unifiedData = new UDC.UnifiedData(data); 845 } 846 this.unifiedData.addRecord(data); 847 } 848 } 849 } 850 }) 851 ``` 852 8533. 拖拽数据提前准备。 854 855 在onPreDrag中可以提前接收到准备发起拖拽的信号。若数据量较大,此时可以事先准备数据。 856 857 ```ts 858 .onPreDrag((status: PreDragStatus) => { 859 if (status == PreDragStatus.PREPARING_FOR_DRAG_DETECTION) { 860 this.loadData() 861 } 862 }) 863 ``` 864 8654. 数据准备未完成时设置主动阻塞拖拽。 866 867 在发起拖拽时,应判断数据是否已准备完成。若数据未准备完成,则需向系统发出[WAITTING](../reference/apis-arkui/js-apis-arkui-dragController.md#dragstartrequeststatus18)信号。此时,若手指做出移动手势,背板图将停留在原地,直至应用发出READY信号或超出主动阻塞的最大限制时间(5s)。若数据已准备完成,则可直接将数据设置到[dragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7)中。此外,在使用主动阻塞功能时,需保存当前的dragEvent,并在数据准备完成时进行数据设置;在非主动阻塞场景下,不建议保存当前的dragEvent。 868 869 ```ts 870 .onDragStart((event: DragEvent) => { 871 this.dragEvent = event; 872 if (this.finished == false) { 873 this.getUIContext().getDragController().notifyDragStartRequest(dragController.DragStartRequestStatus.WAITING); 874 } else { 875 event.setData(this.unifiedData); 876 } 877 }) 878 ``` 879 880**完整示例:** 881 882```ts 883import { image } from '@kit.ImageKit'; 884import { unifiedDataChannel as UDC } from '@kit.ArkData'; 885import { dragController } from '@kit.ArkUI'; 886 887@Entry 888@Component 889struct GridEts { 890 @State pixmap: image.PixelMap|undefined = undefined 891 @State numbers: number[] = [] 892 @State isSelectedGrid: boolean[] = [] 893 @State previewData: DragItemInfo[] = [] 894 @State numberBadge: number = 0; 895 unifiedData: UnifiedData|undefined = undefined; 896 timeout: number = 1 897 finished: boolean = false; 898 dragEvent: DragEvent|undefined; 899 900 @Styles 901 normalStyles(): void{ 902 .opacity(1.0) 903 } 904 905 @Styles 906 selectStyles(): void{ 907 .opacity(0.4) 908 } 909 910 onPageShow(): void { 911 let i: number = 0 912 for(i=0;i<500;i++){ 913 this.numbers.push(i) 914 this.isSelectedGrid.push(false) 915 this.previewData.push({}) 916 } 917 } 918 919 loadData() { 920 this.timeout = setTimeout(() => { 921 //数据准备完成后的状态 922 if (this.dragEvent) { 923 this.dragEvent.setData(this.unifiedData); 924 } 925 this.getUIContext().getDragController().notifyDragStartRequest(dragController.DragStartRequestStatus.READY); 926 this.finished = true; 927 }, 4000); 928 } 929 930 @Builder 931 RandomBuilder(idx: number) { 932 Column() 933 .backgroundColor(Color.Blue) 934 .width(50) 935 .height(50) 936 .opacity(1.0) 937 } 938 939 build() { 940 Column({ space: 5 }) { 941 Button('全选') 942 .onClick(() => { 943 for (let i=0;i<this.isSelectedGrid.length;i++) { 944 if (this.isSelectedGrid[i] === false) { 945 this.numberBadge++; 946 this.isSelectedGrid[i] = true; 947 let data: UDC.Image = new UDC.Image(); 948 data.uri = '/resource/image.jpeg'; 949 if (!this.unifiedData) { 950 this.unifiedData = new UDC.UnifiedData(data); 951 } 952 this.unifiedData.addRecord(data); 953 let gridItemName = 'grid' + i; 954 // 选中状态下提前调用componentSnapshot中的get接口获取pixmap 955 this.getUIContext().getComponentSnapshot().get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ 956 this.pixmap = pixmap 957 this.previewData[i] = { 958 pixelMap:this.pixmap 959 } 960 }) 961 } 962 } 963 }) 964 Grid() { 965 ForEach(this.numbers, (idx: number) => { 966 GridItem() { 967 Column() 968 .backgroundColor(Color.Blue) 969 .width(50) 970 .height(50) 971 .opacity(1.0) 972 .id('grid'+idx) 973 } 974 .dragPreview(this.previewData[idx]) 975 .selectable(true) 976 .selected(this.isSelectedGrid[idx]) 977 // 设置多选显示效果 978 .stateStyles({ 979 normal : this.normalStyles, 980 selected: this.selectStyles 981 }) 982 .onClick(()=>{ 983 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx]; 984 if (this.isSelectedGrid[idx]) { 985 let data: UDC.Image = new UDC.Image(); 986 data.uri = '/resource/image.jpeg'; 987 if (!this.unifiedData) { 988 this.unifiedData = new UDC.UnifiedData(data); 989 } 990 this.unifiedData.addRecord(data); 991 this.numberBadge++; 992 let gridItemName = 'grid' + idx; 993 // 选中状态下提前调用componentSnapshot中的get接口获取pixmap 994 this.getUIContext().getComponentSnapshot().get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{ 995 this.pixmap = pixmap; 996 this.previewData[idx] = { 997 pixelMap:this.pixmap 998 } 999 }) 1000 } else { 1001 this.numberBadge--; 1002 for (let i=0; i<this.isSelectedGrid.length; i++) { 1003 if (this.isSelectedGrid[i] === true) { 1004 let data: UDC.Image = new UDC.Image(); 1005 data.uri = '/resource/image.jpeg'; 1006 if (!this.unifiedData) { 1007 this.unifiedData = new UDC.UnifiedData(data); 1008 } 1009 this.unifiedData.addRecord(data); 1010 } 1011 } 1012 } 1013 }) 1014 .onPreDrag((status: PreDragStatus) => { 1015 // 1.长按时通知,350ms回调 1016 if (status == PreDragStatus.PREPARING_FOR_DRAG_DETECTION) { 1017 // 2.用户按住一段时间,还没有松手,有可能会拖拽,此时可准备数据 1018 this.loadData() 1019 } else if (status == PreDragStatus.ACTION_CANCELED_BEFORE_DRAG) { 1020 // 3.用户停止拖拽交互,取消数据准备(模拟方法:定时器取消) 1021 clearTimeout(this.timeout); 1022 } 1023 }) 1024 // >=500ms,移动超过10vp触发 1025 .onDragStart((event: DragEvent) => { 1026 this.dragEvent = event; 1027 if (this.finished == false) { 1028 this.getUIContext().getDragController().notifyDragStartRequest(dragController.DragStartRequestStatus.WAITING); 1029 } else { 1030 event.setData(this.unifiedData); 1031 } 1032 }) 1033 .onDragEnd(() => { 1034 this.finished = false; 1035 }) 1036 .dragPreviewOptions({numberBadge: this.numberBadge},{isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true}) 1037 }, (idx: string) => idx) 1038 } 1039 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 1040 .columnsGap(5) 1041 .rowsGap(10) 1042 .backgroundColor(0xFAEEE0) 1043 }.width('100%').margin({ top: 5 }) 1044 } 1045} 1046``` 1047 1048 1049 1050## 支持悬停检测 1051Spring Loading,即拖拽悬停检测(又叫弹簧加载)是拖拽操作的一项增强功能,允许用户在拖动过程中通过悬停在目标上自动触发视图跳转,提供了使用的便利性。建议在所有支持页面切换的区域均实现该功能。 1052 1053> 该能力从API version 20开始支持。 1054 1055以下为常见的适合支持该功能的场景: 1056 1057- 在文件管理器中,拖动文件并悬停在文件夹上时,文件夹可以自动打开。 1058- 在桌面启动器中,拖动文件并悬停在应用程序图标上时,应用程序可以自动打开。 1059 1060除了实现视图切换跳转功能,该能力也可用于特定视图的激活。例如,在用户将一段文本拖拽至按钮上停留后,可激活一个文本输入框。用户随后可将所拖拽文本移动至该输入框上方释放,触发搜索结果展示,实现单手高效完成整个操作。 1061 1062 1063 1064### 触发原理 1065 1066要实现这些能力,需要在组件上注册onDragSpringLoading接口,并传入一个用于处理拖拽悬停触发通知的回调。使用该接口后,该组件将如同注册了onDrop接口的组件一样,成为一个可拖入目标,并且遵循与onDrop相同的命中检测规则,即:在悬停位置下方,仅有一个组件可以接收拖拽事件响应,并且总是首个被检测到的组件 1067 1068Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 -> 结束。在结束之前,如果用户重新开始移动,会自动中断Spring Loading,并通知应用取消。如果在悬停检测期间移动,且尚未进入Spring Loading状态,则不会触发取消通知。 1069 1070 1071 1072应用通过回调接收当前的状态,动态改变UI显示,从而达到用户提醒的效果。 1073 1074| 状态 | 含义 | 建议处理方式 | 1075| :----- | :------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------- | 1076| BEGIN | 用户已经在本组件上方悬停不动维持了一段时间,开始进入 Spring Loading 状态 | 修改背景色或改变组件尺寸,强化提醒用户继续保持悬停不动。 | 1077| UPDATE | 用户继续维持不动,系统周期性下发刷新通知,默认3次 | 通过通知中携带的sequence是否为奇偶数,来决定是否重置UI显示,以此达到周期性变化的提醒效果。 | 1078| END | 用户已保持悬停不动足够多的时间,整个Spring Loading检测与触发完整结束 | 进行页面跳转或视图切换。 | 1079| CANCEL | 悬停进入BEGIN状态后,用户重新移动或其他情况打断了悬停检测,无法再进行整个Spring Loading状态的触发 | 重置和恢复UI显示,取消视图切换相关的状态和逻辑。 | 1080 1081>**说明** 1082> 1083>1. 在同一个组件内持续保持不动,整个Spring Loading仅会触发一轮,不会重复触发,直到拖离当前组件后再重新进入。 1084>2. 同一个组件上即可以实现Spring Loading,也可以实现onDrop/onDragEnter等拖拽事件。 1085 1086 1087### 触发自定义 1088 1089可以自定义修改Spring Loading检测参数,动态决定是否继续触发。 1090 10911.触发参数自定义 1092 1093 onDragSpringLoading接口还提供了一个可选参数configuration供应用自定义静止检测时长以及触发间隔与次数等配置,可以通过此参数来个性化定义Spring Loading触发条件。但绝大数多情况下,不需要进行修改,使用系统默认配置即可。 1094 1095 configuration参数必须在检测开始前准备就绪。系统一旦启动Spring Loading检测过程,将不再从该参数读取配置。然而,可以通过回调中传入的context对象中的updateCon figuration方法动态更新配置。此动态更新仅对当前触发有效,不会影响通过configuration的配置。 1096 1097 推荐使用默认配置,或通过onDragSpringLoading接口的configuration配置固定参数。在绝大多数情况下,无需在Spring Loading过程中动态修改这些检测参数。但若需针对不同的拖拽数据类型提供不同的用户提示效果,则可考虑使用此功能。 1098 1099 >**说明** 1100 > 1101 >不要设置过长的时间间隔和过多的触发次数,这对于用户提醒通常没有意义。 1102 11032.动态终止 1104 1105 当系统检测到用户悬停足够时长,回调onDragSpringLoading接口设置到回调函数时,有机会决定即将出现的Spring Loading通知是否继续,这发生在需要观察用户拖拽的数据类型并与自身业务逻辑结合的情况下。 1106 1107 以下是一段伪代码示例: 1108 ```typescript 1109 .onDragSpringLoading((context: DragSpringLoadingContext)=>{ 1110 // 检查当前的状态 1111 if (context.state == DragSpringLoadingState.BEGIN) { 1112 // 检查用户所拖拽的数据类型是否自己能够处理的 1113 boolean isICanHandle = false; 1114 let dataSummary = context?.dragInfos?.dataSummary; 1115 if (dataSummary != undefined) { 1116 for (const [type, size] of dataSummary) { 1117 if (type === "general.plain-text") { // 只能处理纯文本类型 1118 isICanHandle = true; 1119 break; 1120 } 1121 } 1122 } 1123 // 如果数据无法处理,直接终止Spring Loading 1124 if (!isICanHandle) { 1125 context.abort(); 1126 return; 1127 } 1128 } 1129 }) 1130 ``` 1131 11323.禁用Spring Loading 1133 1134 如果不再需要该组件上响应任何Spring Loading事件,则可以通过传递null给onDragSpringLoading来明确关闭响应。 1135 1136 ```typescript 1137 .onDragSpringLoading(null) 1138 ``` 1139 1140### 实现示例 1141 1142下面通过实现搜索设备的简单示例来展示如何通过`onDragSpringLoading`实现提醒和视图切换。 1143 11441.准备一些组件 1145 1146 为了简化示例,准备一个可拖出文字的组件以供用户拖出待搜索的文字,并添加一个按钮控件,用于响应Spring Loading来进一步激活视图。被激活的视图通过`bindSheet`实现,内部配置有一个输入框控件用于接收拖拽文本,以及一个文本组件用于展示搜索结果。 1147 1148 ```typescript 1149 build() { 1150 Column() { 1151 Column() { 1152 Text('双击文字选择后拖出: \n DeviceName') 1153 .fontSize(30) 1154 .copyOption(CopyOptions.InApp) // 开启copyOption之后,文本组件即可支持选择内容进行拖拽 1155 }.padding({bottom:30}) 1156 1157 Button('搜索设备').width('80%').height('80vp').fontSize(30) 1158 .bindSheet($$this.isShowSheet, this.SheetBuilder(), { 1159 detents: [SheetSize.MEDIUM, SheetSize.LARGE, 600], 1160 preferType: SheetType.BOTTOM, 1161 title: { title: '搜索设备' }, 1162 }) 1163 }.width('100%').height('100%') 1164 .justifyContent(FlexAlign.Center) 1165 } 1166 ``` 11672.实现SheetBuilder 1168 1169 实现半模态弹框的UI界面。 1170 1171 ```typescript 1172 @Builder 1173 SheetBuilder() { 1174 Column() { 1175 // 输入框 1176 TextInput({placeholder: '拖入此处'}) 1177 .width('80%').borderWidth(1).borderColor(Color.Black) 1178 .onChange((value: string)=>{ 1179 if (value.length == 0) { 1180 this.isSearchDone = false; 1181 return; 1182 } 1183 // 此处简化处理,直接显示固定搜索结果 1184 this.isSearchDone = true; 1185 }) 1186 if (this.isSearchDone) { 1187 Text(this.searchResult).fontSize(30) 1188 } 1189 }.width('100%').height('100%') 1190 } 1191 ``` 1192 11933.为Button控件添加进入和离开的响应 1194 1195 为了达到提醒效果,为目标组件也增加`onDragEnter`和`onDragLeave`的处理。当用户拖拽文字进入到组件范围时,变化背景色,以提醒用户在此处停留。 1196 1197 ```typescript 1198 .onDragEnter(()=>{ 1199 // 当用户拖拽进入按钮范围,即提醒用户,此处可处理数据 1200 this.buttonBackgroundColor = this.reminderColor 1201 }) 1202 .onDragLeave(()=>{ 1203 // 当用户拖拽离开按钮范围,恢复UI 1204 this.buttonBackgroundColor = this.normalColor 1205 }) 1206 ``` 1207 12084.实现Spring Loading响应 1209 1210 实现一个Spring Loading的响应函数,处理所有状态,如下: 1211 1212 ```typescript 1213 handleSpringLoading(context: dragController.SpringLoadingContext) { 1214 // BEGIN 状态时检查拖拽数据类型 1215 if (context.state == dragController.DragSpringLoadingState.BEGIN) { 1216 // 进行必要判断,决定是否要终止触发 1217 return; 1218 } 1219 if (context.state == dragController.DragSpringLoadingState.UPDATE) { 1220 // 刷新提醒 1221 return; 1222 } 1223 // 处理Spring Loading结束,触发视图切换 1224 if (context.state == dragController.DragSpringLoadingState.END) { 1225 // 视图激活或跳转 1226 return; 1227 } 1228 // 处理CANCEL状态,复原UI 1229 if (context.state == dragController.DragSpringLoadingState.CANCEL) { 1230 // 恢复状态与UI 1231 return; 1232 } 1233 } 1234 ``` 1235 1236**完整代码如下** 1237 1238 ```typescript 1239 import { dragController } from '@kit.ArkUI'; 1240 import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; 1241 1242 @Entry 1243 @ComponentV2 1244 struct Index { 1245 @Local isShowSheet: boolean = false; 1246 private searchResult: string = '搜索结果:\n 设备 1\n 设备 2\n 设备 3\n ... ...'; 1247 @Local isSearchDone: boolean = false; 1248 private reminderColor: Color = Color.Green; 1249 private normalColor: Color = Color.Blue; 1250 @Local buttonBackgroundColor: Color = this.normalColor; 1251 1252 @Builder 1253 SheetBuilder() { 1254 Column() { 1255 // 输入框 1256 TextInput({placeholder: '拖入此处'}) 1257 .width('80%').borderWidth(1).borderColor(Color.Black).padding({bottom: 5}) 1258 .onChange((value: string)=>{ 1259 if (value.length == 0) { 1260 this.isSearchDone = false; 1261 return; 1262 } 1263 // 此处简化处理,直接显示固定搜索结果 1264 this.isSearchDone = true; 1265 }) 1266 if (this.isSearchDone) { 1267 Text(this.searchResult).fontSize(20).textAlign(TextAlign.Start).width('80%') 1268 } 1269 }.width('100%').height('100%') 1270 } 1271 1272 // 检查拖拽数据类型是否包含所希望的plain-text 1273 checkDataType(dataSummary: unifiedDataChannel.Summary | undefined): boolean { 1274 let summary = dataSummary?.summary; 1275 if (summary == undefined) { 1276 return false; 1277 } 1278 1279 let dataSummaryObjStr: string = JSON.stringify(summary); 1280 let dataSummaryArray: Array<Array<string>> = JSON.parse(dataSummaryObjStr); 1281 let isDataTypeMatched: boolean = false; 1282 dataSummaryArray.forEach((record: Array<string>) => { 1283 if (record[0] == 'general.plain-text') { 1284 isDataTypeMatched = true; 1285 } 1286 }); 1287 return isDataTypeMatched; 1288 } 1289 1290 // 处理BEGIN状态 1291 handleBeginState(context: SpringLoadingContext): boolean { 1292 // 检查用户所拖拽的数据类型是否自己能够处理的 1293 if (this.checkDataType(context?.dragInfos?.dataSummary)) { 1294 return true; 1295 } 1296 // 如果数据无法处理,直接终止Spring Loading 1297 context.abort(); 1298 return false; 1299 } 1300 1301 // Spring Loading处理入口 1302 handleSpringLoading(context: SpringLoadingContext) { 1303 // BEGIN 状态时检查拖拽数据类型 1304 if (context.state == dragController.DragSpringLoadingState.BEGIN) { 1305 if (this.handleBeginState(context)) { 1306 // 我们已经在onDragEnter时刷新了提醒色,进入Spring Loading状态时,恢复UI,提醒用户继续保持不动 1307 this.buttonBackgroundColor = this.normalColor; 1308 } 1309 return; 1310 } 1311 if (context.state == dragController.DragSpringLoadingState.UPDATE) { 1312 // 奇数次UPDATE通知刷新提醒UI,偶数次复原UI 1313 if (context.currentNotifySequence % 2 != 0) { 1314 this.buttonBackgroundColor = this.reminderColor; 1315 } else { 1316 this.buttonBackgroundColor = this.normalColor; 1317 } 1318 return; 1319 } 1320 // 处理Spring Loading结束,触发视图切换 1321 if (context.state == dragController.DragSpringLoadingState.END) { 1322 this.isShowSheet = true; 1323 return; 1324 } 1325 // 处理CANCEL状态,复原UI 1326 if (context.state == dragController.DragSpringLoadingState.CANCEL) { 1327 this.buttonBackgroundColor = this.normalColor; 1328 return; 1329 } 1330 } 1331 1332 build() { 1333 Column() { 1334 Column() { 1335 Text('双击文字选择后拖出: \n DeviceName') 1336 .fontSize(30) 1337 .copyOption(CopyOptions.InApp) // 开启copyOption之后,文本组件即可支持选择内容进行拖拽 1338 }.padding({bottom:30}) 1339 1340 Button('搜索设备').width('80%').height('80vp').fontSize(30) 1341 .bindSheet($$this.isShowSheet, this.SheetBuilder(), { 1342 detents: [SheetSize.MEDIUM, SheetSize.LARGE, 600], 1343 preferType: SheetType.BOTTOM, 1344 title: { title: '搜索设备' }, 1345 }) 1346 .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT]) 1347 .backgroundColor(this.buttonBackgroundColor) 1348 .onDragEnter(()=>{ 1349 // 当用户拖拽进入按钮范围,即提醒用户,此处是可以处理数据的 1350 this.buttonBackgroundColor = this.reminderColor 1351 }) 1352 .onDragLeave(()=>{ 1353 // 当用户拖拽离开按钮范围,恢复UI 1354 this.buttonBackgroundColor = this.normalColor 1355 }) 1356 .onDragSpringLoading((context: SpringLoadingContext)=>{ 1357 this.handleSpringLoading(context); 1358 }) 1359 }.width('100%').height('100%') 1360 .justifyContent(FlexAlign.Center) 1361 } 1362 } 1363``` 1364 1365**运行效果** 1366 1367 1368 1369<!--RP1--><!--RP1End-->