鸿蒙跨设备协同开发05——跨设备拖拽

如果你也对鸿蒙开发感兴趣,加入“Harmony自习室”吧!扫描下方名片,关注公众号,公众号更新更快,同时也有更多学习资料和技术讨论群。

1、概述

当用户拥有两台平板设备时,可以共享一套键鼠,通过跨设备拖拽,一步将设备A的素材拖拽到设备B快速创作,实现跨设备的协同工作体验。演示如下:

跨端拖拽提供跨设备的键鼠共享能力,支持在平板或2in1类型的任意两台设备之间拖拽文件、文本。

当前HarmonyOS系统应用中,文件管理器、浏览器支持拖出;备忘录支持拖入。用户可以体验以下场景:

  • 将A设备文件管理器中的图片拖拽至B设备的备忘录应用。

  • 将A设备备忘录中的文本拖拽至B设备的备忘录应用,并在B设备中使用A设备连接的键盘输入,协同操作。

【在开发跨设备拖拽的功能时,系统将自动完成键鼠穿越和跨设备的数据传递】

使用跨设备拖拽开发需要满足以下基本条件:

  • 设备需要是:HarmonyOS NEXT Developer Preview0及以上版本的平板或2in1设备。

  • 双端设备需要登录同一华为账号。

  • 双端设备需要打开Wi-Fi和蓝牙开关,并接入同一个局域网。

  • 打开键鼠穿越开关。

  • 应用本身预置的资源文件(即应用在安装前的HAP包中已经存在的资源文件)不支持跨设备拖拽。

2、拖拽开发

ArkUI框架对以下组件实现了默认的拖拽能力,支持对数据的拖出或拖入响应,开发者只需要将这些组件的draggable属性设置为true,即可使用默认拖拽能力。

  • 默认支持拖出能力的组件(可从组件上拖出数据):Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink。

  • 默认支持拖入能力的组件(目标组件可响应拖入数据):Search、TextInput、TextArea、Video。

其中Text、TextInput、TextArea、Hyperlink、Image和RichEditor组件的draggable属性默认为true。

我们也可以通过实现通用拖拽事件来自定义拖拽响应。

其他组件需要我们将draggable属性设置为true,并在onDragStart等接口中实现数据传输相关内容,才能正确处理拖拽。

2.1 接口说明

整个拖拽过程涉及到的API总共有7个,分别是:

onDragStart(event: (event: DragEvent, extraParams?: string) => CustomBuilder | DragItemInfo)onDragEnter(event: (event: DragEvent, extraParams?: string) => void)onDragMove(event: (event: DragEvent, extraParams?: string) => void)onDragLeave(event: (event: DragEvent, extraParams?: string) => void)onDrop(event: (event: DragEvent, extraParams?: string) => void)onDragEnd(event: (event: DragEvent, extraParams?: string) => void)onPreDrag(event: (preDragStatus: PreDragStatus) => void)

这7个API功能和作用分别介绍如下:

👉🏻 onDragStart

触发时机:第一次拖拽此事件绑定的组件时,长按时间 >= 500ms,然后手指移动距离 >= 10vp(如果长按触发时间 < 500ms,长按事件优先拖拽事件响应)

onDragStart(event: (event: DragEvent, extraParams?: string) => CustomBuilder | DragItemInfo)

针对默认支持拖出能力的组件,如果我们设置了onDragStart,系统会优先执我们设置的onDragStart,并根据执行情况决定是否使用系统默认的拖出能力:

    • 如果开发者返回了自定义背板图,则不再使用系统默认的拖拽背板图;

    • 如果开发者设置了拖拽数据,则不再使用系统默认填充的拖拽数据。

【注意:文本类组件Text、Search、TextInput、TextArea、RichEditor对选中的文本内容进行拖拽时,不支持背板图的自定义】

onDragStart方法涉及到三个类:DragEvent、CustomBuilder、DragItemInfo。分别介绍如下:

⭐️ DragEvent表示拖拽事件,结构定义如下:​​​​​​​

class DragEvent {/* 功能描述:当拖拽结束时,是否使能并使用系统默认落位动效。应用可将该值设定为true来禁用系统默认落位动效,并实现自己的自定义落位动效。当不配置或设置为false时,系统默认落位动效生效,当松手位置的控件可接收拖拽的数据时,落位为缩小消失动效,若不可接收数据,则为放大消失动效。当未禁用系统默认落位动效情况下,应用不应再实现自定义动效,以避免动效上的冲突。*/+ useCustomDropAnimation: boolean;// 切换复制和剪贴模式的角标显示状态。+ dragBehavior: DragBehavior;// 还包含以下方法:setData(unifiedData: UnifiedData): void // 向DragEvent中设置拖拽相关数据。getData(): UnifiedData // 从DragEvent中获取拖拽相关数据。数据获取结果请参考错误码说明。getSummary(): Summary  // 从DragEvent中获取拖拽相关数据的简介。setResult(dragRect: DragResult): void // 向DragEvent中设置拖拽结果。getResult(): DragResult // 从DragEvent中获取拖拽结果。getPreviewRect(): Rectangle // 获取拖拽跟手图相对于当前窗口的位置,以及跟手图尺寸信息,单位VP,其中x和y代表跟手图左上角的窗口坐标,width和height代表跟手图的尺寸。getVelocityX(): number // 获取当前拖拽的x轴方向拖动速度。坐标轴原点为屏幕左上角,单位为vp,分正负方向速度,从左往右为正,反之为负。getVelocityY(): number // 获取当前拖拽的y轴方向拖动速度。坐标轴原点为屏幕左上角,单位为vp,分正负方向速度,从上往下为正,反之为负。getVelocity(): number // 获取当前拖拽的主方向拖动速度。为xy轴方向速度的平方和的算术平方根。getWindowX(): number // 当前拖拽点相对于窗口左上角的x轴坐标,单位为vp。getWindowY(): number // 当前拖拽点相对于窗口左上角的y轴坐标,单位为vp。getDisplayX(): number // 当前拖拽点相对于屏幕左上角的x轴坐标,单位为vp。getDisplayY(): number // 当前拖拽点相对于屏幕左上角的y轴坐标,单位为vp。getModifierKeyState(Array<string>) => bool // 获取功能键按压状态。报错信息请参考以下错误码。支持功能键 'Ctrl'|'Alt'|'Shift'|'Fn',设备外接带Fn键的键盘不支持Fn键查询。}// 其中DragBehavior是一个枚举,定义如下:enum DragBehavior {COPY, //  指定对数据的处理方式为复制。MOVE //  指定对数据的处理方式为剪切。}

⭐️ CustomBuilder表示自定义UI描述,定义如下:​​​​​​​

// 生成用户自定义组件,在使用时结合@Builder使用。type CustomBuilder = () => any | void;

⭐️ DragItemInfo比CustomBuilder可以设置更多的信息,定义如下:​​​​​​​

class DragItemInfo {pixcelMap: PixelMap; // 设置拖拽过程中显示的图片。builder: CustomBuilder; // 刚刚介绍过d的自定义UI描述extraInfo: string; // 拖拽项(DragItemInfo)的描述}

在DragEvent中,还包括了一些关联的类,例如:UnifiedData、Summary、DragResult、Rectangle。他们的定义如下:

  • UnifiedData及其关联的类如下

class UnifiedData {  // 当前统一数据对象中所有数据记录的属性,包含时间戳、标签、粘贴范围以及一些附加数据等。  properties: UnifiedDataProperties;}// 定义统一数据对象中所有数据记录的属性,包含时间戳、标签、粘贴范围以及一些附加数据等。class UnifiedDataProperties {  extras: Record<string, object> // 是一个字典类型对象,用于设置其他附加属性数据。非必填字段,默认值为空字典对象。  tag: string // 用户自定义标签。非必填字段,默认值为空字符串。  timestamp: Date // UnifiedData的生成时间戳。默认值为1970年1月1日(UTC)。  shareOptions: ShareOptions // 指示UnifiedData支持的设备内使用范围,非必填字段,默认值为CROSS_APP。  getDelayData: GetDelayData // 延迟获取数据回调。当前只支持同设备剪贴板场景,后续场景待开发。非必填字段,默认值为undefined。}// UDMF支持的设备内使用范围类型枚举。enum ShareOptions {  IN_APP = 0 // 表示允许在本设备同应用内使用。  CROSS_APP = 1 // 表示允许在本设备内跨应用使用。}// 对UnifiedData的延迟封装,支持延迟获取数据。type GetDelayData = (type: string) => UnifiedData

  • Summary结构如下:

class Summary {  summary: Record<string, number> // 是一个字典类型对象,key表示数据类型(UniformDataType),value为统一数据对象中该类型记录大小总和(单位:Byte)。  totalSize: number // 统一数据对象内记录总大小(单位:Byte)。}
  • DragResult(枚举)结构如下:

enum DragResult {  DRAG_SUCCESSFUL // 拖拽成功,在onDrop中使用。  DRAG_FAILED // 拖拽失败,在onDrop中使用。  DRAG_CANCELED // 拖拽取消,在onDrop中使用。  DROP_ENABLED // 组件允许落入,在onDragMove中使用。  DROP_DISABLED // 组件不允许落入,在onDragMove中使用。}
  • Rectangle结构如下:

class Rectangle {x: Length // 触摸点相对于组件左上角的x轴坐标。默认值:0vpy: Length // 触摸点相对于组件左上角的y轴坐标。默认值:0vpwidth: Length // 触摸热区的宽度。默认值:'100%'height: Length // 触摸热区的高度。默认值:'100%'}type Length = string | number | Resource;

👉🏻 onDragEnter

触发时机:被拖拽的内容进入到释放目标的组件范围内时,触发回调(当监听了onDrop事件时,此事件才有效)

onDragEnter(event: (event: DragEvent, extraParams?: string) => void)

👉🏻 onDragMove

触发时机:被拖拽的内容在释放目标的组件范围内移动时,触发回调,(当监听了onDrop事件时,此事件才有效)

onDragMove(event: (event: DragEvent, extraParams?: string) => void)

👉🏻 onDragLeave

触发时机:被拖拽的内容从释放目标的组件范围内移出时,触发回调,(当监听了onDrop事件时,此事件才有效)

onDragLeave(event: (event: DragEvent, extraParams?: string) => void)

👉🏻 onDrop

触发时机:被拖拽的内容从释放目标的组件上方释放时,触发回调,(当监听了onDrop事件时,此事件才有效)

onDrop(event: (event: DragEvent, extraParams?: string) => void)

如果我们没有在onDrop中主动调用event.setResult()设置拖拽接收的结果,则系统按照数据接收成功处理。

👉🏻 onDragEnd

触发时机:绑定次事件的组件触发的拖拽结束后

onDragEnd(event: (event: DragEvent, extraParams?: string) => void)

👉🏻 onPreDrag

触发时机:绑定此事件的组件,当触发拖拽发起前的不同阶段时,触发回调。

onPreDrag(event: (preDragStatus: PreDragStatus) => void)

其中,PreDragStatus是一个枚举,定义如下:​​​​​​​

enum PreDragStatus {ACTION_DETECTING_STATUS = 0, // 拖拽手势启动阶段。(按下50ms时触发)READY_TO_TRIGGER_DRAG_ACTION = 1, // 拖拽准备完成,可发起拖拽阶段。(按下500ms时触发)PREVIEW_LIFT_STARTED = 2, // 拖拽浮起动效发起阶段。(按下800ms时触发)PREVIEW_LIFT_FINISHED = 3, // 拖拽浮起动效结束阶段。(浮起动效完全结束时触发)PREVIEW_LANDING_STARTED = 4, // 拖拽落回动效发起阶段。(落回动效发起时触发)PREVIEW_LANDING_FINISHED = 5, // 拖拽落回动效结束阶段。(落回动效结束时触发)ACTION_CANCELED_BEFORE_DRAG = 6 // 拖拽浮起落位动效中断。(已满足READY_TO_TRIGGER_DRAG_ACTION状态后,未达到动效阶段,手指抬手时触发)}

在拖拽接口中,都有一个参数叫 extraParams,这个参数用于表达组件在拖拽中需要用到的额外信息,extraParams是Json对象转换的string字符串,可以通过Json.parse转换的Json对象获取如下属性:​​​​​​​

{  // 当拖拽事件设在父容器的子元素时,selectedIndex表示当前被拖拽子元素是父容器第selectedIndex个子元素,selectedIndex从0开始。  // 仅在ListItem组件的拖拽事件中生效。  selectedIndex: number;     // 当前拖拽元素在List组件中放下时,insertIndex表示被拖拽元素插入该组件的第insertIndex个位置,insertIndex从0开始。  // 仅在List组件的拖拽事件中生效。  insertIndex: number;}

2.2、示例

示例效果如下:

示例代码如下:​​​​​​​

// xxx.etsimport { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';import { promptAction } from '@kit.ArkUI';import { BusinessError } from '@kit.BasicServicesKit';@Entry@Componentstruct Index {  @State targetImage: string = '';  @State targetText: string = 'Drag Text';  @State imageWidth: number = 100;  @State imageHeight: number = 100;  @State imgState: Visibility = Visibility.Visible;  @State videoSrc: string = 'resource://RAWFILE/02.mp4';  @State abstractContent: string = "abstract";  @State textContent: string = "";  @State backGroundColor: Color = Color.Transparent;  @Builder  pixelMapBuilder() {    Column() {      Image($r('app.media.icon'))        .width(120)        .height(120)        .backgroundColor(Color.Yellow)    }  }  getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {    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("getData failed, code = " + (e as BusinessError).code + ", message = " + (e as BusinessError).message);      return false;    }  }  getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {    if (this.getDataFromUdmfRetry(event, callback)) {      return;    }    setTimeout(() => {      this.getDataFromUdmfRetry(event, callback);    }, 1500);  }  private PreDragChange(preDragStatus: PreDragStatus): void {    if (preDragStatus == PreDragStatus.READY_TO_TRIGGER_DRAG_ACTION) {      this.backGroundColor = Color.Red;    } else if (preDragStatus == PreDragStatus.ACTION_CANCELED_BEFORE_DRAG      || preDragStatus == PreDragStatus.PREVIEW_LANDING_FINISHED) {      this.backGroundColor = Color.Blue;    }  }  build() {    Row() {      Column() {        Text('start Drag')          .fontSize(18)          .width('100%')          .height(40)          .margin(10)          .backgroundColor('#008888')        Image($r('app.media.icon'))          .width(100)          .height(100)          .draggable(true)          .margin({ left: 15 })          .visibility(this.imgState)          .onDragEnd((event) => {            // onDragEnd里取到的result值在接收方onDrop设置            if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {              promptAction.showToast({ duration: 100, message: 'Drag Success' });            } else if (event.getResult() === DragResult.DRAG_FAILED) {              promptAction.showToast({ duration: 100, message: 'Drag failed' });            }          })        Text('test drag event')          .width('100%')          .height(100)          .draggable(true)          .margin({ left: 15 })          .copyOption(CopyOptions.InApp)        TextArea({ placeholder: 'please input words' })          .copyOption(CopyOptions.InApp)          .width('100%')          .height(50)          .draggable(true)        Search({ placeholder: 'please input you word' })          .searchButton('Search')          .width('100%')          .height(80)          .textFont({ size: 20 })        Column() {          Text('change video source')        }.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);          return { builder: () => {            this.pixelMapBuilder()          }, extraInfo: 'extra info' };        })        Column() {          Text('this is abstract')            .fontSize(20)            .width('100%')        }.margin({ left: 40, top: 20 })        .width('100%')        .height(100)        .onDragStart((event) => {          this.backGroundColor = Color.Transparent;          let data: unifiedDataChannel.PlainText = new unifiedDataChannel.PlainText();          data.abstract = 'this is abstract';          data.textContent = 'this is content this is content';          (event as DragEvent).setData(new unifiedDataChannel.UnifiedData(data));        })        .onPreDrag((status: PreDragStatus) => {          this.PreDragChange(status);        })        .backgroundColor(this.backGroundColor)      }.width('45%')      .height('100%')      Column() {        Text('Drag Target Area')          .fontSize(20)          .width('100%')          .height(40)          .margin(10)          .backgroundColor('#008888')        Image(this.targetImage)          .width(this.imageWidth)          .height(this.imageHeight)          .draggable(true)          .margin({ left: 15 })          .border({ color: Color.Black, width: 1 })          .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.imageWidth = Number(rect.width);              this.imageHeight = Number(rect.height);              this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri;              event.useCustomDropAnimation = false;              this.imgState = Visibility.None;              // 显式设置result为successful,则将该值传递给拖出方的onDragEnd              event.setResult(DragResult.DRAG_SUCCESSFUL);            })          })        Text(this.targetText)          .width('100%')          .height(100)          .border({ color: Color.Black, width: 1 })          .margin(15)          .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])          .onDrop((dragEvent?: DragEvent) => {            this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {              let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();              let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;              this.targetText = plainText.textContent;            })          })        Video({ src: this.videoSrc, previewUri: $r('app.media.icon') })          .width('100%')          .height(200)          .controls(true)          .allowDrop([uniformTypeDescriptor.UniformDataType.VIDEO])        Column() {          Text(this.abstractContent).fontSize(20).width('100%')          Text(this.textContent).fontSize(15).width('100%')        }        .width('100%')        .height(100)        .margin(20)        .border({ color: Color.Black, width: 1 })        .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])        .onDrop((dragEvent?: DragEvent) => {          this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {            let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();            let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;            this.abstractContent = plainText.abstract as string;            this.textContent = plainText.textContent;          })        })      }.width('45%')      .height('100%')      .margin({ left: '5%' })    }    .height('100%')  }}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值