HarmonyOS7新特性智感握姿:基于手势感知的智能交互设计

智感握姿:基于手势感知的智能交互设计

摘要

本文深入解析 HarmonyOS 沉浸光感开发案例中的"智感握姿"交互场景,详细阐述如何利用 @kit.MultimodalAwarenessKit 的 motion 手势感知 API,实时监听用户握持手机的手势状态(左手/右手),动态调整悬浮按钮的布局定位,并结合 Spring 弹性动画实现流畅的位置过渡。通过源码级别的代码剖析,本文揭示了从能力检测、监听注册、布局动态切换到动画编排的全链路实现方案,为构建"主动适应"的智能交互体验提供可参考的工程实践。


一、引言

随着智能手机屏幕尺寸不断增大,单手操作场景在日常使用中的占比越来越高。用户在地铁上、行走中或仅有一只手持机时,往往需要 thumb-friendly 的界面布局。然而,传统移动应用的 UI 布局通常是静态的------悬浮按钮固定在屏幕右下角,底部标签栏始终居中对齐------这种"以我为主"的设计并未考虑用户当下的握持姿势,导致远端的 UI 元素难以触达,操作效率下降。

"智感握姿"(Smart Reach)的设计理念正是要打破这种僵化:让界面主动适应用户的握持习惯,而非要求用户适应界面。当系统检测到用户用左手握持手机时,悬浮按钮自动移至屏幕左侧;切换至右手握持时,按钮平滑回归右侧。这一看似简单的交互背后,融合了手势感知、动态布局、弹性动画、生命周期管理以及材质系统等多重技术栈。

本文将围绕 HarmonyOS 沉浸光感开发案例中的 SmartReachView.ets 文件(路径:products/entry/src/main/ets/view/SmartReachView.ets),从以下六个维度进行源码级的深度解读:

  • 手势感知:motion API 的监听机制与能力降级
  • 动态布局:基于 RelativeContainer 的 alignRules 实时切换
  • 弹性动画:Spring 弹簧曲线驱动的平滑过渡
  • 生命周期:资源注册与释放的全链路管理
  • 材质视觉:HdsTabs 与 MaterialUtil 的沉浸式风格
  • 断点适配:BreakpointType 实现的多屏间距自适应

二、核心能力:motion 手势感知 API

2.1 @kit.MultimodalAwarenessKit 能力概述

@kit.MultimodalAwarenessKit 是 HarmonyOS 提供的多模态感知能力套件,涵盖运动状态感知、环境感知、生物感知等多种维度。其中,motion 模块专门负责设备运动与用户手势的监测。在"智感握姿"场景中,我们只关注一个核心事件------用户握持手机的左右手切换

项目在使用该 API 前,需要在 module.json5 中声明对应权限:

json5 复制代码
{
  "requestPermissions": [
    {
      "name": "ohos.permission.DETECT_GESTURE",
      "reason": "$string:gesture_reason",
      "usedScene": {
        "abilities": [
          "SmartreachAbility"
        ],
        "when": "inuse"
      }
    }
  ]
}

该权限的声明语义是"应用感知手势操作",仅在 SmartreachAbility 前台使用时生效(when: "inuse"),符合最小权限原则。

在代码入口处,通过以下方式导入 motion 模块:

typescript 复制代码
import { motion } from '@kit.MultimodalAwarenessKit';

2.2 motion.on('holdingHandChanged') 监听机制

motion.on 是注册手势事件的入口方法,第一个参数为事件名称 'holdingHandChanged',第二个参数为回调函数。回调函数接收一个 motion.HoldingHandStatus 枚举值,该枚举包含 LEFT_HAND_HELD(左手握持)和 RIGHT_HAND_HELD(右手握持)两个取值。

SmartReachView.ets 中,回调的实现如下:

typescript 复制代码
handleHoldingHandChange: Callback<motion.HoldingHandStatus> = (status: motion.HoldingHandStatus) => {
  Logger.info(TAG, `handle on holdingHandChanged:::${status}`);
  this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 288, 30) }, () => {
    if (canIUse('SystemCapability.MultimodalAwareness.Motion')) {
      if (status === motion.HoldingHandStatus.LEFT_HAND_HELD) {
        this.floatingAlignRules = {
          left: { anchor: '__container__', align: HorizontalAlign.Start },
          bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
        };
      } else if (status === motion.HoldingHandStatus.RIGHT_HAND_HELD) {
        this.floatingAlignRules = {
          right: { anchor: '__container__', align: HorizontalAlign.End },
          bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
        };
      }
    }
  });
}

这里有两个关键设计值得注意:

  1. 类型化回调 :使用 Callback<motion.HoldingHandStatus> 类型标注,确保回调签名与事件系统严格匹配。
  2. 动画包裹逻辑 :状态变更与 UI 更新通过 animateTo 包裹,而非直接赋值------这样当 floatingAlignRules 被修改时,布局的变化会以动画而非突变的方式呈现。

2.3 canIUse 能力检测与降级

HarmonyOS 提供了 canIUse 全局函数,用于在运行时检测当前设备是否支持特定系统能力。在"智感握姿"场景中,该函数被用于双重防护:

typescript 复制代码
if (canIUse('SystemCapability.MultimodalAwareness.Motion')) {
  motion.on('holdingHandChanged', this.handleHoldingHandChange);
} else {
  Logger.error(TAG, `Can not handle on holdingHandChanged`);
}

SystemCapability.MultimodalAwareness.Motion 是 motion API 的底层系统能力标识。当设备不支持该能力时(例如某些平板或较老的机型),代码不会崩溃,而是安全地跳过注册。同时,回调内部也嵌套了同样的检测:

typescript 复制代码
if (canIUse('SystemCapability.MultimodalAwareness.Motion')) {
  if (status === motion.HoldingHandStatus.LEFT_HAND_HELD) { /* ... */ }
}

这种"双重检测"策略,结合 try-catch 异常捕获,构建了完整的降级路径:能力不可用时,悬浮按钮保持默认的右侧位置,应用其余功能不受影响。


三、智能定位:动态悬浮按钮的布局实现

3.1 RelativeContainer 相对布局

RelativeContainer 是 HarmonyOS 提供的相对布局容器,允许子组件通过 alignRules 属性,以自身或容器(__container__)为锚点进行对齐定位。在 SmartReachView.etsbuild() 方法中,悬浮按钮 Row 处于 RelativeContainer 内,通过 alignRules 实现位置控制:

typescript 复制代码
build() {
  HdsNavDestination() {
    RelativeContainer() {
      HdsTabs({ controller: this.controller }) {
        // ... tab content
      }
      // ... barFloatingStyle 配置

      Row() {
        SymbolGlyph($r('sys.symbol.square_and_pencil_fill'))
          .fontColor([$r('sys.color.icon_on_primary')])
          .fontSize($r('sys.float.Title_M'))
      }
      .alignRules(this.floatingAlignRules)
      // ... margin, backgroundColor, borderRadius, etc.
    }
  }
}

悬浮按钮使用了 SymbolGlyph 组件(系统符号图标),square_and_pencil_fill 表示"编辑"语义。按钮以 Row 包裹,通过 borderRadius('50%')aspectRatio(1) 形成一个 56vp 的圆形按钮。

3.2 基于握持状态的 alignRules 动态切换

核心状态变量 floatingAlignRules 初始化为右侧对齐:

typescript 复制代码
@Local floatingAlignRules: AlignRuleOption = {
  right: { anchor: '__container__', align: HorizontalAlign.End },
  bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
};

motion.on 回调触发时,根据 HoldingHandStatus 动态切换该值:

握持状态 规则组 含义
LEFT_HAND_HELD left: { anchor: '__container__', align: HorizontalAlign.Start } 按钮左对齐
RIGHT_HAND_HELD right: { anchor: '__container__', align: HorizontalAlign.End } 按钮右对齐

__container__ 始终指向最近的 RelativeContainer 父容器,HorizontalAlign.StartHorizontalAlign.End 分别对应 LTR 布局下的左边缘和右边缘。底部锚点始终固定在容器底部(VerticalAlign.Bottom),因此按钮仅在水平方向上左右切换,垂直位置保持不变。

因为 floatingAlignRules 使用 @Local 装饰器标记为响应式状态,当其值变更时,框架自动触发 UI 重新渲染------再配合 animateTo 动画,用户感知到的就是一次流畅的飞行动画而非生硬的跳转。

3.3 BreakpointType 间距自适应

为了在不同屏幕尺寸下保持合适的视觉边距,悬浮按钮的 margin 使用了 BreakpointType 工具类实现断点自适应:

typescript 复制代码
.margin({
  left: new BreakpointType({
    sm: $r('sys.float.padding_level8'),
    md: $r('sys.float.padding_level12'),
    lg: $r('sys.float.padding_level16')
  }).getValue(this.globalInfoModel.widthBreakpoint),
  right: new BreakpointType({
    sm: $r('sys.float.padding_level8'),
    md: $r('sys.float.padding_level12'),
    lg: $r('sys.float.padding_level16')
  }).getValue(this.globalInfoModel.widthBreakpoint),
  bottom: 100,
})

BreakpointType 的完整实现位于 BreakpointSystem.ets

typescript 复制代码
export class BreakpointType<T> {
  private xs: T;
  private sm: T;
  private md: T;
  private lg: T;
  private xl: T;

  public constructor(param: BreakpointTypeOptions<T>) {
    this.xs = param.xs ?? param.sm;
    this.sm = param.sm;
    this.md = param.md;
    this.lg = param.lg;
    this.xl = param.xl ?? param.lg;
  }

  public getValue(currentBreakpoint: WidthBreakpoint): T {
    if (currentBreakpoint === WidthBreakpoint.WIDTH_XS) return this.xs;
    if (currentBreakpoint === WidthBreakpoint.WIDTH_SM) return this.sm;
    if (currentBreakpoint === WidthBreakpoint.WIDTH_MD) return this.md;
    if (currentBreakpoint === WidthBreakpoint.WIDTH_XL) return this.xl;
    return this.lg;
  }
}
  • sm(小屏手机):8vp 间距
  • md(中屏/折叠展开):12vp 间距
  • lg(平板/大屏):16vp 间距

xsxl 分别 fallback 到 smlg,确保所有断点都有合理的默认值。globalInfoModel.widthBreakpointWindowUtil 在应用启动时通过 window.on('windowSizeChange') 持续更新,保证了每一次屏幕尺寸变化后,按钮间距都能自动适配。

底层 bottom: 100 是一个固定值,确保按钮始终位于底部标签栏上方,避免被 tab bar 遮挡。


四、Spring 弹性动画:让位置过渡更自然

4.1 curves.interpolatingSpring 参数解析

Spring 动画是 Apple 原生设计语言中广泛使用的物理曲线,其核心思想是模拟弹簧的物理运动------被拖拽后释放,物体会经历加速、减速、过冲、回弹、最终稳定在目标位置的过程。这种曲线带来的体验远比线性或淡入淡出曲线更加"生动"和"自然"。

HarmonyOS 的 curves 模块提供了 interpolatingSpring 工厂方法,构造一个弹簧插值曲线:

typescript 复制代码
curves.interpolatingSpring(0, 1, 288, 30)

四个参数从左到右依次为:

参数 含义
velocity 0 初始速度,单位 vp/s,0 表示从静止开始
mass 1 弹簧振子质量,影响"惯性感",越大越"重"
stiffness 288 弹簧劲度系数,值越大弹性越强、回弹越明显
damping 30 阻尼系数,值越大衰减越快,超过临界值则无过冲

因为 damping = 30 相对于 stiffness = 288 处于轻微欠阻尼区间,所以弹簧动画会呈现一次干净利落的过冲回弹效果------按钮"飞"到目标位置后会轻微弹跳一下再稳定,给予用户充分的物理反馈感。这一参数的组合(288/30)是经过实际体验调优的结果,既保证回弹可见又不过度夸张。

4.2 animateTo 与 Spring 动画的结合

getUIContext().animateTo() 是 HarmonyOS 的显式动画接口,接受一个动画配置对象和闭包。闭包中对响应式状态的修改会被动效化:

typescript 复制代码
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 288, 30) }, () => {
  this.floatingAlignRules = { /* 新的对齐规则 */ };
});

这段代码的执行流程可以拆解为:

  1. animateTo 开始记录动画上下文;
  2. 闭包内 this.floatingAlignRules 被赋新值,触发 @Local 装饰器的响应式通知;
  3. RelativeContainer 检测到子组件的 alignRules 发生变化,重新计算布局;
  4. 但此时布局更新被动画上下文拦截------它不是直接跳转到最终位置,而是以 interpolatingSpring 曲线,从旧位置插值到新位置;
  5. 整个过渡过程中,按钮的位置、大小(如果需要)等属性都被逐帧计算并渲染。

需要特别强调的是,只有 floatingAlignRules 的变更被动画化 ,而按钮的 marginwidthbackgroundColor 等属性不变,所以动画只发生在水平方向的位置移动上------这正是我们期望的单轴平滑过渡。


五、全生命周期管理

5.1 aboutToAppear:注册监听与引导弹窗

HarmonyOS 组件的 aboutToAppear 生命周期方法在组件即将挂载到视图树时调用,是执行初始化操作的理想时机。SmartReachView 在此方法内完成了两项关键工作:

typescript 复制代码
aboutToAppear(): void {
  this.showAlert();
  try {
    if (canIUse('SystemCapability.MultimodalAwareness.Motion')) {
      motion.on('holdingHandChanged', this.handleHoldingHandChange);
      Logger.info(TAG, `Succeed handle on holdingHandChanged`);
    } else {
      Logger.error(TAG, `Can not handle on holdingHandChanged`);
    }
  } catch (error) {
    Logger.error(TAG, `Failed on holdingHandChanged. cause${error.message}`);
  }
}
  • 引导弹窗showAlert() 首先执行,向用户展示功能说明弹窗(详见 5.3 节);
  • 能力检测与注册 :通过 canIUse + try-catch 双重保护,安全注册手势监听。

这里有一个微妙的设计顺序:先弹窗,再注册监听。这样在用户查看引导说明时,系统已经准备好接收手势事件;如果反过来,用户可能在看到弹窗前就切换了握持姿势,导致第一次状态丢失。

5.2 aboutToDisappear:取消监听与资源释放

当组件从视图树移除时,aboutToDisappear 被调用,此处必须清理所有已注册的系统资源,避免内存泄漏或事件泄漏:

typescript 复制代码
aboutToDisappear(): void {
  try {
    if (canIUse('SystemCapability.MultimodalAwareness.Motion')) {
      motion.off('holdingHandChanged', this.handleHoldingHandChange);
    } else {
      Logger.error(TAG, `Can not handle off holdingHandChanged`);
    }
  } catch (error) {
    Logger.error(TAG, `Failed off holdingHandChanged. cause${error.message}`);
  }
  this.closeAlert();
}

motion.off 接受与 on 相同的事件名和回调引用,精确地移除之前注册的监听器。这里传入了 this.handleHoldingHandChange 的具体方法引用,支持同一事件上注册多个回调时的精准解注册。

关闭引导弹窗的 closeAlert() 放在末尾,但无论其执行是否成功,手势监听都已先被移除,不会出现"弹窗已关但手势仍在监听"的竞态问题。

5.3 引导对话框的设计实现

引导弹窗通过 getUIContext().getPromptAction().openCustomDialog() 创建自定义对话框,并使用 MaterialUtil.getMaterialOptions 为其应用沉浸式材质:

typescript 复制代码
showAlert() {
  try {
    const smartPromptAction = this.getUIContext().getPromptAction();
    smartPromptAction.openCustomDialog(MaterialUtil.getMaterialOptions<promptAction.CustomDialogOptions>({
      builder: () => { this.alertBuilder(); },
      autoCancel: false,
    }) as promptAction.CustomDialogOptions)
      .then((dialogId: number) => { this.dialogComponentId = dialogId; })
      .catch((error: BusinessError) => {
        Logger.error(TAG, `show alert failed. cause code: ${error.code}; msg: ${error.message}`);
      });
  } catch (error) {
    Logger.error(TAG, `show alert failed. cause code: ${error.code}; msg: ${error.message}`);
  }
}

对话框的 UI 通过 @Builder alertBuilder() 构建,包含标题、说明文本、示意图片和确认按钮:

typescript 复制代码
@Builder
alertBuilder() {
  Column() {
    Row() {
      Text($r('app.string.alert_title'))
        .fontSize($r('sys.float.Title_S'))
        .fontColor($r('sys.color.font_primary'))
        .fontWeight(FontWeight.Bold)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .height(56)

    Column() {
      Text($r('app.string.alert_msg'))
        .fontSize($r('sys.float.Subtitle_M'))
        .fontWeight(FontWeight.Medium)
        .fontColor($r('sys.color.font_primary'))
        .margin({ top: $r('sys.float.padding_level1') })
    }
    .padding({ left: $r('sys.float.padding_level8'), right: $r('sys.float.padding_level8') })

    Image($r('app.media.ic_smart_reach'))
      .width('70%')
    Column() {
      Text($r('app.string.alert_desc'))
        .fontSize($r('sys.float.Subtitle_S'))
        .fontColor($r('sys.color.font_tertiary'))

      Row({ space: 16 }) {
        Button($r('app.string.okay'))
          .onClick(() => { this.closeAlert(); })
          .buttonStyle(ButtonStyleMode.EMPHASIZED)
          .height(40)
          .width('50%')
      }
      .padding({ top: $r('sys.float.padding_level4') })
      .justifyContent(FlexAlign.Center)
      .width('100%')
    }
    .padding({
      left: $r('sys.float.padding_level8'), top: $r('sys.float.padding_level4'),
      right: $r('sys.float.padding_level8'), bottom: $r('sys.float.padding_level8'),
    })
  }
}

弹窗的 autoCancel: false 设置要求用户必须点击"知道了"按钮才能关闭,确保功能说明被充分阅读。关闭方法 closeAlert 通过保存的 dialogComponentId 调用 closeCustomDialog,并在前后重置 ID 状态。


六、材质与视觉风格

6.1 HdsTabs 的 barFloatingStyle 配置

底部标签栏使用了 @kit.UIDesignKit 提供的 HdsTabs 组件,并配置了 barFloatingStyle 实现悬浮效果:

typescript 复制代码
HdsTabs({ controller: this.controller }) {
  // ... TabContent
}
.barOverlap(true)
.vertical(false)
.barPosition(BarPosition.End)
.barFloatingStyle({
  barBottomMargin: this.globalInfoModel.naviIndicatorHeight > 0
    ? this.globalInfoModel.naviIndicatorHeight
    : $r('sys.float.padding_level8'),
  adaptToHandedness: true,
  systemMaterialEffect: {
    materialType: hdsMaterial.MaterialType.ADAPTIVE,
    materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
  }
})

属性解读:

  • barOverlap(true):标签栏与内容区域重叠,形成"悬浮"在内容之上的视觉层次;
  • barPosition(BarPosition.End):标签栏位于底部;
  • barBottomMargin :动态计算底部边距。当系统存在导航指示条(手势导航的横条)时,使用 naviIndicatorHeight 避让;否则使用 8vp 默认间距;
  • adaptToHandedness: true:这是与"智感握姿"理念一致的配置------当系统检测到用户惯用手时,标签栏的布局会自适应偏移,进一步强化单手可达性;
  • systemMaterialEffect :使用 hdsMaterial.MaterialType.ADAPTIVE 自适应材质,系统会根据主题和背景动态调整标签栏的模糊与透明度。

6.2 MaterialUtil 工具类的封装

MaterialUtil 工具类位于 products/entry/src/main/ets/util/MaterialUtil.ets,封装了两种材质应用方案:

方案一:getMaterialModifier --- 用于组件修饰器方式

typescript 复制代码
public static getMaterialModifier(options: ModifierOptions): CommonModifier {
  if (deviceInfo.apiAvailable('26.0.0') && hdsMaterial.getSystemMaterialTypes().length > 0) {
    const materialOptions: uiMaterial.ImmersiveOptions = {
      style: uiMaterial.ImmersiveStyle.ULTRA_THIN,
      interactive: true,
      lightEffect: { color: undefined }
    };
    if (options.materialColor) {
      materialOptions.materialColor = options.materialColor;
    }
    options.modifier.systemMaterial(new uiMaterial.ImmersiveMaterial(materialOptions));
  } else {
    if (options.backgroundColor) {
      options.modifier.backgroundColor(options.backgroundColor);
    }
    if (options.backgroundBlurStyle) {
      options.modifier.backgroundBlurStyle(options.backgroundBlurStyle);
    }
  }
  return options.modifier;
}

方案二:getMaterialOptions --- 用于自定义弹窗等场景

typescript 复制代码
public static getMaterialOptions<T>(option: T): MaterialOptions<T> | T {
  if (deviceInfo.apiAvailable('26.0.0') && hdsMaterial.getSystemMaterialTypes().length > 0) {
    const materialOptions = option as MaterialOptions<T>;
    materialOptions.systemMaterial = new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THICK,
      interactive: true,
      lightEffect: { color: undefined }
    });
    return materialOptions;
  }
  return option;
}

两种方案的共同逻辑是:

  1. 能力检测 :通过 deviceInfo.apiAvailable('26.0.0') 判断 API 版本是否满足 26.0.0 及以上,再通过 hdsMaterial.getSystemMaterialTypes().length > 0 判断当前设备是否支持系统级材质;
  2. 降级处理 :当系统不支持沉浸式材质时,回退到传统的 backgroundColor + backgroundBlurStyle 方案;
  3. 交互性开启interactive: true 允许事件穿透材质层,确保按钮点击等交互不受影响。

在"智感握姿"场景中,引导弹窗使用的是方案二的 getMaterialOptions,因为它需要将一个 promptAction.CustomDialogOptions 对象"注入"材质属性;而按钮和标签栏的材质则由 HdsTabs 内部通过 systemMaterialEffect 配置处理。


七、总结与最佳实践

"智感握姿"方案通过 motion 手势感知能力,实现了悬浮按钮随握持姿势动态切换的智能交互。从工程实现的角度,我们可以总结出以下最佳实践:

7.1 架构设计要点

  1. 能力检测先行 :所有系统级 API 调用前,都应当使用 canIUsedeviceInfo.apiAvailable 进行运行时能力检测,并结合 try-catch 提供降级路径。本项目在 aboutToAppearaboutToDisappear 以及回调内部三处都进行了检测,做到了零信任式防御编程。

  2. 响应式状态驱动 UI :使用 @Local 装饰器标记布局规则变量 floatingAlignRules,当手势事件触发时仅需修改状态值,框架自动完成布局重新计算,声明式编程范式大幅降低了状态管理的复杂性。

  3. 动画包裹状态变更 :将 alignRules 的切换放在 animateTo 闭包内执行,利用 curves.interpolatingSpring 弹簧曲线提供自然的物理过渡。弹簧参数(质量、刚度、阻尼)应经过实测调优,以达到"干净利落的回弹"而非"反复振荡"的体验。

  4. 生命周期对称管理aboutToAppear 中注册的东西,一定要在 aboutToDisappear 中释放。motion.onmotion.off 成对出现,且传入相同的事件名和回调引用,确保精确配对。

7.2 可复用组件划分

模块 文件 职责
SmartReachView SmartReachView.ets 手势感知 + 动态布局 + 完整交互逻辑
BreakpointType BreakpointSystem.ets 断点适配工具类,通用性强
MaterialUtil MaterialUtil.ets 材质工具封装,含 API 版本检测与降级
GlobalInfoModel model/GlobalInfoModel.ets 全局状态模型,驱动断点与安全区域

7.3 未来扩展方向

  • 多实例支持:当前仅有一个悬浮按钮跟随手势变化,未来可扩展为整个工具栏或菜单面板随之移动;
  • 手势组合检测 :结合 motion 模块的其他事件(如设备旋转、摇晃),实现更丰富的上下文感知交互;
  • 惯用手记忆:将用户习惯的握持方式持久化到本地存储,在应用启动时自动恢复对应布局,减少重复识别开销。

总而言之,"智感握姿"是一个小而精的交互设计案例,它展示了 HarmonyOS 多模态感知能力与声明式 UI 框架的深度结合,为构建"懂用户"的智能应用提供了完整的技术参考路径。

相关推荐
2301_768103491 小时前
HarmonyOS趣味相机实战第24篇:前摄镜像、旋转补偿与识别框坐标统一
图像处理·harmonyos·arkts·camerakit·坐标映射
国服第二切图仔1 小时前
21-MCP服务
harmonyos
绝世番茄1 小时前
HarmonyOS List 上拉加载更多(LoadMore)深度实战指南
华为·list·harmonyos·鸿蒙
勇踏前人未索之境2 小时前
Unity打包运行于鸿蒙手机
unity·智能手机·harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器离线专业渲染管线
华为·编辑器·harmonyos·鸿蒙
Helen_cai2 小时前
OpenHarmony 项目统一全局样式、尺寸、色彩主题封装 ThemeUtil(API23)
开发语言·前端·javascript·华为·harmonyos
youtootech3 小时前
HarmonyOS《柚兔学伴》项目实战12-Lottie 动画集成
华为·harmonyos
l134062082353 小时前
HarmonyOS应用开发实战:小事记 - 多媒体文件上传:@ohos.net.http 的 multipart/form-data 请求构造
后端·华为·harmonyos·鸿蒙系统
youtootech3 小时前
HarmonyOS《柚兔学伴》项目实战16-录音功能与权限管理
华为·harmonyos