自学内容网 自学内容网

Flutter鸿蒙化中的Plugin

前言

大家知道Flutter和鸿蒙通信方式和Flutter和其他平台通信方式都是一样的, 都是使用Platform Channel API来通信。

那么鸿蒙中这些通信的代码是写在哪里? 如何编写的了?
下面我们简单的学习下。

鸿蒙项目内Plugin

在我们开发App的过程中,可能有这样的需求:
在鸿蒙平台上特有的,并且需要调用鸿蒙原生的API来完成的。那么我们可以在在ohos平台上创建一个Plugin的方式来支持这个功能。

示例的通信方式使用:MethodChannel的方式。

Flutter端实现

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

鸿蒙端实现

创建Plugin的插件类

首先我们需要创建一个插件类, 继承自FlutterPlugin类, 并实现其中的方法

export default class TestPlugin implements FlutterPlugin {
// 通道
  private channel?: MethodChannel;
//获取唯一的类名 类似安卓的Class<? extends FlutterPlugin ts无法实现只能用户自定义
  getUniqueClassName(): string {
    return 'TestPlugin'
  }

// 当插件从engine上分离的时候调用
  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.channel?.setMethodCallHandler(null);
  }

// 当插件挂载到engine上的时候调用
  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.test.channel");
    //  给通道设置回调监听 
    this.channel.setMethodCallHandler({
      onMethodCall(call: MethodCall, result: MethodResult) {
        switch (call.method) {
          case "testData":
            console.log(`接收到flutter传递过来的参shu ===================`)
            break;
          default:
            result.notImplemented();
            break;
        }
      }
    })
  }
}

注册Plugin

我们创建完Plugin了, 我们还需要再EntryAbility中去注册我们的插件

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    this.addPlugin(new TestPlugin());
  }
}

完成上述两步之后,我们就可以使用这个专属鸿蒙的插件来完成鸿蒙上特有的功能了。

开发纯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平台的通道名称、调用方法尽量和原来保持一致,有助于理解。

// 执行flutter指令创建plugin插件时, 会自动创建这个类
export default class ImagesPickerPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private pluginBinding: FlutterPluginBinding | null = null;
  // 当前处理代理对象
  private delegate: ImagePickerDelegate | null = null

  constructor() {
  }

  getUniqueClassName(): string {
    return "ImagesPickerPlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
  // 后续用到的页面context,都是需要重binding对象中获取, 如果你直接this.getcontext 等方法获取, 可能不是页面的context
    this.pluginBinding = binding;
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "chavesgu/images_picker");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.pluginBinding = null;
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

//  插件挂载到ablitity上的时候
  onAttachedToAbility(binding: AbilityPluginBinding): void {
    if (!this.pluginBinding) {
      return
    }
    this.delegate = new ImagePickerDelegate(binding.getAbility().context, this.pluginBinding.getApplicationContext());
  }

  onDetachedFromAbility() {

  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method === "pick") {
      // 解析参数
      let count = call.argument("count") as number;
      // let language = call.argument("language") as string;
      let pickType = call.argument("pickType") as string;
      // let supportGif = call.argument("gif") as boolean;
      // let maxTime = call.argument("maxTime") as number;
      let maxSize = call.argument("maxSize") as number;
      if (this.delegate !== null) {
        this.delegate.pick(count, pickType, maxSize, result)
      }
    } else if (call.method === "saveImageToAlbum" || call.method === "saveVideoToAlbum") {
      // 保存图片
      let filePath = call.argument("path") as string; // 图片路径
      if (this.delegate !== null) {
        this.delegate.saveImageOrVideo(filePath, call.method === "saveImageToAlbum", result)
      }
    } else if (call.method === "openCamera") {
      let pickType = call.argument("pickType") as string;
      let maxSize = call.argument("maxSize") as number;
      if (this.delegate !== null) {
        this.delegate.openCamear(pickType, maxSize, result)
      }
    } else {
      result.notImplemented()
    }
  }
}

注意:这个插件内开发代码是没有代码提示的, 也不会自动检车报错, 只有你运行测试demo时, 编译时才会报错,所以建议大家把插件的功能在一个demo中完成,在把代码拷贝过来。

逻辑实现代码:

import ArrayList from '@ohos.util.ArrayList';
import common from '@ohos.app.ability.common';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { BusinessError } from '@kit.BasicServicesKit';
import picker from '@ohos.multimedia.cameraPicker';
import camera from '@ohos.multimedia.camera';
import dataSharePredicates from '@ohos.data.dataSharePredicates';
import { fileUri } from '@kit.CoreFileKit';
import FileUtils from './FileUtils'
import fs from '@ohos.file.fs';
import {
  MethodResult,
} from '@ohos/flutter_ohos';
import abilityAccessCtrl, { PermissionRequestResult } from '@ohos.abilityAccessCtrl';


export default class ImagePickerDelegate {
  // 当前UIAblitity的context
  private context: common.Context
  // 插件绑定的context
  private bindContext: common.Context
  // 构造方法
  constructor(context: common.Context, bindContext: common.Context) {
    this.context = context
    this.bindContext = bindContext
  }

  // 选择相册图片和视频
  pick(count: number, pickType: string, maxSize: number, callback: MethodResult) {
    // 创建一个选择配置
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();

    // 媒体选择类型
    let mineType: photoAccessHelper.PhotoViewMIMETypes = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    if (pickType === "PickType.all") {
      mineType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
    } else if (pickType === "PickType.video") {
      mineType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
    }
    photoSelectOptions.MIMEType = mineType
    photoSelectOptions.maxSelectNumber = count; // 选择媒体文件的最大数目

    let uris: Array<string> = [];
    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    // 通过photoViewPicker对象来打开相册图片
    photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      uris = photoSelectResult.photoUris;
      console.info('photoViewPicker.select to file succeed and uris are:' + uris);
      this.hanlderSelectResult(uris, callback)
    }).catch((err: BusinessError) => {
      console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
    })
  }

  // 处理打开相机照相/录制
  async openCamear(type: string, maxSize: number, callback: MethodResult) {
    // 定义一个媒体类型数组
    let mediaTypes: Array<picker.PickerMediaType> = [picker.PickerMediaType.PHOTO];
    if (type === "PickType.all") {
      mediaTypes = [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO]
    } else if (type === "PickType.video") {
      mediaTypes = [picker.PickerMediaType.VIDEO]
    }
    try {
      let pickerProfile: picker.PickerProfile = {
        cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
      };
      let pickerResult: picker.PickerResult = await picker.pick(this.context,
        mediaTypes, pickerProfile);
      console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));

      // 获取uri的路径和媒体类型
      let resultUri = pickerResult["resultUri"] as string
      let mediaTypeTemp = pickerResult["mediaType"] as string
      // 需要把uri转换成沙河路径
      let realPath = FileUtils.getPathFromUri(this.bindContext, resultUri);
      if (mediaTypeTemp === "video") {
        // 需要获取缩略图
        callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
      } else {
        // 图片无需设置缩略图
        callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`the pick call failed. error code: ${err.code}`);
    }
  }

  // 处理保存图片
  async saveImageOrVideo(path: string, isImage: boolean, callback: MethodResult) {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    atManager.requestPermissionsFromUser(this.context,
      ['ohos.permission.WRITE_IMAGEVIDEO', 'ohos.permission.READ_IMAGEVIDEO'],
      async (err: BusinessError, data: PermissionRequestResult) => {
        if (err) {
          console.log(`requestPermissionsFromUser fail, err->${JSON.stringify(err)}`);
        } else {
          console.info('data:' + JSON.stringify(data));
          console.info('data permissions:' + data.permissions);
          console.info('data authResults:' + data.authResults);
          //转换成uri
          let uriTemp = fileUri.getUriFromPath(path);
          //打开文件
          let fileTemp = fs.openSync(uriTemp, fs.OpenMode.READ_ONLY);
          //读取文件大小
          let info = fs.statSync(fileTemp.fd);
          //缓存照片数据
          let bufferImg: ArrayBuffer = new ArrayBuffer(info.size);
          //写入缓存
          fs.readSync(fileTemp.fd, bufferImg);
          //关闭文件流
          fs.closeSync(fileTemp);
          let phHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
          try {
            const uritemp = await phHelper.createAsset(isImage ? photoAccessHelper.PhotoType.IMAGE :
            photoAccessHelper.PhotoType.VIDEO, isImage ? 'jpg' : "mp4"); // 指定待创建的文件类型、后缀和创建选项,创建图片或视频资源
            const file = await fs.open(uritemp, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
            await fs.write(file.fd, bufferImg);
            await fs.close(file.fd);

            callback.success(true);
          } catch (error) {
            console.error(`error=========${JSON.stringify(error)}`)
            callback.success(false);
          }
        }
      });

  }


  // 处理选中结果
  hanlderSelectResult(uris: Array<string>, callback: MethodResult) {
    // 定义一个path数组
    let pathList: ArrayList<string> = new ArrayList();
    for (let path of uris) {
      // if (path.search("video") < 0) {
      //   path = await this.getResizedImagePath(path, this.pendingCallState.imageOptions);
      // }
      this.getVideoThumbnail(path)
      let realPath = FileUtils.getPathFromUri(this.bindContext, path);
      pathList.add(realPath);
    }

    let uriModels: UriModel[] = [];
    pathList.forEach(element => {
      uriModels.push({
        thumbPath: element,
        path: element,
        size: 500
      })
    });

    callback.success(uriModels)
  }

  // 获取视频的缩略图
  async getVideoThumbnail(uri: string) {
    //建立视频检索条件,用于获取视频
    console.log("开始获取缩略图==========")
    let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
    predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uri);
    let fetchOption: photoAccessHelper.FetchOptions = {
      fetchColumns: [],
      predicates: predicates
    };

    // let size: image.Size = { width: 720, height: 720 };
    let phelper = photoAccessHelper.getPhotoAccessHelper(this.context)
    let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phelper.getAssets(fetchOption);
    console.log(`fetchResult=========${JSON.stringify(fetchResult)}`)
    let asset = await fetchResult.getFirstObject();
    console.info('asset displayName = ', asset.displayName);
    asset.getThumbnail().then((pixelMap) => {
      console.info('getThumbnail successful ' + pixelMap);
    }).catch((err: BusinessError) => {
      console.error(`getThumbnail fail with error: ${err.code}, ${err.message}`);
    });
  }
}

// 定义一个返回的对象
interface UriModel {
  thumbPath: string;
  path: string;
  size: number;
}

工具类代码 :

/*
 * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import util from '@ohos.util';
import Log from '@ohos/flutter_ohos/src/main/ets/util/Log';

const TAG = "FileUtils";

export default class FileUtils {
  static getPathFromUri(context: common.Context | null, uri: string, defExtension?: string) {
    Log.i(TAG, "getPathFromUri : " + uri);
    let inputFile: fs.File;
    try {
      inputFile = fs.openSync(uri);
    } catch (err) {
      Log.e(TAG, "open uri file failed err:" + err)
      return null;
    }
    if (inputFile == null) {
      return null;
    }
    const uuid = util.generateRandomUUID();
    if (!context) {
      return
    }
    {
      const targetDirectoryPath = context.cacheDir + "/" + uuid;
      try {
        fs.mkdirSync(targetDirectoryPath);
        let targetDir = fs.openSync(targetDirectoryPath);
        Log.i(TAG, "mkdirSync success targetDirectoryPath:" + targetDirectoryPath + " fd: " + targetDir.fd);
        fs.closeSync(targetDir);
      } catch (err) {
        Log.e(TAG, "mkdirSync failed err:" + err);
        return null;
      }

      const inputFilePath = uri.substring(uri.lastIndexOf("/") + 1);
      const inputFilePathSplits = inputFilePath.split(".");
      Log.i(TAG, "getPathFromUri inputFilePath: " + inputFilePath);
      const outputFileName = inputFilePathSplits[0];
      let extension: string;
      if (inputFilePathSplits.length == 2) {
        extension = "." + inputFilePathSplits[1];
      } else {
        if (defExtension) {
          extension = defExtension;
        } else {
          extension = ".jpg";
        }
      }
      const outputFilePath = targetDirectoryPath + "/" + outputFileName + extension;
      const outputFile = fs.openSync(outputFilePath, fs.OpenMode.CREATE);
      try {
        Log.i(TAG, "copyFileSync inputFile fd:" + inputFile.fd + " outputFile fd:" + outputFile.fd);
        fs.copyFileSync(inputFile.fd, outputFilePath);
      } catch (err) {
        Log.e(TAG, "copyFileSync failed err:" + err);
        return null;
      } finally {
        fs.closeSync(inputFile);
        fs.closeSync(outputFile);
      }
      return outputFilePath;
    }
  }
}

编写完上述代码就可以运行example工程去测试相关功能了。 当测试完成之后 , 我们可以把整个源码工程拷贝到flutter工程中, 通过集成本地package的方式来集成这个package。或者你可以在发一个新的pacage到pub.dev上, 然后在按照原有方式集成即可。

参考资料

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


原文地址:https://blog.csdn.net/yong_19930826/article/details/145259660

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!