1# Implementing Unified Drag and Drop 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiangtao92--> 5<!--Designer: @piggyguy--> 6<!--Tester: @songyanhong--> 7<!--Adviser: @HelloCrease--> 8 9Unified drag and drop refers to a data transfer interaction triggered by a mouse device or gesture. Users can drag data from one component (the drag source) and drop it into another (the drop target) to initiate a response. In this interaction, the drag source provides the data, while the drop target receives and processes it, thereby enabling users to easily move, copy, or delete data. 10 11## Basic Concepts 12 13* Drag operation: an operation that begins when a user selects a draggable component, continues when the user drags the component on the screen, and ends when the user releases the component on a droppable component. 14* Drag preview (background): a visual representation of the data being dragged. You can set it by using [CustomerBuilder](../reference/apis-arkui/arkui-ts/ts-types.md#custombuilder8) or [DragItemInfo](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragiteminfo) of [onDragStart](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragstart), or by using the universal attribute [dragPreview](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#dragpreview11). 15* Drag data: data being dragged, encapsulated using the UDMF API [UnifiedData](../reference/apis-arkdata/js-apis-data-unifiedDataChannel.md#unifieddata) to ensure data consistency and security. 16* Drag source: component that initiates the drag operation and provides data, typically with characteristics for responding to dragging. 17* Drop target: component that can receive and process drag data, and is able to perform corresponding actions based on the data being dropped. 18* Drag point: point of contact between the mouse device or finger and the screen. It is used to determine whether data enters a drop target. The determination is based on whether the contact point is within the bounds of the component. 19 20## Drag Process 21 22The drag process encompasses both gesture-based dragging and mouse-based dragging. This distinction helps clarify the timing of callback event triggers. 23 24### Gesture-based Drag Process 25 26If a drag operation is triggered by a long press gesture, ArkUI checks whether the current component is draggable before initiating the drag. For components that are draggable by default ([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)), ArkUI checks whether their [draggable](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#draggable) attribute is set to **true** (the system predefines the default value of this attribute for these components through [system resources](../quick-start/resource-categories-and-access.md#system-resources)); for other components, ArkUI also checks whether the **onDragStart** callback is set. If the attribute or callback is set as required, ArkUI starts dragging once the user has long pressed the component for 500 ms or longer, and displays a drag preview once the user has long pressed the component for 800 ms. When the drag operation is used together with a menu controlled by the **isShow** attribute for visibility, avoid delaying the display of the menu by 800 ms after the user's action. Otherwise, unexpected behavior may occur. 27 28Below you can see the drag process initiated by a gesture (finger or stylus). 29 30 31 32### Mouse-based Drag Process 33 34When a mouse device is used as the pointer, ArkUI starts dragging once the draggable component has been moved with the left mouse button by more than 1 vp. 35 36A drag and drop can occur in a single application, or start in one application and end in another. The following callback events are provided for you to detect the dragging status and intervene in the default dragging behavior of the system. 37 38| Callback Event| Description| 39| ---------------- | ------------------------| 40| [onDragStart](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragstart) | Triggered when a draggable component is dragged.<br>You can use this callback to detect the initiation of dragging behavior. You can also set the drag data and drag preview in this callback. To avoid extra performance overhead, it is recommended that the drag preview be returned in the form of a pixel map, instead of using **customBuilder**.| 41| [onDragEnter](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragenter) | Triggered when the drag point enters the bounds of the component. This callback is called only when the component listens for the [onDrop](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondrop) event.| 42| [onDragMove](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragmove) | Triggered when the drag point moves within the bounds of the component, but only if the component listens for the **onDrop** event.<br>During the movement, the **setResult** API in[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7) can be used to affect the system appearance in some scenarios.<br>1. Set **DragResult.DROP\_ENABLED**.<br>2. Set **DragResult.DROP\_DISABLED**.| 43| [onDragLeave](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragleave) | Triggered when the drag point leaves the bounds of the component. This callback is called only when the component listens for the **onDrop** event.<br>By default, the **onDragLeave** callback is not called in the following cases:<br>1. An item in a parent component is dragged to one of its child components.<br>2. The layout of the drop target component overlaps that of the drag source component.<br>Since API version 12, the [setDragEventStrictReportingEnabled](../reference/apis-arkui/arkts-apis-uicontext-dragcontroller.md#setdrageventstrictreportingenabled12) API in [UIContext](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md) can be used to trigger the **onDragLeave** event in a strict fashion.| 44| [onDrop](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondrop) | Triggered when the dragged item is dropped on the component. The drag result must be set in this callback through the **setResult** API in **DragEvent**. Otherwise, the **getResult** API in the **onDragEnd** method of the drag source only returns the default result **DragResult.DRAG\_FAILED**.<br>This callback is where you can intervene in the default drop processing behavior. The system preferentially executes the **onDrop** callback and processes the drag data based on the **setResult** API in the callback function.<br>1. If **DragResult.DRAG\_SUCCESSFUL** is set, you need to process the data on your own; the system does not process the data.<br>2. If **DragResult.DRAG\_FAILED** is set, the system does not process the data.<br>3. If **DragResult.DRAG\_CANCELED** is set, the system does not process the data.<br>4. Setting **DragResult.DROP\_ENABLED** or **DragResult.DROP\_DISABLED** will be ignored, producing the same effect as **DragResult.DRAG\_SUCCESSFUL**.| 45| [onDragEnd](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragend10) | Triggered when dragging of the component ends.| 46| [onPreDrag](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#onpredrag12) | Triggered when the component enters a state prior to a drop and drop operation.<br>You can use this callback to listen for the value of [PreDragStatus](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#predragstatus12) to prepare corresponding data.<br>1. **ACTION\_DETECTING\_STATUS**: A drag gesture is being detected. Triggered when the component is long pressed for 50 ms.<br>2. **READY\_TO\_TRIGGER\_DRAG\_ACTION**: The component is ready to be dragged. Triggered when the component is long pressed for 500 ms.<br>3. **PREVIEW\_LIFT\_STARTED**: A lift animation is started. Triggered when the component is long pressed for 800 ms.<br>4. **PREVIEW\_LIFT\_FINISHED**: A lift animation is finished. Triggered at the completion of the lift animation.<br>5. **PREVIEW\_LANDING\_STARTED**: A drop animation is started. Triggered when the drop animation starts.<br>6. **PREVIEW\_LANDING\_FINISHED**: A drop animation is finished. Triggered when the drop animation ends.<br>7. **ACTION\_CANCELED\_BEFORE\_DRAG**: A drop animation is terminated. Triggered when the finger is lifted off the screen after the component enters the **READY_TO_TRIGGER_DRAG_ACTION** state.| 47 48[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7) provides getters to obtain information about the drag operation. The table below lists whether the getters can return valid data in the corresponding drag callbacks. 49| Callback Event| onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop | onDragEnd | 50| - | - | - | - | - | - | - | 51| getData |—|—|—|—| Supported|—| 52| getSummary |—| Supported| Supported| Supported| Supported|—| 53| getResult |—|—|—|—|—| Supported| 54| getPreviewRect |—|—|—|—| Supported|—| 55| getVelocity/X/Y |—| Supported| Supported| Supported| Supported|—| 56| getWindowX/Y | Supported| Supported| Supported| Supported| Supported|—| 57| getDisplayX/Y | Supported| Supported| Supported| Supported| Supported|—| 58| getX/Y | Supported| Supported| Supported| Supported| Supported|—| 59| behavior |—|—|—|—|—| Supported| 60 61[DragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7) also provides setters to transfer information to the system, which may affect how the system handles UI or data. The table below lists the stages in the callbacks where the setters should be executed for the information to be accepted and processed by the system. 62| Callback Event| onDragStart | onDragEnter | onDragMove | onDragLeave | onDrop | 63| - | - | - | - | - | - | 64| useCustomDropAnimation |—|—|—|—| Supported| 65| setData | Supported|—|—|—|—| 66| setResult | Supported; can be used to prevent dragging initiation by setting failed or cancel| Supported; not passed as the final result to **onDragEnd**| Supported; not passed as the final result to **onDragEnd**| Supported; not passed as the final result to **onDragEnd** | Supported; passed as the final result to **onDragEnd**| 67| behavior |—| Supported| Supported| Supported| Supported| 68 69## Drag Preview 70 71The drag preview is an image displayed during the drag and drop operation. It is a visual representation of the drag data, not the component itself. You can set it to any supported image that you want to display to users. The **customBuilder** or **pixelMap** object returned by the **onDragStart** callback can be used to set the drag preview displayed during dragging and moving, with a snapshot of the component being used as the default drag preview during a lift animation. The **customBuilder** or **pixelMap** object set by the **dragPreview** attribute can be used to set the drag preview during a lift animation and dragging. If no custom drag preview is set, the system uses a snapshot of the component by default. 72 73You can set the opacity, rounded corners, shadow, and blur for the drag preview. For details, see [Drag and Drop Control](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md). 74 75 76 77**Constraints**: 78 79* For a container component, if the internal content exceeds the bounds of the component due to **position**, **offset**, or other settings, the component snapshot does not capture the excess content. To show the excess content, you can expand the container scope or customize the container. 80* Regardless of whether you use a custom builder or rely on the default snapshot mechanism, the snapshot process does not support transformation APIs, including [scale](../reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md#scale) and [rotate](../reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md#rotate). 81 82## Drag and Drop Capability 83 84### General Drag and Drop Adaptation 85 86The following uses the [Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md) component as an example to describe the basic procedure for drag and drop development and the precautions to be taken during development. 87 881. Make the component draggable. 89 90 Set the **draggable** attribute to **true** and set the **onDragStart** callback function. In the callback function, you can use UDMF to set the drag data and return the custom drag preview. 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 // The custom drag preview is returned in onDragStart. 110 return dragItemInfo; 111 }) 112 ``` 113 114 The gesture-based drag operation is initiated by a long press gesture bound at the underlying layer. If a long press gesture is also bound to the dragged component, gesture conflict will occur, resulting in dragging to fail. To address this issue, you can use parallel gestures. 115 116 ```ts 117 .parallelGesture(LongPressGesture().onAction(() => { 118 this.getUIContext().getPromptAction().showToast({ duration: 100, message: 'Long press gesture trigger' }); 119 })) 120 ``` 121 1222. Customize the drag preview. 123 124 Prepare a pixel map for the custom drag preview within the callback triggered by [onPreDrag](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#onpredrag12) after a long press of 50 ms. 125 126 ```ts 127 .onPreDrag((status: PreDragStatus) => { 128 if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) { 129 this.getComponentSnapshot(); 130 } 131 }) 132 ``` 133 134 To generate a pixel map, you can use [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. To make sure the [onDragLeave](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#ondragleave) event is triggered as expected, use the [setDragEventStrictReportingEnabled](../reference/apis-arkui/arkts-apis-uicontext-dragcontroller.md#setdrageventstrictreportingenabled12) API. 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. Set the badge displayed during dragging. 184 185 You can set [allowDrop](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#allowdrop) to define the allowed data types for dropping, which affects the badge display. The **COPY** badge is displayed when the drag data matches the allowed data types, the **FORBIDDEN** badge is displayed when it does not, and the **MOVE** badge is displayed if **allowDrop** is not set. The following example allows only data of HYPERLINK and PLAIN\_TEXT types defined in UnifiedData. 186 187 ```ts 188 .allowDrop([uniformTypeDescriptor.UniformDataType.HYPERLINK, uniformTypeDescriptor.UniformDataType.PLAIN_TEXT]) 189 ``` 190 191 If the **onDrop** callback is implemented, you can control the badge display by setting [DragResult](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragresult10) to **DROP_ENABLED** in **onDragMove** and setting [DragBehavior](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragbehavior10) to **COPY** or **MOVE**. The following code forces the badge to display **MOVE** during a drag operation: 192 193 ```ts 194 .onDragMove((event) => { 195 event.setResult(DragResult.DROP_ENABLED); 196 event.dragBehavior = DragBehavior.MOVE; 197 }) 198 ``` 199 2005. Receive drag data. 201 202 Set the **onDrop** callback to handle the drag data and determine the drag result. 203 204 ```ts 205 .onDrop((dragEvent?: DragEvent) => { 206 // Obtain the drag data. 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 // Explicitly set the result to successful, and then pass this value to onDragEnd of the drag source. 215 event.setResult(DragResult.DRAG_SUCCESSFUL); 216 }) 217 ``` 218 219 Data transfer is managed by UDMF, which may experience latency when dealing with large data volumes. Therefore, you are advised to implement a retry mechanism with a 1500 ms delay after the initial data acquisition fails. 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. The drag initiator can detect the result of the drag operation by setting the **onDragEnd** callback. 251 252 ```ts 253 import { promptAction } from '@kit.ArkUI'; 254 255 .onDragEnd((event) => { 256 // The result value obtained from onDragEnd is set in onDrop of the drop target. 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**Sample Code** 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 // Obtain UDMF data with a retry mechanism of 1500 ms if the initial attempt fails. 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 // Use the createFromBuilder API of componentSnapshot to capture a snapshot of a custom 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 // Prepare a custom screenshot pixel map after a 50 ms long press is detected. 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 // Bind a parallel gesture to trigger a custom long press gesture. 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 // Prepare a custom drag preview in advance. 369 .onPreDrag((status: PreDragStatus) => { 370 this.PreDragChange(status); 371 }) 372 .onDragEnd((event) => { 373 // The result value obtained from onDragEnd is set in onDrop of the drop target. 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 // Set the drag behavior to MOVE, which means no badge is displayed. 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 // Obtain the drag data. 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 // Explicitly set the result to successful, and then pass this value to onDragEnd of the drag source. 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### Multi-Select Drag and Drop Adaptation 427 428Since API version 12, the **GridItem** and **ListItem** components, which are child components of [Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md) and [List](../reference/apis-arkui/arkui-ts/ts-container-list.md), respectively, support multi-select drag and drop, which can be initiated through the **onDragStart** API. 429 430The following uses **Grid** as an example to describe the basic procedure for multi-select drag and drop development and key considerations during development. 431 4321. Enable multi-select drag and drop. 433 434 Create **GridItem** child components and bind the **onDragStart** callback to them. In addition, set the **GridItem** components to be selectable. 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 Multi-select drag and drop is disabled by default. To enable it, set **isMultiSelectionEnabled** to **true** in the **DragInteractionOptions** parameter of the [dragPreviewOptions](../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-drop.md#dragpreviewoptions11) API. **DragInteractionOptions** also has the **defaultAnimationBeforeLifting** parameter, which, when set to **true**, applies a default scaling down animation as the lift animation for the component. 454 455 ```ts 456 .dragPreviewOptions({isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true}) 457 ``` 458 459 To maintain the selected state, set the **selected** attribute of the **GridItem** components to **true**. For example, you can use [onClick](../reference/apis-arkui/arkui-ts/ts-universal-events-click.md#onclick) to set a specific component to the selected state. 460 461 ```ts 462 .selected(this.isSelectedGrid[idx]) 463 .onClick(()=>{ 464 this.isSelectedGrid[idx] = !this.isSelectedGrid[idx] 465 }) 466 ``` 467 4682. Optimize the multi-select drag and drop performance. 469 470 In multi-select drag and drop scenarios, there is a clustering animation effect when multiple items are selected. This effect captures a snapshot of the selected components currently displayed on the screen, which can incur high performance costs if there are too many selected components. To save on performance, multi-select drag and drop allows for the use of a snapshot from **dragPreview** as the basis for the clustering animation. 471 472 ```ts 473 .dragPreview({ 474 pixelMap:this.pixmap 475 }) 476 ``` 477 478 To obtain a snapshot of a component, you can call the [this.getUIContext().getComponentSnapshot().get()](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#get12) API when the component is selected. The following shows how to use the component ID to obtain the snapshot. 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. Set the multi-select display effects. 498 499 Use [stateStyles](../reference/apis-arkui/arkui-ts/ts-universal-attributes-polymorphic-style.md#statestyles) to set display effects for selected and unselected states for easy distinction. 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. Adapt the number badge. 519 520 Configure the number badge for multi-select drag and drop using the **numberBadge** parameter in **dragPreviewOptions**, adjusting it based on the number of selected items. 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 // Set the numberBadge parameter in dragPreviewOptions for the number badge in multi-select scenarios. 534 .dragPreviewOptions({numberBadge: this.numberBadge}) 535 ``` 536 537**Sample Code** 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 // Set the multi-select display effects. 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 // Call the get API in componentSnapshot to obtain the component snapshot pixel map on selection. 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 // Enable multi-select and set the number badge. 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### Custom Drop Animation Adaptation 632 633When you need to create custom drop animations, you can disable the default system animations. Since API version 18, ArkUI provides the [executeDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#executedropanimation18) API, which allows you to define your own drop animations. The following provides step-by-step instructions using the **Image** component as an example, along with key points to keep in mind during development. 634 6351. Configure drag and drop settings for the component. 636 Set **draggable** to **true** and configure callbacks such as **onDragStart** and **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. Define your custom animation. 648 649 Use the [animateTo](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md#animateto) API to create a custom animation. For example, you can change the size of the component. 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. Trigger the custom drop animation. 662 663 Configure the **onDrop** callback to receive the drag data. Execute your custom drop animation using the [executeDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#executedropanimation18) API. Set [useCustomDropAnimation](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7) to **true** to disable the default system animation. 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**Sample Code** 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### Handling Large Volumes of Data 769 770When dealing with a large number of items or large data volumes during drag and drop operations, processing all the data at once can negatively impact the user experience. The following uses the **Grid** component as an example to provide recommended practices for handling large data volumes during drag and drop operations, along with key points to keep in mind during development. 771 7721. Enable multi-select drag and drop. 773 774 Create **GridItem** child components and set their state to be selectable. Enable multi-select drag and drop by setting **isMultiSelectionEnabled** to **true**. Use the selected state to distinguish whether an item is selected. 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 To maintain performance, limit the maximum number of items for multi-select drag and drop to 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. Add data incrementally when items are selected. 815 816 When dealing with large data volumes, you are advised to add data records incrementally using [addRecord](../reference/apis-arkdata/js-apis-data-unifiedDataChannel.md#addrecord) as items are selected. This avoids significant performance overhead from processing all data at once during the drag operation. 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 // Call the get API in componentSnapshot to obtain the component snapshot pixel map on selection. 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. Prepare data in advance. 854 855 Use the **onPreDrag** callback to receive a signal that a drag operation is about to start. If the data volume is large, prepare the data in advance. 856 857 ```ts 858 .onPreDrag((status: PreDragStatus) => { 859 if (status == PreDragStatus.PREPARING_FOR_DRAG_DETECTION) { 860 this.loadData() 861 } 862 }) 863 ``` 864 8654. Block the drag operation if data preparation is not complete. 866 867 When initiating a drag operation, check whether the data is ready. If the data is not yet ready, send a [WAITING](../reference/apis-arkui/js-apis-arkui-dragController.md#dragstartrequeststatus18) signal to the system to block the drag operation. In this case, if the user performs a drag gesture, the drag preview will remain stationary until the application sends a READY signal or the maximum blocking time limit (5 seconds) is exceeded. If the data is ready, you can directly set it to [dragEvent](../reference/apis-arkui/arkui-ts/ts-universal-events-drag-drop.md#dragevent7). Note that when using the blocking feature, you need to save the current **dragEvent** and set the data when preparation is complete. In non-blocking scenarios, saving the current **dragEvent** is not recommended. 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**Sample Code** 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 // State after data preparation is complete. 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('Select All') 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 // Call the get API in componentSnapshot to obtain the component snapshot pixel map on selection. 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 // Set the multi-select display effects. 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 // Call the get API in componentSnapshot to obtain the component snapshot pixel map on selection. 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. Long press notification. Callback upon 350 ms. 1016 if (status == PreDragStatus.PREPARING_FOR_DRAG_DETECTION) { 1017 // 2. The user presses and holds for a period of time without releasing, which may lead to dragging: Prepare data at this time. 1018 this.loadData() 1019 } else if (status == PreDragStatus.ACTION_CANCELED_BEFORE_DRAG) { 1020 // 3. The user stops the drag operation: Cancel data preparation (simulation method: cancel the timer). 1021 clearTimeout(this.timeout); 1022 } 1023 }) 1024 // Triggered when the component is pressed for 500 ms or longer and moved more than 10 vp. 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## Spring Loading (Hover Detection) Support 1051Spring loading, also known as drag hover detection or spring-loaded navigation, is an enhanced drag and drop capability that allows users to automatically trigger view transitions by hovering over targets during drag operations. This feature significantly improves operational efficiency and is recommended for implementation in all page transition scenarios. 1052 1053> This feature is supported since API version 20. 1054 1055This feature is particularly useful in the following scenarios: 1056 1057- File management: Dragging a file over a folder automatically expands the folder. 1058- Home screen launcher: Hovering a file over an application icon automatically launches the application. 1059 1060Beyond view transitions, spring loading can also activate specific UI elements. For example, when a user drags text and hovers it over a button, a text boxcan be activated. The user can then move the dragged text into this text box and release it to display search results, enabling efficient one-handed operation. 1061 1062 1063 1064### Implementation Principle 1065 1066To implement this feature, register the **onDragSpringLoading** API on a component and pass a callback to handle hover trigger notifications. Once registered, the component acts as a drop target (similar to components using the **onDrop** API) and follows the same hit detection rules: Only the topmost component under the hover position receives the drag event response. 1067 1068The spring loading process follows three distinct phases: hover detection -> callback notification -> completion. If the user continues dragging before completion, spring loading is automatically canceled, triggering a cancellation notification to the application. However, if dragging is resumed during the hover detection phase before the spring loading state is entered, no cancellation notification is sent. 1069 1070 1071 1072Applications receive state updates through callbacks, enabling dynamic UI adjustments. 1073 1074| State | Description | Recommended Action | 1075| :----- | :------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------- | 1076| BEGIN | The user has hovered over the component long enough to start entering the spring loading state. | Change the background color or adjust the component size to provide visual feedback. | 1077| UPDATE | The user holds the pointer device stationary, and the system sends periodic refresh notifications (default: 3 times). | Use the notification sequence parity to determine whether to reset UI display, creating pulsating visual effects.| 1078| END | The user has hovered long enough to complete the entire spring loading process. | Execute page transitions or view changes. | 1079| CANCEL | The user continues dragging or hover detection is interrupted in BEGIN state, preventing completion of hover detection.| Reset the UI state and cancel view transition-related states and logic. | 1080 1081>**NOTE** 1082> 1083>1. Keeping the pointer device stationary within the same component triggers only one complete spring loading cycle. The cycle will not repeat until the pointer device leaves and re-enters the component. 1084>2. A component can support both spring loading and other drag events (for example, **onDrop** and **onDragEnter**). 1085 1086 1087### Triggering Customization 1088 1089You can customize spring loading detection parameters to dynamically determine whether to continue triggering. 1090 10911. Trigger parameter customization 1092 1093 The **onDragSpringLoading** API includes an optional **configuration** parameter, which allows you to customize settings such as detection duration, trigger intervals, and number of triggers. This enables personalized definition of spring loading trigger conditions. In most cases, however, the system's default configuration is sufficient, and no modifications are required. 1094 1095 The **configuration** parameter must be set before detection begins. Once the system initiates the spring loading detection process, it will no longer read updates from this parameter. Yet, you can still dynamically adjust configurations during detection using the **updateConfiguration** API in the **context** object provided using the callback. These dynamic updates apply only to the current trigger and do not affect the base configurations set through the **configuration** parameter. 1096 1097 It is recommended that you use either the default configuration or fixed parameters through the **configuration** parameter in **onDragSpringLoading**. In most cases, dynamically modifying detection parameters during spring loading is unnecessary. However, this functionality can be useful if you need to provide different user feedback based on the type of dragged data. 1098 1099 >**NOTE** 1100 > 1101 >Avoid setting excessively long time intervals or overly frequent trigger counts, as these typically fail to provide meaningful user feedback. 1102 11032. Dynamic termination 1104 1105 When the system detects sufficient hover duration and invokes the **onDragSpringLoading** callback, you can decide whether to allow the pending spring loading notification to proceed. This is particularly useful when you need to check the type of dragged data and align it with your service logic. 1106 1107 The following pseudocode demonstrates this dynamic termination functionality: 1108 ```typescript 1109 .onDragSpringLoading((context: DragSpringLoadingContext)=>{ 1110 // Check the current state. 1111 if (context.state == DragSpringLoadingState.BEGIN) { 1112 // Verify whether the dragged data type can be processed. 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") { // Only plain text can be processed. 1118 isICanHandle = true; 1119 break; 1120 } 1121 } 1122 } 1123 // Terminate spring loading if data cannot be processed. 1124 if (!isICanHandle) { 1125 context.abort(); 1126 return; 1127 } 1128 } 1129 }) 1130 ``` 1131 11323. Disabling spring loading 1133 1134 If you no longer need a component to respond to spring loading, you can explicitly disable the feature by passing **null** to **onDragSpringLoading**: 1135 1136 ```typescript 1137 .onDragSpringLoading(null) 1138 ``` 1139 1140### Example 1141 1142The following example demonstrates how to implement the device search functionality using **onDragSpringLoading**, including visual feedback and view switching triggered by drag hover interactions. 1143 11441. Prepare components. 1145 1146 To simplify the example, create two core components: a draggable text component and a button control. The button responds to spring loading to activate a view implemented using **bindSheet**, containing a text box for receiving dragged text and a text component for displaying search results. 1147 1148 ```typescript 1149 build() { 1150 Column() { 1151 Column() { 1152 Text('Double-click to select and drag text: \n DeviceName') 1153 .fontSize(30) 1154 .copyOption(CopyOptions.InApp) // Enable text selection and dragging when copyOption is enabled. 1155 }.padding({bottom:30}) 1156 1157 Button('Search Devices').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: 'Search Devices' }, 1162 }) 1163 }.width('100%').height('100%') 1164 .justifyContent(FlexAlign.Center) 1165 } 1166 ``` 11672. Implement **SheetBuilder**. 1168 1169 Implements the UI for the sheet. 1170 1171 ```typescript 1172 @Builder 1173 SheetBuilder() { 1174 Column() { 1175 // Text box 1176 TextInput({placeholder: 'Drag text here'}) 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 // Simplified handling: display fixed search results. 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. Add the enter and leave response to the button. 1194 1195 To provide visual feedback, add **onDragEnter** and **onDragLeave** handlers to the target component. When text is dragged over the component, the background color changes to prompt the user. 1196 1197 ```typescript 1198 .onDragEnter(()=>{ 1199 // Change the button color when dragged text enters the area. 1200 this.buttonBackgroundColor = this.reminderColor 1201 }) 1202 .onDragLeave(()=>{ 1203 // Restore the original color when text leaves the area. 1204 this.buttonBackgroundColor = this.normalColor 1205 }) 1206 ``` 1207 12084. Implement spring loading response. 1209 1210 Implement a spring loading handler to process all states. 1211 1212 ```typescript 1213 handleSpringLoading(context: dragController.SpringLoadingContext) { 1214 // Check the drag data type during the BEGIN state. 1215 if (context.state == dragController.DragSpringLoadingState.BEGIN) { 1216 // Add necessary checks to decide whether to terminate the process. 1217 return; 1218 } 1219 if (context.state == dragController.DragSpringLoadingState.UPDATE) { 1220 // Provide periodic visual reminders during hovering. 1221 return; 1222 } 1223 // Handle completion: Trigger view transition. 1224 if (context.state == dragController.DragSpringLoadingState.END) { 1225 // Activate or switch views. 1226 return; 1227 } 1228 // Handle cancellation: Restore the UI. 1229 if (context.state == dragController.DragSpringLoadingState.CANCEL) { 1230 // Restore the state and UI. 1231 return; 1232 } 1233 } 1234 ``` 1235 1236**Complete Code** 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 = 'Search results:\n Device 1\n Device 2\n Device 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 // Text box 1256 TextInput({placeholder: 'Drag text here'}) 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 // Simplified handling: display fixed search results. 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 // Check whether the dragged data contains 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 // Handle the BEGIN state. 1291 handleBeginState(context: SpringLoadingContext): boolean { 1292 // Verify whether the dragged data type can be processed. 1293 if (this.checkDataType(context?.dragInfos?.dataSummary)) { 1294 return true; 1295 } 1296 // Terminate spring loading if data cannot be processed. 1297 context.abort(); 1298 return false; 1299 } 1300 1301 // Spring Loading handler. 1302 handleSpringLoading(context: SpringLoadingContext) { 1303 // Check the drag data type during the BEGIN state. 1304 if (context.state == dragController.DragSpringLoadingState.BEGIN) { 1305 if (this.handleBeginState(context)) { 1306 // The visual feedback is already provided in onDragEnter. When the spring loading state is entered, restore the UI to prompt the user to remain stationary. 1307 this.buttonBackgroundColor = this.normalColor; 1308 } 1309 return; 1310 } 1311 if (context.state == dragController.DragSpringLoadingState.UPDATE) { 1312 // Alternate the UI reminder color based on the notification sequence parity. 1313 if (context.currentNotifySequence % 2 != 0) { 1314 this.buttonBackgroundColor = this.reminderColor; 1315 } else { 1316 this.buttonBackgroundColor = this.normalColor; 1317 } 1318 return; 1319 } 1320 // Handle completion: Trigger view transition. 1321 if (context.state == dragController.DragSpringLoadingState.END) { 1322 this.isShowSheet = true; 1323 return; 1324 } 1325 // Handle cancellation: Restore the 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('Double-click to select and drag text: \n DeviceName') 1336 .fontSize(30) 1337 .copyOption(CopyOptions.InApp) // Enable text selection and dragging when copyOption is enabled. 1338 }.padding({bottom:30}) 1339 1340 Button('Search Devices').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: 'Search Devices' }, 1345 }) 1346 .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT]) 1347 .backgroundColor(this.buttonBackgroundColor) 1348 .onDragEnter(()=>{ 1349 // Change the button color when dragged text enters the area. 1350 this.buttonBackgroundColor = this.reminderColor 1351 }) 1352 .onDragLeave(()=>{ 1353 // Restore the original color when text leaves the area. 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<!--RP1--><!--RP1End--> 1367<!--no_check-->