【鸿蒙开发】第二十五章 自由流转

目录

1 场景介绍

2 基本概念

3 典型场景

4 应用接续

4.1 应用接续特性简介

4.1.1 运作机制

4.1.2 发起接续的场景

4.2 应用接续开发指导

4.2.1 介绍

4.2.2 约束与限制

4.2.3 接口说明

4.2.4 开发步骤

4.2.5 迁移功能可选配置

4.2.5.1 动态配置迁移能力

4.2.5.2 设置迁移能力的时机

4.2.5.3 保证迁移连续性

4.2.6 按需迁移页面栈

4.2.7 按需退出

4.2.8 支持同应用中不同Ability跨端迁移

4.2.9 快速启动目标应用

4.2.10 使用分布式数据对象迁移数据

4.2.10.1 申请权限

4.2.10.2 基础数据迁移

4.2.10.3 文件资产迁移

5 跨设备拖拽

5.1 跨设备拖拽特性简介

运作机制

5.2 跨设备拖拽开发指导

5.2.1 约束与限制

5.2.2 接口说明

5.2.3 开发示例

6 跨设备剪贴板

6.1 跨设备剪贴板特性简介

6.1.1 运作机制

6.2 跨设备剪贴板开发指导

6.2.1 约束与限制

6.2.2 接口说明

6.2.3 开发示例


1 场景介绍

随着全场景多设备的生活方式不断深入,用户拥有的设备越来越多,不同设备都能在适合的场景下提供良好的体验,例如手可以提供及时的信息查看能力,电视可以带来沉浸的观影体验。但是,每个设备也有使用场景的局限,例如在电视上输入文本相对移动设备来说是非常糟糕的体验。当多个设备通过分布式操作系统能够相互感知、进而整合成一个超级终端时,设备与设备之间就可以取长补短、相互帮助,为用户提供更加自然流畅的分布式体验。

在HarmonyOS中,将跨多设备的分布式操作统称为流转;根据使用场景的不同,流转又分为跨端迁移多端协同两种具体场景。

2 基本概念

  • 流转

    在HarmonyOS中泛指跨多设备的分布式操作。流转能力打破设备界限,多设备联动,使用户应用程序可分可合可流转,实现如邮件跨设备编辑、多设备协同健身、多屏游戏等分布式业务。流转为开发者提供更广的使用场景和更新的产品视角,强化产品优势,实现体验升级。流转按照使用场景可分为跨端迁移多端协同

  • 跨端迁移

    在用户使用设备的过程中,当使用情境发生变化时(例如从室内走到户外或者周围有更合适的设备等),之前使用的设备可能已经不适合继续当前的任务,此时,用户可以选择新的设备来继续当前的任务,原设备可按需决定是否退出任务,这就是跨端迁移场景

    常见的跨端迁移场景实例:在平板上播放的视频,迁移到智慧屏继续播放,从而获得更佳的观看体验;平板上的视频应用退出。

    在应用开发层面,跨端迁移指在A端运行的UIAbility迁移到B端上,完成迁移后,B端UIAbility继续任务,而A端UIAbility可按需决定是否退出。

  • 多端协同

    用户拥有的多个设备,可以作为一个整体,为用户提供比单设备更加高效、沉浸的体验,这就是多端协同场景

    常见的多端协同场景实例:

    场景一:两台设备A和B打开备忘录同一篇笔记进行双端协同编辑,在设备A上可以使用本地图库中的图片资源插入编辑,设备B上进行文字内容编辑。

    场景二:设备A上正在和客户进行聊天,客户需要的资料在设备B上,可以通过聊天软件打开设备B上的文档应用选择到想要的资料回传到设备A上,然后通过聊天软件发送给客户。

    在应用开发层面,多端协同指多端上的不同UIAbility/ServiceExtensionAbility同时运行、或者交替运行实现完整的业务;或者多端上的相同UIAbility/ServiceExtensionAbility同时运行实现完整的业务。

3 典型场景

  • 媒体播控:使用媒体播控,可以简单高效地将音频投放到其他HarmonyOS设备上播放,如在手机上播放的音频,可以投到2in1设备上继续播放。
  • 应用接续:指当用户在一个设备上操作某个应用时,可以在另一个设备的同一个应用中快速切换,并无缝衔接上一个设备的应用体验。
  • 跨设备拖拽:跨端拖拽提供跨设备的键鼠共享能力,支持在平板或2in1类型的任意两台设备之间拖拽文件、文本。
  • 跨设备剪贴板:当用户拥有多台设备时,可以通过跨设备剪贴板的功能,在A设备的应用上复制一段文本,粘贴到B设备的应用中,高效地完成多设备间的内容共享。

4 应用接续

4.1 应用接续特性简介

应用接续,指当用户在一个设备上操作某个应用时,可以在另一个设备的同一个应用中快速切换,并无缝衔接上一个设备的应用体验

比如在用户使用过程中,使用情景发生了变化,之前使用的设备不再适合继续当前任务,或者周围有更合适的设备,此时用户可以选择使用新的设备来继续当前的任务。接续完成后,之前设备的应用可退出或保留,用户可以将注意力集中在被拉起的设备上,继续执行任务。

如图所示,在手机上编辑备忘录,到办公室后切换到平板上继续编辑,完成任务的无缝衔接。

4.1.1 运作机制

  1. 在源端,通过UIAbility的onContinue()回调,开发者可以保存待接续的业务数据。

    例如,浏览器应用完成应用接续,在源端浏览一个页面,到对端继续浏览。系统将自动保存页面状态,如当前页面的浏览进度;开发者需要通过onContinue接口保存页面url等业务内容。

  2. 分布式框架提供跨设备应用界面、页面栈以及业务数据的保存和恢复机制,负责将数据从源端发送到对端。
  3. 在对端,同一UIAbility通过onCreate/onNewWant接口恢复业务数据。

4.1.2 发起接续的场景

针对不同类型的应用,推荐的应用接续发起的界面及接续同步内容如下:

  • 浏览器:网页内容详情页,网页浏览进度同步
  • 备忘录:备忘录详情页,备忘浏览进度同步
  • 新闻:新闻详情页,新闻浏览进度同步
  • 阅读:小说阅读页,小说阅读进度同步
  • 视频:视频播放页,视频播放进度同步
  • 音乐:音乐播放页,歌单播放页,音乐播放进度同步
  • 会议:会议界面,当前会议同步
  • 邮件:新建邮件、回复转发邮件、阅读某封邮件界面,编辑内容及附件同步
  • 办公编辑:某条编辑页面,编辑内容同步
  • CAD:CAD编辑界面,编辑内容同步
  • 地图:路线查询、导航界面,当前路线及导航同步

4.2 应用接续开发指导

4.2.1 介绍

通过应用接续,可以实现将应用当前任务(包括页面控件状态变量等)迁移到目标设备,并在目标设备上接续使用。

可以实现的功能包括:

  • 存储及恢复自定义数据(应用业务内容)。
  • 存储及恢复页面路由信息和页面控件状态数据。
  • 应用兼容性检测。
  • 支持应用根据实际使用场景动态设置迁移状态(默认迁移状态为ACTIVE激活状态)。

    如编辑类应用在编辑文本的页面下才需要迁移,其他页面不需要迁移,则可以通过setMissionContinueState进行控制。

  • 支持应用动态选择是否进行页面栈恢复(默认进行页面栈信息恢复)。

    如应用希望自定义迁移到其他设备后显示的页面,则可以通过wantConstant.Params进行控制。

  • 支持应用动态选择流转成功后是否退出迁移源端应用(默认流转成功后退出迁移源端应用)。则可以通过@ohos.app.ability.wantConstant (wantConstant)进行控制。

4.2.2 约束与限制

需同时满足以下条件,才能使用该功能:

  • 设备限制

    HarmonyOS NEXT Developer Preview0及以上版本的设备。

  • 使用限制
    • 双端设备需要登录同一华为账号。
    • 双端设备需要打开Wi-Fi和蓝牙开关。

      条件允许时,建议双端设备接入同一个局域网,可提升数据传输的速度。

    • 应用接续只能在同应用(UIAbility)之间触发,双端设备都需要有该应用。
    • 为了接续体验,在onContinue回调中使用wantParam传输的数据需要控制在100KB以下,大数据量请使用分布式数据对象进行同步。

4.2.3 接口说明

以下为实现应用接续的主要接口,详细的接口说明可查阅参考文档

接口名

描述

onContinue(wantParam : {[key: string]: Object}): OnContinueResult

接续源端在该回调中保存迁移所需要的数据,同时返回是否同意迁移:

  • AGREE:表示同意。
  • REJECT:表示拒绝,如应用在onContinue中异常可以直接REJECT。
  • MISMATCH:表示版本不匹配,接续源端应用可以在onContinue中获取到迁移对端应用的版本号,进行协商后,如果版本不匹配导致无法迁移,可以返回该错误。

onCreate(want: Want, param: AbilityConstant.LaunchParam): void;

接续目的端为冷启动或多实例应用热启动时,在该回调中完成数据恢复,并触发页面恢复。

onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void;

接续目的端为单实例应用热启动时,在该回调中完成数据恢复,并触发页面恢复。

4.2.4 开发步骤

1. 启用应用接续能力。

module.json5文件的abilities中,将continuable标签配置为“true”,表示该UIAbility可被迁移。配置为false的UIAbility将被系统识别为无法迁移且该配置默认值为false。

{
  "module": {
    ...
    "abilities": [
      {
        ...
        "continuable": true,
      }
    ]
  }
}

2. 根据需要配置应用启动模式类型,配置详情请参照UIAbility组件启动模式

3. 在源端UIAbility中实现onContinue()接口。

当应用触发迁移时,onContinue()接口在源端被调用,开发者可以在该接口中保存迁移数据,实现应用兼容性检测,决定是否支持此次迁移。

  1. 保存迁移数据:开发者可以将要迁移的数据通过键值对的方式保存在wantParam中。
  2. (可选)检测应用兼容性:开发者可以在触发迁移时从onContinue()入参wantParam.version获取到迁移对端应用的版本号,与迁移源端应用版本号做兼容校验。应用在校验版本兼容性失败后,需要提示用户迁移失败的原因。

    说明

    如果迁移过程中的兼容性问题对于应用迁移体验影响较小或无影响,可以跳过该步骤。

  3. 返回迁移结果:开发者可以通过onContinue()回调的返回值决定是否支持此次迁移,接口返回值详见AbilityConstant.OnContinueResult
  • onContinue()接口传入的wantParam参数中,有部分字段由系统预置,开发者可以使用这些字段用于业务处理。同时,应用在保存自己的wantParam参数时,也应注意不要使用同样的key值,避免被系统覆盖导致数据获取异常。详见下表:

    字段

    含义

    version

    对端应用的版本号

    targetDevice

    对端设备的networkId

import { AbilityConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { promptAction } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onContinue(wantParam: Record<string, Object>) {
    let targetVersion  = wantParam.version;  // 获取迁移对端应用的版本号
    // 应用可根据源端版本号设置支持接续的最小兼容版本号,源端版本号可从app.json5文件中的versionCode字段获取;防止目标端版本号过低导致不兼容。
    let versionThreshold: number = 0; // 替换为应用自己支持兼容的最小版本号
    // 兼容性校验    
    if (targetVersion < versionThreshold) {
      // 建议在校验版本兼容性失败后,提示用户拒绝迁移的原因
      promptAction.showToast({
           message: '目标端应用版本号过低,不支持接续,请您升级应用版本后再试',
           duration: 2000
      })
      // 在兼容性校验不通过时返回MISMATCH
      return AbilityConstant.OnContinueResult.MISMATCH;
    }
    console.info(`onContinue version = ${wantParam.version}, targetDevice: ${wantParam.targetDevice}`)
    // 迁移数据保存
    let continueInput = '迁移的数据';
    if (continueInput) {
      // 将要迁移的数据保存在wantParam的自定义字段(如:data)中;
      wantParam["data"] = continueInput;
    }
     return AbilityConstant.OnContinueResult.AGREE;
  }
}

4. 在Stage模型中,应用在不同启动模式下将调用不同的接口,以恢复数据、加载界面。

不同情况下的函数调用如下图所示:

说明

  1. 应用迁移启动时,无论是冷启动还是热启动,都会在执行完onCreate()/onNewWant()后,触发onWindowStageRestore()生命周期函数,不执行onWindowStageCreate())生命周期函数。
  2. 开发者如果在onWindowStageCreate()中进行了一些应用启动时必要的初始化,那么迁移后需要在onWindowStageRestore()中执行同样的初始化操作,避免应用异常。

在目的端设UIAbility中实现onCreate()与onNewWant()接口,恢复迁移数据。

  • onCreate实现示例
    • 目的端设备上,在onCreate中根据launchReason判断该次启动是否为迁移LaunchReason.CONTINUATION。

    • 开发者可以从want中获取保存的迁移数据。

    • 若开发者使用系统页面栈恢复功能,则需要在onCreate()/onNewWant()执行完成前,调用restoreWindowStage(),来触发带有页面栈的页面恢复,如果不需要迁移页面栈可以参考按需迁移页面栈部分。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  storage : LocalStorage = new LocalStorage();
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) {
      // 将上述的保存的数据取出恢复
      let continueInput = '';
      if (want.parameters != undefined) {
        continueInput = JSON.stringify(want.parameters.data);
        console.info(`continue input ${continueInput}`)
      }
      // 触发页面恢复
      this.context.restoreWindowStage(this.storage);
    }
  }
}

说明

接口restoreWindowStage(this.storage)必须在同步接口方法中执行,如果在异步回调中执行,可能会导致应用迁移后页面加载失败。

  • 如果是单实例应用,需要额外实现onNewWant()接口,实现方式与onCreate()的实现相同。

在onNewWant()中判断迁移场景,恢复数据,并触发页面恢复

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  storage : LocalStorage = new LocalStorage();
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info(`EntryAbility onNewWant ${AbilityConstant.LaunchReason.CONTINUATION}`)
    if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) {
      // 将上述的保存的数据取出恢复
      let continueInput = '';
      if (want.parameters != undefined) {
        continueInput = JSON.stringify(want.parameters.data);
        console.info(`continue input ${continueInput}`);
      }
      this.context.restoreWindowStage(this.storage);
    }
  }
}

4.2.5 迁移功能可选配置

4.2.5.1 动态配置迁移能力

API version 10起,提供了支持动态配置迁移能力的功能。即应用可以根据实际使用场景,在需要迁移功能时,设置开启应用迁移能力;在业务不需要迁移时,则可以关闭迁移能力。开发者可以通过调用setMissionContinueState接口对迁移能力进行设置。

接口状态值

含义

AbilityConstant.ContinueState.ACTIVE

应用当前可迁移能力开启

AbilityConstant.ContinueState.INACTIVE

应用当前可迁移能力关闭

4.2.5.2 设置迁移能力的时机

默认状态下,应用的迁移能力为ACTIVE状态,即可以迁移。

如果需要实现某些特殊场景,比如只在具体某个页面下支持迁移,或者只在某个事件发生时才支持迁移,可以按照如下步骤进行配置。

1. 在Ability的onCreate生命周期回调中,关闭迁移能力。

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // ...
    this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE, (result) => {
      console.info(`setMissionContinueState: ${JSON.stringify(result)}`);
    });
    // ...
  }
}

2. 如果需要在具体某个页面中打开迁移能力,可以在该页面的onPageShow()函数中设置。

// PageName.ets
import { AbilityConstant, common } from '@kit.AbilityKit';
@Entry
@Component
struct PageName {
  private context = getContext(this) as common.UIAbilityContext;
  build() {
    // ...
  }
  // ...
  onPageShow(){
  // 进入该页面时,将应用设置为可迁移状态
    this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
      console.info('setMissionContinueState ACTIVE result: ', JSON.stringify(result));
    });
  }
}

3. 如果在某个组件的触发事件中打开迁移能力,可以在该事件中设置。下面以Button组件的onClick事件为例进行介绍。

// PageName.ets
import { AbilityConstant, common } from '@kit.AbilityKit';
@Entry
@Component
struct PageName {
  private context = getContext(this) as common.UIAbilityContext;
  build() {
    // ...
    Button() {
      //...
    }.onClick(()=>{
    //点击该按钮时,将应用设置为可迁移状态
      this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
        console.info('setMissionContinueState ACTIVE result: ', JSON.stringify(result));
      });
    })
  }
}
4.2.5.3 保证迁移连续性

由于迁移加载时,目标端拉起的应用可能执行过自己的迁移状态设置命令(如:冷启动时目标端在onCreate中设置了INACTIVE;热启动时对端已打开了不可迁移的页面,迁移状态为INACTIVE等情况)。为了保证迁移过后的应用依然具有可以迁移回源端的能力,应在onCreate和onNewWant的迁移调用判断中,将迁移状态设置为ACTIVE

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
  // ...
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // ...
    // 迁移冷启动时,设置状态为可迁移
    this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
      console.info(`setMissionContinueState: ${JSON.stringify(result)}`);
    });
  }
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // ...
    // 迁移热启动时,设置状态为可迁移
    if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) {
      this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
        console.info('setMissionContinueState ACTIVE result: ', JSON.stringify(result));
      });
    }
  }
  // ...
}

4.2.6 按需迁移页面栈

支持应用动态选择是否进行页面栈恢复(默认进行页面栈信息恢复)。如果应用不想使用系统默认恢复的页面栈,则可以设置不进行页面栈迁移,而需要在onWindowStageRestore设置迁移后进入的页面,参数定义见SUPPORT_CONTINUE_PAGE_STACK_KEY

说明

  1. 当前仅支持router路由的页面栈信息自动恢复,暂不支持navigation路由的页面栈自动恢复。
  2. 如果应用使用navigation路由,可以设置不进行页面栈迁移,并将需要接续的页面(或页面栈)信息保存在want中传递,然后在目标端手动加载指定页面。

应用在源端的页面栈中存在Index和Second路由,而在目标端恢复时不需要按照源端页面栈进行恢复,需要恢复到指定页面。

示例:应用迁移不需要自动迁移页面栈信息

// EntryAbility.ets
import { AbilityConstant, UIAbility, wantConstant } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
  // ...
  onContinue(wantParam: Record<string, Object>) {
    console.info(`onContinue version = ${wantParam.version}, targetDevice: ${wantParam.targetDevice}`);
    wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] = false;
    return AbilityConstant.OnContinueResult.AGREE;
  }
  // ...
  onWindowStageRestore(windowStage: window.WindowStage) {
      // 若不需要自动迁移页面栈信息,则需要在此处设置应用迁移后进入的页面
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        console.info('Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      console.info('Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });
  }
}

4.2.7 按需退出

支持应用动态选择迁移成功后是否退出迁移源端应用(默认迁移成功后退出迁移源端应用)。如果应用不想让系统自动退出迁移源端应用,则可以设置不退出,参数定义见SUPPORT_CONTINUE_SOURCE_EXIT_KEY

示例:应用迁移设置不需要迁移成功后退出迁移源端应用

import { AbilityConstant, UIAbility, wantConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
  // ...
  onContinue(wantParam: Record<string, Object>) {
    console.info(`onContinue version = ${wantParam.version}, targetDevice: ${wantParam.targetDevice}`);
    wantParam[wantConstant.Params.SUPPORT_CONTINUE_SOURCE_EXIT_KEY] = false;
    return AbilityConstant.OnContinueResult.AGREE;
  }
  // ...
}

4.2.8 支持同应用中不同Ability跨端迁移

一般情况下,跨端迁移的双端是同Ability之间,但有些应用在不同设备类型下的同一个业务Ability名称不同(即异Ability),为了支持该场景下的两个Ability之间能够完成迁移,可以通过在module.json5文件的abilities标签中配置迁移类型continueType进行关联。 需要迁移的两个Ability的continueType字段取值必须保持一致,示例如下:

说明

  • continueType在本应用中要保证唯一,字符串以字母、数字和下划线组成,最大长度127个字节,不支持中文。
  • continueType标签类型为字符串数组,如果配置了多个字段,当前仅第一个字段会生效。

 // 设备A
   {
     "module": {
       // ...
       "abilities": [
         {
           // ...
           "name": "Ability-deviceA",
           "continueType": ['continueType1'], // continueType标签配置
         }
       ]
     }
   }

   // 设备B
   {
     "module": {
       // ...
       "abilities": [
         {
           // ...
           "name": "Ability-deviceB",
           "continueType": ['continueType1'], // 与设备A相同的continueType标签
         }
       ]
     }
   }

4.2.9 快速启动目标应用

默认情况下,发起迁移后不会立即拉起对端的目标应用,而是等待迁移数据从源端传输到对端后才会拉起应用。若应用希望在用户发起接续后立即被拉起,减少等待时间,提升体验,可以在module.json5文件的continueType标签中添加“_ContinueQuickStart”后缀,配置快速启动目标应用能力。示例如下:

{
  "module": {
    // ...
    "abilities": [
      {
        // ...
        "name": "EntryAbility"
        "continueType": ['EntryAbility_ContinueQuickStart'], // 如果已经配置了continueType标签,可以在该标签值后添加'_ContinueQuickStart'后缀;如果没有配置continueType标签,可以使用AbilityName + '_ContinueQuickStart'作为continueType标签实现快速拉起目标应用
      }
    ]
  }
}

配置了快速拉起的应用,在用户发起接续时会立即收到一次launchReason为提前拉起(PREPARE_CONTINUATION)的onCreate()/onNewWant()请求,随后再收到一次launchReason为接续拉起(CONTINUATION)的onNewWant()请求。如下所示:

场景

生命周期函数

launchParam.launchReason

第一次启动请求

onCreate (冷启动)

或 onNewWant (热启动)

AbilityConstant.LaunchReason.PREPARE_CONTINUATION

第二次启动请求

onNewWant

AbilityConstant.LaunchReason.CONTINUATION

如果没有配置快速拉起,则触发迁移时只会收到一次启动请求:

场景

生命周期函数

launchParam.launchReason

一次启动请求

onCreate (冷启动)

或 onNewWant (热启动)

AbilityConstant.LaunchReason.CONTINUATION

配置快速拉起后,对应的 onCreate()/onNewWant() 接口实现可参考如下示例:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

export default class MigrationAbility extends UIAbility {
  storage : LocalStorage = new LocalStorage();

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onCreate');

    // 1.已配置快速拉起功能,应用立即启动时触发应用生命周期回调
    if (launchParam.launchReason === AbilityConstant.LaunchReason.PREPARE_CONTINUATION) {
      //若应用迁移数据较大,可在此处添加加载页面(页面中显示loading等)
      //可处理应用自定义跳转、时序等问题
      // ...
    }
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant');

    // 1.已配置快速拉起功能,应用立即启动时触发应用生命周期回调
    if (launchParam.launchReason === AbilityConstant.LaunchReason.PREPARE_CONTINUATION) {
      //若应用迁移数据较大,可在此处添加加载页面(页面中显示loading等)
      //可处理应用自定义跳转、时序等问题
      // ...
    }

    // 2.迁移数据恢复时触发应用生命周期回调
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      // 将上述保存的数据从want.parameters中取出恢复
      let continueInput = '';
      if (want.parameters !== undefined) {
        continueInput = JSON.stringify(want.parameters.data);
        hilog.info(DOMAIN_NUMBER, TAG, `continue input ${JSON.stringify(continueInput)}`);
      }
      // 触发页面恢复
      this.context.restoreWindowStage(this.storage);
    }
  }
}

4.2.10 使用分布式数据对象迁移数据

当需要迁移的数据较大(100KB以上)或需要迁移文件时,可以使用分布式数据对象。原理与接口说明详见分布式数据对象跨设备数据同步。

说明

自API 12起,由于直接使用跨设备文件访问实现文件的迁移,难以获取文件同步完成的时间。为了保证更高的成功率,文件的迁移不建议继续通过该方式实现,推荐使用分布式数据对象携带资产的方式。开发者此前通过跨设备文件访问实现的文件迁移依然生效。

4.2.10.1 申请权限

说明

  • 自API 12起,无需申请ohos.permission.DISTRIBUTED_DATASYNC权限。
  • API 11及以前版本,需要执行如下操作。
  1. 声明ohos.permission.DISTRIBUTED_DATASYNC权限,详见声明权限
  2. 由于ohos.permission.DISTRIBUTED_DATASYNC权限需要用户授权,应用需在首次启动、或进入接续页面时弹窗向用户申请授权,详见向用户申请授权
4.2.10.2 基础数据迁移

使用分布式数据对象,与上述开发步骤类似,需要在源端onContinue()接口中进行数据保存,并在对端的onCreate()/onNewWant()接口中进行数据恢复。

1. 在源端UIAbility的onContinue()接口中创建分布式数据对象并保存数据,执行流程如下:

说明

  • 分布式数据对象需要先激活,再持久化,因此必须在调用setSessionId()后再调用save()接口。
  • 对于源端迁移后需要退出的应用,为了防止数据未保存完成应用就退出,应采用await的方式等待save()接口执行完毕。从API 12 起,onContinue()接口提供了async版本供该场景使用。
  • 当前,wantParams中“sessionId”字段在迁移流程中被系统占用,建议开发者在wantParams中定义其他key值存储该分布式数据对象生成的id,避免数据异常。

示例代码如下:

  1. onContinue()接口中使用create()接口创建分布式数据对象,将所要迁移的数据填充到分布式数据对象数据中。
  2. 调用genSessionId()接口生成数据对象组网id,并使用该id调用setSessionId()加入组网,激活分布式数据对象。
  3. 使用save()接口将已激活的分布式数据对象持久化,确保源端退出后对端依然可以获取到数据。
  4. 将生成的sessionId通过want传递到对端,供对端激活同步使用。
import { distributedDataObject } from '@kit.ArkData';
import { UIAbility, AbilityConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 业务数据定义
class ParentObject {
  mother: string
  father: string

  constructor(mother: string, father: string) {
    this.mother = mother
    this.father = father
  }
}

// 支持字符、数字、布尔值、对象的传递
class SourceObject {
  name: string | undefined
  age: number | undefined
  isVis: boolean | undefined
  parent: ParentObject | undefined

  constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined, parent: ParentObject | undefined) {
    this.name = name
    this.age = age
    this.isVis = isVis
    this.parent = parent
  }
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
    // ...
    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
    let source: SourceObject = new SourceObject("jack", 18, false, parentSource);

    // 创建分布式数据对象
    this.d_object = distributedDataObject.create(this.context, source);

    // 生成数据对象组网id,激活分布式数据对象
    let dataSessionId: string = distributedDataObject.genSessionId();
    this.d_object.setSessionId(dataSessionId);

    // 将组网id存在want中传递到对端
    wantParam['dataSessionId'] = dataSessionId;

    // 数据对象持久化,确保迁移后即使应用退出,对端依然能够恢复数据对象
    // 从wantParam.targetDevice中获取到对端设备的networkId作为入参
    await this.d_object.save(wantParam.targetDevice as string).then((result:
      distributedDataObject.SaveSuccessResponse) => {
      hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in saving. SessionId: ${result.sessionId},
        version:${result.version}, deviceId:${result.deviceId}`);
    }).catch((err: BusinessError) => {
      hilog.error(DOMAIN_NUMBER, TAG, 'Failed to save. Error: ', JSON.stringify(err) ?? '');
    });
  }
}

2. 在对端UIAbility的onCreate()/onNewWant()中,通过加入与源端一致的分布式数据对象组网进行数据恢复。

  1. 创建空的分布式数据对象,用于接收恢复的数据;
  2. 从want中读取分布式数据对象组网id;
  3. 注册on()接口监听数据变更。在收到status为restore的事件的回调中,实现数据恢复完毕时需要进行的业务操作。
  4. 调用setSessionId()加入组网,激活分布式数据对象。

说明

  • 对端加入组网的分布式数据对象不能为临时变量,因为在分布式数据对象on()接口为异步回调,可能在onCreate()/onNewWant()执行结束后才执行,临时变量被释放可能导致空指针异常。可以使用类成员变量避免该问题。
  • 对端用于创建分布式数据对象的Object,其属性应在激活分布式数据对象前置为undefined,否则会导致新数据加入组网后覆盖源端数据,数据恢复失败。
  • 应当在激活分布式数据对象之前,调用分布式数据对象的on()接口进行注册监听,防止错过restore事件导致数据恢复失败。

示例代码如下:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { distributedDataObject } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 示例数据对象定义与上同

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      // ...
      // 调用封装好的分布式数据对象处理函数
      this.handleDistributedData(want);
    }
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      if (want.parameters !== undefined) {
        // ...
        // 调用封装好的分布式数据对象处理函数
        this.handleDistributedData(want);
      }
    }
  }

  handleDistributedData(want: Want) {
    // 创建空的分布式数据对象
    let remoteSource: SourceObject = new SourceObject(undefined, undefined, undefined, undefined);
    this.d_object = distributedDataObject.create(this.context, remoteSource);

    // 读取分布式数据对象组网id
    let dataSessionId = '';
    if (want.parameters !== undefined) {
      dataSessionId = want.parameters.dataSessionId as string;
    }
    // 添加数据变更监听
    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
      hilog.info(DOMAIN_NUMBER, TAG, "status changed " + sessionId + " " + status + " " + networkId);
      if (status == 'restored') {
        if (this.d_object) {
          // 收到迁移恢复的状态时,可以从分布式数据对象中读取数据
          hilog.info(DOMAIN_NUMBER, TAG, "restored name:" + this.d_object['name']);
          hilog.info(DOMAIN_NUMBER, TAG, "restored parents:" + JSON.stringify(this.d_object['parent']));
        }
      }
    });
    // 激活分布式数据对象
    this.d_object.setSessionId(dataSessionId);
  }
}
4.2.10.3 文件资产迁移

对于图片、文档等文件类数据,需要先将其转换为资产commonType.Asset类型,再封装到分布式数据对象中进行迁移。迁移实现方式与普通的分布式数据对象类似,下面仅针对差异部分进行说明。

  • 在源端,将需要迁移的文件资产保存到分布式数据对象DataObject中,执行流程如下:
  1. 将文件资产拷贝到分布式文件目录下,相关接口与用法详见基础文件接口。
  2. 使用分布式文件目录下的文件创建Asset资产对象。
  3. 将Asset资产对象作为分布式数据对象的根属性保存。

随后,与普通数据对象的迁移的源端实现相同,可以使用该数据对象加入组网,并进行持久化保存。

示例如下:

// 导入模块
import { UIAbility, AbilityConstant } from '@kit.AbilityKit';
import { distributedDataObject, commonType } from '@kit.ArkData';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@ohos.base';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 数据对象定义
class ParentObject {
  mother: string
  father: string

  constructor(mother: string, father: string) {
    this.mother = mother
    this.father = father
  }
}

class SourceObject {
  name: string | undefined
  age: number | undefined
  isVis: boolean | undefined
  parent: ParentObject | undefined
  attachment: commonType.Asset | undefined  // 新增资产属性

  constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined,
    parent: ParentObject | undefined, attachment: commonType.Asset | undefined) {
    this.name = name
    this.age = age
    this.isVis = isVis
    this.parent = parent
    this.attachment = attachment;
  }
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
    // ...

    // 1. 将资产写入分布式文件目录下
    let distributedDir: string = this.context.distributedFilesDir;  // 获取分布式文件目录路径
    let fileName: string = '/test.txt';                        // 文件名
    let filePath: string = distributedDir + fileName;          // 文件路径

    try {
      // 在分布式目录下创建文件
      let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
      hilog.info(DOMAIN_NUMBER, TAG, 'Create file success.');
      // 向文件中写入内容(若资产为图片,可将图片转换为buffer后写入)
      fileIo.writeSync(file.fd, '[Sample] Insert file content here.');
      // 关闭文件
      fileIo.closeSync(file.fd);
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
    }

    // 2. 使用分布式文件目录下的文件创建资产对象
    let distributedUri: string = fileUri.getUriFromPath(filePath); // 获取分布式文件Uri

    // 获取文件参数
    let ctime: string = '';
    let mtime: string = '';
    let size: string = '';
    await fileIo.stat(filePath).then((stat: fileIo.Stat) => {
      ctime = stat.ctime.toString();  // 创建时间
      mtime = stat.mtime.toString();  // 修改时间
      size = stat.size.toString();    // 文件大小
    })

    // 创建资产对象
    let attachment: commonType.Asset = {
      name: fileName,
      uri: distributedUri,
      path: filePath,
      createTime: ctime,
      modifyTime: mtime,
      size: size,
    }

    // 3. 将资产对象作为分布式数据对象的根属性,创建分布式数据对象
    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
    let source: SourceObject = new SourceObject("jack", 18, false, parentSource, attachment);
    this.d_object = distributedDataObject.create(this.context, source);

    // 生成组网id,激活分布式数据对象,save持久化保存
    // ...
}
  • 对端需要先创建一个各属性为空的Asset资产对象作为分布式数据对象的根属性。在接收到on()接口status为restored的事件的回调时,表示包括资产在内的数据同步完成,可以像获取基本数据一样获取到源端的资产对象。

说明

对端创建分布式数据对象时,SourceObject对象中的资产不能直接使用undefined初始化,需要创建一个各属性为空的Asset资产对象,否则会导致资产同步失败。

示例代码如下:

import { UIAbility, Want } from '@kit.AbilityKit';
import { distributedDataObject, commonType } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  handleDistributedData(want: Want) {
    // ...
    // 创建一个各属性为空的资产对象
    let attachment: commonType.Asset = {
      name: '',
      uri: '',
      path: '',
      createTime: '',
      modifyTime: '',
      size: '',
    }

    // 使用该空资产对象创建分布式数据对象,其余基础属性可以直接使用undefined
    let source: SourceObject = new SourceObject(undefined, undefined, undefined, undefined, attachment);
    this.d_object = distributedDataObject.create(this.context, source);

    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
        if (status == 'restored') {
          // 收到监听的restored回调,表示分布式资产对象同步完成
          hilog.info(DOMAIN_NUMBER, TAG, "restored attachment:" + JSON.stringify(this.d_object['attachment']));
        }
    });
    // ...
  }
}

若应用想要同步多个资产,可采用两种方式实现:

  1. 可将每个资产作为分布式数据对象的一个根属性实现,适用于要迁移的资产数量固定的场景。
  2. 可以将资产数组传化为Object传递,适用于需要迁移的资产个数会动态变化的场景(如用户选择了不定数量的图片)。当前不支持直接将资产数组作为根属性传递。

其中方式1的实现可以直接参照添加一个资产的方式添加更多资产。方式2的示例如下所示:

// 导入模块
import { distributedDataObject, commonType } from '@kit.ArkData';
import { UIAbility } from '@kit.AbilityKit';

// 数据对象定义
class SourceObject {
  name: string | undefined
  assets: Object | undefined  // 分布式数据对象的中添加一个Object属性

  constructor(name: string | undefined, assets: Object | undefined) {
    this.name = name
    this.assets = assets;
  }
}

// 该函数用于将资产数组转为Record
GetAssetsWrapper(assets: commonType.Assets): Record<string, commonType.Asset> {
  let wrapper: Record<string, commonType.Asset> = {}
  let num: number = assets.length;
  for (let i: number = 0; i < num; i++) {
    wrapper[`asset${i}`] = assets[i];
  }
  return wrapper;
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
    // ...

    // 创建了多个资产对象
    let attachment1: commonType.Asset = {
      // ...
    }

    let attachment2: commonType.Asset = {
      // ...
    }

    // 将资产对象插入资产数组
    let assets: commonType.Assets = [];
    assets.push(attachment1);
    assets.push(attachment2);

    // 将资产数组转为Record Object,并用于创建分布式数据对象
    let assetsWrapper: Object = this.GetAssetsWrapper(assets);
    let source: SourceObject = new SourceObject("jack", assetsWrapper);
    this.d_object = distributedDataObject.create(this.context, source);

    // ...
}

5 跨设备拖拽

5.1 跨设备拖拽特性简介

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

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

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

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

开发者可以根据实际需求,实现组件的拖入或拖出,即可接入跨设备拖拽。

运作机制

  1. 用户使用鼠标点击组件,触发拖拽事件。
  2. 应用设置拖拽数据。
  3. 系统完成跨设备数据处理,此过程应用不感知。
  4. 用户松手触发拖拽松手事件。
  5. 目的端应用获取并处理拖拽数据。

5.2 跨设备拖拽开发指导

在开发跨设备拖拽的功能时,系统将自动完成键鼠穿越和跨设备的数据传递,应用可按照本设备上的开发示例,完成拖拽事件的开发。

5.2.1 约束与限制

需同时满足以下条件,才能使用该功能:

  • 设备限制

    HarmonyOS NEXT Developer Preview0及以上版本平板或2in1设备

  • 使用限制
    • 双端设备需要登录同一华为账号
    • 双端设备需要打开Wi-Fi和蓝牙开关,并接入同一个局域网
    • 打开穿越开关
    • 应用本身预置的资源文件(即应用在安装前的HAP包中已经存在的资源文件)不支持跨设备拖拽。

5.2.2 接口说明

在开发具体功能前,请先查阅参考文档,获取详细的接口说明。

  • 拖拽控制:设置组件是否可以响应拖拽事件的属性。组件均需要设置draggable属性才能响应拖拽事件。

    当前部分组件默认支持拖拽控制。应用使用这些组件时,只需要将draggable设置为true,系统将根据组件的支持情况,自动实现onDragStart的写信息或onDrop的读信息。

  • 拖拽事件:组件被鼠标选中后拖拽时触发的事件。

    应用应根据实际需求,实现组件拖入或拖出。

5.2.3 开发示例

拖拽事件通过鼠标左键来操作和响应,开发示例请参考:拖拽事件

6 跨设备剪贴板

6.1 跨设备剪贴板特性简介

剪贴板分为本地剪贴板跨设备剪贴板,本地剪贴板提供设备内的内容复制粘贴,跨设备剪贴板提供跨设备的内容复制粘贴。

当用户拥有多台设备时,可以通过跨设备剪贴板的功能,在A设备的应用上复制一段文本,粘贴到B设备的应用中,高效地完成多设备间的内容共享。

当开发者正在开发一款浏览器类应用,或是备忘录、笔记、邮件等富文本编辑类应用时,均可接入跨设备剪贴板,提升用户体验。

6.1.1 运作机制

1. 用户在设备A复制数据。

2. 系统剪贴板服务将处理相关数据,并完成数据同步。此过程开发者不感知。

3. 用户在设备B粘贴来自设备A的数据。

6.2 跨设备剪贴板开发指导

在开发跨设备剪贴板的功能时,系统将自动完成跨设备的数据传递,应用可按照本设备上的开发示例,完成跨设备剪贴板的开发。

6.2.1 约束与限制

设备版本

使用限制

HarmonyOS NEXT Developer Preview0及以上

  • 双端设备需要登录同一华为账号
  • 双端设备需要打开Wi-Fi和蓝牙开关。

    条件允许时,建议双端设备接入同一个局域网,可提升数据传输的速度。

  • 双端设备在过程中需解锁、亮屏

HarmonyOS版本3.2及以上

  • 双端设备需要登录同一华为账号
  • 双端设备需要打开Wi-Fi和蓝牙开关,并接入同一个局域网
  • 双端设备在过程中需解锁、亮屏

6.2.2 接口说明

在开发具体功能前,请先查阅参考文档,获取详细的接口说明。

接口

说明

getSystemPasteboard(): SystemPasteboard

获取系统剪贴板对象。

createData(mimeType: string, value: ValueType): PasteData

构建一个自定义类型的剪贴板内容对象。

setData(data: PasteData): Promise<void>

将数据写入系统剪贴板,使用Promise异步回调。

getData( callback: AsyncCallback<PasteData>): void

读取系统剪贴板内容,使用callback异步回调。

应用使用自定义控件后台访问剪贴板需要申请ohos.permission.READ_PASTEBOARD。

getRecordCount(): number

获取剪贴板内容中条目的个数。

getPrimaryMimeType(): string

获取剪贴板内容中首个条目的数据类型。

getPrimaryText(): string

获取首个条目的纯文本内容。

6.2.3 开发示例

说明

跨设备复制的数据两分钟内有效。

  • 设备A复制数据,写入到剪贴板服务。
import pasteboard from '@ohos.pasteboard';
import { BusinessError } from '@ohos.base';

export async function setPasteDataTest(): Promise<void> {
  let text: string = 'hello world';
  let pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
  let systemPasteBoard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
  await systemPasteBoard.setData(pasteData).catch((err: BusinessError) => {
    console.error(`Failed to set pastedata. Code: ${err.code}, message: ${err.message}`);
  });
}
  • 设备B粘贴数据,读取剪贴板内容。
import pasteboard from '@ohos.pasteboard';
import { BusinessError } from '@ohos.base';

export async function getPasteDataTest(): Promise<void> {
  let systemPasteBoard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
  systemPasteBoard.getData((err: BusinessError, data: pasteboard.PasteData) => {
    if (err) {
      console.error(`Failed to get pastedata. Code: ${err.code}, message: ${err.message}`);
      return;
    }
    // 对pastedata进行处理,获取类型,个数等
    let recordCount: number = data.getRecordCount(); // 获取剪贴板内record的个数
    let types: string = data.getPrimaryMimeType(); // 获取剪贴板内数据的类型
    let primaryText: string = data.getPrimaryText(); // 获取剪贴板内数据的内容
  });
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值