Flutter鸿蒙化中的Plugin
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)!