欢迎光临
我们一直在努力

UniApp 中使用鸿蒙智感握姿技术详解

前言

大家好!我是全栈开发者Jack,最近在开发一款名为 BabyOne 的母婴育儿应用。在开发过程中,我遇到了一个有趣的需求:如何让悬浮按钮根据用户的握持方式自动调整位置?比如用户用左手握持手机时,按钮应该出现在右侧方便点击;用右手握持时,按钮应该出现在左侧。华为鸿蒙又新推出了好玩的智感握姿能力,刚好可以满足这个需求。

为了实现这个功能,我深入研究了鸿蒙系统的智感握姿(华为官方文档)能力,并封装了 jack-holding-hand 插件。本文将详细介绍如何在 UniApp 项目中使用这项技术,打造更智能的用户体验。

什么是智感握姿?

智感握姿是基于鸿蒙系统提供的Multimodal Awareness Kit(多模态融合感知服务)实现的一项智能感知能力,可以实时检测用户握持手机的方式,包括:

  • 📱 未握持(状态值:0):手机放置在桌面或其他地方
  • 👈 左手握持(状态值:1):用户用左手握持手机
  • 👉 右手握持(状态值:2):用户用右手握持手机
  • 🤲 双手握持(状态值:3):用户双手握持手机
  • ❓ 未识别(状态值:16):无法识别握持方式

握持状态转换图

在这里插入图片描述

应用场景

智感握姿技术可以应用于多种场景:

  • 智能悬浮按钮:根据握持手自动调整按钮位置,提升单手操作体验
  • 单手模式优化:检测到单手握持时,自动调整界面布局,让重要按钮更容易点击
  • 游戏控制:根据握持方式动态调整游戏控制按钮的位置
  • 无障碍功能:为不同握持方式提供更好的交互体验
  • 阅读应用:根据握持方式调整翻页按钮位置
  • jack-holding-hand 插件介绍

    为了简化在 UniApp 中使用鸿蒙智感握姿的流程,我封装了 jack-holding-hand 插件。这是一个基于 UTS 开发的跨平台插件,完全开源。

    平台支持

    • ✅ HarmonyOS:使用 @kit.MultimodalAwarenessKit 原生 API

    核心特性

  • 简单易用:统一的 API 接口,几行代码即可实现
  • 实时监听:握持状态变化时实时回调
  • 完善的错误处理:提供详细的错误信息
  • 开箱即用:本文提供完整源码,无需下载,直接复制即可使用
  • TypeScript 支持:完整的类型定义,开发体验更好
  • 为什么要封装这个插件?

    在开发 BabyOne 应用时,我需要在多个页面使用智能悬浮按钮。直接使用鸿蒙原生 API 存在以下问题:

  • 代码重复:每个页面都要写一遍订阅和取消订阅逻辑
  • 平台差异:HarmonyOS 的 API UniApp 无法直接使用
  • 错误处理复杂:需要处理各种异常情况
  • 状态管理困难:需要手动维护握持状态
  • 因此,我将功能封装成了 jack-holding-hand 插件,大大简化了使用流程。

    技术架构

    插件目录结构

    uni_modules/jack-holding-hand/
    ├── utssdk/
    │ ├── interface.uts # 接口定义
    │ └── app-harmony/ # 鸿蒙平台实现
    │ ├── index.uts # 鸿蒙握姿检测实现
    │ └── module.json5 # 权限配置
    └── package.json

    核心 API 设计

    插件提供了 2 个核心 API:

    API说明返回值
    subscribeHoldingHand(options) 订阅握持手状态变化 void
    unsubscribeHoldingHand(options) 取消订阅握持手状态变化 void

    整体架构图

    在这里插入图片描述

    插件完整源码

    为了方便大家使用,这里提供 jack-holding-hand 插件的完整源代码,可以直接复制到你的项目中使用。

    1. utssdk/interface.uts(接口定义)

    export interface Uni {
    /**
    * subscribeHoldingHand()
    * @description
    * 订阅握持手状态变化感知事件
    * @param {SubscribeHoldingHandOptions} options
    * @return {void}
    */

    subscribeHoldingHand(options : SubscribeHoldingHandOptions) : void;

    /**
    * unsubscribeHoldingHand()
    * @description
    * 取消订阅握持手状态变化感知事件
    * @param {UnsubscribeHoldingHandOptions} options
    * @return {void}
    */

    unsubscribeHoldingHand(options ?: UnsubscribeHoldingHandOptions) : void;
    }

    /**
    * 握持手状态枚举
    * 0: 未握持
    * 1: 左手握持
    * 2: 右手握持
    * 3: 双手握持
    * 16: 未识别
    */

    export type HoldingHandStatus = 0 | 1 | 2 | 3 | 16;

    /**
    * 握持手状态变化回调
    */

    export type HoldingHandChangeCallback = (status : HoldingHandStatus) => void;

    /**
    * 订阅握持手状态选项
    */

    export type SubscribeHoldingHandOptions = {
    /**
    * 握持手状态变化回调
    */

    onChange : HoldingHandChangeCallback,
    /**
    * 接口调用成功的回调函数
    * @defaultValue null
    */

    success ?: SubscribeHoldingHandSuccessCallback | null,
    /**
    * 接口调用失败的回调函数
    * @defaultValue null
    */

    fail ?: SubscribeHoldingHandFailCallback | null,
    /**
    * 接口调用结束的回调函数(调用成功、失败都会执行)
    * @defaultValue null
    */

    complete ?: SubscribeHoldingHandCompleteCallback | null
    };

    export type SubscribeHoldingHandSuccess = {
    /**
    * 成功信息
    */

    errMsg : string
    };

    export type SubscribeHoldingHandSuccessCallback = (result : SubscribeHoldingHandSuccess) => void;

    export type SubscribeHoldingHandFail = {
    /**
    * 错误信息
    */

    errMsg : string
    };

    export type SubscribeHoldingHandFailCallback = (result : SubscribeHoldingHandFail) => void;

    export type SubscribeHoldingHandComplete = {
    /**
    * 信息
    */

    errMsg : string
    };

    export type SubscribeHoldingHandCompleteCallback = (result : SubscribeHoldingHandComplete) => void;

    /**
    * 取消订阅握持手状态选项
    */

    export type UnsubscribeHoldingHandOptions = {
    /**
    * 接口调用成功的回调函数
    * @defaultValue null
    */

    success ?: UnsubscribeHoldingHandSuccessCallback | null,
    /**
    * 接口调用失败的回调函数
    * @defaultValue null
    */

    fail ?: UnsubscribeHoldingHandFailCallback | null,
    /**
    * 接口调用结束的回调函数(调用成功、失败都会执行)
    * @defaultValue null
    */

    complete ?: UnsubscribeHoldingHandCompleteCallback | null
    };

    export type UnsubscribeHoldingHandSuccess = {
    /**
    * 成功信息
    */

    errMsg : string
    };

    export type UnsubscribeHoldingHandSuccessCallback = (result : UnsubscribeHoldingHandSuccess) => void;

    export type UnsubscribeHoldingHandFail = {
    /**
    * 错误信息
    */

    errMsg : string
    };

    export type UnsubscribeHoldingHandFailCallback = (result : UnsubscribeHoldingHandFail) => void;

    export type UnsubscribeHoldingHandComplete = {
    /**
    * 信息
    */

    errMsg : string
    };

    export type UnsubscribeHoldingHandCompleteCallback = (result : UnsubscribeHoldingHandComplete) => void;

    2. utssdk/app-harmony/index.uts(鸿蒙平台实现)

    import {
    HoldingHandStatus,
    HoldingHandChangeCallback,
    SubscribeHoldingHandOptions,
    SubscribeHoldingHandSuccess,
    SubscribeHoldingHandFail,
    SubscribeHoldingHandComplete,
    UnsubscribeHoldingHandOptions,
    UnsubscribeHoldingHandSuccess,
    UnsubscribeHoldingHandFail,
    UnsubscribeHoldingHandComplete
    } from '../interface.uts'

    export {
    HoldingHandStatus,
    HoldingHandChangeCallback,
    SubscribeHoldingHandOptions,
    SubscribeHoldingHandSuccess,
    SubscribeHoldingHandFail,
    SubscribeHoldingHandComplete,
    UnsubscribeHoldingHandOptions,
    UnsubscribeHoldingHandSuccess,
    UnsubscribeHoldingHandFail,
    UnsubscribeHoldingHandComplete
    }

    import { motion } from '@kit.MultimodalAwarenessKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';

    // 全局回调存储
    let globalCallback : HoldingHandChangeCallback | null = null;

    /**
    * 订阅握持手状态变化感知事件
    */

    export function subscribeHoldingHand(options : SubscribeHoldingHandOptions) : void {
    try {
    // 保存回调
    globalCallback = options.onChange;

    // 创建回调函数
    let callback : Callback<motion.HoldingHandStatus> = (data : motion.HoldingHandStatus) => {
    hilog.info(0, 'HoldingHand', `握持手状态变化: ${data}`);

    // 调用用户传入的回调
    if (globalCallback != null) {
    globalCallback(data as HoldingHandStatus);
    }
    };

    // 订阅握持手状态变化
    motion.on('holdingHandChanged', callback);

    hilog.info(0, 'HoldingHand', '订阅握持手状态成功');

    // 成功回调
    let result : SubscribeHoldingHandSuccess = {
    errMsg: "subscribeHoldingHand:ok"
    };
    const completeResult : SubscribeHoldingHandComplete = {
    errMsg: "subscribeHoldingHand:ok"
    }
    options?.success?.(result);
    options?.complete?.(completeResult);

    } catch (err) {
    let error = err as BusinessError;
    hilog.error(0, 'HoldingHand', `订阅握持手状态失败: code=${error.code}, message=${error.message}`);

    let result : SubscribeHoldingHandFail = {
    errMsg: error.message ?? "subscribeHoldingHand:fail"
    };
    const completeResult : SubscribeHoldingHandComplete = {
    errMsg: error.message ?? "subscribeHoldingHand:fail"
    }
    options?.fail?.(result);
    options?.complete?.(completeResult);
    }
    }

    /**
    * 取消订阅握持手状态变化感知事件
    */

    export function unsubscribeHoldingHand(options ?: UnsubscribeHoldingHandOptions) : void {
    try {
    // 取消订阅
    motion.off('holdingHandChanged');

    // 清空回调
    globalCallback = null;

    hilog.info(0, 'HoldingHand', '取消订阅握持手状态成功');

    // 成功回调
    let result : UnsubscribeHoldingHandSuccess = {
    errMsg: "unsubscribeHoldingHand:ok"
    };
    const completeResult : UnsubscribeHoldingHandComplete = {
    errMsg: "unsubscribeHoldingHand:ok"
    }
    options?.success?.(result);
    options?.complete?.(completeResult);

    } catch (err) {
    let error = err as BusinessError;
    hilog.error(0, 'HoldingHand', `取消订阅握持手状态失败: code=${error.code}, message=${error.message}`);

    let result : UnsubscribeHoldingHandFail = {
    errMsg: error.message ?? "unsubscribeHoldingHand:fail"
    };
    const completeResult : UnsubscribeHoldingHandComplete = {
    errMsg: error.message ?? "unsubscribeHoldingHand:fail"
    }
    options?.fail?.(result);
    options?.complete?.(completeResult);
    }
    }

    3. utssdk/app-harmony/module.json5(权限配置文件)

    {
    "module": {
    "requestPermissions": [
    {
    "name": "ohos.permission.DETECT_GESTURE", // 配置需要请求的权限
    }
    ]
    }
    }

    鸿蒙平台实现原理

    1. 引入鸿蒙 SDK

    import { motion } from '@kit.MultimodalAwarenessKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';

    鸿蒙的智感握姿能力来自 @kit.MultimodalAwarenessKit 模块,这是一个多模态感知套件,提供了多种智能感知能力。

    2. 订阅握持手状态变化

    使用 motion.on() 方法订阅握持手状态变化事件:

    // 创建回调函数
    let callback : Callback<motion.HoldingHandStatus> = (data : motion.HoldingHandStatus) => {
    hilog.info(0, 'HoldingHand', `握持手状态变化: ${data}`);

    // 调用用户传入的回调
    if (globalCallback != null) {
    globalCallback(data as HoldingHandStatus);
    }
    };

    // 订阅握持手状态变化
    motion.on('holdingHandChanged', callback);

    3. 取消订阅

    使用 motion.off() 方法取消订阅:

    // 取消订阅
    motion.off('holdingHandChanged');

    // 清空回调
    globalCallback = null;

    4. 状态值说明

    鸿蒙系统返回的握持手状态值:

    状态值常量名说明
    0 NONE 未握持
    1 LEFT 左手握持
    2 RIGHT 右手握持
    3 BOTH 双手握持
    16 UNKNOWN 未识别

    5. 工作流程时序图

    在这里插入图片描述

    快速开始

    第一步:获取插件

    方式一:通过 UniApp 插件市场安装(推荐)

    在 UniApp 插件市场 搜索 jack-holding-hand 直接安装。

    方式二:手动创建

  • 在你的 UniApp 项目的 uni_modules 中创建新的UTS API插件

  • 将上面「插件完整源码」章节中的代码,按照文件路径复制或替换到对应位置

  • 第二步:在页面中使用

    1. 导入插件

    <script>
    // 使用条件编译,仅在鸿蒙平台导入
    // #ifdef APP-HARMONY
    import { subscribeHoldingHand, unsubscribeHoldingHand } from '@/uni_modules/jack-holding-hand'
    // #endif

    export default {
    data() {
    return {
    holdingStatus: 1
    }
    }
    }
    </script>

    2. 订阅握持手状态

    mounted() {
    // #ifdef APP-HARMONY
    this.subscribeHoldingHand();
    // #endif
    },

    methods: {
    subscribeHoldingHand() {
    // #ifdef APP-HARMONY
    subscribeHoldingHand({
    onChange: (status) => {
    console.log('握持手状态:', status);
    this.holdingStatus = status;

    // 根据状态做不同处理
    switch(status) {
    case 0:
    console.log('未握持');
    break;
    case 1:
    console.log('左手握持');
    // 可以调整悬浮按钮到右侧
    break;
    case 2:
    console.log('右手握持');
    // 可以调整悬浮按钮到左侧
    break;
    case 3:
    console.log('双手握持');
    break;
    case 16:
    console.log('未识别');
    break;
    }
    },
    success: (res) => {
    console.log('订阅成功', res);
    uni.showToast({
    title: '握持检测已启动',
    icon: 'success'
    });
    },
    fail: (err) => {
    console.error('订阅失败', err);
    uni.showToast({
    title: '握持检测启动失败',
    icon: 'none'
    });
    }
    });
    // #endif
    }
    }

    3. 取消订阅

    beforeUnmount() {
    // #ifdef APP-HARMONY
    this.unsubscribeHoldingHand();
    // #endif
    },

    methods: {
    unsubscribeHoldingHand() {
    // #ifdef APP-HARMONY
    unsubscribeHoldingHand({
    success: (res) => {
    console.log('取消订阅成功', res);
    },
    fail: (err) => {
    console.error('取消订阅失败', err);
    }
    });
    // #endif
    }
    }

    完整示例:智能悬浮按钮

    下面是一个完整的智能悬浮按钮示例,会根据握持手自动调整位置:

    <template>
    <view class="page">
    <view class="content">
    <text class="title">智能悬浮按钮示例</text>
    <text class="status">当前握持状态: {{ statusText }}</text>
    </view>

    <!– #ifdef APP-HARMONY –>
    <!– 智能悬浮按钮 –>
    <view
    class="float-button"
    :style="floatButtonStyle"
    @click="handleButtonClick"
    >

    <text class="button-text">{{ buttonEmoji }}</text>
    </view>
    <!– #endif –>
    </view>
    </template>

    <script>
    // #ifdef APP-HARMONY
    import { subscribeHoldingHand, unsubscribeHoldingHand } from '@/uni_modules/jack-holding-hand'
    // #endif

    export default {
    data() {
    return {
    holdingStatus: 1,
    buttonPosition: 'right' // 'left' 或 'right'
    }
    },

    computed: {
    statusText() {
    const textMap = {
    0: '未握持',
    1: '左手握持',
    2: '右手握持',
    3: '双手握持',
    16: '未识别'
    }
    return textMap[this.holdingStatus] || '检测中…'
    },

    buttonEmoji() {
    const emojiMap = {
    0: '📱',
    1: '👈',
    2: '👉',
    3: '🤲',
    16: '❓'
    }
    return emojiMap[this.holdingStatus] || '🔘'
    },

    floatButtonStyle() {
    const baseStyle = {
    position: 'fixed',
    bottom: '200rpx',
    width: '120rpx',
    height: '120rpx',
    borderRadius: '60rpx',
    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    boxShadow: '0 8rpx 24rpx rgba(102, 126, 234, 0.4)',
    transition: 'all 0.3s ease',
    zIndex: 1000
    }

    // 根据握持状态调整位置
    if (this.buttonPosition === 'left') {
    baseStyle.left = '30rpx'
    } else {
    baseStyle.right = '30rpx'
    }

    return baseStyle
    }
    },

    mounted() {
    // #ifdef APP-HARMONY
    this.subscribeHoldingHand()
    // #endif
    },

    beforeUnmount() {
    // #ifdef APP-HARMONY
    this.unsubscribeHoldingHand()
    // #endif
    },

    methods: {
    subscribeHoldingHand() {
    // #ifdef APP-HARMONY
    subscribeHoldingHand({
    onChange: (status) => {
    this.holdingStatus = status

    // 根据握持状态调整按钮位置
    switch(status) {
    case 1: // 左手握持
    this.buttonPosition = 'right' // 按钮放右侧
    break
    case 2: // 右手握持
    this.buttonPosition = 'left' // 按钮放左侧
    break
    case 3: // 双手握持
    this.buttonPosition = 'right' // 默认右侧
    break
    default:
    // 未握持或未识别,保持当前位置
    break
    }
    },
    success: (res) => {
    console.log('订阅成功', res)
    },
    fail: (err) => {
    console.error('订阅失败', err)
    }
    })
    // #endif
    },

    unsubscribeHoldingHand() {
    // #ifdef APP-HARMONY
    unsubscribeHoldingHand({
    success: (res) => {
    console.log('取消订阅成功', res)
    }
    })
    // #endif
    },

    handleButtonClick() {
    uni.showToast({
    title: '按钮被点击',
    icon: 'success'
    })
    }
    }
    }
    </script>

    <style scoped>
    .page {
    min-height: 100vh;
    background: #F5F7FA;
    }

    .content {
    padding: 60rpx 40rpx;
    }

    .title {
    font-size: 40rpx;
    font-weight: 700;
    color: #333;
    display: block;
    margin-bottom: 30rpx;
    }

    .status {
    font-size: 28rpx;
    color: #666;
    display: block;
    }

    .float-button {
    cursor: pointer;
    }

    .button-text {
    font-size: 60rpx;
    }
    </style>

    进阶应用:智能悬浮按钮组件

    为了让智能悬浮按钮更容易复用,我封装了一个 smart-float-button 组件。这个组件内部集成了握持手检测,会自动根据握持方式调整位置。

    组件源码

    <!– components/smart-float-button/smart-float-button.vue –>
    <template>
    <!– #ifdef APP-HARMONY –>
    <view
    class="smart-float-button"
    :style="buttonStyle"
    @click="handleClick"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
    >

    <slot>
    <text class="default-icon">+</text>
    </slot>
    </view>
    <!– #endif –>
    </template>

    <script>
    // #ifdef APP-HARMONY
    import { subscribeHoldingHand, unsubscribeHoldingHand } from '@/uni_modules/jack-holding-hand'
    // #endif

    export default {
    name: 'SmartFloatButton',
    props: {
    // 按钮大小
    size: {
    type: Number,
    default: 120
    },
    // 距离顶部的距离
    top: {
    type: Number,
    default: 200
    },
    // 距离边缘的距离
    margin: {
    type: Number,
    default: 30
    },
    // 背景颜色
    bgColor: {
    type: String,
    default: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
    }
    },
    data() {
    return {
    holdingStatus: 1,
    position: 'right', // 'left' 或 'right'
    isDragging: false,
    startX: 0,
    startY: 0,
    currentTop: 0,
    currentLeft: 0
    }
    },
    computed: {
    buttonStyle() {
    const style = {
    width: `${this.size}rpx`,
    height: `${this.size}rpx`,
    borderRadius: `${this.size / 2}rpx`,
    background: this.bgColor,
    top: `${this.currentTop || this.top}rpx`
    }

    if (this.position === 'left') {
    style.left = `${this.margin}rpx`
    } else {
    style.right = `${this.margin}rpx`
    }

    return style
    }
    },
    mounted() {
    // #ifdef APP-HARMONY
    this.subscribeHoldingHand()
    // #endif
    this.currentTop = this.top
    },
    beforeUnmount() {
    // #ifdef APP-HARMONY
    this.unsubscribeHoldingHand()
    // #endif
    },
    methods: {
    subscribeHoldingHand() {
    // #ifdef APP-HARMONY
    subscribeHoldingHand({
    onChange: (status) => {
    this.holdingStatus = status

    // 根据握持状态调整位置
    switch(status) {
    case 1: // 左手握持
    this.position = 'right'
    break
    case 2: // 右手握持
    this.position = 'left'
    break
    case 3: // 双手握持
    this.position = 'right'
    break
    }
    }
    })
    // #endif
    },

    unsubscribeHoldingHand() {
    // #ifdef APP-HARMONY
    unsubscribeHoldingHand()
    // #endif
    },

    handleClick() {
    if (!this.isDragging) {
    this.$emit('click')
    }
    },

    handleTouchStart(e) {
    this.isDragging = false
    this.startX = e.touches[0].clientX
    this.startY = e.touches[0].clientY
    },

    handleTouchMove(e) {
    const moveX = Math.abs(e.touches[0].clientX this.startX)
    const moveY = Math.abs(e.touches[0].clientY this.startY)

    if (moveX > 5 || moveY > 5) {
    this.isDragging = true
    }
    },

    handleTouchEnd() {
    setTimeout(() => {
    this.isDragging = false
    }, 100)
    }
    }
    }
    </script>

    <style scoped>
    .smart-float-button {
    position: fixed;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
    transition: all 0.3s ease;
    z-index: 1000;
    cursor: pointer;
    }

    .default-icon {
    font-size: 60rpx;
    color: #fff;
    font-weight: 300;
    }
    </style>

    使用智能悬浮按钮组件

    <template>
    <view class="page">
    <!– 页面内容 –>
    <view class="content">
    <text>这是页面内容</text>
    </view>

    <!– #ifdef APP-HARMONY –>
    <!– 使用智能悬浮按钮 –>
    <smart-float-button
    :size="120"
    :top="200"
    :margin="30"
    bgColor="linear-gradient(135deg, #FF6B6B 0%, #FFE66D 100%)"
    @click="handleFloatButtonClick"
    >

    <!– 自定义按钮内容 –>
    <text style="font-size: 60rpx; color: #fff;">🎯</text>
    </smart-float-button>
    <!– #endif –>
    </view>
    </template>

    <script>
    // #ifdef APP-HARMONY
    import SmartFloatButton from '@/components/smart-float-button/smart-float-button.vue'
    // #endif

    export default {
    // #ifdef APP-HARMONY
    components: {
    SmartFloatButton
    },
    // #endif

    methods: {
    handleFloatButtonClick() {
    console.log('悬浮按钮被点击')
    uni.showToast({
    title: '按钮点击',
    icon: 'success'
    })
    }
    }
    }
    </script>

    实战案例:BabyOne 应用中的应用

    在我开发的 BabyOne 母婴育儿应用中,智能悬浮按钮被广泛应用于多个场景:

    场景一:快速导航按钮

    在首页、课程页、发现页等主要页面,我添加了智能悬浮按钮,用户可以快速访问常用功能:

    <template>
    <view class="home-page">
    <!– 页面内容 –>

    <!– #ifdef APP-HARMONY –>
    <smart-float-button @click="showQuickMenu">
    <text style="font-size: 50rpx; color: #fff;"></text>
    </smart-float-button>
    <!– #endif –>
    </view>
    </template>

    <script>
    export default {
    methods: {
    showQuickMenu() {
    uni.showActionSheet({
    itemList: ['成长记录', '喂养记录', '睡眠记录', '疫苗提醒'],
    success: (res) => {
    switch(res.tapIndex) {
    case 0:
    uni.navigateTo({ url: '/pages/growth/index' })
    break
    case 1:
    uni.navigateTo({ url: '/pages/feeding/index' })
    break
    case 2:
    uni.navigateTo({ url: '/pages/sleep/index' })
    break
    case 3:
    uni.navigateTo({ url: '/pages/vaccine/index' })
    break
    }
    }
    })
    }
    }
    }
    </script>

    场景二:阅读辅助按钮

    在育儿知识、食谱详情等阅读页面,智能悬浮按钮可以提供收藏、分享等功能:

    <template>
    <view class="article-page">
    <!– 文章内容 –>
    <scroll-view class="article-content" scroll-y>
    <text>{{ articleContent }}</text>
    </scroll-view>

    <!– #ifdef APP-HARMONY –>
    <smart-float-button @click="showArticleActions">
    <text style="font-size: 50rpx; color: #fff;"></text>
    </smart-float-button>
    <!– #endif –>
    </view>
    </template>

    <script>
    export default {
    data() {
    return {
    isFavorited: false
    }
    },
    methods: {
    showArticleActions() {
    uni.showActionSheet({
    itemList: [
    this.isFavorited ? '取消收藏' : '收藏文章',
    '分享给好友',
    '复制链接'
    ],
    success: (res) => {
    switch(res.tapIndex) {
    case 0:
    this.toggleFavorite()
    break
    case 1:
    this.shareArticle()
    break
    case 2:
    this.copyLink()
    break
    }
    }
    })
    },

    toggleFavorite() {
    this.isFavorited = !this.isFavorited
    uni.showToast({
    title: this.isFavorited ? '已收藏' : '已取消收藏',
    icon: 'success'
    })
    },

    shareArticle() {
    // TODO: 分享逻辑
    uni.showToast({
    title: '分享成功',
    icon: 'success'
    })
    },

    copyLink() {
    // TODO: 复制链接逻辑
    uni.showToast({
    title: '复制成功',
    icon: 'success'
    })
    }
    }
    }
    </script>

    场景三:学习互动按钮

    在儿童学习页面(识字、学动物、学水果等),智能悬浮按钮可以触发语音播报:

    <template>
    <view class="learn-page">
    <!– 学习内容 –>
    <view class="learn-card">
    <image :src="currentItem.image" class="item-image" />
    <text class="item-name">{{ currentItem.name }}</text>
    </view>

    <!– #ifdef APP-HARMONY –>
    <smart-float-button @click="speakCurrentItem">
    <text style="font-size: 50rpx; color: #fff;">🔊</text>
    </smart-float-button>
    <!– #endif –>
    </view>
    </template>

    <script>
    // #ifdef APP-HARMONY
    import { ttsSpeak } from '@/uni_modules/jack-tts'
    // #endif

    export default {
    data() {
    return {
    currentItem: {
    name: '苹果',
    pinyin: 'píng guǒ',
    image: '/static/images/fruits/apple.jpg'
    }
    }
    },
    methods: {
    speakCurrentItem() {
    // #ifdef APP-HARMONY
    ttsSpeak({
    text: `${this.currentItem.name}${this.currentItem.pinyin}`,
    speed: 0.8,
    pitch: 1.1
    })
    // #endif
    }
    }
    }
    </script>

    性能优化建议

    1. 避免频繁订阅和取消订阅

    建议在应用启动时全局订阅一次,而不是在每个页面都订阅:

    // App.vue
    export default {
    onLaunch() {
    // #ifdef APP-HARMONY
    this.initHoldingHandDetection()
    // #endif
    },

    methods: {
    initHoldingHandDetection() {
    // #ifdef APP-HARMONY
    const { subscribeHoldingHand } = require('@/uni_modules/jack-holding-hand')

    subscribeHoldingHand({
    onChange: (status) => {
    // 将状态保存到全局状态管理
    this.$store.commit('setHoldingStatus', status)
    }
    })
    // #endif
    }
    }
    }

    2. 使用防抖优化位置调整

    避免握持状态频繁变化导致的性能问题:

    methods: {
    subscribeHoldingHand() {
    // #ifdef APP-HARMONY
    let timer = null

    subscribeHoldingHand({
    onChange: (status) => {
    // 防抖处理
    if (timer) clearTimeout(timer)

    timer = setTimeout(() => {
    this.updateButtonPosition(status)
    }, 300)
    }
    })
    // #endif
    },

    updateButtonPosition(status) {
    switch(status) {
    case 1:
    this.buttonPosition = 'right'
    break
    case 2:
    this.buttonPosition = 'left'
    break
    }
    }
    }

    常见问题 FAQ

    Q1: 为什么订阅失败?

    A: 可能的原因:

  • 权限未配置:检查插件中的 module.json5 中是否添加了 ohos.permission.DETECT_GESTURE 权限
  • 设备不支持:部分老旧设备可能不支持智感握姿功能
  • 系统版本过低:需要 HarmonyOS NEXT 或更高版本
  • Q2: 如何测试握持手检测功能?

    A:

  • 在真机上测试(模拟器可能不支持)
  • 尝试不同的握持方式:单手左、单手右、双手
  • 观察日志输出和 UI 变化
  • 使用本文提供的测试页面进行调试
  • 调试技巧

    1. 添加详细日志

    subscribeHoldingHand({
    onChange: (status) => {
    console.log('[HoldingHand] 状态变化:', {
    status,
    statusText: this.getStatusText(status),
    timestamp: new Date().toLocaleTimeString()
    })

    this.holdingStatus = status
    },
    success: (res) => {
    console.log('[HoldingHand] 订阅成功:', res)
    },
    fail: (err) => {
    console.error('[HoldingHand] 订阅失败:', {
    errMsg: err.errMsg,
    timestamp: new Date().toLocaleTimeString()
    })
    }
    })

    2. 使用 HiLog 查看系统日志

    在鸿蒙平台实现中,我们使用了 hilog 记录日志,可以通过 DevEco Studio 查看:

    import { hilog } from '@kit.PerformanceAnalysisKit';

    // 记录信息日志
    hilog.info(0, 'HoldingHand', `握持手状态变化: ${data}`);

    // 记录错误日志
    hilog.error(0, 'HoldingHand', `订阅失败: code=${error.code}, message=${error.message}`);

    在 HBuilderX 中,打开原生日志,可以通过标签 HoldingHand 过滤相关日志。

    最佳实践总结

  • 全局订阅:在 App.vue 中订阅一次,通过状态管理共享状态
  • 条件编译:使用 #ifdef APP-HARMONY 确保代码只在鸿蒙平台编译
  • 权限配置:确保在插件的 module.json5 中配置了 ohos.permission.DETECT_GESTURE 权限
  • 生命周期管理:在页面卸载时取消订阅,避免内存泄漏
  • 错误处理:始终添加 fail 回调,提供友好的错误提示
  • 性能优化:使用防抖处理状态变化,避免频繁更新 UI
  • 用户体验:提供手动调整选项,不完全依赖自动检测
  • 测试充分:在真机上测试各种握持方式,确保功能正常
  • 插件市场发布

    jack-holding-hand 插件已在 UniApp 插件市场发布,可以直接搜索安装:

    插件地址:https://ext.dcloud.net.cn/plugin?name=jack-holding-hand

    总结

    通过本文,我分享了如何在 UniApp 项目中使用鸿蒙智感握姿技术。jack-holding-hand 插件让这项技术的使用变得简单,只需几行代码就能实现智能悬浮按钮等功能。

    在我的 BabyOne 应用中,智能悬浮按钮大大提升了用户的单手操作体验,特别是对于需要一边抱孩子一边使用手机的家长来说,这个功能非常实用。

    希望本文能帮助你快速掌握在 UniApp 中使用鸿蒙智感握姿的方法!如果你有任何问题或建议,欢迎在评论区交流讨论。


    💡 提示:本文示例代码已在 HarmonyOS NEXT 上测试通过。如果你在使用过程中遇到问题,欢迎在评论区留言!

    ⭐ 如果本文对你有帮助,欢迎点赞、收藏、关注!也欢迎分享给更多需要的开发者!

    赞(0)
    未经允许不得转载:171主机测评 » UniApp 中使用鸿蒙智感握姿技术详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址