基于鸿蒙原生手持感知能力,实现设备握持姿态实时识别,左手持机图片居左、右手持机图片居右,配合流畅布局动画,打造更贴合单手操作习惯的新闻阅读体验。
效果说明
请求手持握姿势检测权限,授权成功即可识别左右手姿态,新闻卡片会根据握持方式自动交换图文位置 ,切换过程平滑无卡顿。

核心技术
- 申请权限配置
module.json5配置权限:ohos.permission.DETECT_GESTURE - 动态权限申请
手势权限为敏感权限,通过abilityAccessCtrl动态申请,保证功能可用。 - 握持状态识别
使用motion模块监听holdingHandChanged事件,获取左手/右手握持状态。 - 响应式状态驱动
通过@Local声明响应式变量,状态变更自动刷新UI布局。 - 容器动画
在组件容器上配置非对称动画,布局顺序变化时自动执行平滑过渡。
实现思路
- 页面初始化时申请
ohos.permission.DETECT_GESTURE权限。 - 权限通过后,注册握持姿态监听。
- 检测到左手/右手时,更新
isRightMode状态。 - 状态变化触发新闻卡片非对称动画执行切换图文排列方向
- 离开页面生命周期函数中关闭监听这一点不要忘记哦。
真机实测中发现太灵敏,偶发手机脱离手放到桌子上也会执行一次,可能我手机带着手机壳,或者放的时候误触。
关键代码说明
typescript
// 状态变更自动切换布局,直接访问this.isRightMode,不通过传参
@Builder
NewsCard(item: NewsItem) {
Row() {
if (this.isRightMode) {
this.NewsTextColumn(item)
this.NewsImage(item)
} else {
this.NewsImage(item)
this.NewsTextColumn(item)
}
}
}
运行要求
- 必须真机运行,模拟器不支持握持传感器。
- 开启「手势检测」权限。
- 鸿蒙版本:6.0+、API20+
完整示例
javascript
import { motion } from '@kit.MultimodalAwarenessKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { abilityAccessCtrl } from '@kit.AbilityKit';
// 新闻数据模型
class NewsItem {
id: number;
title: string;
summary: string;
imageColor: Color;
constructor(id: number, title: string, summary: string, color: Color) {
this.id = id;
this.title = title;
this.summary = summary;
this.imageColor = color;
}
}
@Entry
@ComponentV2
struct Index {
@Local isRightMode: boolean = false;
@Local newsList: NewsItem[] = [
new NewsItem(1, "鸿蒙Next正式发布", "纯血鸿蒙不再兼容安卓,开启移动操作系统新纪元。", Color.Blue),
new NewsItem(2, "V哥聊技术", "深度解析ArkTS语言特性,带你弯道超车。", Color.Red),
new NewsItem(3, "2026行业展望", "AI赛道爆发,普通程序员如何抓住最后的机会?", Color.Green),
new NewsItem(4, "SpaceX星舰发射", "马斯克火星殖民计划又近了一步,震撼全人类。", Color.Orange),
new NewsItem(5, "周末去哪儿玩", "发现城市周边的小众露营地,放松身心好去处。", Color.Pink),
];
// 握持状态变化回调
private holdingHandCallback = (data: motion.HoldingHandStatus) => {
switch (data) {
case motion.HoldingHandStatus.LEFT_HAND_HELD:
this.isRightMode = false;
break;
case motion.HoldingHandStatus.RIGHT_HAND_HELD:
this.isRightMode = true;
break;
default:
break;
}
};
async aboutToAppear(): Promise<void> {
const atManager = abilityAccessCtrl.createAtManager();
const context = this.getUIContext().getHostContext();
try {
atManager.requestPermissionsFromUser(context, ['ohos.permission.DETECT_GESTURE'],
(err, data) => {
if (err) {
console.error(`申请失败: ${err.message}`);
} else {
if (data.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.info("用户已授权");
motion.on('holdingHandChanged', this.holdingHandCallback);
promptAction.showToast({ message: "已开启智能握持适配" });
console.info('握持状态监听已启动');
} else {
promptAction.showToast({ message: "请开启手势检测权限" });
console.error('权限被拒绝');
}
}
});
} catch (err) {
let error = err as BusinessError;
console.error(`权限申请失败: ${error.code}, ${error.message}`);
}
}
aboutToDisappear(): void {
try {
motion.off('holdingHandChanged', this.holdingHandCallback);
console.info('握持状态监听已关闭');
} catch (err) {
console.error('关闭监听失败');
}
}
// 构建单个新闻卡片(带动画)
@Builder
NewsCard(item: NewsItem) {
Row({space:20}) {
if (this.isRightMode) {
// 右手模式:文字在左,图片在右
this.NewsTextColumn(item)
this.NewsImage(item)
} else {
// 左手模式:图片在左,文字在右
this.NewsImage(item)
this.NewsTextColumn(item)}
}
.width('100%')
.padding(12)
.margin({ bottom: 8 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.1)', offsetY: 2 })
.animation({ duration: 300, curve: Curve.EaseInOut })
}
@Builder
NewsTextColumn(item: NewsItem) {
Column() {
Text(item.title)
.fontSize(16)
.fontColor(Color.Black)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ right: 12 })
.transition(
TransitionEffect.asymmetric(
TransitionEffect.OPACITY.combine(TransitionEffect.translate({ x: 30 })).animation({ duration: 300, curve: Curve.EaseInOut }),
TransitionEffect.OPACITY.combine(TransitionEffect.translate({ x: -30 })).animation({ duration: 300, curve: Curve.EaseInOut })
)
)
}
@Builder
NewsImage(item: NewsItem) {
Row()
.width(80)
.height(80)
.backgroundColor(item.imageColor)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.transition(
TransitionEffect.asymmetric(
TransitionEffect.OPACITY.combine(TransitionEffect.translate({ x: -30 })).animation({ duration: 300, curve: Curve.EaseInOut }),
TransitionEffect.OPACITY.combine(TransitionEffect.translate({ x: 30 })).animation({ duration: 300, curve: Curve.EaseInOut })
)
)
}
build() {
Column() {
// 顶部提示栏
Row() {
Text(this.isRightMode ? "右手模式(图在右)" : "左手模式(图在左)")
.fontSize(14)
.fontColor(Color.White)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('rgba(0,0,0,0.6)')
.borderRadius(20)
}
.width('100%')
.padding(12)
.justifyContent(FlexAlign.Center)
// 新闻列表
List() {
ForEach(this.newsList, (item: NewsItem) => {
ListItem() {
this.NewsCard(item)
}
}, (item: NewsItem) => item.id.toString())
}
.width('100%')
.height('100%')
.padding({ left: 12, right: 12 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
代码下载地址:DetectGestureDemo
总结
本案例很简单,通过申请手持检测权限获取能力,通过监听手持状态修改状态变量,通过非对称动画完成图文左右切换,我比较懒就不找图了用不同颜色替代,不用的时候记得关闭监听。