HarmonyOS 鸿蒙Next 应用间信息分享

发布于 1周前 作者 yuanlaile 最后一次编辑是 5天前 来自 鸿蒙OS

概述

本文主要针对应用分享,如何使用Share Kit完成跨应用的内容分享(文本、图片、视频、链接等)。

概念:

  • 宿主应用:要分享数据的应用
  • 目标应用:要接受数据的应用

场景示例

  • 相册向其他应用分享图片(图片资源)
  • 相册分享视频到视频编辑软件内进行剪辑
  • 相册分享图片到微信朋友圈,发朋友圈
  • 选择文本后分享到其他应用,例如笔记,待办等。

宿主应用

传递示例大杂烩(包括图片、文本、App Linking、视频)

关键问题:如何定制分享内容,如何拉起分享面板。

示例代码目录结构参考:

img

这里面要注意,其实真正跨进程传输时的数据是不大的,设计的就是封装的数据,里面核心的就是文本和图片,所以在传递这些的时候是要考虑200KB上限的,自己在实操过程中遇到了视频需要传预览图,预览图过大,导致报错的,不过有错误码是可以很快定位问题。

核心逻辑:构造分享的数据包,然后拉起分享面板。

难点

  1. 部分数据需要写到目录中以下以应用沙箱目录为例,其他如公共目录,需要重写writeToSandBox方法。
  2. 每一类数据如何去进行构造,例如文本就比较简单,但是视频就比较复杂还有预览图、写文件等等。需要有示例参考,对开发者只需要替换部分代码即可。

注意:以下代码执行前记得往rawfile中放入test.jpg、startIcon.png(用应用默认创建时base/media就可以)、video.mp4。不然会报错。

import { common } from '@kit.AbilityKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { harmonyShare, systemShare } from '@kit.ShareKit';
import promptAction from '@ohos.promptAction';

interface ButtonFunctions {
  title: string
  callback: () => void;
}

@Entry
@Component
struct Index {
  private btns: ButtonFunctions[] = [
    { title: '分享链接', callback: this.shareAppLinking },
    { title: '分享文本', callback: this.shareText },
    { title: '分享图片', callback: this.shareImages },
    { title: '分享视频', callback: this.shareVideos },
  ];

  writeToSandBox(resources: string[]) {
    const context = getContext(this);
    const rm = context.resourceManager;
    for (const resource of resources) {
      rm.getRawFileContent(resource, (_, value) => {
        let myBuffer: ArrayBufferLike = value.buffer;
        let filePath = context.filesDir + `/${resource}`;
        let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
        let writeLen = fileIo.writeSync(file.fd, myBuffer);
        console.log(`Successful write ${resource} to SandBox.`)
        fileIo.closeSync(file);
      })
    }
  }

  aboutToAppear(): void {
    const resources = ['startIcon.png', 'test.jpg', 'video.mp4'];
    this.writeToSandBox(resources);
  }

  // 分享文本内容
  shareText() {
    // 构造ShareData,需配置一条有效数据信息
    let shareData: systemShare.SharedData = new systemShare.SharedData({
      utd: utd.UniformDataType.TEXT,
      content: '这是一段文本内容',
      title: '文本内容', // 不传title字段时,显示content
      description: '文本描述',
      // thumbnail: new Uint8Array() // 推荐传入适合的缩略图 不传则显示默认text图标
    });
    shareData.addRecord({
      utd: utd.UniformDataType.TEXT,
      content: '这是一段文本内容',
      title: '文本内容', // 不传title字段时,显示content
      description: '文本描述',
    });
    let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
    let context = getContext(this) as common.UIAbilityContext;
    controller.show(context, {
      selectionMode: systemShare.SelectionMode.SINGLE,
      previewMode: systemShare.SharePreviewMode.DETAIL,
    }).then(() => {
      console.info('ShareController show success.');
    }).catch((error: BusinessError) => {
      console.error(`ShareController shows error. code: ${error.code}, message: ${error.message}`);
    });
  }

  shareImages() {
    // 构造ShareData,需配置一条有效数据信息
    const contextFaker: Context = getContext(this);
    const fakerPath = contextFaker.filesDir;
    // 写文件
    let filePath = fakerPath + '/test.jpg'; // 仅为示例 请替换正确的文件路径
    // 获取精准的utd类型
    let utdTypeId = utd.getUniformDataTypeByFilenameExtension('.jpg', utd.UniformDataType.IMAGE);
    let shareData: systemShare.SharedData = new systemShare.SharedData({
      utd: utdTypeId,
      uri: fileUri.getUriFromPath(filePath),
      title: '图片标题', // 不传title字段时,显示图片文件名
      description: '图片描述', // 不传description字段时,显示图片大小
      // thumbnail: new Uint8Array() // 优先使用传递的缩略图预览  不传则默认使用原图做预览图
    });
    shareData.addRecord({
      utd: utdTypeId,
      uri: fileUri.getUriFromPath(filePath),
      title: '图片标题', // 不传title字段时,显示图片文件名
      description: '图片描述', // 不传description字段时,显示图片大小
    });
    // 进行分享面板显示
    let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
    let context = getContext(this) as common.UIAbilityContext;
    controller.show(context, {
      selectionMode: systemShare.SelectionMode.SINGLE, //是从record里面选一个还是全部发出去
      previewMode: systemShare.SharePreviewMode.DETAIL,
    }).then(() => {
      console.info('ShareController show success.');
    }).catch((error: BusinessError) => {
      console.error(`ShareController shows error. code: ${error.code}, message: ${error.message}`);
    });
  }

  async shareVideos() {
    try {
      // 生成视频封面图
      const contextFaker: Context = getContext(this);
      let thumbnailPath = contextFaker.filesDir + '/test.jpg'; // 仅为示例 请替换正确的文件路径
      const imageSourceApi: image.ImageSource = image.createImageSource(thumbnailPath);
      let opts: image.InitializationOptions = { size: { height: 6, width: 6 } }
      const pixelMap: image.PixelMap = await imageSourceApi.createPixelMap(opts);
      const imagePackerApi: image.ImagePacker = image.createImagePacker();
      const buffer: ArrayBuffer = await imagePackerApi.packing(pixelMap, {
        // 当前只支持'image/jpeg','image/webp'和'image/png'类型图片.
        format: 'image/jpeg',
        // JPEG编码中设定输出图片质量的参数,取值范围为0-100.
        // 建议适当压缩,图片过大无法拉起分享.
        quality: 10
      });
      // 构造ShareData,需配置一条有效数据信息
      let filePath = contextFaker.filesDir + '/video.mp4'; // 仅为示例 请替换正确的文件路径
      let shareData: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.VIDEO,
        uri: fileUri.getUriFromPath(filePath),
        title: '视频标题', // 不传title字段时,显示视频文件名
        description: '视频描述', // 不传description字段时,显示视频大小
        // thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
      });
      // 进行分享面板显示
      let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
      let context = getContext(this) as common.UIAbilityContext;
      controller.show(context, {
        selectionMode: systemShare.SelectionMode.SINGLE,
        previewMode: systemShare.SharePreviewMode.DETAIL,
      }).then(() => {
        console.info('ShareController show success.');
      }).catch((error: BusinessError) => {
        console.error(`ShareController shows error. code: ${error.code}, message: ${error.message}`);
      });
    } catch (e) {
      console.log(`error happended.${e.message}`)
    }
  }

  async shareAppLinking() {
    // 生成应用图标缩略图
    try {
      const contextFaker: Context = getContext(this);
      let thumbnailPath = contextFaker.filesDir + '/startIcon.png';
      const imageSourceApi: image.ImageSource = image.createImageSource(thumbnailPath);
      let opts: image.InitializationOptions = { size: { height: 6, width: 6 } }
      const pixelMap: image.PixelMap = await imageSourceApi.createPixelMap(opts);
      const imagePackerApi: image.ImagePacker = image.createImagePacker();
      const buffer: ArrayBuffer = await imagePackerApi.packing(pixelMap, {
        // 当前只支持'image/jpeg','image/webp'和'image/png'类型图片.
        format: 'image/jpeg',
        // JPEG编码中设定输出图片质量的参数,取值范围为0-100.
        // 建议适当压缩,图片过大无法拉起分享.
        quality: 30
      });
      // 构造ShareData,需配置一条有效数据信息
      let shareData: systemShare.SharedData = new systemShare.SharedData({
        utd: utd.UniformDataType.HYPERLINK,
        // App Linking链接 仅为示例
        content: 'https://appgallery.huawei.com/app/detail?id=com.huawei.hmsapp.books',
        title: '应用名称', // 不穿title时 显示链接
        description: '应用描述', // 不传则不显示描述内容
        thumbnail: new Uint8Array(buffer) // 推荐传入应用图标 不传则显示默认html图标
      });
      // 进行分享面板显示
      let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
      let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
      controller.show(context, {
        previewMode: systemShare.SharePreviewMode.DEFAULT,
        selectionMode: systemShare.SelectionMode.SINGLE
      }).then(() => {
        console.info('ShareController show success.');
      }).catch((error: BusinessError) => {
        console.error(`ShareController shows error. code: ${error.code}, message: ${error.message}`);
      });
    } catch (error) {
      console.error(`Something error happened. code: ${error.code}, message: ${error.message}`);
    }
  }

  build() {
    Column() {
      Text('应用间分享(宿主应用)')
        .fontColor('#E6000000')
        .fontSize(30)
        .fontWeight(700)
        .lineHeight(40)
        .margin({ top: 64 })
      List({ space: 12 }) {
        ForEach(this.btns, (btn: ButtonFunctions) => {
          ListItem() {
            Button(btn.title).onClick(btn.callback).width('100%')
          }
        }, (btn: ButtonFunctions) => btn.title)
      }
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({ left: 16, right: 16, bottom: 16 })
  }
}
复制

配置操作区

这个比较简单,在controller.show的时候配置一个excludedAbilities即可,参考宿主应用配置操作区

目标应用

两种处理方式

  • 方式一:分享面板点击应用后直接跳转到应用内。

  • 方式二

    :分享面板点击应用后先弹出二级菜单,在二级菜单中精细化用户需求后再跳转到应用内,以下为研究理解。看到效果就明白了其实就是在分享面板上再弹出了一个半模态框。

    • 例如从相册分享图片到抖音,有可能是为了发抖音,有可能是为了发给朋友
    • 同样的分享到微信有可能是发朋友圈,有可能是发给最近联系的朋友
    • 还有个场景是分享文本到任务里,会自动在二级菜单中有一个创建任务的面板,点完之后直接创建甚至都不需要进入应用。
    • 猜测用户想拿这个东西干什么,然后提供对应的操作面板,在操作面板细化功能分支后,直接完成或到应用中处理
    • 好处就是不需要脱离当前的应用

配置module.json5 && Ability中获取参数

方式一配置参考

如何告诉系统我可以处理哪一类的数据,例如我作为视频剪辑软件可以处理视频、图片等等,就需要在module.json5中进行配置。配置参考以下代码,scheme固定为file,utd参考UniformDataType,maxFileSupported为该类型支持的最大个数。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home",
              "ohos.want.action.sendData"
            ],
            "uris": [
              {
                "scheme": "file",
                "utd": "general.text",
                "maxFileSupported": 1
              },
              {
                "scheme": "file",
                "utd": "general.image",
                "maxFileSupported": 9
              },
              {
                "scheme": "file",
                "utd": "general.video",
                "maxFileSupported": 1
              }
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
      }
    ]
  }
}
复制

EntryAbility参考:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { systemShare } from '@kit.ShareKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
  handleParam(want: Want) {
    const records = AppStorage.get<systemShare.SharedRecord[]>('records') ?? [];
    // 获取分享的数据,可能有多条数据
    systemShare.getSharedData(want)
      .then((data: systemShare.SharedData) => {
        data.getRecords().forEach((record: systemShare.SharedRecord) => {
          records?.push(record);
        });
        AppStorage.setOrCreate('records', records);
      })
      .catch((error: BusinessError) => {
        console.error(`Failed to getSharedData. Code: ${error.code}, message: ${error.message}`);
      })
  }

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    this.handleParam(want);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleParam(want);
  }

  onDestroy(): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}
复制

方式二配置参考

以上是说明应用支持哪个文件类型,还有一个要配置的就是如果选用二级菜单,那么要调整skills的位置,以及要调整目录结构了。

img

module.json5参考下文,在extensionAbilities中新增一个TestShareAbility。abilities里还是可以正常配置的,而且别把skills删了,不然应用无法手动点击打开了。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home",
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
      },
      {
        "name": "TestShareAbility",
        "srcEntry": "./ets/abilities/TestShareAbility.ets",
        "type": "share",
        "exported": true,
        "label": "$string:EntryAbility_label",
        "icon": "$media:app_icon",
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
        "skills": [
          {
            "actions": [
              "ohos.want.action.sendData"
            ],
            // 目标应用在配置支持接收的数据类型时,需穷举支持的UTD,比如:支持全部图片类型,可声明:general.image
            // maxFileSupported 对于归属指定类型的文件,标识一次支持接收的最大数量。默认为0,代表不支持此类文件的分享。文件类型归属关系参考:@ohos.data.uniformTypeDescriptor (标准化数据定义与描述)
            "uris": [
              {
                "scheme": "file",
                "utd": "general.text",
                "maxFileSupported": 1
              },
              {
                "scheme": "file",
                "utd": "general.png",
                "maxFileSupported": 1
              },
              {
                "scheme": "file",
                "utd": "general.jpeg",
                "maxFileSupported": 1
              },
              {
                "scheme": "file",
                "utd": "general.video",
                "maxFileSupported": 1
              }
            ]
          }
        ]
      },
    ]
  }
}
复制

TestShareAbility参考

import { ShareExtensionAbility, UIExtensionContentSession, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { systemShare } from '@kit.ShareKit';

export default class TestShareAbility extends ShareExtensionAbility {
  onSessionCreate(want: Want, session: UIExtensionContentSession) {
    const records = AppStorage.get<systemShare.SharedRecord[]>('records') ?? [];
    systemShare.getSharedData(want)
      .then((data: systemShare.SharedData) => {
        // 处理分享的数据,可能有多条
        data.getRecords().forEach((record: systemShare.SharedRecord) => {
          records?.push(record);
        });
        AppStorage.setOrCreate('records', records);
        session.loadContent('pages/Second'); // 加载提供出去的页面内容,就是二级弹窗
      })
      .catch((error: BusinessError) => {
        console.error(`Failed to getSharedData. Code: ${error.code}, message: ${error.message}`);
        session.terminateSelf();
      })
  }
}
复制

经过上述操作,就将数据放到了AppStorage中的records里。

使用获取的数据

在页面中使用自定义的数据,具体实现需要业务方做,以下只作为参考,把所有传递过来的数据进行展示。

import { common } from '@kit.AbilityKit';
import { systemShare } from '@kit.ShareKit';

@Entry
@Component
struct Index {
  @StorageProp('records') records: systemShare.SharedRecord[] = [];

  build() {
    Scroll() {
      if (this.records.length > 0) {
        List({ space: 20 }) {
          ForEach(this.records, (record: systemShare.SharedRecord, index: number) => {
            ListItem() {
              Column() {
                if ('general.jpeg' === record?.utd) {
                  Image(record.uri).width('100%')
                } else if ('general.text' === record?.utd) {
                  Text(record.content)
                    .fontSize(50)
                    .fontWeight(FontWeight.Bold)
                } else if ('general.hyperlink' === record.utd) {
                  Text(record?.content)
                  Button('点击跳转').onClick(() => {
                    const context = getContext(this) as common.UIAbilityContext;
                    context.openLink(record?.content);
                  })
                } else if ('general.video' === record.utd) {
                  Video({
                    src: record.uri,
                  })
                    .width('80%')
                    .height(500)
                    .objectFit(ImageFit.Contain)

                } else {
                  Text(JSON.stringify(record))
                    .fontSize(50)
                    .fontWeight(FontWeight.Bold)
                }
              }
              .width('100%')
              .justifyContent(FlexAlign.Center)
            }
          }, (record: systemShare.SharedRecord, index: number) => index + '')
        }
        .divider({
          strokeWidth: 2,
          color: Color.Orange
        })
      } else {
        Text('暂无分享内容')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
    }
  }
}
复制

二级面板内业务也可参考上述代码自行实现。以上代码以index作为了key,实际不建议这样做,上述代码只做示例。

二级面板使用时,注意还需要在src/main/resources/base/profile/main_pages.json中配置,按照上面TestShareAbility加载的是Second,那么main_pages.json参考如下。

{
  "src": [
    "pages/Index",
    "pages/Second"
  ]
}
复制

二级面板关闭时告知分享面板的行为

比较简单,参考:二级面板关闭分享面板

社交类应用贡献数据给分享推荐区

需要接入意图框架,若未接入使用意图框架会有问题。接入流程参考:Intents Kit接入流程。接入完了再参考:共享联系人信息到分享推荐区

常见问题

  1. 模拟器有很多不支持的,例如图片、视频、App Linking都打不开,甚至目标应用冷启动会丢失第一条数据,还是需要用真机,真机没有上述问题。

示例代码

本帖最后

1 回复

HarmonyOS 鸿蒙Next应用间信息分享功能十分强大且安全。以下是对该功能的专业解读:

HarmonyOS 鸿蒙Next支持应用间通过URI(统一资源标识符)和FD(文件描述符)两种方式分享文件。这两种方式都配备了相应的安全控制机制,确保文件分享的安全性和可靠性。在分享过程中,应用需要获得用户的授权,并且只能访问授权的文件。对于敏感数据,还可以使用数据加密技术进一步保护。

此外,HarmonyOS 鸿蒙Next还提供了Share Kit等API,简化了应用间信息分享的实现。开发者可以利用这些API,轻松地构建出功能丰富的分享功能。例如,通过systemShare.ShareData构造分享数据,使用addRecord添加多条分享记录,再构建ShareController对象并调用show方法启动分享面板。

值得注意的是,应用在分享信息时,应遵守相关的隐私政策和法律法规,确保用户数据的安全和合法使用。

如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html。

回到顶部