鸿蒙原生实战:智感握姿 – 左右手自动适配新闻列表

基于鸿蒙原生手持感知能力,实现设备握持姿态实时识别,左手持机图片居左、右手持机图片居右,配合流畅布局动画,打造更贴合单手操作习惯的新闻阅读体验。

效果说明

请求手持握姿势检测权限,授权成功即可识别左右手姿态,新闻卡片会根据握持方式自动交换图文位置 ,切换过程平滑无卡顿。

核心技术

  1. 申请权限配置
    module.json5配置权限:ohos.permission.DETECT_GESTURE
  2. 动态权限申请
    手势权限为敏感权限,通过 abilityAccessCtrl 动态申请,保证功能可用。
  3. 握持状态识别
    使用 motion 模块监听 holdingHandChanged 事件,获取左手/右手握持状态。
  4. 响应式状态驱动
    通过 @Local 声明响应式变量,状态变更自动刷新UI布局。
  5. 容器动画
    在组件容器上配置非对称动画,布局顺序变化时自动执行平滑过渡。

实现思路

  1. 页面初始化时申请 ohos.permission.DETECT_GESTURE 权限。
  2. 权限通过后,注册握持姿态监听。
  3. 检测到左手/右手时,更新 isRightMode 状态。
  4. 状态变化触发新闻卡片非对称动画执行切换图文排列方向
  5. 离开页面生命周期函数中关闭监听这一点不要忘记哦。

真机实测中发现太灵敏,偶发手机脱离手放到桌子上也会执行一次,可能我手机带着手机壳,或者放的时候误触。

关键代码说明

typescript 复制代码
// 状态变更自动切换布局,直接访问this.isRightMode,不通过传参
@Builder
NewsCard(item: NewsItem) {
  Row() {
    if (this.isRightMode) {
      this.NewsTextColumn(item)
      this.NewsImage(item)
    } else {
      this.NewsImage(item)
      this.NewsTextColumn(item)
    }
  }
}

运行要求

  1. 必须真机运行,模拟器不支持握持传感器。
  2. 开启「手势检测」权限。
  3. 鸿蒙版本: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

总结

本案例很简单,通过申请手持检测权限获取能力,通过监听手持状态修改状态变量,通过非对称动画完成图文左右切换,我比较懒就不找图了用不同颜色替代,不用的时候记得关闭监听。

相关推荐
独特的螺狮粉2 小时前
Flutter 框架跨平台鸿蒙开发 - 睡眠白噪音开发纪录
flutter·华为·harmonyos·鸿蒙
提子拌饭1332 小时前
Flutter 框架跨平台鸿蒙开发 - 商用项目看板应用
flutter·华为·harmonyos
小雨天気.2 小时前
Flutter 框架跨平台鸿蒙开发 - 企业项目任务清单应用
flutter·华为·harmonyos
梁山好汉(Ls_man)3 小时前
鸿蒙_组件内和组件外使用@Builder自定义构建函数的区别
华为·harmonyos·arkts·鸿蒙·arkui
李李李勃谦3 小时前
Flutter 框架跨平台鸿蒙开发 - 志愿者活动应用
flutter·华为·harmonyos
小雨天気.3 小时前
Flutter 框架跨平台鸿蒙开发 - 实战棋谱记录应用
flutter·华为·harmonyos
浮芷.3 小时前
Flutter 框架跨平台鸿蒙开发 - 神奇的DIY教程应用
flutter·华为·harmonyos
李李李勃谦3 小时前
Flutter 框架跨平台鸿蒙开发 - 环保知识应用
flutter·华为·harmonyos
autumn20054 小时前
Flutter 框架跨平台鸿蒙开发 - 车辆管理应用
flutter·华为·harmonyos