首页 > 编程笔记 > 通用技能 阅读:4

ArkUI拖曳事件详解(附带实例)

拖曳框架提供了一种通过鼠标或手势触屏传递数据的方式,即从一个组件位置拖出数据,并将它拖入另一个组件位置上进行响应。拖出一方提供数据,拖入一方接收和处理数据。该操作可以让用户方便地移动、复制或删除指定内容。

拖曳事件主要包括以下几个概念:

手势拖曳

对于手势长按触发拖曳的场景,在发起拖曳前,框架会对当前组件是否可拖曳进行校验:
当满足上述可拖曳条件时,长按大于或等于 500ms 即可触发拖曳,长按 800ms 后开始进行预览图的浮起动效。

当拖曳操作与菜单功能一起使用,并通过 isShow 方式控制菜单显隐时,不建议在用户操作了 800ms 后再控制菜单显示,因为这可能会导致非预期的行为。

手势拖曳(手指/手写笔)触发拖曳的流程如下图所示:


图 1 手势拖曳触发拖曳流程

鼠标拖曳

鼠标拖曳属于即拖即走,只要鼠标左键在可拖曳的组件上按下并移动大于 1vp,即可触发拖曳。

当前支持应用内和跨应用的鼠标拖曳,提供了多个回调事件供开发者感知拖曳状态,并干预系统默认的拖曳行为,具体如下表所示。

表:应用内和跨应用鼠标拖曳的事件回调
回调事件 说明
onDragStart 当可拖拽的组件产生拖拽动作时触发。该回调可以感知拖拽行为的发起,开发者可在 onDragStart 方法中设置拖拽传递的数据以及自定义拖拽背板图。推荐开发者使用 pixelmap 的方式返回背板图,不推荐使用 customBuilder 的方式,因为后者会有额外的性能开销。
onDragEnter 当拖拽活动的拖拽点进入组件范围内时触发,只有在该组件监听了 onDrop 事件时,此回调才会被触发。
onDragMove 当拖拽点在组件范围内移动时触发;只有在该组件监听了 onDrop 事件时,此回调才会被触发。在此过程中可通过 DragEvent 中的 setResult 方法影响系统在部分场景下的外观。当在此事件中将 DragEvent 的结果设置为 DragResult.DROP_ENABLED 时,表示当前组件可以接收被拖拽的元素;将 DragEvent 的结果设置为 DragResult.DROP_DISABLED 时,表示当前组件不允许接收被拖拽的元素。
onDragLeave 当拖拽点离开组件范围时触发;只有在该组件监听了 onDrop 事件时,此回调才会被触发。针对以下两种情况默认不会发送 onDragLeave 事件:①父组件移动到子组件;②目标组件与当前组件布局有重叠。从 API version 12 开始,可通过 UIContext 中的 setDragEventStrictReportingEnabled 方法严格触发 onDragLeave 事件。
onDrop 当用户在组件范围内释放拖拽的内容时触发。需在此回调中通过 DragEvent 中的 setResult 方法设置拖拽结果,否则在拖出方组件的 onDragEnd 方法中通过 getResult 方法只能拿到默认的处理结果 DragResult.DRAG_FAILED。该回调也是开发者干预系统默认拖入处理行为的地方,系统会优先执行开发者的 onDrop 回调,并通过在该回调中执行 setResult 方法来告知系统如何处理所拖拽的数据:
  • DragResult.DRAG_SUCCESSFUL:数据完全由开发者处理,系统不进行处理。
  • DragResult.DRAG_FAILED:系统不再继续处理拖拽的数据,处理过程结束。
  • DragResult.DRAG_CANCELED:表示取消此次拖拽,系统无须进行任何数据处理。
  • DragResult.DROP_ENABLED 或 DragResult.DROP_DISABLED:系统会忽略该设置,视同 DragResult.DRAG_FAILED 处理。
onDragEnd 当用户释放拖拽时,拖拽活动结束,发起拖出动作的组件会触发该回调。
onPreDrag 绑定此事件的组件,在拖拽开始前的不同阶段,会触发该回调。开发者可以使用该回调监听 PreDragStatus 类型参数值,在发起拖拽前的不同阶段准备不同的数据:
  • ACTION_DETECTING_STATUS:拖拽手势启动阶段(按下 50 ms 时触发)。
  • READY_TO_TRIGGER_DRAG_ACTION:拖拽准备完成,可发起拖拽阶段(按下 500 ms 时触发)。
  • PREVIEW_LIFT_STARTED:拖拽浮起动效发起阶段(按下 800 ms 时触发)。
  • PREVIEW_LIFT_FINISHED:拖拽浮起动效结束阶段(浮起动效完全结束时触发)。
  • PREVIEW_LANDING_STARTED:拖拽落回动效发起阶段(落回动效发起时触发)。
  • PREVIEW_LANDING_FINISHED:拖拽落回动效结束阶段(落回动效结束时触发)。
  • ACTION_CANCELED_BEFORE_DRAG:拖拽浮起落回动效中断(已满足 READY_TO_TRIGGER_DRAG_ACTION 状态后,未达到动效阶段,手指抬手时触发)。

DragEvent 支持 get 方法获取拖曳行为的相关信息,下表列出了 get 方法在对应拖曳回调中是否能返回有效数据。

表:get方法获取拖曳行为的相关信息列表
回调事件 onDragStart onDragEnter onDragMove onDragLeave onDrop onDragEnd
getData - - - - 支持 -
getSummary - 支持 支持 支持 支持 -
getResult - - - - - 支持
getPreviewRect 支持 - - - 支持 -
getVelocity/X/Y - 支持 支持 支持 支持 -
getWindowX/Y 支持 支持 支持 支持 支持 -
getDisplayX/Y 支持 支持 支持 支持 支持 -
getX/Y 支持 支持 支持 支持 支持 -
behavior - - - - - 支持

DragEvent 支持 set 方法向系统传递信息,这些信息部分会影响系统对 UI 或数据的处理方式。下表列出了 set 方法应该在回调的哪个阶段执行才会被系统接收并处理。

表:set方法在各阶段执行效果
回调事件 onDragStart onDragEnter onDragMove onDragLeave onDrop
useCustomDropAnimation - - - - 支持
setData 支持 - - - -
setResult 支持,可通过 set failed 或 cancel 来阻止拖曳的发起 支持,不作为最终结果传递给 onDragEnd 支持,不作为最终结果传递给 onDragEnd 支持,不作为最终结果传递给 onDragEnd 支持,作为最终结果传递给 onDragEnd
behavior - 支持 支持 支持 支持

拖曳背板图

拖曳移动过程中显示的拖曳背板图,并非组件本身,而是用户拖动数据的表示,开发者可以将其设置为任意可显示的图像。

onDragStart 回调返回的 customBuilder 或 pixelmap 可以设置拖曳移动过程中的背板图,浮起图默认使用组件本身的截图;dragpreview 属性设置的 customBuilder 或 pixelmap 可以设置浮起和拖曳过程的背板图。如果开发者没有配置背板图,则系统会默认取组件本身的截图作为浮起及拖曳过程中的背板图。拖曳背板图当前支持设置透明度、圆角、阴影和模糊效果。

对于容器组件,如果内部内容通过 position、offset 等手段使得绘制区域超出了容器组件范围,那么系统无法截取到范围之外的内容。在此情况下,如果一定要让浮起及拖曳背板包含范围之外的内容,则可考虑通过扩大容器范围或使用自定义方式。不管是使用自定义 builder 还是系统默认的截图方式,截图都暂时无法应用 scale、rotate 等图形变换效果。

通用拖曳适配

要启用组件的拖曳功能,需把 draggable 属性设置为 true,并设置 onDragStart 回调,回调中可以通过 UDMF 设置拖曳的数据,并返回自定义拖曳背板图。手势场景触发拖曳依赖底层绑定的长按手势,若开发者在被拖曳组件上也绑定了长按手势,则会与底层的长按手势发生竞争,导致拖曳失败。开发者可以使用并行手势来解决此类问题。

自定义拖曳背板图的 pixmap 可以通过 onPreDrag 回调函数来设置。该回调函数会在长按 50ms 时触发,开发者可以在此时准备拖曳时所需的背板图和其他相关数据。

如果开发者希望严格控制 onDragLeave 事件的触发,则可以通过调用 setDragEventStrictReportingEnabled 方法来启用此功能。启用后,onDragLeave 事件会在拖曳元素离开目标区域时准确触发。通过设置 allowDrop 属性,开发者可以定义组件允许接收的数据类型,并据此控制拖曳时的角标显示:
此外,在实现 onDrop 回调的情况下,还可以通过在 onDragMove 中设置 DragResult为DROP_ENABLED,并设置 DragBehavior 为 COPY 或 MOVE,来控制角标的显示。

要处理拖曳数据,开发者需要设置 onDrop 回调,并在该回调中处理接收到的拖曳数据,同时显示设置拖曳结果。

数据的传递是通过 UDMF 实现的,在数据较大时可能存在时延,因此在首次获取数据失败时建议加 1500ms 的延迟重试机制,拖曳发起方可以通过设置 onDragEnd 回调感知拖曳结果。

具体的代码实现如下:
// 入口组件
@Entry
@Component
struct Demo0601 {
  // 目标图片 URI
  @State targetImage: string = '';
  // 图片显示大小(宽高相同)
  @State imageSize: number = 100;
  // 图片可见状态
  @State imgState: Visibility = Visibility.Visible;
  // 拖拽时使用的自定义像素图
  @State pixmap: image.PixelMap | undefined = undefined;

  /* ========= 自定义 Builder:用于生成拖拽背板图 ========= */
  @Builder pixelMapBuilder() {
    Column() {
      Image($r('app.media.background'))
        .width(120)
        .height(120)
        .backgroundColor(Color.Yellow);
    }
  }

  /* ========= UDMF 数据获取:带 1500 ms 重试机制 ========= */
  // 重试获取 UDMF 数据
  getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void): boolean {
    try {
      let data: UnifiedData = event.getData();
      if (!data) { return false; }

      let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
      if (!records || records.length <= 0) { return false; }

      callback(event);
      return true;
    } catch (e) {
      console.log('获取数据异常:' + (e as BusinessError).message);
      return false;
    }
  }

  // 首次获取失败后延迟 1500 ms 再次尝试
  getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
    if (this.getDataFromUdmfRetry(event, callback)) { return; }
    setTimeout(() => {
      this.getDataFromUdmfRetry(event, callback);
    }, 1500);
  }

  /* ========= 提前生成拖拽背板像素图 ========= */
  // 调用 createFromBuilder 获取自定义 builder 的截图
  private getComponentSnapshot(): void {
    this.getUIContext()
      .getComponentSnapshot()
      .createFromBuilder(
        () => this.pixelMapBuilder(),
        (error: Error, pixmap: image.PixelMap) => {
          if (error) { return; }
          this.pixmap = pixmap;
        }
      );
  }

  // 长按 50 ms 时提前准备自定义截图
  private PreDragChange(preDragStatus: PreDragStatus): void {
    if (preDragStatus === PreDragStatus.ACTION_DETECTING_STATUS) {
      this.getComponentSnapshot();
    }
  }

  /* ========= UI 构建 ========= */
  build() {
    Row() {
      Column() {
        Text('开始进行拖拽');

        /* ------------ 拖拽源组件 ------------ */
        Row() {
          Image($r('app.media.background'))
            .width(this.imageSize)
            .height(this.imageSize)
            .visibility(this.imgState)
            // 并行手势:允许同时响应系统拖拽与自定义长按
            .parallelGesture(
              LongPressGesture()
                .onAction(() => {
                  promptAction.showToast({
                    duration: 100,
                    message: '长按手势进行拖拽'
                  });
                })
            )
            // 拖拽开始
            .onDragStart((event) => {
              // 构造拖拽数据
              let data: unifiedDataChannel.Image = new unifiedDataChannel.Image();
              data.imageUri = 'common/harmonyos.jpg';
              let unifiedData = new unifiedDataChannel.UnifiedData(data);
              event.setData(unifiedData);

              // 返回自定义拖拽背板
              let dragItemInfo: DragItemInfo = {
                pixelMap: this.pixmap,
                extraInfo: '额外信息'
              };
              return dragItemInfo;
            })
            // 提前准备背板
            .onPreDrag((status: PreDragStatus) => {
              this.PreDragChange(status);
            })
            // 拖拽结束
            .onDragEnd((event) => {
              // 接收方 onDrop 中设置的 result 会在此拿到
              if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
                promptAction.showToast({ duration: 100, message: '拖拽成功' });
              } else if (event.getResult() === DragResult.DRAG_FAILED) {
                promptAction.showToast({ duration: 100, message: '拖拽失败' });
              }
            });
        }

        Text('拖拽的目标区域');

        /* ------------ 拖拽目标组件 ------------ */
        Row() {
          Image(this.targetImage)
            .width(this.imageSize)
            .height(this.imageSize)
            .draggable(true)
            .margin({ left: 15 })
            .border({ color: Color.Black, width: 1 })
            // 拖拽进入目标区域时行为
            .onDragMove((event) => {
              // 控制角标显示类型为 MOVE(即不额外显示角标)
              event.setResult(DragResult.DROP_ENABLED);
              event.dragBehavior = DragBehavior.MOVE;
            })
            // 允许接收的数据类型
            .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
            // 接收并处理拖拽数据
            .onDrop((dragEvent?: DragEvent) => {
              this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
                let records: Array<unifiedDataChannel.UnifiedRecord> =
                  event.getData().getRecords();
                let rect: Rectangle = event.getPreviewRect();
                this.imageSize = Number(rect.width);
                this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri;
                this.imgState = Visibility.None;

                // 显式设置成功结果,回传给拖出方的 onDragEnd
                event.setResult(DragResult.DRAG_SUCCESSFUL);
              });
            });
        }
      }.width('100%')
    }
  }
}
代码运行效果如下图所示:


图 2 通用拖曳适配效果

相关文章