1. API使用指导

应用接入拖拽涉及ArkUI提供的OnDragStart、OnDrop等接口的使用,若应用需要自定义实现拖拽的触发逻辑,则应掌握DragController系接口的。为自定义处理各类拖拽数据,还应掌握 UDMF 相关接口的使用。针对一些资源在网络的场景,如在线Image,还应借助Http相关能力实现在线资源的下载落盘处理。

1.1 ArkUI Drag

ArkUI框架对以下组件实现了默认的拖拽能力,支持对数据的拖出或拖入响应,开发者只需要将这些组件的draggable属性设置为true,即可使用默认拖拽能力。其中,Text、TextInput、TextArea、Hyperlink、Image和RichEditor组件的draggable属性默认为true。默认支持拖出能力的组件(可从组件上拖出数据):Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink默认支持拖入能力的组件(目标组件可响应拖入数据):Search、TextInput、TextArea、Video开发者也可以通过实现通用拖拽事件来自定义拖拽响应。其他组件需要开发者将draggable属性设置为true,并在onDragStart等接口中实现数据传输相关内容,才能正确处理拖拽。

1.1.1 OnDragStart系列接口(框架能力)

draggable

设置当前组件是否可拖拽,系统默认可拖出的组件有Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink。

allowDrop

标识当前组件上支持拖拽落入的数据类型,该数据类型是UniformDataType定义标识的。

OnPreDrag

绑定该事件的组件,在触发拖拽前的不同阶段时,触发回调,可在该接口中实现一些在拖拽发起前需要完成的动作,如产生拖拽缩略图等。

DragItemInfo

该接口用于描述拖拽过程中缩略图的相关信息,可设置PixelMap或实现CustomBuilder来实现自定义拖拽背板图。若已设置过PixelMap,则CustomerBuilder将会被忽略, 使用CustomBuilder性能较差于使用PixelMap。

DragItemInfo中extraInfo标识一些系统侧需要处理的额外数据,三方应用一般不涉及。

DragEvent

拖拽事件,在拖拽起拖时可设置对应拖拽数据,在拖拽过程中可获取当前拖拽事件的坐标信息、拖拽数据类型、拖拽数据等信息。

OnDragStart

第一次拖拽此事件绑定的组件时,长按时间 >= 500ms,然后手指移动距离 >= 10vp,则触发回调。针对默认支持拖出能力的组件,如果开发者设置了onDragStart,优先执行开发者的onDragStart,并根据执行情况决定是否使用系统默认的拖出能力,具体为:如果开发者返回了自定义背板图,则不再使用系统默认的拖拽背板图;如果开发者设置了拖拽数据,则不再使用系统默认填充的拖拽数据。文本类组件Text、Search、TextInput、TextArea、RichEditor对选中的文本内容进行拖拽时,不支持背板图的自定义。当onDragStart与菜单预览一起使用或使用了默认支持拖出能力的组件时,预览及菜单项上的自定义内容不支持拖拽。

OnDrop

绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。如果开发者没有在onDrop中主动调用event.setResult()设置拖拽接收的结果,则系统按照数据接收成功处理.通常在该事件回调中执行拖拽落位后的数据处理逻辑,如刷新UI、在线资源下载、落入数据发送、落盘等业务逻辑。该回调中不应该同步执行耗时操作,否则可能导致抬手操作后拖拽背板延迟消失的问题,应使用JS提供的异步处理机制避免此类问题。

OnDragEnter

拖拽进入组件范围内时,触发回调,当监听了onDrop事件时,此事件才有效。通常在该事件回调中实现一些动效以标识当前拖拽的响应。

OnDragMove

拖拽过程在该组件范围内移动时,触发该回调,但该事件只在监听OnDrop事件后才会被成功触发。

OnDragLeave

拖拽离开组件范围内时,触发回调,当监听了onDrop事件时,此事件才有效。注意:监听OnDragEnter、OnDragLeave事件时必须监听OnDrop事件,否则不能正常响应事件,且在拖拽过程中存在不能监听到OnDragLeave的情况;若要100%保证能够监听到OnDragLeave事件,则应该使用setDragEventStrictReportingEnabled方法严格触发onDragLeave事件。

DragController (自定义拖拽)

参考示例:

// Step 1:获取到组件UI上下文
let uiContext: UIContext = this.getUIContext() as UIContext;
// Step 2:通过组件UI上下文的DragController 创建 drag action 对象
this.dragAction = uiContext.getDragController().createDragAction(this.customBuilder, dragInfo)
if(!this.dragAction){
  return
}
// Step 3: 注册拖拽状态监听,以便处理拖拽结束(已落位的结果)
this.dragAction.on('statusChange', (dragAndDropInfo)=>{
  if (dragAndDropInfo.status == dragController.DragStatus.ENDED){ // 已经落位结束
    console.info("drag has end with result " + dragAndDropInfo.event.getResult());
    if (!this.dragAction) {
      return
    }
    this.dragAction.off('statusChange')
  }
})

// Step 4:发起拖拽
this.dragAction.startDrag().then(()=>{}).catch((err:Error)=>{
  console.error("start drag Error:" + err.message);
})

主动发起拖拽能力,传入拖拽发起后跟手效果所拖拽的对象以及携带拖拽信息。通过回调返回结果。

参考链接:getDragControlleronPreDrag

1.1.2 ArkUI涉及的参考文档

拖拽事件拖拽控制@ohos.arkui.dragController (DragController),通过NDK接口对接拖拽:拖拽事件NDK

1.2 UDMF

UDMF提供标准化数据定义:提供HarmonyOS跨应用、跨设备的统一数据类型标准,包含标准化数据类型和标准化数据结构。

参考文档:@ohos.data.unifiedDataChannel (标准化数据通路)@ohos.data.commonType (数据通用类型)@ohos.data.uniformTypeDescriptor (标准化数据定义与描述)@ohos.data.uniformDataStruct (标准化数据结构)

其它接口文档:addEntry通过标准化数据通路实现数据共享

1.3 网络 API

若拖拽的数据是在线资源时,则应由拖拽落入方实现在线资源的下载处理逻辑,可参考以下API使用实现对应功能。

参考文档:@ohos.request (上传下载)HTTP数据请求

2. 拖拽场景指导及示例

2.1 文字拖拽

Text 组件默认可拖拽,若要实现长按选中浮起后拖拽的逻辑,则应给Text组件设置 CopyOptions 属性,并且设置draggable属性为true。

Search、TextInput、TextArea 、RichEditor组件均默认支持可落入,若要实现自定义组件内落入文字,则可按如下方式实现示例代码如下所示

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';

@Entry
@Component
struct TextDemo {
  @State message: string = '';
  controller: RichEditorController = new RichEditorController();
  options: RichEditorOptions = { controller: this.controller };

  build() {
    Column() {
      // 文字拖出
      Column() {
        Text('123456').copyOption(CopyOptions.InApp).draggable(true)
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      // 文字拖入
      Column() {
        Text(this.message).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
          .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
          // Text组件需在onDrop回调中实现落入数据的处理
          .onDrop((event: DragEvent, extraParams: string) => {
            let data: UnifiedData = event.getData();
            let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
            if (records[0].getType() === uniformTypeDescriptor.UniformDataType.PLAIN_TEXT) {
              let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
              this.message = plainText.textContent;
            }
          })
        TextArea({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
        Search({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
        TextInput({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
        RichEditor(this.options).draggable(true).width('100%').height('20%')
      }
      .width('70%').height('50%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }
    .width('100%').height('100%')
  }
}

2.1.2 长按选中当前Text组件的全部文本

因默认情况长按之后选中改的仅为按压处的几个字符,若要实现长按选中当前Text组件的全部文本,则可在 LongPressGesture 手势中实现回调并结合 Selection 实现该效果.

Text(this.message)
    .copyOption(CopyOptions.InApp)
    .draggable(true)
    . parallelGesture(
        LongPressGesture({ repeat: true })
          // 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
          .onAction((event: GestureEvent) => {
            this.beginIndex = 0;
            this.endIndex = endIndexOfTextContent;
          })
    .selection(this.beginIndex, this.endIndex)

2.1.3 消息以气泡形式拖出(IM应用Chat界面消息气泡场景)

该场景本质上是对气泡组件本身进行拖拽,但是在OnDragStart回调中设置拖拽数据为气泡组件上锁承载的文字消息内容,具体实现可如下所示

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';

@Entry
@Component
struct ParentComponentDemo {
  @State message: string = 'Hello World';
  @State text: string = '';

  build() {
    Column() {
      // 拖出
      Column() {
        Child({text: this.message})
          .width('50%').height('50%').draggable(true).backgroundColor(Color.Green)
          .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
          .onDragStart((event: DragEvent) => {
            let data: unifiedDataChannel.PlainText = new unifiedDataChannel.PlainText();
            data.abstract = this.message;
            data.textContent = this.message;
            (event as DragEvent).setData(new unifiedDataChannel.UnifiedData(data));
          })
      }
      .width('70%').height('40%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      // 拖入
      Column() {
        Text(this.text)
          .width('50%').height('50%').backgroundColor(Color.Red).textAlign(TextAlign.Center)
          .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
          .onDrop((event: DragEvent, extraParams: string) => {
            let data: UnifiedData = event.getData();
            let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
            if (records[0].getType() === uniformTypeDescriptor.UniformDataType.PLAIN_TEXT) {
              let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
              this.text = plainText.textContent;
            }
          })
      }
      .width('70%').height('40%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }
    .width('100%').height('100%')
  }
}

@Component
struct Child{
  @State text: string = '';
  build(){
    Column(){
      Text(this.text).copyOption(CopyOptions.InApp).draggable(true)
    }
    .width('100%').height('100%')
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
  }
}

2.2 图片拖拽

Image 组件默认可拖拽,可不设置draggable来实现图片拖拽功能。

2.2.1 本地图片资源拖拽

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'

@Entry
@Component
struct ImageLocal {
  @State targetImage: string | PixelMap | null = null;
  controller: RichEditorController = new RichEditorController();
  options: RichEditorOptions = { controller: this.controller };

  build() {
    Column() {
      // 本地图片资源拖出
      Column() {
        Image($r('app.media.test'))
          .objectFit(ImageFit.Contain).draggable(true)
          .width('70%').height('70%')
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      // 本地图片资源拖入
      Column() {
        Image(this.targetImage)
          .objectFit(ImageFit.Contain)
          .width('70%').height('70%')
          .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE]) // 如需匹配扩展名需使用UDS结构
          .onDrop((event: DragEvent, extraParams: string) => {
            let data: UnifiedData = event.getData();
            let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
            if (records[0].getType() === uniformTypeDescriptor.UniformDataType.IMAGE) {
              let image: unifiedDataChannel.Image = records[0] as unifiedDataChannel.Image;
              this.targetImage = image.imageUri;
            }
          })
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        RichEditor(this.options).draggable(true).width('100%').height('100%')
          .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
          .onDrop((event: DragEvent, extraParams: string) => {
            let data: UnifiedData = event.getData();
            let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
            if (records[0].getType() === uniformTypeDescriptor.UniformDataType.IMAGE) {
              let image: unifiedDataChannel.Image = records[0] as unifiedDataChannel.Image;
              this.controller.addImageSpan(image.imageUri);
            }
          })
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }.width('100%').height('100%')
  }
}

2.2.2 在线图片资源拖拽

加载在线图片资源需在module.json5文件中添加网络权限:

"requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
import { uniformTypeDescriptor } from '@kit.ArkData'
import { image } from '@kit.ImageKit';
import request from '@ohos.request';
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { buffer } from '@kit.ArkTS';

@Entry
@Component
struct ImageOnline {
  @State targetImage: string | PixelMap | null = null;
  controller: RichEditorController = new RichEditorController();
  options: RichEditorOptions = { controller: this.controller };
  pixelMap: image.PixelMap | undefined = undefined
  context = getContext(this) as common.UIAbilityContext;
  filesDir = this.context.filesDir;

  build() {
    Column() {
      // 在线图片资源拖出
      Column() {
        Image('http://sns-img-hw.xhscdn.com/1040g008313f5sogu045g4a4pehn7be0k6obctpg?imageView2/2/w/1080/format/reif/q/90')
          .objectFit(ImageFit.Contain).draggable(true)
          .width('70%').height('70%')
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      // 在线图片资源拖入
      Column() {
        Image(this.targetImage)
          .objectFit(ImageFit.Contain)
          .width('70%').height('70%')
          .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
          .onDrop((event: DragEvent, extraParams: string) => {
            let records: Array<udmf.UnifiedRecord> = dragEvent.getData().getRecords();
            let uri = (records[0] as udmf.Image).imageUri;
            if (typeof uri == 'string') {
              this.targetImage = uri;

              // 落盘到本地
              request.downloadFile(this.context, {
                url: uri,
                filePath: this.filesDir + '/test.png'
              }).then((downloadTask: request.DownloadTask) => {
                let file = fs.openSync(this.filesDir + '/test.png', fs.OpenMode.READ_WRITE);
                let arrayBuffer = new ArrayBuffer(1024);
                let readLen = fs.readSync(file.fd, arrayBuffer);
                let buf = buffer.from(arrayBuffer, 0, readLen);
                console.info(`The content of file: ${buf.toString()}`);
                fs.closeSync(file);
              })
            }
          })
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        RichEditor(this.options).draggable(true).width('100%').height('100%')
          .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
          .onDrop((event: DragEvent, extraParams: string) => {
            let arr: Record<string, object> = JSON.parse(extraParams) as Record<string, object>;
            let uri = arr['extraInfo'];
            if (typeof uri == 'string') {
              this.controller.addImageSpan(uri);
            }
          })
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }.width('100%').height('100%')
  }
}

2.2.3 PixelMap拖拽

import { common } from '@kit.AbilityKit'
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import fs from '@ohos.file.fs';

@Entry
@Component
struct PixelMapDrag {
  @State targetImage: string | PixelMap | null = null;
  private context = getContext(this) as common.UIAbilityContext;
  filesDir = this.context.filesDir;

  build() {
    Column() {
      // PixelMap拖出
      Column() {
        Image(this.context.resourceManager.getDrawableDescriptor($r('app.media.test').id).getPixelMap())
          .objectFit(ImageFit.Contain).draggable(true)
          .width('70%').height('70%')
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)


      // PixelMap拖入
      Column() {
        Image(this.targetImage)
          .objectFit(ImageFit.Contain)
          .width('70%').height('70%')
          .allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP])
          .onDrop(async (event: DragEvent, extraParams: string) => {
            let data: UnifiedData = event.getData();
            let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
            if (records[0].getType() === uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP) {
              let record: unifiedDataChannel.SystemDefinedPixelMap = (records[0]) as unifiedDataChannel.SystemDefinedPixelMap;
              this.targetImage = await this.createPixelMap(record);

              // 落盘到本地
              const imagePackerApi = image.createImagePacker();
              let packOpts : image.PackingOption = { format:"image/jpeg", quality:98 };
              const path : string = this.context.cacheDir + "/pixel_map.jpg";
              let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
              imagePackerApi.packToFile(this.targetImage, file.fd, packOpts).then(() => {
                // 直接打包进文件
              }).catch((error : BusinessError) => {
                console.error('Failed to pack the image. And the error is: ' + error);
              })
            }
          })
      }
      .width('70%').height('30%')
      .border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }.width('100%').height('100%')
  }

  public async createPixelMap(pixelMap: unifiedDataChannel.SystemDefinedPixelMap): Promise<image.PixelMap | null> {
    let mWidth: number = (pixelMap.details?.width ?? -1) as number;
    let mHeight: number = (pixelMap.details?.height ?? -1) as number;
    let mPixelFormat: image.PixelMapFormat =
      (pixelMap.details?.['pixel-format'] ?? image.PixelMapFormat.UNKNOWN) as image.PixelMapFormat;
    let mItemPixelMapData: Uint8Array = pixelMap.rawData;
    const opts: image.InitializationOptions = {
      editable: false, pixelFormat: mPixelFormat, size: {
        height: mHeight,
        width: mWidth
      }
    };
    const buffer: ArrayBuffer = mItemPixelMapData.buffer.slice(mItemPixelMapData.byteOffset,
      mItemPixelMapData.byteLength + mItemPixelMapData.byteOffset);
    try {
      let pixelMap: image.PixelMap = await image.createPixelMap(buffer, opts);
      return pixelMap;
    } catch (err) {
      console.error('dragtest--> getPixelMap', err);
      return null;
    }
  }
}

2.3 文件拖拽

文件拖拽,以Video为例:

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { fileIo, fileUri } from '@kit.CoreFileKit';

@Component
export struct ResourceVideoDragDemo {
  @State context: Context = getContext(this);
  @State videoWidth: number = 200;
  @State videoHeight: number = 200;
  @State innerResource: Resource | string | null = null;
  @State originalResource: string = '';
  private controller: VideoController | undefined;

  build() {
    NavDestination() {
      Column() {
        Column() {
          // 文件拖出
          Column() {
            Text('原视频').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            Video({ src: $rawfile('VID_1725089040_001.mp4'), controller: this.controller })
              .width(200)
              .height(200)
              .autoPlay(true)
              .loop(true)
              .draggable(true)
              .border({ radius: 8 })
                // 长按之后,将视频文件拷贝到沙箱路径
              .parallelGesture(
                LongPressGesture({ repeat: false })
                  .onAction((event: GestureEvent) => {
                    this.context.resourceManager.getRawFd('VID_1725089040_001.mp4', (err, data) => {
                      let filePath = this.context.filesDir + '/VID_1725089040_001.mp4';
                      console.log('文件路径:' + filePath);
                      let dest = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
                      let bufsize = 4096;
                      let buf = new ArrayBuffer(bufsize);
                      let off = 0, len = 0, readedLen = 0;
                      // 通过buffer将rawfile文件内容copy到沙箱路径
                      while (len = fileIo.readSync(data.fd, buf, { offset: data.offset + off, length: bufsize })) {
                        readedLen += len;
                        fileIo.writeSync(dest.fd, buf, { offset: off, length: len });
                        off = off + len;
                        if ((data.length - readedLen) < bufsize) {
                          bufsize = data.length - readedLen;
                        }
                      }
                      fileIo.close(dest.fd);
                      this.context.resourceManager.closeRawFd('VID_1725089040_001.mp4');
                      this.originalResource = fileUri.getUriFromPath(filePath);
                      console.log('originalResource:' + this.originalResource);
                    })
                  })
              )
              .onDragStart((event: DragEvent) => {
                let video = new unifiedDataChannel.Video();
                video.videoUri = this.originalResource;
                let unifiedData = new unifiedDataChannel.UnifiedData(video);
                event.setData(unifiedData);
              })
          }

          Column() {
            Text('视频拖拽落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            Video({ src: this.innerResource, controller: this.controller })
              .width(200)
              .height(200)
              .border({ radius: 8 })
              .width(this.videoWidth)
              .height(this.videoWidth)
              .visibility(this.innerResource === null ? Visibility.Hidden : Visibility.Visible)
              .autoPlay(true)
              .loop(true)
          }
          .allowDrop([uniformTypeDescriptor.UniformDataType.VIDEO])
          // 文件拖入
          .onDrop(async (event: DragEvent) => {
            let dragData = event.getData();
            if (dragData != undefined) {
              let records = dragData.getRecords();
              let type = records[0].getType();
              switch (type) {
                case uniformTypeDescriptor.UniformDataType.VIDEO:
                  let record: unifiedDataChannel.Video = (records[0]) as unifiedDataChannel.Video;
                  this.innerResource = record.videoUri;
                  break;
                default:
                  return;
              }
              event.setResult(0)
            }
          })
          .width(300)
          .height(300)
          .border({ width: 1, radius: 5, style: BorderStyle.Dashed })
        }.justifyContent(FlexAlign.SpaceBetween)
        .layoutWeight(1)
        .margin({ bottom: 10 })
      }.width('100%').height('100%')
    }
    .title('资源视频video拖拽示例')
  }
}

2.4 图文拖拽

图文拖入时,需要将dragEvent中的数据的图文分开依次处理,可由如下示例代码实现

2.4.1 Text组件图文拖拽

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import common from '@ohos.app.ability.common';
import { ImageUtil } from '../utils/ImageUtil';

@Component
export struct TextImageDragDemo {
  @State targetImage: string | PixelMap | null = null;
  @State targetTextContent: string = "图文落入框";
  @State imageWidth: number = 200;
  @State imageHeight: number = 200;
  @State isDraggable: boolean = true
  @State opacityValue: number = 1;
  private context = getContext(this) as common.UIAbilityContext

  build() {
    NavDestination() {
      Column() {
        Column() {
          // 图文拖出
          Column() {
            Text('原图文').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            Text() {
              Span('测试图文拖拽')
              ImageSpan(this.context.resourceManager.getDrawableDescriptor($r('app.media.2238350494').id).getPixelMap())
                .width(200)
                .height(200)
                .border({ radius: 8 })
            }
            .opacity(this.opacityValue)
            .onDragLeave((_event: DragEvent) => {
              console.info('dragtest onDragMove')
              this.opacityValue = 0.4
            })
            .onDragMove((_event: DragEvent) => {
              console.info('dragtest onDragMove')
              this.opacityValue = 0.4
            })
            .copyOption(CopyOptions.InApp)
          }

          // 图文拖入
          Column() {
            Text('图文落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            Text() {
              Span(this.targetTextContent)
              ImageSpan(this.targetImage)
                .width(this.imageWidth).height(this.imageHeight)
                .visibility(this.targetImage === null ? Visibility.Hidden : Visibility.Visible)
            }
          }
          .allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP,
            uniformTypeDescriptor.UniformDataType.IMAGE,
            uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
            uniformTypeDescriptor.UniformDataType.TEXT])
          .onDragMove((_event: DragEvent) => {
            this.opacityValue = 0.4
          })
          .onDrop(async (event: DragEvent) => {
            console.info('dragtest start onDrop')
            let dragData = event.getData();
            if (dragData != undefined) {
              let records = dragData.getRecords();
              for (let i = 0; i < records.length; i++) {
                console.info('dragtest get records', JSON.stringify(i))
                let type = records[i].getType();
                switch (type) {
                  case uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP:
                    console.info('dragtest type is PIXEL_MAP')
                    let record: unifiedDataChannel.SystemDefinedPixelMap = (records[i]) as unifiedDataChannel.SystemDefinedPixelMap;
                    this.targetImage = await ImageUtil.createPixelMap(record);
                    break;
                  case uniformTypeDescriptor.UniformDataType.IMAGE:
                    console.info('dragtest type is IMAGE')
                    let image: unifiedDataChannel.Image = (records[i]) as unifiedDataChannel.Image;
                    this.targetImage = image.imageUri;
                    break;
                  case uniformTypeDescriptor.UniformDataType.PLAIN_TEXT:
                    console.info('dragtest type is PLAIN_TEXT')
                    let plainText: unifiedDataChannel.PlainText = records[i] as unifiedDataChannel.PlainText;
                    this.targetTextContent = plainText.textContent;
                    console.log('dragtest targetTextContent:' + this.targetTextContent);
                    break;
                  case uniformTypeDescriptor.UniformDataType.TEXT:
                    console.info('dragtest type is TEXT')
                    let currentText: unifiedDataChannel.Text = records[i] as unifiedDataChannel.Text;
                    let text: string = !!currentText.details ? currentText.details['value'] : '';
                    this.targetTextContent = this.targetTextContent.concat(text);
                    console.log('dragtest targetTextContent:' + this.targetTextContent);
                    break;
                  default:
                    console.info('dragtest type is error')
                    return;
                }
              }
              event.setResult(0)
              this.opacityValue = 1
            }
          })
          .width(300)
          .height(300)
          .border({ width: 1, radius: 5, style: BorderStyle.Dashed })
        }.justifyContent(FlexAlign.SpaceBetween)
        .layoutWeight(1)
        .margin({ bottom: 10 })
      }.width("100%").height('100%')

    }
    .title('图文拖拽示例')
  }
}

2.4.2 RichEditor组件图文拖拽

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { ImageUtil } from '../utils/ImageUtil';

@Component
export struct RichImageDragDemo {
  @State targetImage: string | PixelMap | null = null;
  @State targetTextContent: string = "";
  @State imageWidth: number = 200;
  @State imageHeight: number = 200;
  @State isDraggable: boolean = true
  @State opacityValue: number = 1;
  sourceController: RichEditorController = new RichEditorController();
  targetController: RichEditorController = new RichEditorController();
  private context = getContext(this) as common.UIAbilityContext

  build() {
    NavDestination() {
      Column() {
        Column() {
          // 图文拖出
          Column() {
            Text('富文本区域图片').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            RichEditor({ controller: this.sourceController })
              .onReady(() => {
                this.sourceController.addTextSpan("012345",
                  {
                    style:
                    {
                      fontColor: Color.Orange,
                      fontSize: 30
                    }
                  })
                this.sourceController.addImageSpan(
                  this.context.resourceManager.getDrawableDescriptor($r('app.media.2238350494').id).getPixelMap(),
                  {
                    imageStyle:
                    {
                      size: ["157px", "157px"]
                    }
                  })
              })
          }

          // 图文拖入
          Column() {
            Text('图文落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
            RichEditor({ controller: this.targetController })
          }
          .allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP,
            uniformTypeDescriptor.UniformDataType.IMAGE,
            uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
            uniformTypeDescriptor.UniformDataType.TEXT])
          .onDragMove((_event: DragEvent) => {
            this.opacityValue = 0.4
          })
          .onDrop(async (event: DragEvent) => {
            console.info('dragtest start onDrop')
            let dragData = event.getData();
            this.targetController.getCaretOffset();
            if (dragData != undefined) {
              let records = dragData.getRecords();
              for (let i = 0; i < records.length; i++) {
                console.info('dragtest get records', JSON.stringify(i))
                let type = records[i].getType();
                switch (type) {
                  case uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP:
                    console.info('dragtest type is PIXEL_MAP')
                    let record: unifiedDataChannel.SystemDefinedPixelMap = (records[i]) as unifiedDataChannel.SystemDefinedPixelMap;
                    this.targetImage = await ImageUtil.createPixelMap(record);
                    this.targetController.addImageSpan(this.targetImage,
                      {
                        imageStyle:
                        {
                          size: ["175px", "175px"]
                        },
                        offset: this.targetController.getCaretOffset()
                      })
                    break;
                  case uniformTypeDescriptor.UniformDataType.IMAGE:
                    console.info('dragtest type is IMAGE')
                    let image: unifiedDataChannel.Image = (records[i]) as unifiedDataChannel.Image;
                    this.targetImage = image.imageUri;
                    this.targetController.addImageSpan(this.targetImage,
                      {
                        imageStyle:
                        {
                          size: ["175px", "175px"]
                        },
                        offset: this.targetController.getCaretOffset()
                      })
                    break;
                  case uniformTypeDescriptor.UniformDataType.PLAIN_TEXT:
                    console.info('dragtest type is PLAIN_TEXT')
                    let plainText: unifiedDataChannel.PlainText = records[i] as unifiedDataChannel.PlainText;
                    this.targetTextContent = plainText.textContent;
                    console.log('dragtest targetTextContent:' + this.targetTextContent);
                    this.targetController.addTextSpan(this.targetTextContent,
                      {
                        style:
                        {
                          fontColor: Color.Orange,
                          fontSize: 30
                        },
                        offset: this.targetController.getCaretOffset()
                      })
                    break;
                  case uniformTypeDescriptor.UniformDataType.TEXT:
                    console.info('dragtest type is TEXT')
                    let currentText: unifiedDataChannel.Text = records[i] as unifiedDataChannel.Text;
                    let text: string = !!currentText.details ? currentText.details['value'] : '';
                    this.targetTextContent = this.targetTextContent.concat(text);
                    console.log('dragtest targetTextContent:' + this.targetTextContent);
                    this.targetController.addTextSpan(this.targetTextContent,
                      {
                        style:
                        {
                          fontColor: Color.Orange,
                          fontSize: 30
                        },
                        offset: this.targetController.getCaretOffset()
                      })
                    break;
                  default:
                    console.info('dragtest type is error')
                    return;
                }
              }
              event.setResult(0)
              this.opacityValue = 1
            }
            this.targetTextContent = "";
            this.targetImage = null;
          })
          .width(300)
          .height(300)
          .border({ width: 1, radius: 5, style: BorderStyle.Dashed })
        }.justifyContent(FlexAlign.SpaceBetween)
        .layoutWeight(1)
        .margin({ bottom: 10 })
      }.width("100%").height('100%')
    }
    .title('富文本中图文拖拽示例')
  }
}

2.4.3 多entry

import { uniformDataStruct, uniformTypeDescriptor, unifiedDataChannel } from '@kit.ArkData';
import { fileUri, fileIo as fs } from '@kit.CoreFileKit'
import { common } from '@kit.AbilityKit'

@Entry
@Component
struct Index {
  @State ondrop_uri: string = '';
  @State description:string = '';
  uiContext = this.getUIContext();
  udKey: string = '';
  build() {
    Column() {
      Column() {
        Text("拖出方")
        Text("oriUri:" + 'file://data/image/1.png')
        Text("description:" + 'This is the description of the hyperlink')
      }
      .width(300)
      .height(200)
      .borderWidth(2)
      .margin(20)
      .onDragStart((event)=>{

        //创建FILE_URI类型数据
        let fileUriDetails : Record<string, string> = {
          'attr1': 'value1',
          'attr2': 'value2',
        }
        let fileUri : uniformDataStruct.FileUri = {
          uniformDataType : 'general.file-uri',
          oriUri : 'file://data/image/1.png',
          fileType : 'general.image',
          details : fileUriDetails,
        }

        //创建HYPERLINK类型数据
        let hyperlink : uniformDataStruct.Hyperlink = {
          uniformDataType:'general.hyperlink',
          url : 'file://data/image/1.png',
          description : 'This is the description of the hyperlink',
        }

        //新建一个新的unifiedData
        let unifiedData = new unifiedDataChannel.UnifiedData();
        //新建一个UnifiedRecord 初始化塞入FORM类型的数据(可自定义)
        let record = new unifiedDataChannel.UnifiedRecord(uniformTypeDescriptor.UniformDataType.HYPERLINK, hyperlink);
        //addEntry 添加另一份FILE_URI数据到record里面
        record.addEntry(uniformTypeDescriptor.UniformDataType.FILE_URI, fileUri);
        //将record塞入unifiedData
        unifiedData.addRecord(record);
        //将unifiedData塞入dragEvent
        event.setData(unifiedData)
      })

      Button("Reset")
        .onClick(()=>{
          this.ondrop_uri = ''
          this.description = ''
        })

      Column() {
        Text("落入方")
        Text("oriUri:" + this.ondrop_uri)
        Text("description:" + this.description)
      }
      .width(300)
      .height(200)
      .borderWidth(2)
      .margin(20)
      .onDrop((event)=>{
        //配置异步拖拽参数
        let context = this.uiContext.getHostContext() as common.UIAbilityContext;
        let pathDir: string = context.distributedFilesDir;
        let destUri = fileUri.getUriFromPath(pathDir);

        //配置数据处理回调
        let progressListener: unifiedDataChannel.DataProgressListener = (progress: unifiedDataChannel.ProgressInfo, dragData: UnifiedData|null) => {
          if(dragData != null) {
            //拿到records
            let records:Array<unifiedDataChannel.UnifiedRecord> = dragData.getRecords();
            //循环遍历查询
            for (let i = 0; i < records.length; i++) {
              let unifiedDataRecord = records[i] as unifiedDataChannel.UnifiedRecord;
              //找到对应类型的数据 getEntry
              let fileUriRead : uniformDataStruct.FileUri = unifiedDataRecord.getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri;
              if (fileUriRead != undefined) {
                //拿到数据类型中对应的字段
                console.info(`oriUri: ${fileUriRead.oriUri}`);
                this.ondrop_uri = fileUriRead.oriUri;
              }
              let hyperlink = unifiedDataRecord.getEntry(uniformTypeDescriptor.UniformDataType.HYPERLINK) as uniformDataStruct.Hyperlink;
              if (hyperlink != undefined) {
                console.info(`description: ${hyperlink.description}`);
                this.description = hyperlink.description as string;
              }
            }
          } else {
            console.log('dragData is undefined');
          }
          console.log(`percentage: ${progress.progress}`);
        };

        //配置异步获取数据options
        let options: DataSyncOptions = {
          destUri: destUri,
          fileConflictOptions: unifiedDataChannel.FileConflictOptions.OVERWRITE, //配置数据冲突处理方式
          progressIndicator: unifiedDataChannel.ProgressIndicator.DEFAULT,  //配置进度条,数据处理超过500ms生效
          dataProgressListener: progressListener,
        }
        try {
          this.udKey = (event as DragEvent).startDataLoading(options);
          console.log('udKey: ', this.udKey);
        } catch(e) {
          console.log(`startDataLoading errorCode: ${e.code}, errorMessage: ${e.message}`);
        }
      }, {disableDataPrefetch: true}) //当使用startDataLoading获取数据时需设置该参数为true,防止拖拽提前获取数据。

      Button('取消数据传输')
        .onClick(() => {
          try {
            this.getUIContext().getDragController().cancelDataLoading(this.udKey);
          } catch (e) {
            console.log(`cancelDataLoading errorCode: ${e.code}, errorMessage: ${e.message}`);
          }
        })
        .margin({top: 10})

    }
    .height('100%')
    .width('100%')
  }
}

3.共性问题解决方案

3.1 拖出的PixelMap落入到备忘录、中转站等时与拖出端存在色差

拖出PixelMap时,需要设置当前PixelMap的编码格式,否则落入方不能确定该PixelMap的编码方式,按照默认行为处理可能出现存在色差的情况。

PixelMap的默认编码格式是 RGBA,若拖出方未指定PixelMap的编码格式,则落入方按照RGBA格式解码即可。

若拖出方有指定了PixelMap的编码格式,则落入方通过systemDefinedPixelMap.details; details['pixel-format'] 获取拖出方的编码格式,再以该格式解码即可。

示例代码见图片拖拽下PixelMap拖拽。

PixelMap 使用指导

3.2 Image组件是在线资源时拖出后不能正常落位

若Image组件拖出时,其资源文件是在线地址,则需要拖拽落入方实现对在线数据的下载处理逻辑

当前系统应用中 备忘录、中转站、小艺均已支持在线图片资源的落入处理

处理在线资源落入时,可通过 Http 模块提供的下载能力实现在线uri的处理,如下示例代码所示

 http.createHttp().request('http://sns-img-hw.xhscdn.com/1040g008313f5sogu045g4a4pehn7be0k6obctpg?imageView2/2/w/1080/format/reif/q/90',
        {
            method: http.RequestMethod.GET,
            connectTimeout: 60000,
            readTimeout: 60000
        },
        (error: BusinessError, data: http.HttpResponse) => {
        let imageData: ArrayBuffer = data?.result as ArrayBuffer
        let imageSource: image.ImageSource = image.createImageSource(imageData)
        imageSource.getImageInfo((err, data) => {
            let opts: image.DecodingOptions = {
            editable: true,
            desiredSize: {
                height: data.size.height,
                width: data.size.width
            }
            }
            imageSource.createPixelMap(opts, async (err, pixelMap) => {
            if (err) return
            const offScreenCanvas = new OffscreenCanvas(data.size.width, data.size.height, LengthMetricsUnit.PX)
            const offScreenContext: OffscreenCanvasRenderingContext2D = offScreenCanvas.getContext('2d')
            offScreenContext.drawImage(pixelMap, 0, 0, offScreenCanvas.width, offScreenCanvas.height)
            offScreenContext.textAlign = 'right'
            offScreenContext.fillStyle = 'rgba(255, 255, 255, 0.7)'
            offScreenContext.font = vp2px(8) + 'px'
            offScreenContext.shadowBlur = 2
            offScreenContext.shadowColor = 'rgba(0, 0, 0, 0.3)'
            offScreenContext.fillText(`小红书号`, offScreenCanvas.width - vp2px(10), offScreenCanvas.height - vp2px(11))

            this.pixelMap = offScreenContext.getPixelMap(0, 0, offScreenCanvas.width, offScreenCanvas.height);
            const imagePackerApi = image.createImagePacker();
            const context = getContext(this)
            let packOpts : image.PackingOption = { format:"image/png", quality:98 };
            let pathDir = context.filesDir
            let path = '/cache.png'
            this.writeFileSync(pathDir, path)
            let file = await fs.open(context.filesDir+path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

3.3 拖拽的触发手势长按浮起与长按手势之间存在冲突

3.3.1 长按手势与拖拽发起手势冲突

若组件既需要处理长按手势,又需要支持可拖拽能力(因拖拽行为的触发为长按500ms并移动10vp),则可通过 parallelGesture 实现多个手势事件的处理,而不产生冲突。

ParallelGesture使用参考:

// xxx.ets
@Entry
@Component
struct LongPressGestureExample {
  @State count: number = 0

  build() {
    Column() {
      Text('LongPress onAction:' + this.count).fontSize(28)
        .ParallelGesture(
        LongPressGesture({ repeat: true })
          // 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
          .onAction((event: GestureEvent) => {
            if (event && event.repeat) {
              this.count++
            }
          })
            // 长按动作一结束触发
          .onActionEnd((event: GestureEvent) => {
            this.count = 0
          })
        )
        .onDragStart((event:DragEvent) => {
            //  设置拖拽数据和自定义拖拽背板
        })
    }
    .height(200)
    .width(300)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

3.3.2 长按弹窗、弹出子菜单与拖拽手势发起冲突的情况

// xxx.ets
@Entry
@Component
struct LongPressGestureExample {
  @State count: number = 0;

  build() {
    Column() {
      Text('LongPress onAction:' + this.count).fontSize(28)
        .ParallelGesture(
        LongPressGesture({ repeat: true })
          // 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
          .onAction((event: GestureEvent) => {
            // 弹出子窗或子菜单
          })
            // 长按动作一结束触发
          .onActionEnd((event: GestureEvent) => {
            this.count = 0;
          })
        )
        .onDragStart((event:DragEvent) => {
            //  设置拖拽数据和自定义拖拽背板
        })
        .onDragMove((event:DragEvent) => {
            // 隐藏子窗或子菜单,避免中断拖拽手势
        })
        .onDrop((event:DragEvent) => {
            // 必须监听onDrop事件,否则onDragMove事件回调不会被触发
        })
    }
    .height(200)
    .width(300)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

3.4 图文混排数据拖拽落位后与原拖出方排版格式有差异

当前拖拽数据中图文混排数据可通过HTML实现,但拖拽落入方组件可能并非HTML格式,如RichEditor、RichText等。

当前系统能力对各类富文本数据的格式转换能力较弱,不能保证排版样式一致。

3.5文字无法长按选中后再起拖

文字若要长按选中后再起拖,必须要设置copyOptions属性,否则无法触发长按选中逻辑,示例如下所示

 Text(this.message)
    .copyOption(CopyOptions.InApp)
    .draggable(true)

若要实现长按全选,则应监听长按手势,并结合selection接口实现

Text(this.message)
    .copyOption(CopyOptions.InApp)
    .draggable(true)
    . parallelGesture(
        LongPressGesture({ repeat: true })
          // 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
          .onAction((event: GestureEvent) => {
            this.beginIndex = 0;
            this.endIndex = endIndexOfTextContent;
          })
    .selection(this.beginIndex, this.endIndex)

3.6 拖拽落入到某个组件后不能触发其他组件的动作

eg:拖拽到某个IM应用的编辑框后,不能触发其发送按钮高亮标识可发送

本质上,该场景是应用自身应该实现的处理逻辑,可通过状态变量、Provider Consumer 注解等多种方式实现。

3.7 拖拽到文字某些"输入框"后不响应拖拽

问题原因,因ArkUI默认支持落入的组件包括"Search、TextInput、TextArea、Video",若应用实现时输入框使用的是Text组件实现,则默认不可响应拖拽事件.

修改方案

若要实现对拖拽事件的响应,则应主动调用allowDrop、onDrop逻辑实现落入。示例代码可如下所示:

content: string = "请输入"
Text(this.message)
    .allowDrop([
uniformTypeDescriptor.UniformDataType.PLAIN_TEXT, uniformTypeDescriptor.UniformDataType.TEXT])
    .onDrop((event:DragEvent) => {
        let currentText: unifiedDataChannel.Text = records as unifiedDataChannel.Text;
        let text: string = !!currentText.details ? currentText.details['value'] : ''; 
        this.content= this.content.concat(text);
})

3.8 拖拽到小艺后显示不支持处理此类数据

问题原因

1、该问题通常是由于拖拽数据源设置不当导致的问题,如拖拽文件时文件的uri为非法参数,则在落位获取数据时,UDMF不能正常读取数据,导致落位失败

2、72及之前版本,小艺未对PixelMap 和 在线Image资源的落位做适配,导致落位失败,73版本已修复。

解决办法

在拖拽发起时候应正确设置拖拽数据,避免拖拽数据为空或拖拽数据非法,如无效uri等情况。

3.9 文件、图片等拖入到可落入组件(显示绿色+角标)时不能正常落位

问题原因

1、显示绿色角标是因为落入方组件在allowDrop列表中刚设置了当前拖拽数据类型,标识支持该类数据的落入

2、但是具体是否能够成功落位取决于落入组件在onDrop逻辑中是否正确处理了拖拽数据,如未对数据做处理并刷新UI、读取数据失败等情况都有可能导致落位失败

解决办法

1、落入方应用需明确在onDrop逻辑中是否有对当前拖拽数据做处理并刷新UI

2、排查当前拖拽数据是否非法,如 无效uri、无效在线地址等情况。

3.10 问题定位定界关键日志

判定当前拖拽的Image是不是在线资源

在起拖端搜索如下日志,有则说明是在线资源

hilog | grep "Get uri authority empty or uri scheme not equals to file."

# 若有上述日志,标识当前拖拽的是在线资源

判定当前拖拽数据类型及Summary

在起拖端,日志中搜索AceDrag.*summary,在起拖端即可看到当前拖拽数据的summary

hilog | grep -E "AceDrag.*summary"
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐