HarmonyOS 鸿蒙Next 应用间信息分享
概述
本文主要针对应用分享,如何使用Share Kit完成跨应用的内容分享(文本、图片、视频、链接等)。
概念:
- 宿主应用:要分享数据的应用
- 目标应用:要接受数据的应用
场景示例
- 相册向其他应用分享图片(图片资源)
- 相册分享视频到视频编辑软件内进行剪辑
- 相册分享图片到微信朋友圈,发朋友圈
- 选择文本后分享到其他应用,例如笔记,待办等。
宿主应用
传递示例大杂烩(包括图片、文本、App Linking、视频)
关键问题:如何定制分享内容,如何拉起分享面板。
示例代码目录结构参考:
这里面要注意,其实真正跨进程传输时的数据是不大的,设计的就是封装的数据,里面核心的就是文本和图片,所以在传递这些的时候是要考虑200KB上限的,自己在实操过程中遇到了视频需要传预览图,预览图过大,导致报错的,不过有错误码是可以很快定位问题。
核心逻辑:构造分享的数据包,然后拉起分享面板。
难点:
- 部分数据需要写到目录中以下以应用沙箱目录为例,其他如公共目录,需要重写writeToSandBox方法。
- 每一类数据如何去进行构造,例如文本就比较简单,但是视频就比较复杂还有预览图、写文件等等。需要有示例参考,对开发者只需要替换部分代码即可。
注意:以下代码执行前记得往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的位置,以及要调整目录结构了。
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接入流程。接入完了再参考:共享联系人信息到分享推荐区。
常见问题
- 模拟器有很多不支持的,例如图片、视频、App Linking都打不开,甚至目标应用冷启动会丢失第一条数据,还是需要用真机,真机没有上述问题。
示例代码
本帖最后
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。