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(pixelMap: 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 storage = LocalStorage.getShared() 77 const TAG = '[ExamplePhotoEditorAbility]'; 78 79 @Entry 80 @Component 81 struct Index { 82 @State message: string = 'editImg'; 83 @State originalImage: PixelMap | null = null; 84 @State editedImage: PixelMap | null = null; 85 private newWant ?: Want; 86 87 aboutToAppear(): void { 88 let originalImageUri = 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.info(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 (getContext(this) 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 = 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.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 222 } 223 return ""; 224 } 225 ``` 2263. 将图片拷贝到本地沙箱路径。 227 ```ts 228 let context = getContext(this) 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.info(0x0000, TAG, `readImage failed:${e}`); 242 } finally { 243 fileIo.close(file); 244 } 245 ``` 2464. 在startAbilityByType回调函数中,通过want.uri获取编辑后的图片uri,并做对应的处理。 247 ```ts 248 let context = getContext(this) 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'; 291 292const TAG = 'PhotoEditorCaller'; 293 294@Entry 295@Component 296struct Index { 297 @State message: string = 'selectImg'; 298 @State originalImage: ResourceStr = ""; 299 @State editedImage: PixelMap | null = null; 300 private filePath: string = ""; 301 302 // 根据uri读取图片内容 303 async readImage(uri: string): Promise < PixelMap | null > { 304 hilog.info(0x0000, TAG, "image uri: " + uri); 305 let file: fileIo.File | undefined; 306 try { 307 file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); 308 hilog.info(0x0000, TAG, "file: " + file.fd); 309 310 let imageSourceApi: image.ImageSource = image.createImageSource(file.fd); 311 if(!imageSourceApi) { 312 hilog.info(0x0000, TAG, "imageSourceApi failed"); 313 return null; 314 } 315 let pixmap: image.PixelMap = await imageSourceApi.createPixelMap(); 316 if(!pixmap) { 317 hilog.info(0x0000, TAG, "createPixelMap failed"); 318 return null; 319 } 320 this.editedImage = pixmap; 321 return pixmap; 322 } catch(e) { 323 hilog.info(0x0000, TAG, `readImage failed:${e}`); 324 } finally { 325 fileIo.close(file); 326 } 327 return null; 328 } 329 330 // 图库中选取图片 331 async photoPickerGetUri(): Promise < string > { 332 try { 333 let PhotoSelectOptions = new picker.PhotoSelectOptions(); 334 PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 335 PhotoSelectOptions.maxSelectNumber = 1; 336 let photoPicker = new picker.PhotoViewPicker(); 337 let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions); 338 hilog.info(0x0000, TAG, 339 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult)); 340 return photoSelectResult.photoUris[0]; 341 } catch(error) { 342 let err: BusinessError = error as BusinessError; 343 hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 344 } 345 return ""; 346 } 347 348 build() { 349 Row() { 350 Column() { 351 Text(this.message) 352 .fontSize(50) 353 .fontWeight(FontWeight.Bold) 354 355 Button("selectImg").onClick(event => { 356 // 图库中选取图片 357 this.photoPickerGetUri().then(uri => { 358 hilog.info(0x0000, TAG, "uri: " + uri); 359 360 let context = getContext(this) as common.UIAbilityContext; 361 let file: fileIo.File | undefined; 362 try { 363 file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); 364 hilog.info(0x0000, TAG, "file: " + file.fd); 365 366 let timeStamp = Date.now(); 367 // 将用户图片拷贝到应用沙箱路径 368 fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`); 369 370 this.filePath = context.filesDir + `/original-${timeStamp}.jpg`; 371 this.originalImage = fileUri.getUriFromPath(this.filePath); 372 } catch (e) { 373 hilog.info(0x0000, TAG, `readImage failed:${e}`); 374 } finally { 375 fileIo.close(file); 376 } 377 }) 378 379 }).width('200').margin({ top: 20 }) 380 381 Button("editImg").onClick(event => { 382 let context = getContext(this) as common.UIAbilityContext; 383 let abilityStartCallback: common.AbilityStartCallback = { 384 onError: (code, name, message) => { 385 const tip: string = `code:` + code + ` name:` + name + ` message:` + message; 386 hilog.error(0x0000, TAG, "startAbilityByType:", tip); 387 }, 388 onResult: (result) => { 389 // 获取到回调结果中编辑后的图片uri并做对应的处理 390 let uri = result.want?.uri ?? ""; 391 hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result)); 392 this.readImage(uri).then(imagePixMap => { 393 this.editedImage = imagePixMap; 394 }); 395 } 396 } 397 // 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板 398 let uri = fileUri.getUriFromPath(this.filePath); 399 context.startAbilityByType("photoEditor", { 400 "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri 401 "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板 402 } as Record<string, Object>, abilityStartCallback, (err) => { 403 let tip: string; 404 if (err) { 405 tip = `Start error: ${JSON.stringify(err)}`; 406 hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`); 407 } else { 408 tip = `Start success`; 409 hilog.info(0x0000, TAG, "startAbilityByType: ", `success`); 410 } 411 }); 412 413 }).width('200').margin({ top: 20 }) 414 415 Image(this.originalImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 416 417 Image(this.editedImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 418 } 419 .width('100%') 420 } 421 .height('100%') 422 .backgroundColor(Color.Orange) 423 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 424 } 425} 426 427```