Flutter 与HarmonyOS Next 混合渲染开发实践:以 fluttertpc_scan 三方库为例
一、背景与价值
在跨平台开发中,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 = falseargs?: Paramsbuild() {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 = nullprivate flutterPluginBinding: FlutterPluginBinding | null = nullprivate ability: UIAbility | null = nullprivate 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 = bindingbinding.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 = nullthis.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;@overrideWidget 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 numberthis.customScanVM.setScale(this.mScale)Log.i(TAG, 'initParams mScale=' + this.mScale)let r: number = this.params.get("r") as numberlet g: number = this.params.get("g") as numberlet b: number = this.params.get("b") as numberlet a: number = this.params.get("a") as numbera = 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) / 100Log.i(TAG, 'initParams scanLineColor a =' + a)this.scanLineColor = this.rgbaToHex(a, r, g, b)}
///省略部分代码...
}
2. Flutter 端使用
class ScanPage extends StatelessWidget {ScanController controller = ScanController();@overrideWidget 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
相关文章:
Flutter 与HarmonyOS Next 混合渲染开发实践:以 fluttertpc_scan 三方库为例
一、背景与价值 在跨平台开发中,Flutter 以其高效的 UI 构建能力著称,而鸿蒙 Next(OpenHarmony)则提供了深度系统集成的原生能力。将两者结合,可实现 UI 跨平台 原生功能深度融合 的混合渲染模式。本文以扫描库 flut…...

LangChain4j正式发布-简化将 LLM 集成到 Java 应用程序过程
LangChain4j 的目标是简化将 LLM 集成到 Java 应用程序中的过程。 官网地址 源码地址 开源协议:Apache License 2.0 实现方法 统一 API:LLM 提供程序(如 OpenAI 或 Google Vertex AI)和嵌入(矢量)存储…...

【C++】汇编角度分析栈攻击
栈攻击 介绍原理示例代码汇编分析 介绍原理 核心原理是通过 缓冲区溢出(Buffer Overflow) 等漏洞,覆盖栈上的关键数据(如返回地址、函数指针),从而改变程序执行流程; 在 C 中,每个…...

Vue 3 打开 el-dialog 时使 el-input 获取焦点
运行代码:https://andi.cn/page/622178.html 效果:...

C++23 views::repeat (P2474R2) 深入解析
文章目录 引言C20 Ranges库回顾什么是Rangesstd::views的作用 views::repeat概述基本概念原型定义工作原理应用场景初始化容器模拟测试数据 总结 引言 在C的发展历程中,每一个新版本都会带来一系列令人期待的新特性,这些特性不仅提升了语言的性能和表达…...
HTML5 定位详解:相对定位、绝对定位和固定定位
在HTML5和CSS中,定位(positioning)是控制元素在页面上位置的重要机制。主要有四种定位方式:静态定位(static)、相对定位(relative)、绝对定位(absolute)和固定定位(fixed)。下面我将详细讲解这三种非静态定位方式,并提供相应的源代码示例。 …...

OpenCv高阶(4.0)——案例:海报的透视变换
文章目录 前言一、工具函数模块1.1 图像显示函数1.2 保持宽高比的缩放函数1.3 坐标点排序函数 二、透视变换核心模块2.1 四点透视变换实现 三、主流程技术分解3.1 图像预处理3.2 轮廓检测流程3.3 最大轮廓处理 四、后处理技术4.1 透视变换4.2 形态学处理 五、完整代码总结 前言…...

光谱相机的图像预处理技术
光谱相机的图像预处理技术旨在消除噪声、增强有效信息,为后续分析提供高质量数据。 一、预处理流程与技术要点 辐射校正 辐射定标:将图像灰度值转换为绝对辐射亮度,常用反射率法、辐亮度法和辐照度法消除传感器响应差异࿰…...
CSS 溢出内容处理、可见性控制与盒类型设置深度解析
CSS溢出内容处理、可见性控制与盒类型设置深度解析 一、溢出内容处理(Overflow) 在网页设计中,内容超出容器边界是常见问题。CSS提供了overflow属性及其变体来控制这种情况。 1.1 溢出基本属性 核心属性: overflow: visible&…...

k8s监控方案实践补充(一):部署Metrics Server实现kubectl top和HPA支持
k8s监控方案实践补充(一):部署Metrics Server实现kubectl top和HPA支持 文章目录 k8s监控方案实践补充(一):部署Metrics Server实现kubectl top和HPA支持一、Metrics Server简介二、Metrics Server实战部署…...
从代码学习深度学习 - 实战 Kaggle 比赛:图像分类 (CIFAR-10 PyTorch版)
文章目录 前言1. 读取并整理数据集1.1 读取标签文件1.2 划分训练集和验证集1.3 整理测试集1.4 执行数据整理2. 图像增广2.1 训练集图像变换2.2 测试集(和验证集)图像变换3. 读取数据集3.1 创建 Dataset 对象3.2 创建 DataLoader 对象4. 定义模型4.1 获取 ResNet-18 模型4.2 损…...
【数据结构】二分查找5.12
Basic 需求:在有序数组A内,查找值target 如果找到返回索引 如果找不到返回-1 算法描述: 前提:给定一个内含n个元素的有序数组A(升序),一个待查找值 设置两个索引:i0;jn-1; 如果…...
深入探索向量数据库:构建智能应用的新基础
📌 友情提示: 本文内容由银河易创AI(https://ai.eaigx.com)创作平台的gpt-4-turbo模型辅助生成,旨在提供技术参考与灵感启发。文中观点或代码示例需结合实际情况验证,建议读者通过官方文档或实践进一步确认…...
Swagger go中文版本手册
Swaggo(github.com/swaggo/swag)的注解语法是基于 OpenAPI 2.0 (以前称为 Swagger 2.0) 规范的,并添加了一些自己的约定。 主要官方文档: swaggo/swag GitHub 仓库: 这是最权威的来源。 链接: https://github.com/swaggo/swag重点关注: README.md: 包含了基本的安装、使用…...
Cloudera CDP 7.1.3 主机异常关机导致元数据丢失,node不能与CM通信
问题描述 plaintext ERROR Could not load post-deployment data from /var/run/cloudera-scm-agent/process/ccdeploy_hadoop-conf_etchadoopconf.cloudera.yarn_-8903374259073700469 IOError: [Errno 2] No such file or directory: /var/run/cloudera-scm-agent/proce…...
Redis特性与应用
1、分布式缓存与redis 2、redis数据结构和客户端集成 3、缓存读写模式与数据一致性 本地缓存:Hash Map、Ehcache、Caffeine、Google Guava 分布式缓存:Memcached、redis、Hazelcast、Apache ignite redis:基于键值对内存数据库,支…...

嵌入式调试新宠!J-Scope:免费+实时数据可视化,让MCU调试效率飙升!
📌 痛点直击:调试还在用“断点打印”? 嵌入式开发中,你是否也经历过这些崩溃瞬间? 想实时观察变量变化,代码里插满printf,结果拖垮系统性能? 断点调试打断程序运行,时序…...

微信小程序学习之搜索框
1、第一步,我们在index.json中引入vant中的搜索框控件: {"usingComponents": {"van-search": "vant/weapp/search/index"} } 2、第二步,直接在index.wxml中添加布局: <view class"index…...

Altium Designer AD如何输出PIN带网络名的PDF装配图
Altium Designer AD如何输出PIN带网络名的PDF装配图 文描述在Altium Designer版本中设置焊盘网络名时遇到的问题,网络名大小不一致,部分PAD的网络名称未显示,可能涉及字符大小设置和版本差异。 参考 1.AD导出PCB装配图 https://blog.csd…...

VMware虚拟机 安装 CentOS 7
原文链接: VMware虚拟机 安装 CentOS 7 安装准备 软件: VMware Workstation Pro 17.6.3 镜像: CentOS-7.0-1406-x86_64-DVD.iso 我打包好放这了,VMware 和 CentOS7 ,下载即可。 关于VMware Workstation Pro 17.6.3,傻瓜式安装即可。 CentO…...
关于高并发GIS数据处理的一点经验分享
1、背景介绍 笔者过去几年在参与某个大型央企的项目开发过程中,遇到了十分棘手的难题。其与我们平常接触的项目性质完全不同。在一般的项目中,客户一般只要求我们能够通过桌面软件对原始数据进行加工处理,将各类地理信息数据加工处理成地图/场景和工作空间,然后再将工作空…...
Python训练打卡Day22
复习日: 1.标准化数据(聚类前通常需要标准化) scaler StandardScaler() X_scaled scaler.fit_transform(X) StandardScaler() :这部分代码调用了 StandardScaler 类的构造函数。在Python中,当你在类名后面加上括号…...

Cold Diffusion: Inverting Arbitrary Image Transforms Without Noise论文阅读
冷扩散:无需噪声的任意图像变换反转 摘要 标准扩散模型通常涉及两个核心步骤:图像降质 (添加高斯噪声)和图像恢复 (去噪操作)。本文发现,扩散模型的生成能力并不强烈依赖于噪声的选择…...
2025认证杯数学建模第二阶段C题:化工厂生产流程的预测和控制,思路+模型+代码
2025认证杯数学建模第二阶段思路模型代码,详细内容见文末名片 一、探秘化工世界:问题背景大揭秘 在 2025 年 “认证杯”数学中国数学建模网络挑战赛第二阶段 C 题中,我们一头扎进了神秘又复杂的化工厂生产流程预测与控制领域。想象一下&…...
物联网驱动的共享充电站系统:智能充电的实现原理与技术解析!
随着新能源汽车的快速普及,共享充电站系统作为其核心基础设施,正通过物联网技术的深度赋能,实现从“传统充电”到“智能充电”的跨越式升级。本文将从系统架构、核心技术、优化策略及实际案例等角度,解析物联网如何驱动共享充电站…...

嵌软面试每日一阅----通信协议篇(二)之TCP
一. TCP和UDP的区别 可靠性 TCP:✅ 可靠传输(三次握手 重传机制) UDP:❌ 不可靠(可能丢包) 连接方式 TCP:面向连接(需建立/断开连接) UDP:无连接࿰…...

机器学习 --- 模型选择与调优
机器学习 — 模型选择与调优 文章目录 机器学习 --- 模型选择与调优一,交叉验证1.1 保留交叉验证HoldOut1.2 K-折交叉验证(K-fold)1.3 分层k-折交叉验证Stratified k-fold 二,超参数搜索三,鸢尾花数据集示例四,现实世界数据集示例…...
《Python星球日记》 第58天:Transformer 与 BERT
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、引言一、Transformer 架构简介1. 自注意力机制(Self-Attention)工作原理2. 多头注意力与位置编码多头注意力机制位置编码二、BERT 的结构…...
2900. 最长相邻不相等子序列 I
2900. 最长相邻不相等子序列 I class Solution:def getLongestSubsequence(self, words: List[str], groups: List[int]) -> List[str]:n len(groups) # 获取 groups 列表的长度ans [] # 初始化一个空列表,用于存储结果for i, g in enumerate(groups): # 遍…...

AGI大模型(15):向量检索之调用ollama向量数据库
这里介绍将向量模型下载到本地,这里使用ollama,现在本地安装ollama,这里就不过多结束了。直接从下载开始。 1 下载模型 首先搜索模型,这里使用bge-large模型,你可以根据自己的需要修改。 点击进入,复制命令到命令行工具中执行。 安装后查看: 2 代码实现 先下载ollama…...