一、背景与价值
在跨平台开发中,Flutter 以其高效的 UI 构建能力著称,而鸿蒙 Next(OpenHarmony)则提供了深度系统集成的原生能力。将两者结合,可实现 UI 跨平台 + 原生功能深度融合 的混合渲染模式。本文以扫描库 fluttertpc_scan 为例,详解混合开发的核心流程。
二、环境配置(关键细节优化)
1. 工具链版本要求
工具 | 最低版本 | 备注 |
---|---|---|
Flutter SDK | 3.19.0+ | 支持ohos的FlutterSDK |
DevEco Studio | 5.0.1 Release | 需配置 OpenHarmony 5.0+ SDK |
2. 环境验证
# 验证 Flutter 环境
flutter doctor -v
# 确认鸿蒙 SDK 路径
# DevEco Studio → File → Settings → SDK Manager → OpenHarmony SDK
三、混合渲染实现(代码深度优化)
1. 鸿蒙原生模块开发
1.1 二维码扫描组件封装
@Component
export struct CustomScanPage {
@StorageLink(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME)
@Watch('onBackgroundUpdate') isBackground: boolean = false;
@State params: Map<String, Object> | null = null
@State customScanVM: CustomScanViewModel = CustomScanViewModel.getInstance();
private mXComponentController: XComponentController = new XComponentController();
@State scanLineColor: string = "#ff4caf50"
private mScale: number = 1.0
@State animationOrdinate: number = CameraConstants.SCAN_DIVIDER_OFFSET_BEGIN
@State pauseScan: boolean = false
args?: Params
build() {
Column() {
Stack() {
XComponent({
id: CameraConstants.CAMERA_COMPONENT_ID,
type: CameraConstants.CAMERA_COMPONENT_TYPE,
controller: this.mXComponentController
})
.onLoad(() => {
Log.i(TAG, 'XComponent onLoad')
this.customScanStart()
})
.width(this.customScanVM.cameraCompWidth)
.height(this.customScanVM.cameraCompHeight)
.position({
x: CameraConstants.SCAN_COMPONENT_POSITION_X,
y: CameraConstants.SCAN_COMPONENT_POSITION_Y
})
this.ScanBorder()
}
.alignContent(Alignment.Center)
.height(CameraConstants.SCAN_COMPONENT_WIDTH_100)
.width(CameraConstants.SCAN_COMPONENT_WIDTH_100)
.position({
x: CameraConstants.SCAN_COMPONENT_POSITION_X,
y: CameraConstants.SCAN_COMPONENT_POSITION_Y
})
.backgroundColor(Color.Grey)
}
.height(CameraConstants.SCAN_COMPONENT_WIDTH_100)
.width(CameraConstants.SCAN_COMPONENT_WIDTH_100)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
}
aboutToAppear(): void {
Log.i(TAG, 'aboutToAppear')
this.initParams()
// 注册XComp尺寸修改回调
this.customScanVM.regXCompSizeUpdateListener((width: number, height: number) => {
// 动态更新XComponent的Surface尺寸
this.updateCameraSurfaceSize(width, height);
})
// 注册扫描状态监听回调
this.customScanVM.regScanStatusUpdateListener((isPause: boolean) => {
/*测试,resume ispause=false时,延时显示动画效果,检查是否黑屏*/
if (!isPause) {
setTimeout(() => {
this.pauseScan = isPause;
}, 500);
} else {
this.pauseScan = isPause;
}
})
}
}
1.2 插件编写
export class ScanPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
private channel: MethodChannel | null = null
private flutterPluginBinding: FlutterPluginBinding | null = null
private ability: UIAbility | null = null
private mainWindow: window.Window | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
Log.i(TAG, 'onAttachedToEngine')
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME)
this.channel?.setMethodCallHandler(this)
this.flutterPluginBinding = binding
binding.getPlatformViewRegistry()
.registerViewFactory(CHANNEL_VIEW_NAME, new ScanViewFactory(binding.getBinaryMessenger()))
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
Log.i(TAG, 'onDetachedFromEngine')
this.channel?.setMethodCallHandler(null)
this.channel = null
}
onDetachedFromAbility(): void {
this.ability = null
this.offWindowEvent()
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.ability = binding.getAbility()
}
onWindowEvent(context: Context) {
try {
if (this.mainWindow == null) {
this.mainWindow = FlutterManager.getInstance()
.getWindowStage(FlutterManager.getInstance().getUIAbility(context))
.getMainWindowSync();
this.mainWindow?.on('windowEvent', (data: window.WindowEventType) => {
if (data === window.WindowEventType.WINDOW_SHOWN) {
AppStorage.setOrCreate(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME, false)
} else if (data === window.WindowEventType.WINDOW_HIDDEN) {
AppStorage.setOrCreate(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME, true)
}
});
}
} catch (exception) {
Log.e(TAG, 'Failed to register callback. Cause: ' + JSON.stringify(exception));
}
}
offWindowEvent() {
try {
this.mainWindow?.off('windowEvent');
} catch (exception) {
Log.e(TAG, 'Failed to register callback. Cause: ' + JSON.stringify(exception));
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
this.onWindowEvent(this.ability!.context)
try {
switch (call.method) {
case "getPlatformVersion":
this.getInfo(result)
break;
case "parse":
this.imgParse(call, result)
break;
default:
result.notImplemented()
break;
}
} catch (err) {
Log.e(TAG, 'onMethodCall failed: ' + err);
result.error("BarcodeScanPlugin", "onMethodCall failed with err", err);
}
}
getUniqueClassName(): string {
Log.i(TAG, 'getUniqueClassName')
return TAG
}
}
其中最重要的就是下面这段,这段代码的作用是将一个自定义的原生视图工厂(ScanViewFactory)注册到 Flutter 引擎中。注册完成后,Flutter 端可以通过 CHANNEL_VIEW_NAME 来请求创建这个原生视图。具体来说,当 Flutter 端使用 AndroidView 或 UiKitView(在 iOS 中)时,Flutter 引擎会调用注册的 PlatformViewFactory 来创建对应的原生视图
binding.getPlatformViewRegistry()
.registerViewFactory(CHANNEL_VIEW_NAME, new ScanViewFactory(binding.getBinaryMessenger()))
2. Flutter 端集成(增强健壮性)
2.1 插件接口定义
class Scan {
static const MethodChannel _channel = const MethodChannel('chavesgu/scan');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static Future<String?> parse(String path) async {
final String? result = await _channel.invokeMethod('parse', path);
return result;
}
}
void _onPlatformViewCreated(int id) {
_channel = MethodChannel('chavesgu/scan/method_$id');
_channel?.setMethodCallHandler((MethodCall call) async {
if (call.method == 'onCaptured') {
if (widget.onCapture != null)
widget.onCapture!(call.arguments.toString());
}
});
widget.controller?._channel = _channel;
}
2.2 混合渲染页面
class _ScanViewState extends State<ScanView> {
MethodChannel? _channel;
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'chavesgu/scan_view',
creationParamsCodec: StandardMessageCodec(),
creationParams: {
"r": widget.scanLineColor.red,
"g": widget.scanLineColor.green,
"b": widget.scanLineColor.blue,
"a": widget.scanLineColor.opacity,
"scale": widget.scanAreaScale,
},
onPlatformViewCreated: (id) {
_onPlatformViewCreated(id);
},
);
} else if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'chavesgu/scan_view',
creationParamsCodec: StandardMessageCodec(),
creationParams: {
"r": widget.scanLineColor.red,
"g": widget.scanLineColor.green,
"b": widget.scanLineColor.blue,
"a": widget.scanLineColor.opacity,
"scale": widget.scanAreaScale,
},
onPlatformViewCreated: (id) {
_onPlatformViewCreated(id);
},
);
} else if (defaultTargetPlatform == TargetPlatform.ohos) {
return OhosView(
viewType: 'chavesgu/scan_view',
creationParamsCodec: StandardMessageCodec(),
creationParams: {
"r": widget.scanLineColor.red,
"g": widget.scanLineColor.green,
"b": widget.scanLineColor.blue,
"a": widget.scanLineColor.opacity,
"scale": widget.scanAreaScale,
},
onPlatformViewCreated: (id) {
_onPlatformViewCreated(id);
},
);
} else {
return Text('平台暂不支持');
}
}
}
以上代码根据不同的目标平台(iOS、Android 或 OpenHarmony)创建对应的原生视图。它通过 PlatformView(如 UiKitView、AndroidView 和 OhosView)来嵌入原生代码实现的功能(如二维码扫描)
四、关键技术点讲解
1. Flutter与HarmonyOSNEXT通信
在上述代码中,我们通过creationParams传递了部分数据给原生鸿蒙的view,那他如何接收呢,这就需要在鸿蒙端做处理了,我们在继承FlutterPlugin的类ScanPlugin中注册了一个工厂类ScanViewFactory,当有数据过来的时候,会通过binding.getBinaryMessenger()传递到这个类中
import {
BinaryMessenger,
PlatformView,
Log,
PlatformViewFactory,
StandardMessageCodec
} from '@ohos/flutter_ohos';
import { ScanPlatformView } from '../views/ScanPlatformView';
const TAG: string = "FlutterScanPlugin";
export class ScanViewFactory extends PlatformViewFactory {
private messenger: BinaryMessenger;
constructor(messenger: BinaryMessenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
public create(context: Context, id: number, args: Object): PlatformView {
Log.i(TAG, 'create')
let params: Map<String, Object> = args as Map<String, Object>
return new ScanPlatformView(context, this.messenger, id, params);
}
}
在create函数中获取到了一个HashMap,这个MAP里就是我们将要获取到的数据,在ScanPlatformView类的构造函数中我们将params拿出来,getView会将信息传递给最终要展示扫码界面的鸿蒙UI页面
@Observed
export class ScanPlatformView extends PlatformView implements MethodCallHandler, QRCodeReadListener {
///省略部分代码...
public getView(): WrappedBuilder<[Params]> {
// 返回 WrappedBuilder<[Params]>,严格匹配基类要求
return new WrappedBuilder(CustomScanPage({ args: params }));
}
///省略部分代码...
}
@Component
export struct CustomScanPage {
///省略部分代码...
aboutToAppear(): void {
if(this.args!=null){
const scanPlatformView = this.args.platformView as ScanPlatformView;
this.params = scanPlatformView.params!;
}
if (this.params) {
this.mScale = this.params.get("scale") as number
this.customScanVM.setScale(this.mScale)
Log.i(TAG, 'initParams mScale=' + this.mScale)
let r: number = this.params.get("r") as number
let g: number = this.params.get("g") as number
let b: number = this.params.get("b") as number
let a: number = this.params.get("a") as number
a = Math.max(0, Math.min(255, a));
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// const alpha = Math.max(0, Math.min(255, Math.floor(a * 256.0)));
// 将 alpha 从 0-255 转换为 0-1 的浮点数
// const result = a / 255;
// const alpha = Math.round(result * 100) / 100
Log.i(TAG, 'initParams scanLineColor a =' + a)
this.scanLineColor = this.rgbaToHex(a, r, g, b)
}
///省略部分代码...
}
2. Flutter 端使用
class ScanPage extends StatelessWidget {
ScanController controller = ScanController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: true,
bottom: true,
child: Stack(
children: [
ScanView(
controller: controller,
scanAreaScale: .7,
scanLineColor: Colors.red,
onCapture: (data) {
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('scan result'),
),
body: Center(
child: Text(data),
),
);
},
)).then((value) {
controller.resume();
});
},
),
Positioned(
bottom: 0,
child: Row(
children: [
ElevatedButton(
child: Text("toggleTorchMode"),
onPressed: () {
controller.toggleTorchMode();
},
),
ElevatedButton(
child: Text("pause"),
onPressed: () {
controller.pause();
},
),
ElevatedButton(
child: Text("resume"),
onPressed: () {
controller.resume();
},
),
],
),
),
],
),
),
);
}
}
通过本文的接收,相信开发者可快速实现 Flutter 与鸿蒙 Next 的深度集成,在保证跨平台 UI 一致性的同时,充分发挥鸿蒙原生能力。
声明:因原始仓库https://gitcode.com/openharmony-sig/fluttertpc_scan%E6%9C%89BUG%EF%BC%8C%E6%97%A0%E6%B3%95%E6%8F%90%E4%BA%A4PR%EF%BC%8C%E6%95%85%E6%88%91%E6%89%8D%E9%87%8D%E6%96%B0%E4%B8%8A%E4%BC%A0%E4%BA%86%E4%B8%80%E4%BB%BD%EF%BC%81