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 fileIo.closeSync(file); 116 return pixmap; 117 } catch(e) { 118 hilog.info(0x0000, TAG, `ReadImage failed:${e}`); 119 } finally { 120 fileIo.close(file); 121 } 122 return null; 123 } 124 125 build() { 126 Row() { 127 Column() { 128 Text(this.message) 129 .fontSize(50) 130 .fontWeight(FontWeight.Bold) 131 132 Button("RotateAndSaveImg").onClick(event => { 133 hilog.info(0x0000, TAG, `Start to edit image and save.`); 134 // 编辑图片功能实现 135 this.originalImage?.rotate(90).then(() => { 136 let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }; 137 try { 138 // 调用saveEditedContentWithImage保存图片 139 (getContext(this) as common.PhotoEditorExtensionContext).saveEditedContentWithImage(this.originalImage as image.PixelMap, 140 packOpts).then(data => { 141 if (data.resultCode == 0) { 142 hilog.info(0x0000, TAG, `Save succeed.`); 143 } 144 hilog.info(0x0000, TAG, 145 `saveContentEditingWithImage result: ${JSON.stringify(data)}`); 146 this.newWant = data.want; 147 // data.want.uri存有编辑过图片的uri 148 this.readImageByUri(this.newWant?.uri ?? "").then(imagePixMap => { 149 this.editedImage = imagePixMap; 150 }) 151 }) 152 } catch (e) { 153 hilog.error(0x0000, TAG, `saveContentEditingWithImage failed:${e}`); 154 return; 155 } 156 }) 157 }).margin({ top: 10 }) 158 159 Button("terminateSelfWithResult").onClick((event => { 160 hilog.info(0x0000, TAG, `Finish the current editing.`); 161 162 let session = storage.get('session') as UIExtensionContentSession; 163 // 关闭并回传修改结果给调用方 164 session.terminateSelfWithResult({ resultCode: 0, want: this.newWant }); 165 166 })).margin({ top: 10 }) 167 168 Image(this.originalImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain) 169 170 Image(this.editedImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain) 171 } 172 .width('100%') 173 } 174 .height('100%') 175 .backgroundColor(Color.Pink) 176 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 177 } 178 } 179 180 ``` 1814. 在工程Module对应的module.json5配置文件中注册PhotoEditorExtensionAbility。 182 183 type标签需要配置为"photoEditor",srcEntry需要配置为PhotoEditorExtensionAbility组件所对应的代码路径。 184 185 ```json 186 { 187 "module": { 188 "extensionAbilities": [ 189 { 190 "name": "ExamplePhotoEditorAbility", 191 "icon": "$media:icon", 192 "description": "ExamplePhotoEditorAbility", 193 "type": "photoEditor", 194 "exported": true, 195 "srcEntry": "./ets/PhotoEditorExtensionAbility/ExamplePhotoEditorAbility.ets", 196 "label": "$string:EntryAbility_label", 197 "extensionProcessMode": "bundle" 198 }, 199 ] 200 } 201 } 202 ``` 203## 调用方拉起图片编辑类应用编辑图片 204开发者可以在UIAbility或者UIExtensionAbility的页面中通过接口startAbilityByType拉起图片编辑类应用扩展面板,系统将自动查找并在面板上展示基于[PhotoEditorExtensionAbility](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionAbility.md)实现的图片编辑应用,由用户选择某个应用来完成图片编辑的功能,最终将编辑的结果返回给到调用方,具体步骤如下: 2051. 导入模块。 206 ```ts 207 import { common, wantConstant } from '@kit.AbilityKit'; 208 import { fileUri, picker } from '@kit.CoreFileKit'; 209 ``` 2102. (可选)实现从图库中选取图片。 211 ```ts 212 async photoPickerGetUri(): Promise < string > { 213 try { 214 let PhotoSelectOptions = new picker.PhotoSelectOptions(); 215 PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 216 PhotoSelectOptions.maxSelectNumber = 1; 217 let photoPicker = new picker.PhotoViewPicker(); 218 let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions); 219 return photoSelectResult.photoUris[0]; 220 } catch(error) { 221 let err: BusinessError = error as BusinessError; 222 hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 223 } 224 return ""; 225 } 226 ``` 2273. 将图片拷贝到本地沙箱路径。 228 ```ts 229 let context = getContext(this) as common.UIAbilityContext; 230 let file: fileIo.File | undefined; 231 try { 232 file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); 233 hilog.info(0x0000, TAG, "file: " + file.fd); 234 235 let timeStamp = Date.now(); 236 // 将用户图片拷贝到应用沙箱路径 237 fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`); 238 fileIo.closeSync(file); 239 240 this.filePath = context.filesDir + `/original-${timeStamp}.jpg`; 241 this.originalImage = fileUri.getUriFromPath(this.filePath); 242 } catch (e) { 243 hilog.info(0x0000, TAG, `readImage failed:${e}`); 244 } finally { 245 fileIo.close(file); 246 } 247 ``` 2484. 在startAbilityByType回调函数中,通过want.uri获取编辑后的图片uri,并做对应的处理。 249 ```ts 250 let context = getContext(this) as common.UIAbilityContext; 251 let abilityStartCallback: common.AbilityStartCallback = { 252 onError: (code, name, message) => { 253 const tip: string = `code:` + code + ` name:` + name + ` message:` + message; 254 hilog.error(0x0000, TAG, "startAbilityByType:", tip); 255 }, 256 onResult: (result) => { 257 // 获取到回调结果中编辑后的图片uri并做对应的处理 258 let uri = result.want?.uri ?? ""; 259 hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result)); 260 this.readImage(uri).then(imagePixMap => { 261 this.editedImage = imagePixMap; 262 }); 263 } 264 } 265 ``` 2665. 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板。 267 ```ts 268 let uri = fileUri.getUriFromPath(this.filePath); 269 context.startAbilityByType("photoEditor", { 270 "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri 271 "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板 272 } as Record<string, Object>, abilityStartCallback, (err) => { 273 let tip: string; 274 if (err) { 275 tip = `Start error: ${JSON.stringify(err)}`; 276 hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`); 277 } else { 278 tip = `Start success`; 279 hilog.info(0x0000, TAG, "startAbilityByType: ", `success`); 280 } 281 }); 282 ``` 283 284示例: 285```ts 286import { common, wantConstant } from '@kit.AbilityKit'; 287import { fileUri, picker } from '@kit.CoreFileKit'; 288import { hilog } from '@kit.PerformanceAnalysisKit'; 289import { fileIo } from '@kit.CoreFileKit'; 290import { image } from '@kit.ImageKit'; 291import { BusinessError } from '@kit.BasicServicesKit'; 292import { JSON } from '@kit.ArkTS'; 293 294const TAG = 'PhotoEditorCaller'; 295 296@Entry 297@Component 298struct Index { 299 @State message: string = 'selectImg'; 300 @State originalImage: ResourceStr = ""; 301 @State editedImage: PixelMap | null = null; 302 private filePath: string = ""; 303 304 // 根据uri读取图片内容 305 async readImage(uri: string): Promise < PixelMap | null > { 306 hilog.info(0x0000, TAG, "image uri: " + uri); 307 let file: fileIo.File | undefined; 308 try { 309 file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); 310 hilog.info(0x0000, TAG, "file: " + file.fd); 311 312 let imageSourceApi: image.ImageSource = image.createImageSource(file.fd); 313 if(!imageSourceApi) { 314 hilog.info(0x0000, TAG, "imageSourceApi failed"); 315 return null; 316 } 317 let pixmap: image.PixelMap = await imageSourceApi.createPixelMap(); 318 if(!pixmap) { 319 hilog.info(0x0000, TAG, "createPixelMap failed"); 320 return null; 321 } 322 this.editedImage = pixmap; 323 fileIo.closeSync(file); 324 return pixmap; 325 } catch(e) { 326 hilog.info(0x0000, TAG, `readImage failed:${e}`); 327 } finally { 328 fileIo.close(file); 329 } 330 return null; 331 } 332 333 // 图库中选取图片 334 async photoPickerGetUri(): Promise < string > { 335 try { 336 let PhotoSelectOptions = new picker.PhotoSelectOptions(); 337 PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 338 PhotoSelectOptions.maxSelectNumber = 1; 339 let photoPicker = new picker.PhotoViewPicker(); 340 let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions); 341 hilog.info(0x0000, TAG, 342 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult)); 343 return photoSelectResult.photoUris[0]; 344 } catch(error) { 345 let err: BusinessError = error as BusinessError; 346 hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err)); 347 } 348 return ""; 349 } 350 351 build() { 352 Row() { 353 Column() { 354 Text(this.message) 355 .fontSize(50) 356 .fontWeight(FontWeight.Bold) 357 358 Button("selectImg").onClick(event => { 359 // 图库中选取图片 360 this.photoPickerGetUri().then(uri => { 361 hilog.info(0x0000, TAG, "uri: " + uri); 362 363 let context = getContext(this) as common.UIAbilityContext; 364 let file: fileIo.File | undefined; 365 try { 366 file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); 367 hilog.info(0x0000, TAG, "file: " + file.fd); 368 369 let timeStamp = Date.now(); 370 // 将用户图片拷贝到应用沙箱路径 371 fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`); 372 fileIo.closeSync(file); 373 374 this.filePath = context.filesDir + `/original-${timeStamp}.jpg`; 375 this.originalImage = fileUri.getUriFromPath(this.filePath); 376 } catch (e) { 377 hilog.info(0x0000, TAG, `readImage failed:${e}`); 378 } finally { 379 fileIo.close(file); 380 } 381 }) 382 383 }).width('200').margin({ top: 20 }) 384 385 Button("editImg").onClick(event => { 386 let context = getContext(this) as common.UIAbilityContext; 387 let abilityStartCallback: common.AbilityStartCallback = { 388 onError: (code, name, message) => { 389 const tip: string = `code:` + code + ` name:` + name + ` message:` + message; 390 hilog.error(0x0000, TAG, "startAbilityByType:", tip); 391 }, 392 onResult: (result) => { 393 // 获取到回调结果中编辑后的图片uri并做对应的处理 394 let uri = result.want?.uri ?? ""; 395 hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result)); 396 this.readImage(uri).then(imagePixMap => { 397 this.editedImage = imagePixMap; 398 }); 399 } 400 } 401 // 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板 402 let uri = fileUri.getUriFromPath(this.filePath); 403 context.startAbilityByType("photoEditor", { 404 "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri 405 "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板 406 } as Record<string, Object>, abilityStartCallback, (err) => { 407 let tip: string; 408 if (err) { 409 tip = `Start error: ${JSON.stringify(err)}`; 410 hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`); 411 } else { 412 tip = `Start success`; 413 hilog.info(0x0000, TAG, "startAbilityByType: ", `success`); 414 } 415 }); 416 417 }).width('200').margin({ top: 20 }) 418 419 Image(this.originalImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 420 421 Image(this.editedImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain) 422 } 423 .width('100%') 424 } 425 .height('100%') 426 .backgroundColor(Color.Orange) 427 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 428 } 429} 430 431```