Flutter调用HarmonyOS6原生功能:实现智感握持
今天一时兴起研究了一下智感握持功能,于是顺道把之前一款基于Flutter开发的鸿蒙应用做了智感握持功能的适配。写一篇文章记录分享一下。“智感握持”并不是一个单独页面或复杂业务,而是一个感知左右手握持状态的能力,在本应用中,最终落在一个非常明确的用户体验点:某页面右下角的“新建按钮”会根据左右手握持自动决定在左侧还是右侧出现(有点类似“开发者联盟”APP中社区页面的设计)
一、概述
今天一时兴起研究了一下智感握持功能,于是顺道把之前一款基于Flutter开发的鸿蒙应用做了智感握持功能的适配。写一篇文章记录分享一下。
“智感握持”并不是一个单独页面或复杂业务,而是一个感知左右手握持状态的能力,在本应用中,最终落在一个非常明确的用户体验点:某页面右下角的“新建按钮”会根据左右手握持自动决定在左侧还是右侧出现(有点类似“开发者联盟”APP中社区页面的设计)
相关开发文档:获取用户动作开发指导-Multimodal Awareness Kit(多模态融合感知服务)-硬件-系统 - 华为HarmonyOS开发者
二、整体架构
我们的 UI 是 Flutter 写的,所以必须把 ArkTS 拿到的数据送到 Dart。
标准做法就是 EventChannel:Native 持续推送事件 → Dart 收到一个 Stream。
所以链路是:
- HarmonyOS Motion 推出
HoldingHandStatus/OperatingHandStatus - ArkTS 订阅 Motion,把
status通过 EventChannel 推给 Flutter - Dart Service订阅 EventChannel 得到 Stream
- Dart Controller把 Stream 变成
ChangeNotifier状态 - UI 监听状态变化并做换边动画
三、实现
3.1 权限声明
开发文档中明确指出,使用motion模块获取用户操作手时,需要这些权限。
若没有获取相关权限的话,会直接导致能力不可用。
"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 完全一致
const CHANNEL_NAME = 'XXX/motion_holding_hand'
3.2.2 registerWith()
把 EventChannel 绑到 FlutterEngine
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
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
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():取消订阅
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
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
没有这一步,插件不会生效:
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 完全一致
static const String _channelName = 'lrtimer/motion_holding_hand';
final EventChannel _channel = const EventChannel(_channelName);
3.3.2 stream:只在 ohos 端订阅
Stream<dynamic> get stream {
if (Platform.operatingSystem != 'ohos') {
return const Stream<dynamic>.empty();
}
return _channel.receiveBroadcastStream();
}
3.3.3 parseSide():按文档映射
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 变成全局状态
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:
ChangeNotifierProvider<HoldingHandController>(
create: (_) => HoldingHandController()..start(),
),
- App 启动即订阅
- 任意页面都能通过 Provider 读取当前 side
3.6 UI接入
我们这里用 GripAwareNewFab 做了三件事:
- 读 HoldingHandController 的 side
- side 变了就启动两段式动画:先滑出 → 切边 → 再滑入
- 用 Positioned(left/right) 决定按钮贴哪边
关键监听逻辑:
@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(() {});
}
决定贴左还是贴右:
return Positioned(
left: isLeft ? horizontal : null,
right: isLeft ? null : horizontal,
bottom: bottom,
// ...
);
四、实现效果
智感握持效果
更多推荐


所有评论(0)