Flutter 适配鸿蒙系统:调用原生功能实现相册选取与拍照
项目背景
我们的移动端项目基于 Flutter 开发,为控制开发周期与成本,采用了 HarmonyOS NEXT(简称鸿蒙)的 Flutter 兼容库,并更新了部分三方库为鸿蒙的 Flutter 兼容库。在图片视频选择与拍摄功能上,我们之前调用的是 Android 和 iOS 的原生方法,现在需要为鸿蒙开发一套原生配合使用的方案。
遇到的问题
鸿蒙的 Flutter 兼容库中的 image_picker 库在部分机型上无法正常工作,主要是国内厂商深度定制引起的。虽然可以通过设备类型判断在纯血鸿蒙手机上使用 image_picker,但这样不利于后期维护。因此,我们决定使用 Flutter 通过通道的形式调用鸿蒙原生方式来实现。
开发准备
首先,需要下载适配 Flutter 的鸿蒙 SDK,具体步骤可参考 Flutter SDK 仓库 或我的上一篇文章《Flutter 适配 HarmonyOS 实践_flutter 支持鸿蒙系统》。
开发过程
创建注册调用鸿蒙原生的渠道
在 ohos 项目中,entry/src/main/ets/plugins 目录下会自动生成 GeneratedPluginRegistrant.ets 文件,注册所有使用的兼容鸿蒙的插件。但我们不能在这里注册,因为每次 build 会根据 Flutter 项目中的 pubspec.yaml 文件重新注册。我们需要找到 GeneratedPluginRegistrant 的注册地 EntryAbility.ets,在 plugins 中创建 FlutterCallNativeRegistrant.ets 并注册。
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import FlutterCallNativeRegistrant from '../plugins/FlutterCallNativeRegistrant';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
FlutterCallNativeRegistrant.registerWith(flutterEngine,this)
}
}
创建并初始化插件
在 FlutterCallNativeRegistrant 中初始化 FlutterCallNativePlugin 插件。
export default class FlutterCallNativeRegistrant {
private channel: MethodChannel | null = null;
private photoPlugin?:PhotoPlugin;
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new FlutterCallNativePlugin());
} catch (e) {
}
}
}
绑定通道完成插件中的功能
绑定 MethodChannel,定义 selectPhoto 和 selectVideo 两个方法,用于调用原生的相册选取照片视频和相机拍摄照片视频。
import { FlutterPlugin, FlutterPluginBinding, MethodCall,
MethodCallHandler,
MethodChannel, MethodResult } from "@ohos/flutter_ohos";
import router from '@ohos.router';
import PhotoPlugin from "./PhotoPlugin";
import { UIAbility } from "@kit.AbilityKit";
export default class FlutterCallNativePlugin implements FlutterPlugin,MethodCallHandler{
private channel: MethodChannel | null = null;
private photoPlugin?:PhotoPlugin;
getUniqueClassName(): string {
return "FlutterCallNativePlugin"
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "selectPhoto":
this.photoPlugin = PhotoPlugin.getInstance();
this.photoPlugin.setDataInfo(call, result ,1)
this.photoPlugin.openImagePicker();
break;
case "selectVideo":
this.photoPlugin = PhotoPlugin.getInstance();
this.photoPlugin.setDataInfo(call, result ,2)
this.photoPlugin.openImagePicker();
break;
default:
result.notImplemented();
break;
}
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_callNative");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
}
}
}
具体步骤
根据传值判断是相册选取还是打开相机
openImagePicker() {
if (this.type === 1) {
this.openCameraTakePhoto()
} else if (this.type === 2) {
this.selectMedia()
} else {
this.selectMedia()
}
}
相册选取照片或视频
用户需要分享图片、视频等文件时,可通过特定接口拉起系统图库,用户选择资源后完成分享。此接口无需申请权限,适用于界面 UIAbility。
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
if (this.mediaType === 1) {
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
} else if (this.mediaType === 2) {
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE
}
photoSelectOptions.maxSelectNumber = this.mediaType === 2 ? 1 : this.maxCount;
photoSelectOptions.isPhotoTakingSupported = false;
photoSelectOptions.isSearchSupported = false;
let uris: Array<string> = [];
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
uris = photoSelectResult.photoUris;
console.info('photoViewPicker.select to file succeed and uris are:' + uris);
}).catch((err: BusinessError) => {
console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
相机拍摄照片或视频
let pathDir = getContext().filesDir;
let fileName = `${new Date().getTime()}`
let filePath = pathDir + `/${fileName}.tmp`
let result: picker.PickerResult
fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
let uri = fileUri.getUriFromPath(filePath);
let pickerProfile: picker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
saveUri: uri
};
if (this.mediaType === 1) {
result = await picker.pick(getContext(), [picker.PickerMediaType.PHOTO], pickerProfile);
} else if (this.mediaType === 2) {
result = await picker.pick(getContext(), [picker.PickerMediaType.VIDEO], pickerProfile);
}
console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);
视频封面处理
let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
avImageGenerator.fdSrc = avFileDescriptor;
let timeUs = 0;
let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC;
let param: media.PixelMapParams = { width: 300, height: 400 };
let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
avImageGenerator.release();
fs.closeSync(file);
const imagePackerApi: image.ImagePacker = image.createImagePacker();
let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
imagePackerApi.packing(pixelMap, packOpts).then(async (buffer: ArrayBuffer) => {
let fileName = `${new Date().getTime()}.tmp`;
let filePath = getContext().cacheDir + fileName;
let file = fileIo.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, buffer);
let urlStr = fileUri.getUriFromPath(filePath);
resolve(urlStr);
})
路径处理
由于鸿蒙设备上的路径 Flutter 的 MultipartFile.fromFile(ipath) 无法读取,需要将路径转换。
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);
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(".");
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 {
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;
}
}
数据返回
let videoUrl = this.retrieveCurrentDirectoryUri(uris[0]);
let coverImageUrl = this.retrieveCurrentDirectoryUri(videoThumb);
map.set("videoUrl", this.retrieveCurrentDirectoryUri(uris[0]));
map.set("coverImageUrl", this.retrieveCurrentDirectoryUri(videoThumb));
this.result?.success(map);
Flutter 调用鸿蒙原生通过路径上传到服务器
MethodChannel communicateChannel = MethodChannel("flutter_callNative");
final result = await communicateChannel.invokeMethod("selectVideo", vars);
if (result["videoUrl"] != null && result["coverImageUrl"] != null) {
String? video = await FileUploader.uploadFile(result["videoUrl"].toString());
String? coverImageUrl = await FileUploader.uploadFile(result["coverImageUrl"].toString());
}
以上是完整的 Flutter 适配鸿蒙系统,调用原生功能实现相册选取与拍照的开发过程。希望对您有所帮助。创作不易,如果我的内容帮助到了您,烦请点赞关注,分享给需要的人,谢谢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。