Flutter调用HarmonyOS6原生功能:实现智感握持

一、概述

今天一时兴起研究了一下智感握持功能,于是顺道把之前一款基于Flutter开发的鸿蒙应用做了智感握持功能的适配。写一篇文章记录分享一下。

"智感握持"并不是一个单独页面或复杂业务,而是一个感知左右手握持状态的能力,在本应用中,最终落在一个非常明确的用户体验点:某页面右下角的"新建按钮"会根据左右手握持自动决定在左侧还是右侧出现(有点类似"开发者联盟"APP中社区页面的设计)

相关开发文档:获取用户动作开发指导-Multimodal Awareness Kit(多模态融合感知服务)-硬件-系统 - 华为HarmonyOS开发者

@ohos.multimodalAwareness.motion (动作感知能力)-ArkTS API-Multimodal Awareness Kit(多模态融合感知服务)-硬件-系统 - 华为HarmonyOS开发者

二、整体架构

我们的 UI 是 Flutter 写的,所以必须把 ArkTS 拿到的数据送到 Dart。

标准做法就是 EventChannel:Native 持续推送事件 → Dart 收到一个 Stream。

所以链路是:

  1. HarmonyOS Motion 推出 HoldingHandStatus/OperatingHandStatus
  2. ArkTS 订阅 Motion,把 status 通过 EventChannel 推给 Flutter
  3. Dart Service订阅 EventChannel 得到 Stream
  4. Dart Controller 把 Stream 变成 ChangeNotifier 状态
  5. UI 监听状态变化并做换边动画

三、实现

3.1 权限声明

开发文档中明确指出,使用motion模块获取用户操作手时,需要这些权限。

若没有获取相关权限的话,会直接导致能力不可用。

TypeScript 复制代码
"requestPermissions":[
    {
      "name" : "ohos.permission.ACTIVITY_MOTION"
    },
    {
      "name" : "ohos.permission.DETECT_GESTURE"
    }
  ]

3.2 HarmonyOS侧

文件:HoldingHandPlugin.ets

它要做的事:订阅 Motion,拿到 status;把 status 推给 Flutter。

3.2.1 通道名必须一致

Dart 侧会用同一个字符串创建 EventChannel,CHANNEL_NAME 必须和 Dart 完全一致

TypeScript 复制代码
const CHANNEL_NAME = 'XXX/motion_holding_hand'
3.2.2 registerWith()

把 EventChannel 绑到 FlutterEngine

TypeScript 复制代码
  static registerWith(flutterEngine: FlutterEngine): void {
    const channel = new EventChannel(flutterEngine.dartExecutor, CHANNEL_NAME)

    const handler: StreamHandler = {
      onListen: (args: Any, events: EventSink) => {
        HoldingHandPlugin.start(events)
      },
      onCancel: (args: Any) => {
        HoldingHandPlugin.stop()
      },
    }

    channel.setStreamHandler(handler)

    Log.i(TAG, 'registered')
  }

Dart 侧开始监听(receiveBroadcastStream())→ ArkTS 触发 onListen → 我们在 start() 里开始 motion.on(...)

Dart 侧取消监听 → ArkTS 触发 onCancel → 我们在 stop() 里 motion.off(...)

3.2.3 start(events):优先握持手,801 再降级到操作手

订阅握持手优先,失败再 fallback

TypeScript 复制代码
  private static start(events: EventSink): void {
    HoldingHandPlugin.sink = events

    if (HoldingHandPlugin.isListening) {
      return
    }

    HoldingHandPlugin.isListening = true

    HoldingHandPlugin.holdingCallback = (data: motion.HoldingHandStatus) => {
      HoldingHandPlugin.emitStatus('holding', data as number)
    }

    try {
      motion.on('holdingHandChanged', HoldingHandPlugin.holdingCallback)
      HoldingHandPlugin.emitStatus('holding', -1)
      Log.i(TAG, 'motion.on(holdingHandChanged) succeeded')
      return
    } catch (err) {
      const error = err as BusinessError
      Log.e(TAG, 'motion.on(holdingHandChanged) failed, code=' + error.code)

      if (error.code !== 801) {
        HoldingHandPlugin.emitError('holdingHandChanged', error)
        return
      }

      HoldingHandPlugin.tryFallbackToOperating()
    }
  }
3.2.4 tryFallbackToOperating():订阅操作手 + 取一次 recent
TypeScript 复制代码
  private static tryFallbackToOperating(): void {
    HoldingHandPlugin.operatingCallback = (data: motion.OperatingHandStatus) => {
      HoldingHandPlugin.emitStatus('operating', data as number)
    }

    try {
      motion.on('operatingHandChanged', HoldingHandPlugin.operatingCallback)
      Log.i(TAG, 'fallback: motion.on(operatingHandChanged) succeeded')
      try {
        const recent = motion.getRecentOperatingHandStatus()
        HoldingHandPlugin.emitStatus('operating', recent as number)
      } catch (recentErr) {
        // ignore
      }
    } catch (err) {
      const error = err as BusinessError
      HoldingHandPlugin.emitError('operatingHandChanged', error)
      Log.e(TAG, 'fallback: motion.on(operatingHandChanged) failed, code=' + error.code)
    }
  }

降级后:motion.on('operatingHandChanged')

3.2.5 stop():取消订阅
TypeScript 复制代码
  private static stop(): void {
    HoldingHandPlugin.isListening = false

    try {
      motion.off('holdingHandChanged')
    } catch (err) {
      // ignore
    }

    try {
      motion.off('operatingHandChanged')
    } catch (err) {
      // ignore
    }

    HoldingHandPlugin.holdingCallback = null
    HoldingHandPlugin.operatingCallback = null
    HoldingHandPlugin.sink = null
  }

如果不取消订阅,会导致:

  • 页面/订阅重建导致重复注册回调
  • 一直工作,耗电且不可控
3.2.6 emitStatus():真正把数据送到 Flutter
TypeScript 复制代码
  private static emitStatus(source: string, status: number): void {
    if (HoldingHandPlugin.sink == null) {
      return
    }

    HoldingHandPlugin.sink.success({
      source: source,
      status: status,
      ts: Date.now(),
    })
  }
3.2.7 插件注册:EntryAbility.ets

没有这一步,插件不会生效:

TypeScript 复制代码
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import { HoldingHandPlugin } from '../plugins/HoldingHandPlugin';

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    HoldingHandPlugin.registerWith(flutterEngine) // 智感握持
  }
}

3.3 Flutter Service

3.3.1 通道名必须一致

_channelName 必须和鸿蒙侧 HoldingHandPlugin.ets 的 CHANNEL_NAME 完全一致

TypeScript 复制代码
  static const String _channelName = 'lrtimer/motion_holding_hand';

  final EventChannel _channel = const EventChannel(_channelName);
3.3.2 stream:只在 ohos 端订阅
TypeScript 复制代码
  Stream<dynamic> get stream {
    if (Platform.operatingSystem != 'ohos') {
      return const Stream<dynamic>.empty();
    }
    return _channel.receiveBroadcastStream();
  }
3.3.3 parseSide():按文档映射
TypeScript 复制代码
  static const int _leftValue = 1;
  static const int _rightValue = 2;

  HoldingSide parseSide(Object? event) {
    int? status;

    if (event is Map) {
      final raw = event['status'];
      if (raw is int) {
        status = raw;
      }
    }

    status ??= event is int ? event : null;

    if (status == _leftValue) {
      return HoldingSide.left;
    }
    if (status == _rightValue) {
      return HoldingSide.right;
    }
    return HoldingSide.unknown;
  }
}
  • Motion → status 数值
  • Service → 把 status 变成 HoldingSide
  • UI 只看 HoldingSide

3.4 Flutter Controller

把 Stream 变成全局状态

TypeScript 复制代码
void start() {
  _sub?.cancel();
  _sub = _service.stream.listen(
    (event) {
      final next = _service.parseSide(event);
      if (next == _side) {
        return;
      }
      _side = next;
      notifyListeners();
    },
    onError: (_) {
      // 不支持/无权限等情况:保持默认 unknown,不崩溃。
    },
  );
}

3.5 在 App 启动时把 Controller 跑起来

在 main.dart:

TypeScript 复制代码
ChangeNotifierProvider<HoldingHandController>(
  create: (_) => HoldingHandController()..start(),
),
  • App 启动即订阅
  • 任意页面都能通过 Provider 读取当前 side

3.6 UI接入

我们这里用 GripAwareNewFab 做了三件事:

  1. 读 HoldingHandController 的 side
  2. side 变了就启动两段式动画:先滑出 → 切边 → 再滑入
  3. 用 Positioned(left/right) 决定按钮贴哪边

关键监听逻辑:

TypeScript 复制代码
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    final next = context.read<HoldingHandController>();
    if (_controller != next) {
      _controller?.removeListener(_handleGripChanged);
      _controller = next;
      _controller?.addListener(_handleGripChanged);

      // 初始化显示位置
      _displaySide ??= _controller?.side;
      _pendingSide ??= _displaySide;
    }
  }

  void _handleGripChanged() {
    // ...
    _pendingSide = nextSide;

    if (_stage != _FabTransitionStage.idle) {
      return;
    }
    if (_displaySide == nextSide) {
      return;
    }

    _stage = _FabTransitionStage.exiting;
    _anim
      ..reset()
      ..forward();

    setState(() {});
  }

决定贴左还是贴右:

TypeScript 复制代码
return Positioned(
  left: isLeft ? horizontal : null,
  right: isLeft ? null : horizontal,
  bottom: bottom,
  // ...
);

四、实现效果

智感握持效果

相关推荐
2601_949575862 小时前
Flutter for OpenHarmony二手物品置换App实战 - 商品卡片实现
android·flutter
时光慢煮4 小时前
基于 Flutter × OpenHarmony 的文件管家 - 构建常用文件夹区域
flutter·华为·开源·openharmony
2601_949575864 小时前
Flutter for OpenHarmony二手物品置换App实战 - 表单验证实现
android·java·flutter
b2077215 小时前
Flutter for OpenHarmony 身体健康状况记录App实战 - 健康目标实现
python·flutter·harmonyos
血色橄榄枝7 小时前
04-06 Flutter列表清单实现上拉加载 + 下拉刷新 + 数据加载提示 On OpenHarmony
flutter
小风呼呼吹儿7 小时前
Flutter 框架跨平台鸿蒙开发 - 书法印章制作记录应用开发教程
flutter·华为·harmonyos
●VON7 小时前
从系统亮度监听到 UI 重绘:Flutter for OpenHarmony TodoList 深色模式的端到端响应式实现
学习·flutter·ui·openharmony·布局·von
恋猫de小郭7 小时前
Android Gradle Plugin 9.0 发布,为什么这会是个史诗级大坑版本
android·flutter·ios·开源
一起养小猫7 小时前
Flutter实战:从零实现俄罗斯方块(三)交互控制与事件处理
javascript·flutter·交互