1# 拉起图片编辑类应用(startAbilityByType) 2## 使用场景 3当应用自身不具备图片编辑能力、但存在图片编辑的场景时,可以通过startAbilityByType拉起图片编辑类应用扩展面板,由对应的应用完成图片编辑操作。图片编辑类应用可以通过PhotoEditorExtensionAbility实现图片编辑页面,并将该页面注册到图片编辑面板,从而将图片编辑能力开放给其他应用。 4 5流程示意图如下: 6 7 8 9例如:用户在图库App中选择编辑图片时,图库App可以通过startAbilityByType拉起图片编辑类应用扩展面板。用户可以从已实现PhotoEditorExtensionAbility应用中选择一款,并进行图片编辑。 10 11## 接口说明 12 13接口详情参见[PhotoEditorExtensionAbility](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionAbility.md)和[PhotoEditorExtensionContext](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionContext.md)。 14 15| **接口名** | **描述** | 16| -------- | -------- | 17| onStartContentEditing(uri: string, want:Want, session: UIExtensionContentSession):void | 可以执行读取原始图片、加载页面等操作。| 18| saveEditedContentWithImage(pixeMap: image.PixelMap, option: image.PackingOption): Promise\<AbilityResult\> | 传入编辑过的图片的PixelMap对象并保存。 | 19 20## 图片编辑类应用实现图片编辑页面 21 221. 在DevEco Studio工程中手动新建一个PhotoEditorExtensionAbility。 23 1. 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录,如PhotoEditorExtensionAbility。 24 2. 在PhotoEditorExtensionAbility目录中,右键选择“New > File”,新建一个.ets文件,如ExamplePhotoEditorAbility.ets。 252. 在ExamplePhotoEditorAbility.ets中重写onCreate、onForeground、onBackground、onDestroy和onStartContentEditing的生命周期回调。 26 27 其中,需要在onStartContentEditing中加载入口页面文件pages/Index.ets,并将session、uri、实例对象等保存在LocalStorage中传递给页面。 28 29 ```ts 30 import { PhotoEditorExtensionAbility,UIExtensionContentSession,Want } from '@kit.AbilityKit'; 31 import { hilog } from '@kit.PerformanceAnalysisKit'; 32 33 const TAG = '[ExamplePhotoEditorAbility]'; 34 export default class ExamplePhotoEditorAbility extends PhotoEditorExtensionAbility { 35 onCreate() { 36 hilog.info(0x0000, TAG, 'onCreate'); 37 } 38 39 // 获取图片,加载页面并将需要的参数传递给页面 40 onStartContentEditing(uri: string, want: Want, session: UIExtensionContentSession): void { 41 hilog.info(0x0000, TAG, `onStartContentEditing want: ${JSON.stringify(want)}, uri: ${uri}`); 42 43 const storage: LocalStorage = new LocalStorage({ 44 "session": session, 45 "uri": uri 46 } as Record<string, Object>); 47 48 session.loadContent('pages/Index', storage); 49 } 50 51 onForeground() { 52 hilog.info(0x0000, TAG, 'onForeground'); 53 } 54 55 onBackground() { 56 hilog.info(0x0000, TAG, 'onBackground'); 57 } 58 59 onDestroy() { 60 hilog.info(0x0000, TAG, 'onDestroy'); 61 } 62 } 63 64 ``` 653. 在page中实现图片编辑功能。 66 67 图片编辑完成后调用saveEditedContentWithImage保存图片,并将回调结果通过terminateSelfWithResult返回给调用方。 68 69 ```ts 70 import { common } from '@kit.AbilityKit'; 71 import { UIExtensionContentSession, Want } from '@kit.AbilityKit'; 72 import { hilog } from '@kit.PerformanceAnalysisKit'; 73 import { fileIo } from '@kit.CoreFileKit'; 74 import { image } from '@kit.ImageKit'; 75 76 const TAG = '[ExamplePhotoEditorAbility]'; 77 78 @Entry 79 @Component 80 struct Index { 81 @State message: string = 'editImg'; 82 @State originalImage: PixelMap | null = null; 83 @State editedImage: PixelMap | null = null; 84 private newWant ?: Want; 85 private storage = this.getUIContext().getSharedLocalStorage(); 86 87 aboutToAppear(): void { 88 let originalImageUri = this.storage?.get<string>("uri") ?? ""; 89 hilog.info(0x0000, TAG, `OriginalImageUri: ${originalImageUri}.`); 90 91 this.readImageByUri(originalImageUri).then(imagePixMap => { 92 this.originalImage = imagePixMap; 93 }) 94 } 95 96 // 根据uri读取图片内容 97 async readImageByUri(uri: string): Promise < PixelMap | null > { 98 hilog.info(0x0000, TAG, "uri: " + uri); 99 let file: fileIo.File | undefined; 100 try { 101 file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); 102 hilog.info(0x0000, TAG, "Original image file id: " + file.fd); 103 104 let imageSourceApi: image.ImageSource = image.createImageSource(file.fd); 105 if(!imageSourceApi) { 106 hilog.info(0x0000, TAG, "ImageSourceApi failed"); 107 return null; 108 } 109 let pixmap: image.PixelMap = await imageSourceApi.createPixelMap(); 110 if(!pixmap) { 111 hilog.info(0x0000, TAG, "createPixelMap failed"); 112 return null; 113 } 114 this.originalImage = pixmap; 115 return pixmap; 116 } catch(e) { 117 hilog.error(0x0000, TAG, `ReadImage failed:${e}`); 118 } finally { 119 fileIo.close(file); 120 } 121 return null; 122 } 123 124 build() { 125 Row() { 126 Column() { 127 Text(this.message) 128 .fontSize(50) 129 .fontWeight(FontWeight.Bold) 130 131 Button("RotateAndSaveImg").onClick(event => { 132 hilog.info(0x0000, TAG, `Start to edit image and save.`); 133 // 编辑图片功能实现 134 this.originalImage?.rotate(90).then(() => { 135 let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }; 136 try { 137 // 调用saveEditedContentWithImage保存图片 138 (this.getUIContext().getHostContext() as common.PhotoEditorExtensionContext).saveEditedContentWithImage(this.originalImage as image.PixelMap, 139 packOpts).then(data => { 140 if (data.resultCode == 0) { 141 hilog.info(0x0000, TAG, `Save succeed.`); 142 } 143 hilog.info(0x0000, TAG, 144 `saveContentEditingWithImage result: ${JSON.stringify(data)}`); 145 this.newWant = data.want; 146 // data.want.uri存有编辑过图片的uri 147 this.readImageByUri(this.newWant?.uri ?? "").then(imagePixMap => { 148 this.editedImage = imagePixMap; 149 }) 150 }) 151 } catch (e) { 152 hilog.error(0x0000, TAG, `saveContentEditingWithImage failed:${e}`); 153 return; 154 } 155 }) 156 }).margin({ top: 10 }) 157 158 Button("terminateSelfWithResult").onClick((event => { 159 hilog.info(0x0000, TAG, `Finish the current editing.`); 160 161 let session = this.storage?.get('session') as UIExtensionContentSession; 162 // 关闭并回传修改结果给调用方 163 session?.terminateSelfWithResult({ resultCode: 0, want: this.newWant }); 164 165 })).margin({ top: 10 }) 166 167 Image(this.originalImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain) 168 169 Image(this.editedImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain) 170 } 171 .width('100%') 172 } 173 .height('100%') 174 .backgroundColor(Color.Pink) 175 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 176 } 177 } 178 179 ``` 1804. 在工程Module对应的module.json5配置文件中注册PhotoEditorExtensionAbility。 181 182 type标签需要配置为"photoEditor",srcEntry需要配置为PhotoEditorExtensionAbility组件所对应的代码路径。 183 184 ```json 185 { 186 "module": { 187 "extensionAbilities": [ 188 { 189 "name": "ExamplePhotoEditorAbility", 190 "icon": "$media:icon", 191 "description": "ExamplePhotoEditorAbility", 192 "type": "photoEditor", 193 "exported": true, 194 "srcEntry": "./ets/PhotoEditorExtensionAbility/ExamplePhotoEditorAbility.ets", 195 "label": "$string:EntryAbility_label", 196 "extensionProcessMode": "bundle" 197 }, 198 ] 199 } 200 } 201 ``` 202## 调用方拉起图片编辑类应用编辑图片 203开发者可以在UIAbility或者UIExtensionAbility的页面中通过接口startAbilityByType拉起图片编辑类应用扩展面板,系统将自动查找并在面板上展示基于[PhotoEditorExtensionAbility](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionAbility.md)实现的图片编辑应用,由用户选择某个应用来完成图片编辑的功能,最终将编辑的结果返回给到调用方,具体步骤如下: 2041. 导入模块。 205 ```ts 206 import { common, wantConstant } from '@kit.AbilityKit'; 207 import { fileUri, picker } from '@kit.CoreFileKit'; 208 ``` 2092. (可选)实现从图库中选取图片。 210 ```ts 211 async photoPickerGetUri(): Promise < string > { 212 try { 213 let PhotoSelectOptions = new picker.PhotoSelectOptions(); 214 PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 215 PhotoSelectOptions.maxSelectNumber = 1; 216 let photoPicker = new picker.PhotoViewPicker(); 217 let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions); 218 return photoSelectResult.photoUris[0]; 219 } catch(error) { 220 let err: BusinessError = error as BusinessError; 221 hilog.error(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 222 } 223 return ""; 224 } 225 ``` 2263. 将图片拷贝到本地沙箱路径。 227 ```ts 228 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 229 let file: fileIo.File | undefined; 230 try { 231 file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); 232 hilog.info(0x0000, TAG, "file: " + file.fd); 233 234 let timeStamp = Date.now(); 235 // 将用户图片拷贝到应用沙箱路径 236 fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`); 237 238 this.filePath = context.filesDir + `/original-${timeStamp}.jpg`; 239 this.originalImage = fileUri.getUriFromPath(this.filePath); 240 } catch (e) { 241 hilog.error(0x0000, TAG, `readImage failed:${e}`); 242 } finally { 243 fileIo.close(file); 244 } 245 ``` 2464. 在startAbilityByType回调函数中,通过want.uri获取编辑后的图片uri,并做对应的处理。 247 ```ts 248 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 249 let abilityStartCallback: common.AbilityStartCallback = { 250 onError: (code, name, message) => { 251 const tip: string = `code:` + code + ` name:` + name + ` message:` + message; 252 hilog.error(0x0000, TAG, "startAbilityByType:", tip); 253 }, 254 onResult: (result) => { 255 // 获取到回调结果中编辑后的图片uri并做对应的处理 256 let uri = result.want?.uri ?? ""; 257 hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result)); 258 this.readImage(uri).then(imagePixMap => { 259 this.editedImage = imagePixMap; 260 }); 261 } 262 } 263 ``` 2645. 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板。 265 ```ts 266 let uri = fileUri.getUriFromPath(this.filePath); 267 context.startAbilityByType("photoEditor", { 268 "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri 269 "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板 270 } as Record<string, Object>, abilityStartCallback, (err) => { 271 let tip: string; 272 if (err) { 273 tip = `Start error: ${JSON.stringify(err)}`; 274 hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`); 275 } else { 276 tip = `Start success`; 277 hilog.info(0x0000, TAG, "startAbilityByType: ", `success`); 278 } 279 }); 280 ``` 281 282示例: 283```ts 284import { common, wantConstant } from '@kit.AbilityKit'; 285import { fileUri, picker } from '@kit.CoreFileKit'; 286import { hilog } from '@kit.PerformanceAnalysisKit'; 287import { fileIo } from '@kit.CoreFileKit'; 288import { image } from '@kit.ImageKit'; 289import { BusinessError } from '@kit.BasicServicesKit'; 290import { JSON } from '@kit.ArkTS'; 291import { photoAccessHelper } from '@kit.MediaLibraryKit'; 292 293const TAG = 'PhotoEditorCaller'; 294 295@Entry 296@Component 297struct Index { 298 @State message: string = 'selectImg'; 299 @State originalImage: ResourceStr = ""; 300 @State editedImage: PixelMap | null = null; 301 private filePath: string = ""; 302 303 // 根据uri读取图片内容 304 async readImage(uri: string): Promise < PixelMap | null > { 305 hilog.info(0x0000, TAG, "image uri: " + uri); 306 let file: fileIo.File | undefined; 307 try { 308 file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); 309 hilog.info(0x0000, TAG, "file: " + file.fd); 310 311 let imageSourceApi: image.ImageSource = image.createImageSource(file.fd); 312 if(!imageSourceApi) { 313 hilog.info(0x0000, TAG, "imageSourceApi failed"); 314 return null; 315 } 316 let pixmap: image.PixelMap = await imageSourceApi.createPixelMap(); 317 if(!pixmap) { 318 hilog.info(0x0000, TAG, "createPixelMap failed"); 319 return null; 320 } 321 this.editedImage = pixmap; 322 return pixmap; 323 } catch(e) { 324 hilog.error(0x0000, TAG, `readImage failed:${e}`); 325 } finally { 326 fileIo.close(file); 327 } 328 return null; 329 } 330 331 // 图库中选取图片 332 async photoPickerGetUri(): Promise<string> { 333 try { 334 let textInfo: photoAccessHelper.TextContextInfo = { 335 text: 'photo' 336 } 337 let recommendOptions: photoAccessHelper.RecommendationOptions = { 338 textContextInfo: textInfo 339 } 340 let options: photoAccessHelper.PhotoSelectOptions = { 341 MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE, 342 maxSelectNumber: 1, 343 recommendationOptions: recommendOptions 344 } 345 let photoPicker = new photoAccessHelper.PhotoViewPicker(); 346 let photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options); 347 return photoSelectResult.photoUris[0]; 348 } catch (error) { 349 let err: BusinessError = error as BusinessError; 350 hilog.error(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 351 } 352 return ""; 353 } 354 355 build() { 356 Row() { 357 Column() { 358 Text(this.message) 359 .fontSize(50) 360 .fontWeight(FontWeight.Bold) 361 362 Button("selectImg").onClick(event => { 363 // 图库中选取图片 364 this.photoPickerGetUri().then(uri => { 365 hilog.info(0x0000, TAG, "uri: " + uri); 366 367 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 368 let file: fileIo.File | undefined; 369 try { 370 file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); 371 hilog.info(0x0000, TAG, "file: " + file.fd); 372 373 let timeStamp = Date.now(); 374 // 将用户图片拷贝到应用沙箱路径 375 fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`); 376 377 this.filePath = context.filesDir + `/original-${timeStamp}.jpg`; 378 this.originalImage = fileUri.getUriFromPath(this.filePath); 379 } catch (e) { 380 hilog.info(0x0000, TAG, `readImage failed:${e}`); 381 } finally { 382 fileIo.close(file); 383 } 384 }) 385 386 }).width('200').margin({ top: 20 }) 387 388 Button("editImg").onClick(event => { 389 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 390 let abilityStartCallback: common.AbilityStartCallback = { 391 onError: (code, name, message) => { 392 const tip: string = `code:` + code + ` name:` + name + ` message:` + message; 393 hilog.error(0x0000, TAG, "startAbilityByType:", tip); 394 }, 395 onResult: (result) => { 396 // 获取到回调结果中编辑后的图片uri并做对应的处理 397 let uri = result.want?.uri ?? ""; 398 hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result)); 399 this.readImage(uri).then(imagePixMap => { 400 this.editedImage = imagePixMap; 401 }); 402 } 403 } 404 // 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板 405 let uri = fileUri.getUriFromPath(this.filePath); 406 context.startAbilityByType("photoEditor", { 407 "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri 408 "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板 409 } as Record<string, Object>, abilityStartCallback, (err) => { 410 let tip: string; 411 if (err) { 412 tip = `Start error: ${JSON.stringify(err)}`; 413 hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`); 414 } else { 415 tip = `Start success`; 416 hilog.info(0x0000, TAG, "startAbilityByType: ", `success`); 417 } 418 }); 419 420 }).width('200').margin({ top: 20 }) 421 422 Image(this.originalImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 423 424 Image(this.editedImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 425 } 426 .width('100%') 427 } 428 .height('100%') 429 .backgroundColor(Color.Orange) 430 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 431 } 432} 433 434```