文档管理中心
您当前正在浏览新版开发者文档中心,目录分类和层级有所调整。点击左侧当前文档分类名称前的“☰”图标,可切换文档分类。 了解新版目录
最佳实践多设备开发自由流转典型全场景协同开发案例社交通讯全场景协同最佳实践

社交通讯全场景协同最佳实践

概述

在办公、创作和社区交友等应用中,内容发布和浏览分享是用户互动与交流的核心。它允许用户创作并分享包含图片、视频、文字等多媒体信息,从而增强用户间的连接与互动。随着手机、平板、电脑等电子设备的普及,用户对跨设备处理图片/视频/文字及接续编辑内容、在不同设备间无缝切换及快速便捷分享内容的需求日益增长。本文介绍如何通过跨端迁移和多端协同功能,实现不同设备间的快速无缝切换,提升内容发布和浏览分享的便利性。

本文主要包含以下几个方面内容:

  • 跨设备互通:基于分布式协同框架,面向跨设备拍照等业务场景,提供createCollaborationServiceMenuItems(相机设备列表组件)和CollaborationServiceStateDialog(远端相机状态弹窗组件)两个组件。应用只需要调用这两个组件,即可实现跨设备调用相机、拍照、扫描及访问图库的功能。
  • 跨设备拖拽图片/文字:支持跨设备拖拽场景,系统自动完成键鼠穿越和跨设备的数据传递,应用可根据实际需求,使用拖拽控制和拖拽事件能力,实现在平板或电脑类型的任意两台设备间拖拽图片和文本的功能。
  • 跨设备剪贴图片/视频/文字:基于@ohos.pasteboard (剪贴板)能力,支持系统复制、粘贴功能,实现本地剪贴板和跨设备剪贴板的业务场景。本地剪贴板提供设备内的内容复制粘贴,跨设备剪贴板提供跨设备的内容复制粘贴。
  • 跨设备应用接续:基于UIAbility应用组件,通过在本端使用onContinue()接口保存迁移数据,在目的端使用onCreate()或onNewWant()接口恢复迁移数据,实现应用接续的场景。即当用户在一个设备上操作某个应用时,可以在另一个设备的同一个应用中快速切换,并无缝衔接上一个设备的应用体验。
  • 碰一碰插入图片:基于系统手机与PC/2in1碰一碰分享能力,实现将移动端设备图库中的图片分享后插入到电脑端应用页面中。
  • 碰一碰分享页面:基于系统碰一碰分享能力,支持通过碰一碰分享内容页面到其他设备。并结合App Linking技术(详情可参考App Linking Kit简介),可实现内容的快速跨设备分享,直达目标应用,提供高效、便捷、无缝的分享体验。
  • 隔空传送分享页面:基于系统隔空传送能力,支持通过隔空传送手势分享内容页面到其他设备。通过隔空传送手势触发分享App Linking链接,安装应用的接收端可直接打开查看内容。

用户体验

体验

特性

体验视频

跨设备互通

跨设备拖拽

跨设备剪贴

跨设备应用接续

碰一碰插入图片

碰一碰分享页面

隔空传送分享页面

使用限制

展开

限制\场景

跨设备互通

跨设备拖拽

跨设备剪贴

跨设备应用接续

碰一碰分享

隔空传送分享

HarmonyOS NEXT及以上版本的设备

√

√

√

√

√

√

登录同一个华为账号

√

√

√

√

  • 手机与手机碰一碰分享不需要。
  • 手机与电脑碰一碰分享需要。

NA

打开Wi-Fi和蓝牙开关

√

√

√

√

NA

NA

设备需解锁、亮屏

NA

NA

√

NA

√

√

设备类型(垂域常见设备手机、平板、电脑)

  • 本端设备:平板或电脑设备。
  • 远端设备:具有相机能力的手机或平板设备。

必须有电脑设备接入,或者部分支持电脑模式的平板设备接入。

手机、平板、电脑

手机、平板、电脑

  • 本端设备:手机设备。
  • 对端设备:手机、平板(6.1.0(23)版本开始)、电脑设备。仅支持直板手机或折叠手机直板态与电脑屏幕碰一碰分享。

手机、平板、电脑

在"设置>多设备协同"中开启相应功能

√

开启键鼠共享开关。

开启跨设备剪贴板开关。

开启接续开关。

NA

NA

其他

支持根据特定调用策略调用设备,详见“约束与限制>使用限制”中的调用策略部分。

需接入同一个局域网且应用本身预置的资源文件(即应用在安装前的HAP包中已经存在的资源文件)不支持跨设备拖拽。

NA

双端设备都需要安装该应用。

已开启华为分享服务(系统默认开启)。手机与电脑碰一碰交互约束详见使用约束的轻碰屏幕交互约束部分。

需打开设备侧隔空传送开关。

跨设备互通

场景描述

用户在平板或电脑上使用文本编辑应用(如备忘录、邮件、笔记等)时,若当前设备不便拍摄,可利用跨设备互通功能,直接调用手机或平板的相机完成拍摄,也可从其图库中选择图片或视频。手机和平板设备在拍摄灵活性、取景便利性及相机性能上更具优势。所选素材将快速传输至原设备的应用中,帮助用户高效完成图文编辑任务。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:跨设备互通开发指导中的约束与限制章节。

实现原理

场景分析

  1. 用户在本端(平板或电脑)应用界面操作,选择使用远端设备的拍照、扫描或图库功能,向远端发起请求。
  2. 系统将自动唤醒远端设备上的相机、图库或扫描,进入相应的界面。
  3. 使用远端设备完成拍照或选择图片/视频并确认,远端拍摄状态信息实时回传到本端,并将数据插入到本端设备的应用中。

关键技术

跨设备互通的核心在于使用HarmonyOS的分布式协同框架:

  1. 设备发现:通过createCollaborationServiceMenuItems组件获取组网内具有对应能力的设备列表。
  2. 远端唤醒:系统自动唤醒远端设备上的相机、图库或扫描功能。
  3. 状态回传:通过CollaborationServiceStateDialog组件实时回传远端拍摄状态信息。
  4. 数据接收:在onState()回调中处理接收到的数据。

在分布式协同框架下,针对跨设备互通图片/视频信息的业务场景,应用只需要调用createCollaborationServiceMenuItems(相机设备列表组件)和CollaborationServiceStateDialog(远端相机状态弹窗组件)两个组件,即可实现跨端拍照、扫描、图库访问功能,开发者无需关注分布式场景下的数据传输和指令控制等细节,具体运作机制可参考:跨设备互通特性简介中的运作机制章节。

实现方案

  • 拉取远端菜单,选择远端设备

    通过createCollaborationServiceMenuItems组件,获取组网内具有对应能力的设备列表。当用户选择特定的设备功能后,系统将自动激活远端设备的相机或图库,并使设备屏幕自动点亮。可设置传输照片/视频的最大数量canReceiveNumber,范围为1至50张;若设置数量小于或等于0,则不会触发设备的相应功能;若设置数量超过50,则默认为50。

    收起
    自动换行
    深色代码主题
    复制
    1. // Remote menu.
    2. @Builder
    3. MyTestMenu() {
    4. Menu() {
    5. MenuItem({
    6. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.picture_2')),
    7. content: $r('app.string.local_device')
    8. })
    9. .onClick(() => {
    10. if (this.mediaUriArray.length < CommonConstants.MAX_ADD_MEDIA_NUM) {
    11. this.selectImage();
    12. } else {
    13. try {
    14. this.getUIContext().getPromptAction().showToast({ message: $r('app.string.add_picture_prompt') });
    15. } catch (err) {
    16. hilog.error(DOMAIN, TAG, FORMAT, `ShowToast failed. Cause code: ${err.code}, message: ${err.message}`);
    17. }
    18. }
    19. })
    20. if (canIUse('SystemCapability.Collaboration.Service')) {
    21. createCollaborationServiceMenuItems([CollaborationServiceFilter.ALL, CollaborationServiceFilter.VIDEO_PICKER],
    22. CommonConstants.MAX_ADD_MEDIA_NUM)
    23. }
    24. }
    25. }
  • 将远端拍摄状态信息实时回传

    应用将弹出提示框,实时回传远端拍摄状态信息到组件CollaborationServiceStateDialog(远端相机状态弹窗组件),为弹窗组件绑定和实现onState()方法,在业务开始后,此方法将被协同框架调用,用于接收和处理数据。该回调函数接收的数据中,stateCode表示完成状态,bufferType表示回传的数据类型,buffer则是回传数据。当bufferType取值为"general.image"时,buffer返回图片数据;当其取值为"general.video"时,buffer则返回沙箱视频uri路径。

    收起
    自动换行
    深色代码主题
    复制
    1. build() {
    2. Column() {
    3. CollaborationServiceStateDialog({
    4. onState: (stateCode: number, bufferType: string, buffer: ArrayBuffer): void => this.doInsertMedia(stateCode,
    5. bufferType, buffer)
    6. })
    7. // ...
    8. }
    9. // ...
    10. }
  • 将回传数据写入页面

    用户使用远端设备完成拍照或选择图库照片/视频后,通过自定义方法将返回的图片/视频数据写入本端设备的页面。远端设备将自动退出相机或图库界面,恢复到初始状态。

    收起
    自动换行
    深色代码主题
    复制
    1. // Remote images or videos fall into.
    2. doInsertMedia(stateCode: number, bufferType: string, buffer: ArrayBuffer): void {
    3. if (stateCode != 0) {
    4. return;
    5. }
    6. if (this.mediaUriArray.length >= CommonConstants.MAX_ADD_MEDIA_NUM) {
    7. return;
    8. }
    9. if (bufferType === 'general.image') {
    10. let imageSource = image.createImageSource(buffer);
    11. imageSource.createPixelMap().then((pixelMap) => {
    12. let uuid = util.generateRandomUUID();
    13. this.PixelMapToBuffer(pixelMap, uuid);
    14. this.mediaUriArray.push({ imagePixelMap: pixelMap, mediaName: uuid, mediaType: MediaType.MEDIA_IMAGE });
    15. imageSource.release();
    16. })
    17. } else if (bufferType === 'general.video') {
    18. let uuid = util.generateRandomUUID();
    19. let decoder = util.TextDecoder.create('utf-8');
    20. let videoUriStr = decoder.decodeToString(new Uint8Array(buffer));
    21. writeDistributedFile(this.context, uuid, MediaType.MEDIA_VIDEO, undefined, videoUriStr);
    22. this.mediaUriArray.push({ videoUri: videoUriStr, mediaName: uuid, mediaType: MediaType.MEDIA_VIDEO });
    23. }
    24. }

跨设备拖拽图片/文字

场景描述

当用户拥有多台设备时,开启键鼠共享功能,可以实现键鼠在不同设备间的自由移动。通过跨设备拖拽,用户可以轻松地将本端上的素材拖拽到远端,快速完成内容创作,享受高效的跨设备协同工作体验。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:跨设备拖拽中的约束与限制章节。

实现原理

场景分析

  1. 用户通过长按鼠标触发拖拽事件,可以从本端编辑页面将图片或文字拖拽到远端的编辑页面。
  2. 在此过程中,系统自动处理跨设备的数据传输,开发者无需介入。
  3. 当用户释放鼠标时,触发拖拽松手事件,远端应用处理接收到的拖拽数据,并将其写入远端编辑页面。

关键技术

在开发跨设备拖拽功能时,系统会自动处理鼠标的跨设备操作及数据传递。应用可以根据实际需求,实现组件的拖入或拖出,完成拖拽事件的开发。具体运作机制可参考:跨设备拖拽中的运作机制章节。

实现方案

  • 设置组件允许拖拽

    将需要触发拖拽事件和松手事件的Image组件、TextInput组件及TextArea组件设置draggable()为true,以允许拖拽。

  • 设置组件允许落入的类型

    使用allowDrop()方法设置组件允许拖入的数据类型,包括PLAIN_TEXT、IMAGE、OPENHARMONY_PIXEL_MAP等,具体可参考UniformDataType。

  • 定义拖拽事件

    将允许拖入的组件绑定onDrop()事件作为释放目标。当在该组件范围内停止拖拽操作时,将触发回调函数,实现图片的拖出和拖入页面的功能。

    收起
    自动换行
    深色代码主题
    复制
    1. build() {
    2. Column() {
    3. // ...
    4. }
    5. .draggable(true)
    6. .allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE,
    7. uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP])
    8. .onDrop((dragEvent?: DragEvent) => {
    9. // The logic behind the image falling in, achieving image writing.
    10. // ...
    11. })
    12. })
    13. }
    14. // ...
    15. /*
    16. * Adding an image or video.
    17. */
    18. @Builder
    19. addMedia() {
    20. Row() {
    21. List({ space: CommonConstants.LIST_COMM_SPACE }) {
    22. ForEach(this.mediaUriArray, (item: MediaInfo) => {
    23. ListItem() {
    24. if (item.imagePixelMap) {
    25. Image(item.imagePixelMap)// ...
    26. .draggable(true)
    27. .onDragEnd((event) => {
    28. // The logic after dragging and dropping is completed.
    29. // ...
    30. })// ...
    31. } else if (item.videoUri) {
    32. Video({
    33. src: item.videoUri,
    34. posterOptions: {
    35. showFirstFrame: true
    36. }
    37. })// ...
    38. .draggable(true)
    39. .controls(false)
    40. .autoPlay(true)
    41. .loop(true)
    42. .onDragEnd((event) => {
    43. // The logic after dragging and dropping is completed.
    44. // ...
    45. })// ...
    46. }
    47. }
    48. }, (item: MediaInfo, index: number) => JSON.stringify(item) + index)
    49. // ...
    50. }
    51. // ...
    52. }
    53. // ...
    54. }

    当在TextInput和TextArea组件范围内停止拖放行为时,将触发回调,实现文字拖出和写入页面的效果。

    收起
    自动换行
    深色代码主题
    复制
    1. build() {
    2. Flex({ direction: FlexDirection.Column }) {
    3. TextInput({ text: this.mainTitle, placeholder: $r('app.string.text_input_placeholder') })// ...
    4. .draggable(true)
    5. .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
    6. .onDrop((dragEvent?: DragEvent) => {
    7. // The logic after the text falls in, realizing the writing of text.
    8. // ...
    9. })
    10. TextArea({ text: this.textContent, placeholder: $r('app.string.richEditor_placeholder') })// ...
    11. .draggable(true)
    12. .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
    13. .onDrop((dragEvent?: DragEvent) => {
    14. // The logic after the text falls in, realizing the writing of text.
    15. // ...
    16. })
    17. }
    18. .backgroundColor($r('sys.color.background_primary'))
    19. // ...
    20. }

跨设备剪贴图片/视频/文字

场景描述

当用户拥有多台设备时,可以利用跨设备剪贴板的功能,在本端的应用中复制文本、图片或视频,并在远端的应用中粘贴,实现高效的内容共享。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:跨设备剪贴板中的约束与限制章节。

实现原理

场景分析

1. 用户在本端复制数据,写入到系统剪贴板服务。

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

3. 用户在远端读取系统剪贴板内容,粘贴来自本端的数据。

关键技术

在开发跨设备剪贴板功能时,系统会自动处理设备间的数据传输,应用程序可根据实际需求接入跨设备剪贴板,实现跨设备间的数据共享。具体运作机制可参考:跨设备剪贴板中的运作机制章节。

实现方案

  • 本端复制数据,写入到剪贴板服务

    使用SystemPasteboard(系统剪贴板对象)的setData()方法将本端复制的数据写入剪贴板服务。

    收起
    自动换行
    深色代码主题
    复制
    1. // Copy picture or video.
    2. async setPasteDataTest(data: image.PixelMap | string): Promise<void> {
    3. let pasteData: pasteboard.PasteData;
    4. if (typeof data === 'string') {
    5. pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_URI, data);
    6. } else {
    7. pasteData = pasteboard.createData(pasteboard.MIMETYPE_PIXELMAP, data);
    8. }
    9. let systemPasteBoard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
    10. await systemPasteBoard.setData(pasteData).catch((err: BusinessError) => {
    11. hilog.error(DOMAIN, TAG, FORMAT, `Failed to set pastedata. Code: ${err.code}, message: ${err.message}`);
    12. });
    13. }
  • 远端粘贴数据,读取剪贴板内容

    在远端设备上粘贴数据时,使用系统剪贴板对象SystemPasteboard的getData()方法读取剪贴板中的内容,并将数据展示在页面上。

    收起
    自动换行
    深色代码主题
    复制
    1. // Paste picture or video.
    2. async getPasteDataTest(): Promise<void> {
    3. let systemPasteBoard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
    4. systemPasteBoard.getData((err: BusinessError, data: pasteboard.PasteData) => {
    5. if (err) {
    6. hilog.error(DOMAIN, TAG, FORMAT, `Failed to get pastedata. Code: ${err.code}, message: ${err.message}`);
    7. return;
    8. }
    9. // Process pasteData, obtain type, number, etc
    10. // Retrieve the number of records in the clipboard.
    11. let recordCount: number = data.getRecordCount();
    12. // Retrieve the type of data from the clipboard.
    13. let type: string = data.getPrimaryMimeType();
    14. hilog.info(DOMAIN, TAG, FORMAT, `recordCount: ${recordCount}, type: ${type}`);
    15. // Retrieve the content of data from the clipboard.
    16. if (this.mediaUriArray.length < CommonConstants.MAX_ADD_MEDIA_NUM) {
    17. if (type === pasteboard.MIMETYPE_PIXELMAP) {
    18. let primaryPixelMap: image.PixelMap = data.getPrimaryPixelMap();
    19. let uuid = util.generateRandomUUID();
    20. this.PixelMapToBuffer(primaryPixelMap, uuid);
    21. this.mediaUriArray.push({
    22. imagePixelMap: primaryPixelMap,
    23. mediaName: uuid,
    24. mediaType: MediaType.MEDIA_IMAGE
    25. });
    26. } else if (type === pasteboard.MIMETYPE_TEXT_URI) {
    27. let uuid = util.generateRandomUUID();
    28. writeDistributedFile(this.context, uuid, MediaType.MEDIA_VIDEO, undefined, data.getPrimaryUri());
    29. this.mediaUriArray.push({
    30. videoUri: data.getPrimaryUri(),
    31. mediaName: uuid,
    32. mediaType: MediaType.MEDIA_VIDEO
    33. });
    34. }
    35. } else {
    36. this.toastShow = true;
    37. }
    38. });
    39. }

跨设备应用接续

场景描述

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

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:应用接续概述中的约束限制章节。

实现原理

场景分析

  1. 输入数据:用户在本端设备的编辑页面上选择照片/视频、输入标题和正文等文字信息。
  2. 用户点击远端设备Dock栏图标后,本端设备发起接续,数据进行传输。
  3. 远端设备接收接续数据并显示。

场景核心在于应用接续的过程中如何传递数据。对于文字信息可使用分布式数据对象保存,对于图片/视频可以拷贝到分布式文件目录下,使用分布式数据资产作为分布式数据对象的根属性保存。

关键技术

将发起接续的设备称为本端设备,接收数据的设备称为远端设备,接续过程底层依赖分布式框架和软总线,开发者只需要启用接续、保存数据和恢复数据,具体运作机制可参考:应用接续概述中的运作机制章节。

实现方案

  • 启用应用接续能力

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

    收起
    自动换行
    深色代码主题
    复制
    1. {
    2. "module": {
    3. // ...
    4. "abilities": [
    5. {
    6. // ...
    7. "continuable": true,
    8. // ...
    9. }
    10. ],
    11. // ...
    12. }
    13. }
  • 基础数据&文件资产迁移

    对于图片、视频、文档等文件类数据,可以转化成ArrayBuffer类型,保存在分布式文件目录下。

    收起
    自动换行
    深色代码主题
    复制
    1. export function writeDistributedFile(context: common.UIAbilityContext, displayName: string, mediaType: MediaType,
    2. buf?: ArrayBuffer, uri?: string): void {
    3. // The asset is written to the distributed file directory.
    4. // Obtain the distributed file directory path.
    5. let distributedDir: string = context.distributedFilesDir;
    6. let fileName: string = '/' + displayName;
    7. let filePath: string = distributedDir + fileName;
    8. let file: fileIo.File | undefined = undefined;
    9. let srcFile: fileIo.File | undefined = undefined;
    10. try {
    11. // Create a file in a distributed directory.
    12. file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    13. hilog.info(DOMAIN, TAG, FORMAT, 'Create file success.');
    14. if (mediaType === MediaType.MEDIA_IMAGE && buf) {
    15. // Write content to a file (if the asset is a picture, the picture can be converted to a buffer to write)
    16. fileIo.writeSync(file.fd, buf);
    17. } else if (mediaType === MediaType.MEDIA_VIDEO && uri) {
    18. srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
    19. fileIo.copyFileSync(srcFile.fd, file.fd);
    20. }
    21. } catch (error) {
    22. let err: BusinessError = error as BusinessError;
    23. hilog.info(DOMAIN, TAG, FORMAT,
    24. `Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
    25. } finally {
    26. // closed file.
    27. if (file) {
    28. fileIo.closeSync(file.fd);
    29. }
    30. if (srcFile) {
    31. fileIo.closeSync(srcFile.fd);
    32. }
    33. }
    34. }

    使用分布式数据对象时,需要在本端onContinue()接口中进行数据保存。在本端UIAbility的onContinue()接口中,创建分布式数据对象并保存数据,执行流程如下:

    1. 在onContinue()接口中使用create()接口创建分布式数据对象,将所要迁移的数据填充到分布式数据对象数据中。
    2. 如果有图片、视频、文档等文件类数据要迁移,需要先将其转换为资产commonType.Asset类型,再封装到分布式数据对象中进行迁移。
    3. 调用genSessionId()接口生成数据对象组网id,并使用该id调用setSessionId()加入组网,激活分布式数据对象。
    4. 使用save()接口将已激活的分布式数据对象持久化,确保本端退出后远端依然可以获取到数据。
    5. 将生成的sessionId通过want传递到远端,供远端激活同步使用。
    收起
    自动换行
    深色代码主题
    复制
    1. async onContinue(wantParam: Record<string, Object | undefined>): Promise<AbilityConstant.OnContinueResult> {
    2. wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] = false;
    3. wantParam.mediaUriArray = JSON.stringify(AppStorage.get<Array<MediaInfo>>('mediaUriArray'));
    4. try {
    5. // Generate the session ID of the distributed data object.
    6. let sessionId: string = distributedDataObject.genSessionId();
    7. wantParam.distributedSessionId = sessionId;
    8. let currNavPathNames: string[] = AppStorage.get<NavPathStack>('pageInfos')?.getAllPathName() as string[];
    9. wantParam.currContinuePageUrl = currNavPathNames[currNavPathNames?.length - 1];
    10. let mediaUriArray = AppStorage.get<Array<MediaInfo>>('mediaUriArray');
    11. let assets: commonType.Assets = [];
    12. if (mediaUriArray) {
    13. for (let i = 0; i < mediaUriArray.length; i++) {
    14. let append = mediaUriArray[i];
    15. let attachment: commonType.Asset = getAssetInfo(this.context, append);
    16. assets.push(attachment);
    17. }
    18. }
    19. let contentInfo: ContentInfo = new ContentInfo(
    20. AppStorage.get('mainTitle'),
    21. AppStorage.get('textContent'),
    22. AppStorage.get('mediaUriArray'),
    23. AppStorage.get('isShowLocalInfo'),
    24. AppStorage.get('isAddLocalInfo'),
    25. AppStorage.get('selectLocalInfo'),
    26. assets
    27. );
    28. let source = contentInfo.flatAssets();
    29. this.distributedObject = distributedDataObject.create(this.context, source);
    30. this.distributedObject.setSessionId(sessionId).catch((err: BusinessError) => {
    31. hilog.info(DOMAIN, TAG, FORMAT, `SetSessionId failed. Cause code: ${err.code}, message: ${err.message}`);
    32. });
    33. await this.distributedObject.save(wantParam.targetDevice as string).catch((err: BusinessError) => {
    34. hilog.info(DOMAIN, TAG, FORMAT, `Failed to save. Code: ${err.code}, message: ${err.message}`);
    35. });
    36. } catch (error) {
    37. hilog.error(DOMAIN, TAG, FORMAT, 'distributedDataObject failed', `code ${(error as BusinessError).code}`);
    38. }
    39. return AbilityConstant.OnContinueResult.AGREE;
    40. }
  • 基础数据&文件资产恢复

    在远端设备UIAbility的onCreate()/onNewWant()中调用restoreDistributedObject()方法,通过加入与本端一致的分布式数据对象组网进行数据恢复,执行流程如下:

    1. 在restoreDistributedObject()方法中创建空的分布式数据对象,用于接收恢复的数据。
    2. 从want中读取分布式数据对象组网id。
    3. 注册on()接口监听数据变更。在收到status为restore的事件的回调中,实现数据恢复完毕时需要进行的业务操作。由于恢复的数据中有图片/视频文件,调用fileCopy()方法从分布式文件中读取ArrayBuffer,然后把ArrayBuffer转化成对应类型数据进行存储。
    4. 调用setSessionId()加入组网,激活分布式数据对象。
    5. 打开远端设备,点击接续图标,应用打开,页面数据恢复。
    收起
    自动换行
    深色代码主题
    复制
    1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    2. hilog.info(DOMAIN, TAG, FORMAT, 'Ability onCreate');
    3. this.restoreDistributedObject(want, launchParam);
    4. // ...
    5. }
    6. onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    7. hilog.info(DOMAIN, TAG, FORMAT, 'Ability onNewWant');
    8. this.restoreDistributedObject(want, launchParam);
    9. // ...
    10. }
    11. /*
    12. * The peer device receives data.
    13. * @param want
    14. * @param launchParam
    15. * @returns
    16. */
    17. async restoreDistributedObject(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    18. if (launchParam.launchReason !== AbilityConstant.LaunchReason.CONTINUATION) {
    19. return;
    20. }
    21. let mailInfo: ContentInfo = new ContentInfo(undefined, undefined, [], undefined, undefined, undefined, undefined);
    22. this.distributedObject = distributedDataObject.create(this.context, mailInfo);
    23. // Add a data restored listener.
    24. try {
    25. this.distributedObject.on('status',
    26. (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
    27. hilog.info(DOMAIN, TAG, FORMAT, `status changed, sessionId: ${sessionId}`);
    28. hilog.info(DOMAIN, TAG, FORMAT, `status changed, status: ${status}`);
    29. hilog.info(DOMAIN, TAG, FORMAT, `status changed, networkId: ${networkId}`);
    30. if (status === 'restored') {
    31. if (!this.distributedObject) {
    32. return;
    33. }
    34. AppStorage.setOrCreate('mainTitle', this.distributedObject['mainTitle']);
    35. AppStorage.setOrCreate('textContent', this.distributedObject['textContent']);
    36. AppStorage.setOrCreate('isShowLocalInfo', this.distributedObject['isShowLocalInfo']);
    37. AppStorage.setOrCreate('isAddLocalInfo', this.distributedObject['isAddLocalInfo']);
    38. AppStorage.setOrCreate('selectLocalInfo', this.distributedObject['selectLocalInfo']);
    39. AppStorage.setOrCreate('attachments', this.distributedObject['attachments']);
    40. let attachments = this.distributedObject['attachments'] as commonType.Assets;
    41. hilog.info(DOMAIN, TAG, FORMAT,
    42. `attachments: ${JSON.stringify(this.distributedObject['attachments'])}`);
    43. for (const attachment of attachments) {
    44. fileCopy(this.context, attachment, this.mediaUriArray);
    45. }
    46. AppStorage.setOrCreate<Array<MediaInfo>>('mediaUriArray', this.mediaUriArray);
    47. }
    48. });
    49. } catch (err) {
    50. hilog.error(DOMAIN, TAG, FORMAT,
    51. `On status failed. Cause code: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`);
    52. }
    53. let sessionId: string = want.parameters?.distributedSessionId as string;
    54. this.distributedObject.setSessionId(sessionId).catch((err: BusinessError) => {
    55. hilog.info(DOMAIN, TAG, FORMAT, `SetSessionId failed. Cause code: ${err.code}, message: ${err.message}`);
    56. });
    57. let currContinuePageUrl = want.parameters?.currContinuePageUrl as string;
    58. AppStorage.setOrCreate<string>(CommonConstants.CONTINUE_PAGE_URL, currContinuePageUrl);
    59. }

    接续过来的图片、视频,需要从分布式文件目录路径下读取所需的文件,经处理后,转化成需要的数据类型。

    收起
    自动换行
    深色代码主题
    复制
    1. /*
    2. * Copy distributed files.
    3. * @param attachmentRecord
    4. * @param key
    5. */
    6. export function fileCopy(context: common.UIAbilityContext, attachment: commonType.Asset,
    7. mediaUriArray: Array<MediaInfo>): void {
    8. if (canIUse('SystemCapability.DistributedDataManager.CommonType')) {
    9. let mediaName = attachment.name.substring(attachment.name.indexOf('_') + 1);
    10. let mediaType = attachment.name.substring(0, attachment.name.indexOf('_'));
    11. let filePath: string = context.distributedFilesDir + '/' + mediaName;
    12. let savePath: string = context.filesDir + '/' + mediaName;
    13. let file: fileIo.File | undefined = undefined;
    14. let saveFile: fileIo.File | undefined = undefined;
    15. let imageSourceApi: image.ImageSource | undefined;
    16. try {
    17. if (fileIo.accessSync(filePath)) {
    18. saveFile = fileIo.openSync(savePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    19. file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE);
    20. let buf: ArrayBuffer = new ArrayBuffer(Number(attachment.size));
    21. let readSize = 0;
    22. let readLen = fileIo.readSync(file.fd, buf, {
    23. offset: readSize
    24. });
    25. if (mediaType == MediaType.MEDIA_IMAGE) {
    26. let sourceOptions: image.SourceOptions = {
    27. sourceDensity: 120
    28. };
    29. imageSourceApi = image.createImageSource(buf, sourceOptions);
    30. mediaUriArray.push({
    31. imagePixelMap: imageSourceApi.createPixelMapSync(),
    32. mediaName: mediaName,
    33. mediaType: mediaType
    34. });
    35. } else if (mediaType == MediaType.MEDIA_VIDEO) {
    36. mediaUriArray.push({
    37. videoUri: attachment.uri,
    38. mediaName: mediaName,
    39. mediaType: mediaType
    40. })
    41. }
    42. while (readLen > 0) {
    43. readSize += readLen;
    44. fileIo.writeSync(saveFile.fd, buf);
    45. readLen = fileIo.readSync(file.fd, buf, {
    46. offset: readSize
    47. });
    48. }
    49. hilog.info(DOMAIN, TAG, FORMAT, `${attachment.name} synchronized successfully.`);
    50. }
    51. } catch (error) {
    52. let err: BusinessError = error as BusinessError;
    53. hilog.error(DOMAIN, TAG, FORMAT, `${attachment.name} fileCopy failed with err: ${JSON.stringify(err)}`);
    54. } finally {
    55. if (file) {
    56. fileIo.closeSync(file.fd);
    57. }
    58. if (saveFile) {
    59. fileIo.closeSync(saveFile.fd);
    60. }
    61. if (imageSourceApi) {
    62. imageSourceApi.release();
    63. imageSourceApi = undefined;
    64. }
    65. }
    66. }
    67. }

碰一碰插入图片

场景描述

在多设备协同场景中,用户可以通过碰一碰将手机端的图片快速传输到电脑端。例如在手机端浏览图库中的图片,然后碰一碰将图片插入到电脑端的应用中。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:手机与电脑碰一碰分享概述中的使用约束章节。

实现原理

场景分析

  1. 用户在手机设备上浏览图库中的图片,选中想插入的图片。
  2. 基于图库的碰一碰分享功能,手机顶端轻触电脑屏幕拉起碰一碰分享界面后分享图片。
  3. 电脑端接收到分享的图片后,插入到应用页面中。

具体流程可参考:手机与电脑碰一碰分享概述中业务流程章节的“电脑设备作为数据接收端”部分。

关键技术

电脑端接收碰一碰插入图片的核心在于使用harmonyShare的数据接收功能:

  1. 注册数据接收监听:通过harmonyShare.on('dataReceive')注册数据接收监听。
  2. 设置接收能力:定义支持的数据类型和最大数量。
  3. 处理接收数据:在onDataReceived回调中处理接收到的文件数据。
  4. 转换为PixelMap:将接收到的图片数据转换为PixelMap用于显示。

实现方案

  1. 注册数据接收监听:在电脑端应用注册on('dataReceive')监听,接收手机端发送的数据。
    收起
    自动换行
    深色代码主题
    复制
    1. /**
    2. * Add dataReceive listening in 2in1 device type.
    3. */
    4. public dataReceiveListeningPC(uiContext: UIContext) {
    5. if (!canIUse('SystemCapability.Collaboration.HarmonyShare')) {
    6. return;
    7. }
    8. window.getLastWindow(this.context).then(((data) => {
    9. try {
    10. let mainWindowID: number = data.getWindowProperties().id;
    11. harmonyShare.on('dataReceive', {
    12. windowId: mainWindowID, capabilities: [
    13. {
    14. 'utd': uniformTypeDescriptor.UniformDataType.MEDIA,
    15. 'maxSupportedCount': 5
    16. },
    17. {
    18. 'utd': uniformTypeDescriptor.UniformDataType.FILE,
    19. 'maxSupportedCount': 5
    20. }
    21. ]
    22. },
    23. (receiveTarget: harmonyShare.ReceivableTarget) => {
    24. if (!this.context) {
    25. return;
    26. }
    27. // Process the received file data.
    28. receiveTarget.receive(fileUri.getUriFromPath(this.context.filesDir), {
    29. onDataReceived: (shareData: systemShare.SharedData) => {
    30. // ...
    31. },
    32. onResult(resultCode: harmonyShare.ShareResultCode) {
    33. if (resultCode === harmonyShare.ShareResultCode.SHARE_SUCCESS) {
    34. hilog.info(DOMAIN, TAG, FORMAT, 'receive file success');
    35. } else {
    36. hilog.error(DOMAIN, TAG, FORMAT, 'receive failed ' + resultCode);
    37. }
    38. }
    39. });
    40. });
    41. } catch (error) {
    42. hilog.error(DOMAIN, TAG, FORMAT, `getWindowProperties error ${error.code} ${error.message}`);
    43. }
    44. })).catch((error: BusinessError) => {
    45. hilog.error(DOMAIN, TAG, FORMAT, `failed to obtain the window. cause ${error.code} ${error.message}`);
    46. });
    47. }
  2. 处理接收到的图片数据:自定义实现ReceiveCallback()回调函数,在该回调函数中处理接收的数据。例如此处将接收到的图片数据转换为PixelMap,用于在应用中显示。
    收起
    自动换行
    深色代码主题
    复制
    1. let shareRecords = shareData.getRecords();
    2. let mediaUriArray = AppStorage.get('mediaUriArray') as Array<MediaInfo>;
    3. shareRecords.forEach(async (record: systemShare.SharedRecord) => {
    4. if (!record.uri) {
    5. return;
    6. }
    7. let fileName = record.uri.split('/').pop()?.split('.')[0];
    8. if (record.uri.endsWith('png') || record.uri.endsWith('jpg') || record.uri.endsWith('jpeg')) {
    9. let imageUri = record.uri;
    10. let file = fileIo.openSync(imageUri, fileIo.OpenMode.READ_ONLY);
    11. let imageSourceApi: image.ImageSource | undefined = undefined;
    12. try {
    13. imageSourceApi = image.createImageSource(file.fd);
    14. } catch (err) {
    15. hilog.error(DOMAIN, TAG, FORMAT,
    16. `CreateImageSource error: ${(err as BusinessError).code} ${(err as BusinessError).message}`);
    17. } finally {
    18. fileIo.closeSync(file.fd);
    19. }
    20. const imagePackApi = image.createImagePacker();
    21. let packOpts: image.PackingOption = { format: 'image/png', quality: 100 };
    22. imagePackApi.packToData(imageSourceApi!, packOpts).then(async readBuffer => {
    23. let imageSource: image.ImageSource = image.createImageSource(readBuffer as ArrayBuffer);
    24. let option: image.InitializationOptions = {
    25. pixelFormat: 3,
    26. size: {
    27. width: 100,
    28. height: 100
    29. }
    30. };
    31. imageSource.createPixelMap(option).then((pixelMap: PixelMap) => {
    32. if (mediaUriArray.length < CommonConstants.MAX_ADD_MEDIA_NUM) {
    33. mediaUriArray.push({
    34. imagePixelMap: pixelMap,
    35. mediaName: fileName,
    36. mediaType: MediaType.MEDIA_IMAGE
    37. });
    38. } else {
    39. try {
    40. uiContext.getPromptAction().showToast({ message: $r('app.string.add_picture_prompt') });
    41. } catch (err) {
    42. hilog.error(DOMAIN, TAG, FORMAT,
    43. `ShowToast failed. Cause code: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`);
    44. }
    45. }
    46. imageSource.release();
    47. imageSource = undefined;
    48. imageSourceApi?.release();
    49. imageSourceApi = undefined;
    50. });
    51. }).catch((err: BusinessError) => {
    52. hilog.error(DOMAIN, TAG, FORMAT,
    53. `PackToData failed. Cause code: ${err.code}, message: ${err.message}`);
    54. });
    55. }
    56. });
  3. 取消数据接收监听:离开可分享页面(包括应用退至后台等场景)时,取消隔空传送分享监听事件,释放资源。
    收起
    自动换行
    深色代码主题
    复制
    1. /**
    2. * Remove dataReceive listening.
    3. */
    4. public dataReceiveDisableListeningPC() {
    5. if (!canIUse('SystemCapability.Collaboration.HarmonyShare')) {
    6. return;
    7. }
    8. window.getLastWindow(this.context).then(((data) => {
    9. try {
    10. let mainWindowID: number = data.getWindowProperties().id;
    11. harmonyShare.off('dataReceive', {
    12. windowId: mainWindowID, capabilities: [
    13. {
    14. 'utd': uniformTypeDescriptor.UniformDataType.MEDIA,
    15. 'maxSupportedCount': 5
    16. },
    17. {
    18. 'utd': uniformTypeDescriptor.UniformDataType.FILE,
    19. 'maxSupportedCount': 5
    20. }
    21. ]
    22. });
    23. } catch (error) {
    24. hilog.error(DOMAIN, TAG, FORMAT, `getWindowProperties error ${error.code} ${error.message}`);
    25. }
    26. })).catch((error: BusinessError) => {
    27. hilog.error(DOMAIN, TAG, FORMAT, `failed to obtain the window. cause ${error.code} ${error.message}`);
    28. });
    29. }

碰一碰分享页面

场景描述

在多设备协同场景中,用户可以通过碰一碰将当前页面分享到另一台设备。例如在手机端浏览到感兴趣的内容页面,然后碰一碰将页面分享到附近的设备打开。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:

实现原理

场景分析

1. 用户在本端打开应用,点进待分享的浏览详情页。

2. 将本端设备与远端设备进行碰一碰操作。本端设备触发碰一碰分享面板,上滑分享。

3.远端设备弹出接收面板,点击接收可以直达应用分享页面。若点击拒绝,则本端设备上会显示分享被拒绝的提示弹框。

关键技术

碰一碰分享页面的核心在于支持通过碰一碰跨端分享链接。详情可参考碰一碰链接分享。

实现方案

1. 配置App Linking服务。为了通过碰一碰分享实现直达应用的功能,应用需集成App Linking,以确保端到端体验的完整性。具体配置方式可参考配置App Linking服务章节。

收起
自动换行
深色代码主题
复制
  1. {
  2. "module": {
  3. // ...
  4. "abilities": [
  5. {
  6. "name": "EntryAbility",
  7. // ...
  8. "exported": true,
  9. "skills": [
  10. {
  11. "entities": [
  12. "entity.system.home",
  13. // entities must contain "entity.system.browsable"
  14. "entity.system.browsable"
  15. ],
  16. "actions": [
  17. "action.system.home",
  18. // Actions must contain "ohos.want.action.viewData"
  19. "ohos.want.action.viewData"
  20. ],
  21. "uris": [
  22. {
  23. // The scheme must be configured as https
  24. "scheme": "https",
  25. // The host must be configured as the associated domain name
  26. "host": "www.example.com",
  27. "path": ""
  28. }
  29. ],
  30. // domainVerify must be set to true
  31. "domainVerify": true
  32. }
  33. ],
  34. // ...
  35. }
  36. ],
  37. // ...
  38. }
  39. }

2. 注册碰一碰事件的监听on('knockShare')和取消监听off('knockShare')。

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Add knock listening.
  3. */
  4. public immersiveListening() {
  5. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  6. harmonyShare.on('knockShare', (target: harmonyShare.SharableTarget) => {
  7. this.immersiveCallback(target);
  8. });
  9. }
  10. }
  11. /**
  12. * Add knock listening in 2in1 device type.
  13. */
  14. public immersiveListeningPC() {
  15. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  16. window.getLastWindow(this.context).then((data) => {
  17. try {
  18. let mainWindowID: number = data.getWindowProperties().id;
  19. harmonyShare.on('knockShare', { windowId: mainWindowID }, (target: harmonyShare.SharableTarget) => {
  20. this.immersiveCallback(target);
  21. });
  22. } catch (err) {
  23. hilog.error(DOMAIN, TAG, FORMAT,
  24. `GetWindowProperties error: ${(err as BusinessError).code} ${(err as BusinessError).message}`);
  25. }
  26. }).catch((error: BusinessError) => {
  27. hilog.error(DOMAIN, TAG, FORMAT, `GetLastWindow failed. Error: ${error.code} ${error.message}`);
  28. });
  29. }
  30. }
  31. /**
  32. * remove knock listening.
  33. */
  34. public immersiveDisableListening() {
  35. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  36. harmonyShare.off('knockShare');
  37. }
  38. }
  39. /**
  40. * remove knock listening.
  41. */
  42. public immersiveDisableListeningPC() {
  43. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  44. window.getLastWindow(this.context).then((data) => {
  45. try {
  46. let mainWindowID: number = data.getWindowProperties().id;
  47. harmonyShare.off('knockShare', { windowId: mainWindowID });
  48. } catch (err) {
  49. hilog.error(DOMAIN, TAG, FORMAT,
  50. `GetWindowProperties error ${(err as BusinessError).code} ${(err as BusinessError).message}`);
  51. }
  52. }).catch((error: BusinessError) => {
  53. hilog.error(DOMAIN, TAG, FORMAT, `GetLastWindow failed. Error: ${error.code} ${error.message}`);
  54. });
  55. }
  56. }

3. 本端构建分享数据并发起分享。构造分享数据记录ShareRecord,并设置其utd属性为uniformTypeDescriptor.UniformDataType.HYPERLINK(属性含义可参考UniformDataType),content属性则设置为对应的页面链接。构造完成后,调用sharableTarget的share()方法发起分享。

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Knock listening callback.
  3. *
  4. * @param target After the Huawei Share event is triggered,
  5. * you can call back the parameters and share them across devices.
  6. */
  7. public immersiveCallback(target: harmonyShare.SharableTarget) {
  8. let shareData: systemShare.SharedData = this.getLinkShareData();
  9. target.share(shareData);
  10. }
  11. /**
  12. * Get link share data.
  13. * @returns systemShare.SharedData.
  14. */
  15. getLinkShareData(): systemShare.SharedData {
  16. let encodedParams = encodeURIComponent(JSON.stringify(this.pageParamsData));
  17. let title: string = CommonConstants.STRING_DEFAULT_VALUE;
  18. try {
  19. title = this.context!.resourceManager.getStringSync(this.pageParamsData?.userName.id);
  20. } catch (err) {
  21. hilog.error(DOMAIN, TAG, FORMAT,
  22. `resourceManager getStringSync error: ${(err as BusinessError).code} ${(err as BusinessError).message}`);
  23. }
  24. // share app linking.
  25. let shareData: systemShare.SharedData = new systemShare.SharedData({
  26. // Set the shared data type to Link.
  27. utd: uniformTypeDescriptor.UniformDataType.HYPERLINK,
  28. // title
  29. title: title,
  30. // description
  31. description: this.pageParamsData?.title,
  32. // The shared App Linking link is replaced with the real address here.
  33. content: `${CommonConstants.SHARE_URL_SCHEME}://www.example.com?pageUrl=${this.pageUrl}&pageParams=${encodedParams}&sharingMechanism=knockShare`,
  34. });
  35. return shareData;
  36. }

4.(对端已安装应用)对端在onCreate()与onNewWant()生命周期中获取链接中目标页面及传参等信息,供后续跳转使用。

收起
自动换行
深色代码主题
复制
  1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  2. // ...
  3. let pageUrl = getValueFromWant(want, 'pageUrl');
  4. let pageParams = getValueFromWant(want, 'pageParams');
  5. let shareMechanism = getValueFromWant(want, 'sharingMechanism');
  6. if (pageUrl !== '') {
  7. if (shareMechanism === 'gesturesShare') {
  8. // ...
  9. } else if (shareMechanism === 'knockShare') {
  10. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_SHARE_PAGE_URL, pageUrl);
  11. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_SHARE_PAGE_PARAMS, pageParams);
  12. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_IS_SHARE_LINK, true);
  13. }
  14. }
  15. hilog.info(DOMAIN, TAG, FORMAT, `onCreate() pageUrl: ${pageUrl} pageParams:${pageParams}`);
  16. }
  17. onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  18. // ...
  19. let pageUrl = getValueFromWant(want, 'pageUrl');
  20. let pageParams = getValueFromWant(want, 'pageParams');
  21. let shareMechanism = getValueFromWant(want, 'sharingMechanism');
  22. if (pageUrl !== '') {
  23. if (shareMechanism === 'gesturesShare') {
  24. // ...
  25. } else if (shareMechanism === 'knockShare') {
  26. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_SHARE_PAGE_URL, pageUrl);
  27. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_SHARE_PAGE_PARAMS, pageParams);
  28. AppStorage.setOrCreate(CommonConstants.KNOCK_SHARE_IS_SHARE_LINK, true);
  29. }
  30. }
  31. hilog.info(DOMAIN, TAG, FORMAT, `onNewWant() pageUrl: ${pageUrl} pageParams:${pageParams}`);
  32. }

可以将解析逻辑统一封装到工具方法,方便后续调用。

收起
自动换行
深色代码主题
复制
  1. export function getValueFromWant(want: Want, key: string): string {
  2. let value: string = '';
  3. try {
  4. let uri = want?.uri;
  5. if (!uri) {
  6. hilog.error(DOMAIN, TAG, FORMAT, `Failed to getValueFromWant, uri is undefined.`);
  7. return value;
  8. }
  9. // Parse the parameters to obtain the app linking
  10. let urlObject = url.URL.parseURL(want?.uri);
  11. if (key !== 'pageParams') {
  12. value = urlObject.params.get(key) as string;
  13. return value;
  14. }
  15. let encodedData = urlObject.params.get(key);
  16. if (encodedData) {
  17. value = decodeURIComponent(encodedData);
  18. }
  19. return value;
  20. } catch (err) {
  21. let error = err as BusinessError;
  22. hilog.error(DOMAIN, TAG, FORMAT, `Failed to getValueFromWant, error: ${error.code} ${error.message}`);
  23. }
  24. return value;
  25. }

隔空传送分享页面

场景描述

在多设备协同场景中,用户可以通过隔空手势将当前页面分享到另一台设备。例如在手机端浏览内容,通过“一抓一放”手势将页面分享到附近的设备。

说明

该功能的使用需满足设备限制和使用限制,具体约束与限制可参考:使用限制。

实现原理

场景分析

1. 用户在本端打开应用,点进待分享的浏览详情页。

2. 在本端设备执行隔空传送“抓”手势后,在远端设备执行隔空传送“放”手势。

3. 远端设备识别成功后,可以直达应用分享页面。

关键技术

隔空传送基于分享服务,应用通过监听harmonyShare.on('gesturesShare')事件,支持用户通过简单的“一抓一放”手势实现跨端分享。详情可参考关键技术。

实现方案

1.配置App Linking服务。为了通过隔空传送分享实现直达应用的功能,应用需集成App Linking,以确保端到端体验的完整性。具体配置方式可参考配置App Linking服务章节。

收起
自动换行
深色代码主题
复制
  1. {
  2. "module": {
  3. // ...
  4. "abilities": [
  5. {
  6. "name": "EntryAbility",
  7. // ...
  8. "exported": true,
  9. "skills": [
  10. {
  11. "entities": [
  12. "entity.system.home",
  13. // entities must contain "entity.system.browsable"
  14. "entity.system.browsable"
  15. ],
  16. "actions": [
  17. "action.system.home",
  18. // Actions must contain "ohos.want.action.viewData"
  19. "ohos.want.action.viewData"
  20. ],
  21. "uris": [
  22. {
  23. // The scheme must be configured as https
  24. "scheme": "https",
  25. // The host must be configured as the associated domain name
  26. "host": "www.example.com",
  27. "path": ""
  28. }
  29. ],
  30. // domainVerify must be set to true
  31. "domainVerify": true
  32. }
  33. ],
  34. // ...
  35. }
  36. ],
  37. // ...
  38. }
  39. }

2.注册隔空传送事件的监听on('gesturesShare')和取消监听off('gesturesShare')。

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Add gesturesShare listening.
  3. */
  4. public immersiveListening(shareType: ShareType) {
  5. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  6. window.getLastWindow(this.context).then((data) => {
  7. try {
  8. let mainWindowID: number = data.getWindowProperties().id;
  9. harmonyShare.on('gesturesShare', { windowId: mainWindowID }, (target: harmonyShare.SharableTarget) => {
  10. this.immersiveCallback(target, shareType);
  11. });
  12. } catch (err) {
  13. let error = err as BusinessError;
  14. hilog.error(DOMAIN, TAG, FORMAT, `getWindowProperties error ${error.code} ${error.message}`);
  15. }
  16. }).catch((error: BusinessError) => {
  17. hilog.error(DOMAIN, TAG, FORMAT, `immersiveListening error ${error.code} ${error.message}`);
  18. })
  19. }
  20. }
  21. /**
  22. * Remove gesturesShare listening.
  23. */
  24. public immersiveDisableListening() {
  25. if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
  26. window.getLastWindow(this.context).then((data) => {
  27. try {
  28. let mainWindowID: number = data.getWindowProperties().id;
  29. harmonyShare.off('gesturesShare', { windowId: mainWindowID });
  30. } catch (error) {
  31. let err = error as BusinessError;
  32. hilog.error(DOMAIN, TAG, FORMAT, `getWindowProperties error ${err.code} ${err.message}`);
  33. }
  34. }).catch((error: BusinessError) => {
  35. hilog.error(DOMAIN, TAG, FORMAT, `immersiveDisableListening error ${error.code} ${error.message}`);
  36. })
  37. }
  38. }

3.本端构建分享数据并发起分享。构造分享数据记录ShareRecord,并设置其utd属性为uniformTypeDescriptor.UniformDataType.HYPERLINK,content属性则设置为对应的页面链接。构造完成后,调用sharableTarget的share()方法发起分享。

收起
自动换行
深色代码主题
复制
  1. public immersiveCallback(target: harmonyShare.SharableTarget, shareType: ShareType) {
  2. if (shareType === ShareType.LINK_SHARE) {
  3. let shareData: systemShare.SharedData = this.getLinkShareData();
  4. target.share(shareData);
  5. }
  6. }
  7. /**
  8. * Get link share data.
  9. * @returns systemShare.SharedData.
  10. */
  11. getLinkShareData(): systemShare.SharedData {
  12. let encodedParams = encodeURIComponent(JSON.stringify(this.pageParamsData));
  13. let title: string = CommonConstants.STRING_DEFAULT_VALUE;
  14. try {
  15. title = this.context!.resourceManager.getStringSync(this.pageParamsData?.userName.id);
  16. } catch (err) {
  17. hilog.error(DOMAIN, TAG, FORMAT,
  18. `resourceManager getStringSync error: ${(err as BusinessError).code} ${(err as BusinessError).message}`);
  19. }
  20. // share app linking.
  21. let shareData: systemShare.SharedData = new systemShare.SharedData({
  22. // Set the shared data type to Link.
  23. utd: uniformTypeDescriptor.UniformDataType.HYPERLINK,
  24. // title
  25. title: title,
  26. // description
  27. description: this.pageParamsData?.title,
  28. // The shared App Linking link is replaced with the real address here.
  29. content: `${CommonConstants.SHARE_URL_SCHEME}://www.example.com?pageUrl=${this.pageUrl}&pageParams=${encodedParams}&sharingMechanism=gesturesShare`,
  30. });
  31. return shareData;
  32. }

4.(对端已安装应用)对端在onCreate()与onNewWant()生命周期中获取链接中目标页面及传参等信息,供后续跳转使用。

收起
自动换行
深色代码主题
复制
  1. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  2. // ...
  3. let pageUrl = getValueFromWant(want, 'pageUrl');
  4. let pageParams = getValueFromWant(want, 'pageParams');
  5. let shareMechanism = getValueFromWant(want, 'sharingMechanism');
  6. if (pageUrl !== '') {
  7. if (shareMechanism === 'gesturesShare') {
  8. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_SHARE_PAGE_URL, pageUrl);
  9. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_SHARE_PAGE_PARAMS, pageParams);
  10. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_IS_SHARE_LINK, true);
  11. } else if (shareMechanism === 'knockShare') {
  12. // ...
  13. }
  14. }
  15. hilog.info(DOMAIN, TAG, FORMAT, `onCreate() pageUrl: ${pageUrl} pageParams:${pageParams}`);
  16. }
  17. onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  18. // ...
  19. let pageUrl = getValueFromWant(want, 'pageUrl');
  20. let pageParams = getValueFromWant(want, 'pageParams');
  21. let shareMechanism = getValueFromWant(want, 'sharingMechanism');
  22. if (pageUrl !== '') {
  23. if (shareMechanism === 'gesturesShare') {
  24. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_SHARE_PAGE_URL, pageUrl);
  25. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_SHARE_PAGE_PARAMS, pageParams);
  26. AppStorage.setOrCreate(CommonConstants.GESTURES_SHARE_IS_SHARE_LINK, true);
  27. } else if (shareMechanism === 'knockShare') {
  28. // ...
  29. }
  30. }
  31. hilog.info(DOMAIN, TAG, FORMAT, `onNewWant() pageUrl: ${pageUrl} pageParams:${pageParams}`);
  32. }

可以将解析逻辑统一封装到工具方法,方便后续调用。

收起
自动换行
深色代码主题
复制
  1. export function getValueFromWant(want: Want, key: string): string {
  2. let value: string = '';
  3. try {
  4. let uri = want?.uri;
  5. if (!uri) {
  6. hilog.error(DOMAIN, TAG, FORMAT, `Failed to getValueFromWant, uri is undefined.`);
  7. return value;
  8. }
  9. // Parse the parameters to obtain the app linking
  10. let urlObject = url.URL.parseURL(want?.uri);
  11. if (key !== 'pageParams') {
  12. value = urlObject.params.get(key) as string;
  13. return value;
  14. }
  15. let encodedData = urlObject.params.get(key);
  16. if (encodedData) {
  17. value = decodeURIComponent(encodedData);
  18. }
  19. return value;
  20. } catch (err) {
  21. let error = err as BusinessError;
  22. hilog.error(DOMAIN, TAG, FORMAT, `Failed to getValueFromWant, error: ${error.code} ${error.message}`);
  23. }
  24. return value;
  25. }