Flutter 与HarmonyOS Next 混合渲染开发实践:以 fluttertpc_scan 三方库为例

发布于:2025-05-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、背景与价值

在跨平台开发中,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