Flutter鸿蒙化中的Plugin

打印 上一主题 下一主题

主题 1033|帖子 1033|积分 3099

前言

各人知道Flutter和鸿蒙通信方式和Flutter和其他平台通信方式都是一样的, 都是使用Platform Channel API来通信。
那么鸿蒙中这些通信的代码是写在哪里? 怎样编写的了?
下面我们简单的学习下。
鸿蒙项目内Plugin

在我们开发App的过程中,大概有如许的需求:
在鸿蒙平台上特有的,并且需要调用鸿蒙原生的API来完成的。那么我们可以在在ohos平台上创建一个Plugin的方式来支持这个功能。
示例的通信方式使用:MethodChannel的方式。
Flutter端实现

  1. // flutter端创建一个MethodChannel的通道,通道名称必须和鸿蒙指定, 如果创建的名称不一致,会导致无法通信
  2. final channel = const MethodChannel("com.test.channel");
  3. // flutter给鸿蒙端发送消息
  4. channel.invokeMapMethod("testData");
复制代码
鸿蒙端实现

创建Plugin的插件类

首先我们需要创建一个插件类, 继续自FlutterPlugin类, 并实现其中的方法
  1. export default class TestPlugin implements FlutterPlugin {
  2. // 通道
  3.   private channel?: MethodChannel;
  4. //获取唯一的类名 类似安卓的Class<? extends FlutterPlugin ts无法实现只能用户自定义
  5.   getUniqueClassName(): string {
  6.     return 'TestPlugin'
  7.   }
  8. // 当插件从engine上分离的时候调用
  9.   onDetachedFromEngine(binding: FlutterPluginBinding): void {
  10.     this.channel?.setMethodCallHandler(null);
  11.   }
  12. // 当插件挂载到engine上的时候调用
  13.   onAttachedToEngine(binding: FlutterPluginBinding): void {
  14.     this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.test.channel");
  15.     //  给通道设置回调监听
  16.     this.channel.setMethodCallHandler({
  17.       onMethodCall(call: MethodCall, result: MethodResult) {
  18.         switch (call.method) {
  19.           case "testData":
  20.             console.log(`接收到flutter传递过来的参shu ===================`)
  21.             break;
  22.           default:
  23.             result.notImplemented();
  24.             break;
  25.         }
  26.       }
  27.     })
  28.   }
  29. }
复制代码
注册Plugin

我们创建完Plugin了, 我们还需要再EntryAbility中去注册我们的插件
  1. export default class EntryAbility extends FlutterAbility {
  2.   configureFlutterEngine(flutterEngine: FlutterEngine) {
  3.     super.configureFlutterEngine(flutterEngine)
  4.     this.addPlugin(new TestPlugin());
  5.   }
  6. }
复制代码
完成上述两步之后,我们就可以使用这个专属鸿蒙的插件来完成鸿蒙上特有的功能了。
开发纯Dart的package

我们知道,flutter_flutter的仓库对于纯Dart开发的package是完全支持的, 对于纯Dart的package我们主要关注Dart的版本支持。
开发纯Dart的命令:flutter create --template=package hello
对于详细怎样开发纯Dart的package,Flutter官方已经讲的非常详细, 开发,集成详细 可以参考官方文档。Flutter中的Package开发
为现有插件项目添加ohos平台支持

在我们开发Flutter项目适配鸿蒙平台的时候,会有些插件还没有适配ohos平台, 这个时候我们等华为适配, 或者我们自己下载插件的源码, 然后我们自己在源码中编写适配ohos平台的代码
下面以image_picker插件为示例,来学习下怎样为已有插件项目添加ohos的平台支持
创建插件

首先我们需要下载image_picker源码, 然后使用Android studio打开flutter项目。 可以查看到项目标布局
然后通过命令行进入到项目根目次, 执行命令:flutter create . --template=plugin --platforms=ohos
执行完后的目次布局如下:

设置插件

我们创建完ohos平台的插件之后, 我们需要再Plugin工程的pubspec.yaml设置文件中设置ohos平台的插件。

当我们设置完成之后, 我们接下来就可以开始编写ohos平台插件相关内容了。
编写插件内容

在我们编写ohos平台的插件内容时, 我们首先需要知道这个插件是通过什么通道, 调用什么方法来和个个平台通信的。 ohos平台的通道名称、调用方法尽量和原来保持划一,有助于明白。
  1. // 执行flutter指令创建plugin插件时, 会自动创建这个类
  2. export default class ImagesPickerPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  3.   private channel: MethodChannel | null = null;
  4.   private pluginBinding: FlutterPluginBinding | null = null;
  5.   // 当前处理代理对象
  6.   private delegate: ImagePickerDelegate | null = null
  7.   constructor() {
  8.   }
  9.   getUniqueClassName(): string {
  10.     return "ImagesPickerPlugin"
  11.   }
  12.   onAttachedToEngine(binding: FlutterPluginBinding): void {
  13.   // 后续用到的页面context,都是需要重binding对象中获取, 如果你直接this.getcontext 等方法获取, 可能不是页面的context
  14.     this.pluginBinding = binding;
  15.     this.channel = new MethodChannel(binding.getBinaryMessenger(), "chavesgu/images_picker");
  16.     this.channel.setMethodCallHandler(this)
  17.   }
  18.   onDetachedFromEngine(binding: FlutterPluginBinding): void {
  19.     this.pluginBinding = null;
  20.     if (this.channel != null) {
  21.       this.channel.setMethodCallHandler(null)
  22.     }
  23.   }
  24. //  插件挂载到ablitity上的时候
  25.   onAttachedToAbility(binding: AbilityPluginBinding): void {
  26.     if (!this.pluginBinding) {
  27.       return
  28.     }
  29.     this.delegate = new ImagePickerDelegate(binding.getAbility().context, this.pluginBinding.getApplicationContext());
  30.   }
  31.   onDetachedFromAbility() {
  32.   }
  33.   onMethodCall(call: MethodCall, result: MethodResult): void {
  34.     if (call.method === "pick") {
  35.       // 解析参数
  36.       let count = call.argument("count") as number;
  37.       // let language = call.argument("language") as string;
  38.       let pickType = call.argument("pickType") as string;
  39.       // let supportGif = call.argument("gif") as boolean;
  40.       // let maxTime = call.argument("maxTime") as number;
  41.       let maxSize = call.argument("maxSize") as number;
  42.       if (this.delegate !== null) {
  43.         this.delegate.pick(count, pickType, maxSize, result)
  44.       }
  45.     } else if (call.method === "saveImageToAlbum" || call.method === "saveVideoToAlbum") {
  46.       // 保存图片
  47.       let filePath = call.argument("path") as string; // 图片路径
  48.       if (this.delegate !== null) {
  49.         this.delegate.saveImageOrVideo(filePath, call.method === "saveImageToAlbum", result)
  50.       }
  51.     } else if (call.method === "openCamera") {
  52.       let pickType = call.argument("pickType") as string;
  53.       let maxSize = call.argument("maxSize") as number;
  54.       if (this.delegate !== null) {
  55.         this.delegate.openCamear(pickType, maxSize, result)
  56.       }
  57.     } else {
  58.       result.notImplemented()
  59.     }
  60.   }
  61. }
复制代码
注意:这个插件内开发代码是没有代码提示的, 也不会自动检车报错, 只有你运行测试demo时, 编译时才会报错,以是建议各人把插件的功能在一个demo中完成,在把代码拷贝过来。
逻辑实现代码:
  1. import ArrayList from '@ohos.util.ArrayList';
  2. import common from '@ohos.app.ability.common';
  3. import photoAccessHelper from '@ohos.file.photoAccessHelper';
  4. import { BusinessError } from '@kit.BasicServicesKit';
  5. import picker from '@ohos.multimedia.cameraPicker';
  6. import camera from '@ohos.multimedia.camera';
  7. import dataSharePredicates from '@ohos.data.dataSharePredicates';
  8. import { fileUri } from '@kit.CoreFileKit';
  9. import FileUtils from './FileUtils'
  10. import fs from '@ohos.file.fs';
  11. import {
  12.   MethodResult,
  13. } from '@ohos/flutter_ohos';
  14. import abilityAccessCtrl, { PermissionRequestResult } from '@ohos.abilityAccessCtrl';
  15. export default class ImagePickerDelegate {
  16.   // 当前UIAblitity的context
  17.   private context: common.Context
  18.   // 插件绑定的context
  19.   private bindContext: common.Context
  20.   // 构造方法
  21.   constructor(context: common.Context, bindContext: common.Context) {
  22.     this.context = context
  23.     this.bindContext = bindContext
  24.   }
  25.   // 选择相册图片和视频
  26.   pick(count: number, pickType: string, maxSize: number, callback: MethodResult) {
  27.     // 创建一个选择配置
  28.     const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  29.     // 媒体选择类型
  30.     let mineType: photoAccessHelper.PhotoViewMIMETypes = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  31.     if (pickType === "PickType.all") {
  32.       mineType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
  33.     } else if (pickType === "PickType.video") {
  34.       mineType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
  35.     }
  36.     photoSelectOptions.MIMEType = mineType
  37.     photoSelectOptions.maxSelectNumber = count; // 选择媒体文件的最大数目
  38.     let uris: Array<string> = [];
  39.     const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
  40.     // 通过photoViewPicker对象来打开相册图片
  41.     photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
  42.       uris = photoSelectResult.photoUris;
  43.       console.info('photoViewPicker.select to file succeed and uris are:' + uris);
  44.       this.hanlderSelectResult(uris, callback)
  45.     }).catch((err: BusinessError) => {
  46.       console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
  47.     })
  48.   }
  49.   // 处理打开相机照相/录制
  50.   async openCamear(type: string, maxSize: number, callback: MethodResult) {
  51.     // 定义一个媒体类型数组
  52.     let mediaTypes: Array<picker.PickerMediaType> = [picker.PickerMediaType.PHOTO];
  53.     if (type === "PickType.all") {
  54.       mediaTypes = [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO]
  55.     } else if (type === "PickType.video") {
  56.       mediaTypes = [picker.PickerMediaType.VIDEO]
  57.     }
  58.     try {
  59.       let pickerProfile: picker.PickerProfile = {
  60.         cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
  61.       };
  62.       let pickerResult: picker.PickerResult = await picker.pick(this.context,
  63.         mediaTypes, pickerProfile);
  64.       console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
  65.       // 获取uri的路径和媒体类型
  66.       let resultUri = pickerResult["resultUri"] as string
  67.       let mediaTypeTemp = pickerResult["mediaType"] as string
  68.       // 需要把uri转换成沙河路径
  69.       let realPath = FileUtils.getPathFromUri(this.bindContext, resultUri);
  70.       if (mediaTypeTemp === "video") {
  71.         // 需要获取缩略图
  72.         callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
  73.       } else {
  74.         // 图片无需设置缩略图
  75.         callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
  76.       }
  77.     } catch (error) {
  78.       let err = error as BusinessError;
  79.       console.error(`the pick call failed. error code: ${err.code}`);
  80.     }
  81.   }
  82.   // 处理保存图片
  83.   async saveImageOrVideo(path: string, isImage: boolean, callback: MethodResult) {
  84.     let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  85.     atManager.requestPermissionsFromUser(this.context,
  86.       ['ohos.permission.WRITE_IMAGEVIDEO', 'ohos.permission.READ_IMAGEVIDEO'],
  87.       async (err: BusinessError, data: PermissionRequestResult) => {
  88.         if (err) {
  89.           console.log(`requestPermissionsFromUser fail, err->${JSON.stringify(err)}`);
  90.         } else {
  91.           console.info('data:' + JSON.stringify(data));
  92.           console.info('data permissions:' + data.permissions);
  93.           console.info('data authResults:' + data.authResults);
  94.           //转换成uri
  95.           let uriTemp = fileUri.getUriFromPath(path);
  96.           //打开文件
  97.           let fileTemp = fs.openSync(uriTemp, fs.OpenMode.READ_ONLY);
  98.           //读取文件大小
  99.           let info = fs.statSync(fileTemp.fd);
  100.           //缓存照片数据
  101.           let bufferImg: ArrayBuffer = new ArrayBuffer(info.size);
  102.           //写入缓存
  103.           fs.readSync(fileTemp.fd, bufferImg);
  104.           //关闭文件流
  105.           fs.closeSync(fileTemp);
  106.           let phHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
  107.           try {
  108.             const uritemp = await phHelper.createAsset(isImage ? photoAccessHelper.PhotoType.IMAGE :
  109.             photoAccessHelper.PhotoType.VIDEO, isImage ? 'jpg' : "mp4"); // 指定待创建的文件类型、后缀和创建选项,创建图片或视频资源
  110.             const file = await fs.open(uritemp, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  111.             await fs.write(file.fd, bufferImg);
  112.             await fs.close(file.fd);
  113.             callback.success(true);
  114.           } catch (error) {
  115.             console.error(`error=========${JSON.stringify(error)}`)
  116.             callback.success(false);
  117.           }
  118.         }
  119.       });
  120.   }
  121.   // 处理选中结果
  122.   hanlderSelectResult(uris: Array<string>, callback: MethodResult) {
  123.     // 定义一个path数组
  124.     let pathList: ArrayList<string> = new ArrayList();
  125.     for (let path of uris) {
  126.       // if (path.search("video") < 0) {
  127.       //   path = await this.getResizedImagePath(path, this.pendingCallState.imageOptions);
  128.       // }
  129.       this.getVideoThumbnail(path)
  130.       let realPath = FileUtils.getPathFromUri(this.bindContext, path);
  131.       pathList.add(realPath);
  132.     }
  133.     let uriModels: UriModel[] = [];
  134.     pathList.forEach(element => {
  135.       uriModels.push({
  136.         thumbPath: element,
  137.         path: element,
  138.         size: 500
  139.       })
  140.     });
  141.     callback.success(uriModels)
  142.   }
  143.   // 获取视频的缩略图
  144.   async getVideoThumbnail(uri: string) {
  145.     //建立视频检索条件,用于获取视频
  146.     console.log("开始获取缩略图==========")
  147.     let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
  148.     predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uri);
  149.     let fetchOption: photoAccessHelper.FetchOptions = {
  150.       fetchColumns: [],
  151.       predicates: predicates
  152.     };
  153.     // let size: image.Size = { width: 720, height: 720 };
  154.     let phelper = photoAccessHelper.getPhotoAccessHelper(this.context)
  155.     let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phelper.getAssets(fetchOption);
  156.     console.log(`fetchResult=========${JSON.stringify(fetchResult)}`)
  157.     let asset = await fetchResult.getFirstObject();
  158.     console.info('asset displayName = ', asset.displayName);
  159.     asset.getThumbnail().then((pixelMap) => {
  160.       console.info('getThumbnail successful ' + pixelMap);
  161.     }).catch((err: BusinessError) => {
  162.       console.error(`getThumbnail fail with error: ${err.code}, ${err.message}`);
  163.     });
  164.   }
  165. }
  166. // 定义一个返回的对象
  167. interface UriModel {
  168.   thumbPath: string;
  169.   path: string;
  170.   size: number;
  171. }
复制代码
工具类代码 :
  1. /*
  2. * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd.
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. *     http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import common from '@ohos.app.ability.common';
  16. import fs from '@ohos.file.fs';
  17. import util from '@ohos.util';
  18. import Log from '@ohos/flutter_ohos/src/main/ets/util/Log';
  19. const TAG = "FileUtils";
  20. export default class FileUtils {
  21.   static getPathFromUri(context: common.Context | null, uri: string, defExtension?: string) {
  22.     Log.i(TAG, "getPathFromUri : " + uri);
  23.     let inputFile: fs.File;
  24.     try {
  25.       inputFile = fs.openSync(uri);
  26.     } catch (err) {
  27.       Log.e(TAG, "open uri file failed err:" + err)
  28.       return null;
  29.     }
  30.     if (inputFile == null) {
  31.       return null;
  32.     }
  33.     const uuid = util.generateRandomUUID();
  34.     if (!context) {
  35.       return
  36.     }
  37.     {
  38.       const targetDirectoryPath = context.cacheDir + "/" + uuid;
  39.       try {
  40.         fs.mkdirSync(targetDirectoryPath);
  41.         let targetDir = fs.openSync(targetDirectoryPath);
  42.         Log.i(TAG, "mkdirSync success targetDirectoryPath:" + targetDirectoryPath + " fd: " + targetDir.fd);
  43.         fs.closeSync(targetDir);
  44.       } catch (err) {
  45.         Log.e(TAG, "mkdirSync failed err:" + err);
  46.         return null;
  47.       }
  48.       const inputFilePath = uri.substring(uri.lastIndexOf("/") + 1);
  49.       const inputFilePathSplits = inputFilePath.split(".");
  50.       Log.i(TAG, "getPathFromUri inputFilePath: " + inputFilePath);
  51.       const outputFileName = inputFilePathSplits[0];
  52.       let extension: string;
  53.       if (inputFilePathSplits.length == 2) {
  54.         extension = "." + inputFilePathSplits[1];
  55.       } else {
  56.         if (defExtension) {
  57.           extension = defExtension;
  58.         } else {
  59.           extension = ".jpg";
  60.         }
  61.       }
  62.       const outputFilePath = targetDirectoryPath + "/" + outputFileName + extension;
  63.       const outputFile = fs.openSync(outputFilePath, fs.OpenMode.CREATE);
  64.       try {
  65.         Log.i(TAG, "copyFileSync inputFile fd:" + inputFile.fd + " outputFile fd:" + outputFile.fd);
  66.         fs.copyFileSync(inputFile.fd, outputFilePath);
  67.       } catch (err) {
  68.         Log.e(TAG, "copyFileSync failed err:" + err);
  69.         return null;
  70.       } finally {
  71.         fs.closeSync(inputFile);
  72.         fs.closeSync(outputFile);
  73.       }
  74.       return outputFilePath;
  75.     }
  76.   }
  77. }
复制代码
编写完上述代码就可以运行example工程去测试相关功能了。 当测试完成之后 , 我们可以把整个源码工程拷贝到flutter工程中, 通过集成本地package的方式来集成这个package。或者你可以在发一个新的pacage到pub.dev上, 然后在按照原有方式集成即可。
参考资料

Flutter官方的package开发和使用
开发Plugin

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

道家人

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表