# AppShare **Repository Path**: cencus/app-share ## Basic Information - **Project Name**: AppShare - **Description**: 应用间通过分享面板进行数据分享的示例项目 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-22 - **Last Updated**: 2024-11-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 概述 本文主要针对应用分享,如何使用[Share Kit](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/share-introduction-V5)完成跨应用的内容分享(文本、图片、视频、链接等)。 概念: - 宿主应用:要分享数据的应用 - 目标应用:要接受数据的应用 # 场景示例 - 相册向其他应用分享图片(图片资源) - 相册分享视频到视频编辑软件内进行剪辑 - 相册分享图片到微信朋友圈,发朋友圈 - 选择文本后分享到其他应用,例如笔记,待办等。 # 宿主应用 ## 传递示例大杂烩(包括图片、文本、App Linking、视频) 关键问题:如何定制分享内容,如何拉起分享面板。 示例代码目录结构参考: ![](./images/pic1.png) 这里面要注意,其实真正跨进程传输时的数据是不大的,设计的就是封装的数据,里面核心的就是文本和图片,所以在传递这些的时候是要考虑200KB上限的,自己在实操过程中遇到了视频需要传预览图,预览图过大,导致报错的,不过有错误码是可以很快定位问题。 **核心逻辑**:构造分享的数据包,然后拉起分享面板。 **难点**: 1. 部分数据需要写到目录中以下以应用沙箱目录为例,其他如公共目录,需要重写writeToSandBox方法。 2. 每一类数据如何去进行构造,例如文本就比较简单,但是视频就比较复杂还有预览图、写文件等等。需要有示例参考,对开发者只需要替换部分代码即可。 **注意**:以下代码执行前记得往rawfile中放入test.jpg、startIcon.png(用应用默认创建时base/media就可以)、video.mp4。不然会报错。 ```typescript 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即可,参考[宿主应用配置操作区](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/share-app-actions-V5)。 # 目标应用 ## 两种处理方式 - **方式一**:分享面板点击应用后直接跳转到应用内。 - **方式二**:分享面板点击应用后先弹出二级菜单,在二级菜单中精细化用户需求后再跳转到应用内,以下为研究理解。看到效果就明白了其实就是在分享面板上再弹出了一个半模态框。 - 例如从相册分享图片到抖音,有可能是为了发抖音,有可能是为了发给朋友 - 同样的分享到微信有可能是发朋友圈,有可能是发给最近联系的朋友 - 还有个场景是分享文本到任务里,会自动在二级菜单中有一个创建任务的面板,点完之后直接创建甚至都不需要进入应用。 - 猜测用户想拿这个东西干什么,然后提供对应的操作面板,在操作面板细化功能分支后,直接完成或到应用中处理 - 好处就是不需要脱离当前的应用 ## 配置module.json5 && Ability中获取参数 ### 方式一配置参考 如何告诉系统我可以处理哪一类的数据,例如我作为视频剪辑软件可以处理视频、图片等等,就需要在module.json5中进行配置。配置参考以下代码,scheme固定为file,utd参考[UniformDataType](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-data-uniformtypedescriptor-V5#uniformdatatype),maxFileSupported为该类型支持的最大个数。 ```typescript { "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参考: ```typescript 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('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的位置,以及要调整目录结构了。 ![](./images/pic2.png) module.json5参考下文,在extensionAbilities中新增一个TestShareAbility。abilities里还是可以正常配置的,而且别把skills删了,不然应用无法手动点击打开了。 ```json { "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参考 ```typescript 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('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里。 ## 使用获取的数据 在页面中使用自定义的数据,具体实现需要业务方做,以下只作为参考,把所有传递过来的数据进行展示。 ```typescript 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" ] } ``` ## 二级面板关闭时告知分享面板的行为 比较简单,参考:[二级面板关闭分享面板](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/share-sec-panel-back-V5)。 ## 社交类应用贡献数据给分享推荐区 需要接入意图框架,若未接入使用意图框架会有问题。接入流程参考:[Intents Kit接入流程](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/intents-access-flow-V5)。接入完了再参考:[共享联系人信息到分享推荐区](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/share-intents-share-V5)。 # 常见问题 1. 模拟器有很多不支持的,例如图片、视频、App Linking都打不开,甚至目标应用冷启动会丢失第一条数据,还是需要用真机,真机没有上述问题。