• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![zh-cn_image_0000001562820825](figures/zh-cn_image_0000001562820825.png)
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![pixelMap](figures/pixelMap.png)
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![commonDrag](figures/commonDrag.gif)
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![multiDrag](figures/multiDrag.gif)
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![executeDropAnimation](figures/executeDropAnimation.gif)
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![patchDataProcess](figures/patchDataProcess.gif)
1048
1049
1050## 支持悬停检测
1051Spring Loading,即拖拽悬停检测(又叫弹簧加载)是拖拽操作的一项增强功能,允许用户在拖动过程中通过悬停在目标上自动触发视图跳转,提供了使用的便利性。建议在所有支持页面切换的区域均实现该功能。
1052
1053> 该能力从API version 20开始支持。
1054
1055以下为常见的适合支持该功能的场景:
1056
1057- 在文件管理器中,拖动文件并悬停在文件夹上时,文件夹可以自动打开。
1058- 在桌面启动器中,拖动文件并悬停在应用程序图标上时,应用程序可以自动打开。
1059
1060除了实现视图切换跳转功能,该能力也可用于特定视图的激活。例如,在用户将一段文本拖拽至按钮上停留后,可激活一个文本输入框。用户随后可将所拖拽文本移动至该输入框上方释放,触发搜索结果展示,实现单手高效完成整个操作。
1061
1062![drag spring loading example](figures/drag_springloading-01.png)
1063
1064### 触发原理
1065
1066要实现这些能力,需要在组件上注册onDragSpringLoading接口,并传入一个用于处理拖拽悬停触发通知的回调。使用该接口后,该组件将如同注册了onDrop接口的组件一样,成为一个可拖入目标,并且遵循与onDrop相同的命中检测规则,即:在悬停位置下方,仅有一个组件可以接收拖拽事件响应,并且总是首个被检测到的组件
1067
1068Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 -> 结束。在结束之前,如果用户重新开始移动,会自动中断Spring Loading,并通知应用取消。如果在悬停检测期间移动,且尚未进入Spring Loading状态,则不会触发取消通知。
1069
1070![drag spring loading pharse](figures/drag_springloading-02.png)
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![drag spring loading sample gif](figures/spring-loading-record.gif)
1368
1369<!--RP1--><!--RP1End-->