HarmonyOS应用统一拖拽解决方案

描述

概述

拖拽操作是一种直观且高效的数据传输方式,它允许用户通过标准手势(包括用手指、鼠标或触控笔按住并移动)在应用程序之间及内部进行数据传输。

拖拽功能不仅操作便捷,还能与多种系统能力深度融合,拓展出更为广泛的应用场景。例如,跨设备拖拽让用户能在不同设备间无缝传输数据,跨窗口拖拽提升了多任务处理的灵活性。此外,基于拖拽操作还可以开发出更多创新性的应用场景,如AI智能识别、水印添加等,这些创新性的功能接入统称为“统一拖拽”。

本文将介绍几种典型拖拽场景及其具体实现方案,帮助开发者更好地理解和应用拖拽技术。

通过设置组件的拖拽响应,可以自定义拖出数据、拖入数据和拖拽背板图,实现如下场景:

拖拽图像增加水印:为拖拽的图像添加水印,水印内容为图像的拖拽时间。开发者可以在应用时根据需求自定义水印内容,例如标记拖拽图片的来源信息,为图像管理与溯源提供便利。

自定义拖拽背板图:将拖拽中的背板图设置为自定义数据内容。开发者可根据个性化需求打造独特的拖拽视觉效果。

AI识别拖拽内容:通过在接收拖拽内容时增加AI识别功能,使得只能显示文字的组件可以接收图片拖拽并显示图片中的文字信息。开发者可以将此能力应用于拖拽识图搜索。

将拖拽框架与系统的分屏能力、键鼠穿越能力、小艺及中转站结合,可以实现如下场景:

分屏拖拽:演示了分屏拖拽的功能,可以在分屏中打开两个不同的应用,实现跨应用拖拽。

跨设备拖拽:演示了基于键鼠穿越能力的跨设备拖拽,可以在平板和2in1设备中使用此功能以直观便捷地交换数据。

拖入小艺和中转站:演示了小艺和中转站与拖拽框架结合的能力,可以利用中转站暂存拖拽内容或进行跨设备拖拽,也可以利用小艺的AI对话式分析能力处理拖拽内容。

实现原理

拖拽流程可以分为三部分:发起拖拽、拖拽中和释放拖拽。其中,拖出方通过draggable()和onDragStart()等接口处理拖出数据,拖入方通过allowDrop()和onDrop()等接口处理拖入数据,拖拽数据使用UDMF统一数据对象UnifiedData 进行封装。下面,将按照这三个部分依次介绍拖拽的基础实现。

发起拖拽 拖拽中 释放拖拽
HarmonyOS HarmonyOS HarmonyOS

表1 拖拽流程展示

发起拖拽

默认支持拖出能力的组件,如Search、Hyperlink等,在拖出时会使用组件的默认拖出响应。其中Search组件默认拖拽内容为选中的文字,Hyperlink组件默认拖拽内容为超链接地址。如果想自定义组件的拖拽内容,需要在组件的onDragStart()接口中将自定义数据封装成UnifiedData数据对象,通过DragEvent的setData()接口设置拖出数据。对于其他非默认组件或自定义组件,如果想实现其拖出功能,需要将组件的draggable()属性设置为true,并自定义组件的拖拽内容。以Text组件为例,示例代码如下:

 

Text('自定义拖出响应,拖拽video')
  .draggable(true)
  .onDragStart((event) => {
    // 处理拖出数据
    let video: unifiedDataChannel.Video = new unifiedDataChannel.Video();
    video.videoUri = '/resources/rawfile/01.mp4';
    let data: unifiedDataChannel.UnifiedData = new unifiedDataChannel.UnifiedData(video);
    (event as DragEvent).setData(data);
  })

 

可以在onDragStart()中自由地处理拖拽信息,例如为图片添加水印,详情见拖拽图像增加水印。

拖拽中

通过标准手势发起拖拽后,系统会默认将组件本身的截图作为拖拽移动中的背板图。如果想自定义拖拽背板图,需要在组件的onDragStart()接口中通过回调的CustomBuilder或DragItemInfo进行设置。以Text组件为例,示例代码如下:

 

Text('自定义拖拽背板图')
  .draggable(true)
  .onDragStart(() => {
    // 返回自定义背板图
    let dragItemInfo: DragItemInfo = {
      pixelMap: this.pixelMap,
      builder: () => { this.pixelMapBuilder() },
      extraInfo: "this is extraInfo",
    };
    return dragItemInfo;
  })

 

可以将拖拽背板图设置为自定义的图片或者文字,详情见自定义拖拽背板图。

释放拖拽

默认支持拖入能力的组件,如Search等,将目标拖入组件区域内会使用默认拖入响应。如果想自定义组件的拖入响应,需要将组件的allowDrop()属性设置为允许拖入的数据类型,并在其onDrop()接口中通过DragEvent的getData()接口获取拖入数据后,对数据内容进行相应处理。

 

Text(this.targetText)
  .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
  .onDrop((event: DragEvent) => {
    // 处理拖入数据
    let records: Array = event.getData().getRecords();
    let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
    this.targetText = plainText.textContent;
   })

 

可以在onDrop()中处理接收到的数据,例如将图片识别为文字以显示在只支持文字的组件上,详情见AI识别拖拽内容。

拖拽图像增加水印

在拖拽过程中,可以自定义拖出响应,为拖拽图像增加水印,以标识图像的相关信息。下面以在图像中增加拖拽时间水印为例,介绍实现原理。

实现原理

在拖出对象的onDragStart()接口中获取图像信息,调用系统绘制能力drawing在图像上绘制水印,通过DragEvent的setData()接口将水印图像设置为拖拽数据。

开发步骤

1. 将Image的draggable()属性设置为true。

 

// src/main/ets/pages/watermark/Watermark.ets
Image($rawfile('river.png'))
  // ...
  .draggable(true)

 

2. 在拖出对象的onDragStart()接口中,获取图像信息并将其转换成PixelMap。

 

.onDragStart((event: DragEvent) => {
  const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
  let rawFileDescriptor = resourceMgr.getRawFdSync('river.png');
  const imageSourceApi: image.ImageSource = image.createImageSource(rawFileDescriptor);
  let pixelMap = imageSourceApi.createPixelMapSync();
  // ...
})

 

3. 将图片绘制到Canvas画布上,并获取拖拽时间作为水印绘制到画布上的指定位置,得到添加水印的图像。

 

// 获取拖拽时间
this.time = this.getTimeWatermark(systemDateTime.getTime(false));
let markPixelMap = this.addWaterMark(this.time, pixelMap);
// 绘制水印
addWaterMark(watermark: string, pixelMap: image.PixelMap) {
  if (canIUse('SystemCapability.Graphics.Drawing')) {
    watermark = this.context.resourceManager.getStringSync($r('app.string.drag_time')) + watermark;
    let imageWidth = pixelMap.getImageInfoSync().size.width;
    let imageHeight = pixelMap.getImageInfoSync().size.height;
    let imageScale = imageWidth / display.getDefaultDisplaySync().width;
    const canvas = new drawing.Canvas(pixelMap);
    const pen = new drawing.Pen();
    const brush = new drawing.Brush();
    pen.setColor({
      alpha: 102,
      red: 255,
      green: 255,
      blue: 255
    })
    brush.setColor({
      alpha: 102,
      red: 255,
      green: 255,
      blue: 255
    })
    const font = new drawing.Font();
    font.setSize(48 * imageScale);
    let textWidth = font.measureText(watermark, drawing.TextEncoding.TEXT_ENCODING_UTF8);
    const textBlob = drawing.TextBlob.makeFromString(watermark, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);
    canvas.attachBrush(brush);
    canvas.attachPen(pen);
    canvas.drawTextBlob(textBlob, imageWidth - 24 * imageScale - textWidth, imageHeight - 32 * imageScale);
    canvas.detachBrush();
    canvas.detachPen();
  } else {
    hilog.info(0x0000, TAG, 'watermark is not supported');
  }
  return pixelMap;
}

 

4. 将图像打包保存在文件中,调用DragEvent的setData()接口将水印图像设置为拖拽数据。

 

let packOpts: image.PackingOption = { format: 'image/png', quality: 20 };
let file =
  fs.openSync(`${this.context.filesDir}/watermark.png`, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
const imagePackerApi: image.ImagePacker = image.createImagePacker();
imagePackerApi.packToFile(markPixelMap, file.fd, packOpts);
let img: unifiedDataChannel.Image = new unifiedDataChannel.Image();
img.imageUri = fileUri.getUriFromPath(`${this.context.filesDir}/watermark.png`);
let data: unifiedDataChannel.UnifiedData = new unifiedDataChannel.UnifiedData(img);
(event as DragEvent).setData(data);
fs.closeSync(file.fd);

 

自定义拖拽背板图

在拖拽过程中,可以自定义拖拽背板图,展示拖拽数据的相关信息。

实现原理

在拖出对象的onDragStart()接口中,回调自定义的PixelMap作为拖拽中的背板图。

开发步骤

1. 创建自定义组件。

 

// src/main/ets/pages/background/Background.ets
@Builder
pixelMapBuilder() {
  Column() {
    Text($r('app.string.background_content'))
      .fontSize('16fp')
      .fontColor(Color.Black)
      .margin({
        left: '16vp',
        right: '16vp',
        top: '8vp',
        bottom: '8vp'
      })
  }
  .backgroundColor(Color.White)
  .borderRadius(18)
}

 

2. 将自定义组件转换成PixelMap,作为拖拽过程中显示的图片。

说明: 由于CustomBuilder需要离线渲染之后才能使用,存在一定的性能开销和时延,因此推荐开发者优先使用DragItemInfo中的PixelMap方式返回背板图。

 

private getComponentSnapshot(): void {
  this.getUIContext().getComponentSnapshot().createFromBuilder(() => {
    this.pixelMapBuilder()
  },
    (error: Error, pixmap: image.PixelMap) => {
      if (error) {
        hilog.error(0x0000, TAG, JSON.stringify(error));
        return;
      }
      this.pixelMap = pixmap;
    })
}

 

3. 在拖出对象的onDragStart()接口中,将回调的PixelMap作为拖拽中的背板图。

 

Image($r('app.media.mount'))
  // ...
  .onDragStart(() => {
    let dragItemInfo: DragItemInfo = {
      pixelMap: this.pixelMap,
      builder: () => {
        this.pixelMapBuilder()
      },
      extraInfo: "this is extraInfo"
    };
    return dragItemInfo;
  })

 

AI识别拖拽内容

在拖拽过程中,可以自定义拖入响应,以识别拖拽内容并将其输出在释放区内。下面以通过AI识别拖拽图像中的文字为例,介绍实现原理。

实现原理

在拖入对象的onDrop()接口中,通过DragEvent的getData()接口获取拖拽数据后,调用系统文字识别能力textRecognition得到图像中的文字信息。

开发步骤

1. 在拖拽释放区域的allowDrop()接口中设置允许拖入的数据类型为uniformTypeDescriptor.UniformDataType.IMAGE。

 

// src/main/ets/pages/airecognition/AIRecognition.ets
Column() {
  Text(this.textContent)
    // ...
}
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])

 

2. 在拖入对象的onDrop()接口中,调用DragEvent的getData()接口获取拖拽数据。

 

.onDrop(async (event?: DragEvent) => {
  let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData;
  // ...
  let record: Array = dragData.getRecords();
  // ...
  let imageSource = record[0] as unifiedDataChannel.Image;
  // ...
})

 

3. 将拖拽数据转换成颜色数据格式为RGBA_8888的PixelMap类型的视觉信息。

 

const resourceReg = new RegExp('resource');
if (resourceReg.test(imageSource.uri)) {
  const numberReg = new RegExp('[0-9]+');
  let idArray = imageSource.uri.match(numberReg);
  if (idArray !== null) {
    let id = idArray[0];
    let drawableDescriptor = this.context.resourceManager.getDrawableDescriptor(Number(id), 0, 1);
    let pixelMapInit = drawableDescriptor.getPixelMap() as image.PixelMap;
    let imageHeight = pixelMapInit.getImageInfoSync().size.height;
    let imageWidth = pixelMapInit.getImageInfoSync().size.width;
    const readBuffer: ArrayBuffer = new ArrayBuffer(imageHeight * imageWidth * 4);
    pixelMapInit.readPixelsToBufferSync(readBuffer);
    let opts: image.InitializationOptions = {
      editable: true,
      size: { height: imageHeight, width: imageWidth },
      srcPixelFormat: pixelMapInit.getImageInfoSync().pixelFormat,
      pixelFormat: 3,
      alphaType: pixelMapInit.getImageInfoSync().alphaType,
      scaleMode: 0
    };
    let pixelMap: image.PixelMap = image.createPixelMapSync(readBuffer, opts);
    // ...
  }
}

 

4. 调用系统文字识别能力textRecognition获取拖拽数据中的文字信息。

 

let visionInfo: textRecognition.VisionInfo = {
  pixelMap: pixelMap
};
let data = await textRecognition.recognizeText(visionInfo);
let recognitionString = data.value;
this.textContent = recognitionString;

 

分屏拖拽

将拖拽框架与系统的分屏能力结合,可以将数据从一个分屏页面拖拽到另一个分屏页面,实现跨应用拖拽或同应用跨页面拖拽。

使用说明

需要开启软件的分屏权限,并根据需求自定义拖拽响应。

跨设备拖拽

将拖拽框架与系统的键鼠穿越能力结合,可以接入跨设备拖拽,实现在平板或2in1类型的任意两台设备之间拖拽数据。

使用说明

需要满足跨设备拖拽开发指导中的使用限制条件,并根据需求自定义拖拽响应。

拖入小艺和中转站

将数据拖入系统的中转站,可以实现跨应用数据拖拽和跨设备数据流转;将数据拖入小艺,可以利用系统的AI能力处理拖拽数据。

使用限制

应用本身预置的资源文件(即应用在安装前的HAP包中已经存在的资源文件)不支持拖入小艺和中转站。

常见问题-在模拟器中无法实现AI识别拖拽内容

问题现象

将图像拖拽至释放区,无法识别图像中的文字并输出在释放区内。

解决措施

模拟器不支持textRecognition接口的调用,建议使用真机进行调试,详细请参见模拟器与真机的差异。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分