鸿蒙原生 ArkTS 布局深度解析:用 Stack 容器实现 Tooltip 工具提示

适用平台:HarmonyOS NEXT(API 24)
核心技能:Stack 层叠布局 · @State/@Prop 状态管理 · Polygon 矢量绘制 · UI 动画过渡


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

在移动端与桌面端应用的交互设计中,工具提示(Tooltip) 是最基础也最重要的微交互之一。当用户将鼠标悬停在某个元素上时,Tooltip 在元素附近弹出一个信息卡片,提供额外的上下文说明,既不打断操作流,又能恰到好处地传递信息。作为 UI 设计师口中的"优雅的细节",一个精心设计的 Tooltip 能让应用的可用性显著提升,用户无需跳转到帮助页面即可获得所需信息。

在 HarmonyOS NEXT 生态中,ArkTS 作为首选的声明式 UI 开发语言,提供了丰富而灵活的布局容器。其中 Stack 容器 以其「子组件层叠排列」的特性,天然适合实现 Tooltip 这种浮层场景。与传统的绝对定位方式不同,Stack 让开发者以声明式的思维来组织浮层与底层内容的关系,代码更直观、可维护性更强。

本文将从完整的实战项目出发,带读者掌握如何使用 Stack 容器配合 ArkTS 状态管理和动画系统,构建功能完备的 Tooltip 组件库。 我们不仅会实现五种不同方向和风格的 Tooltip,还会深入剖析 @Prop 与 @State 的选择依据、Polygon 矢量绘制的数学原理、动画过渡的最佳实践,以及 API 24 环境下的踩坑记录。所有代码已在 HarmonyOS NEXT(API 24)环境下编译验证通过,可直接在 DevEco Studio 中运行预览。


二、HarmonyOS NEXT 与 ArkTS 布局体系概述

2.1 ArkTS 声明式 UI 的核心思想

ArkTS 基于 TypeScript 扩展,引入了 @Component、@Entry、@State、@Prop 等装饰器。其核心理念有三点:

  1. 状态驱动视图:UI 是状态(@State)的函数,状态变化时框架自动重绘。
  2. 组合优于继承:自定义组件如积木般自由组合。
  3. 链式属性配置:组件通过链式调用配置样式、布局和事件。

2.2 Stack 布局容器的设计哲学

Stack 是唯一允许子组件在 Z 轴(深度方向)上重叠的容器。核心规则:

  • 后入在上:先声明的子组件在底层,后声明的在上层。
  • 灵活的子项定位:通过 .align(Alignment.Top/Bottom/Start/End) 单独控制每个子项。
  • Z-Index 控制:通过 .zIndex() 显式控制层叠顺序。

这些特性使 Stack 成为实现悬浮按钮、提示框、遮罩层、徽标角标等浮层交互的理想容器。


三、项目全景:我们要实现什么

我们将构建包含 5 种不同弹出方向/风格 的 Tooltip 示例应用:

编号弹出方向技术特色
①上方弹出可复用 TooltipBox 组件
②下方弹出箭头方向动态切换
③左侧弹出Polygon 绘制水平三角
④右侧弹出Row 容器水平布局
⑤富文本卡片含标题/按钮的复合组件

每个示例均采用 Stack 容器 实现「触发元素在下、提示框在上」的层叠布局,配合 @State 状态驱动 和 getUIContext().animateTo() 实现平滑动画。


四、逐步拆解:从零构建 Tooltip 组件

4.1 工程搭建与页面路由

在 main_pages.json 注册页面路由:

{
  "src": ["pages/Index", "pages/TooltipSample"]
}

首页通过 router.pushUrl({ url: 'pages/TooltipSample' }) 实现跳转。在 API 24 中 pushUrl 标记为废弃但功能正常,推荐的替代方案是 UIAbilityContext.startAbility()。

4.2 核心组件:TooltipBox

我们将 Tooltip 核心逻辑封装到 TooltipBox 组件,通过 @Prop 暴露参数:

@Component
struct TooltipBox {
  @Prop tipText: string = '这是一个提示信息';
  @Prop tipWidth: number = 200;
  @Prop arrowDirection: string = 'down';
  @Prop isVisible: boolean = false;

  build() {
    Stack() {
      // ① 文本卡片
      Column() {
        Text(this.tipText)
          .fontSize(14).fontColor('#ffffff')
          .textAlign(TextAlign.Center).width('100%')
      }
      .width(this.tipWidth)
      .padding({ left: 16, right: 16, top: 10, bottom: 10 })
      .backgroundColor('#4a4a4a').borderRadius(8)

      // ② 三角箭头(Polygon)
      Polygon()
        .points([[0, 0], [20, 0], [10, 10]])
        .fill('#4a4a4a').width(20).height(10)
        .position({ x: (this.tipWidth - 20) / 2, y: 0 })
    }
    .shadow({ radius: 8, color: 'rgba(0,0,0,0.25)', offsetY: 4 })
    .visibility(this.isVisible ? Visibility.Visible : Visibility.Hidden)
    .transition(
      TransitionEffect.OPACITY
        .combine(TransitionEffect.translate({ y: this.arrowDirection === 'down' ? 8 : -8 }))
    )
  }
}
@Prop vs @State 的选择
  • private:属性完全私有,无法通过构造函数传值。
  • @State:内部状态,父组件传值仅初始化一次,后续父组件更新不会反映到子组件。
  • @Prop:单向数据流,父组件更新时子组件自动重新渲染。

凡是需要父组件驱动更新的属性,都应使用 @Prop。例如 isVisible 由父组件在 onHover 中动态设置,必须用 @Prop 以确保子组件实时响应。

Polygon 绘制三角箭头

三角箭头用 Polygon 通过三个顶点坐标动态绘制。以箭头朝下为例:

Polygon()
  .points([
    [0, 0],    // 左上
    [20, 0],   // 右上
    [10, 10]   // 下方中点(箭头尖)
  ])

该三角形宽 20vp、高 10vp。通过 .position({ x: (tipWidth - 20) / 2, y: 0 }) 居中定位。优势:无需外部图片、矢量化缩放、可动态控制颜色和尺寸。

过渡动画

TransitionEffect.OPACITY 处理淡入淡出,.combine(TransitionEffect.translate({ y: 8 })) 叠加 Y 轴位移。在 onHover 中使用 getUIContext().animateTo() 触发动画:

.onHover((isHover: boolean) => {
  this.getUIContext().animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
    this.isTooltipTopVisible = isHover;
  });
})

注意:API 24 中全局 animateTo() 已废弃,应改用 getUIContext().animateTo()。

4.3 五种 Tooltip 场景的实现

场景一:上方弹出(箭头朝下 ↓)
Stack {
  TooltipBox .align(Alignment.Top)    ← 上层
  Column ("触发目标") .onHover(...)   ← 下层
}

TooltipBox 通过 .align(Alignment.Top) 固定在 Stack 顶部,箭头朝下指向触发元素。

场景二:下方弹出(箭头朝上 ↑)

调换声明顺序:触发目标在下层先声明,TooltipBox 在上层后声明,使用 .align(Alignment.Bottom) 定位,arrowDirection: 'up'。

场景三、四:左/右侧弹出

水平方向 Tooltip 现场构建简化版本,使用 Row 容器排列文字和箭头:

if (this.isTooltipLeftVisible) {
  Row() {
    Text('左侧提示内容')
      .backgroundColor('#4a4a4a').borderRadius(8).shadow(...)
    Polygon().points([[0,0], [0,12], [8,6]])  // 朝右箭头 ▶
      .fill('#4a4a4a').width(8).height(12)
  }
  .align(Alignment.Start)
  .position({ x: -140, y: 30 })
}

箭头方向通过 Polygon 顶点坐标控制:朝右 ▶ 用 [[0,0],[0,12],[8,6]],朝左 ◀ 用 [[8,0],[8,12],[0,6]]。

场景五:富文本 Tooltip

包含标题、分隔线、正文和按钮的复合卡片:

Column {
  Row { Text('📢'), Text('系统通知') }  ← 标题
  Divider()                               ← 分隔线
  Text('HarmonyOS NEXT 原生布局...')      ← 内容
  Button('知道了') .onClick({ toast })    ← 操作
}

证明 Stack + Tooltip 组合不仅限于简单文本,可承载复杂交互内容。


五、深入理解关键技术点

5.1 状态管理的分层设计

--- 父组件(TooltipSample) ---
@State isTooltipTopVisible: boolean = false
@State isTooltipBottomVisible: boolean = false
@State isTooltipLeftVisible: boolean = false
@State isTooltipRightVisible: boolean = false
@State isTooltipCustomVisible: boolean = false
         │ 通过构造函数传值
         ▼
--- 子组件(TooltipBox) ---
@Prop isVisible: boolean = false

父组件拥有状态所有权,onHover 事件在父层处理,状态通过 @Prop 单向流入子组件。好处:单一数据源、可预测渲染路径、子组件无状态副作用。

5.2 条件渲染的两种模式

  1. .visibility(condition ? Visible : Hidden) —— 组件始终在 DOM 树中,适用有过渡动画的场景。
  2. if (condition) { ... } —— condition 为 false 时完全移除 DOM 树,性能更优但不支持 transition 动画。

选择建议:需要过渡动画时用 visibility,追求极致性能时用 if。

5.3 动画性能优化

getUIContext().animateTo() 的工作原理:

  1. 在回调中修改状态变量。
  2. 框架记录状态变更前后的 UI 树差异(diff)。
  3. 在指定 duration 内以指定 curve 插值过渡。
  4. 仅对有变化的属性执行动画,不触发全量重绘。

5.4 与 CSS Tooltip 对比

对于从 Web 前端转来的开发者,ArkTS 的 Tooltip 实现方式与 CSS 有显著差异:

维度Web CSS TooltipArkTS Stack Tooltip
布局position:absolute + z-indexStack 层叠 + .align()
显隐display: none/blockvisibility / if 条件渲染
动画CSS transition.transition() + animateTo()
箭头CSS border 技巧 / ::beforePolygon 矢量绘制
状态管理JS 变量 + DOM 操作@State / @Prop 装饰器
复用需封装 Web Component天然 @Component 复用

ArkTS 的声明式模型与 React/Vue 更为相似,都强调状态驱动 UI和组件化,上手曲线相对平缓。值得注意的是,ArkTS 在编译时会进行静态类型检查,像 offset 属性名冲突这类问题在 Web 项目中可能要到运行时才暴露,但在 ArkTS 中编译阶段即可捕获,这是类型安全的一大优势。


六、踩坑与最佳实践

6.1 offset 不可用作自定义属性名

offset 是基类 CustomComponent 的保留属性(对应 .offset() 方法),自定义属性与之重名会导致类型冲突。应使用具有业务含义的名称如 gap、spacing。

6.2 getUIContext().animateTo() 替代全局 animateTo()

全局 animateTo() 在 API 24 中标记为废弃,应改用组件实例方法 this.getUIContext().animateTo(),语义更清晰且与组件生命周期绑定更紧密。

6.3 @Prop 与 @State 的传值差异

@State 在子组件中维护的是独立副本,父组件变更不会自动传递。而 @Prop 用于接收父组件单向数据流,父组件的每次状态更新都会触发子组件重新渲染。核心原则:子组件需要读写的本地数据用 @State,从父组件接收的只读数据用 @Prop,需要双向同步则用 @Link。 理解这三者的区别是 ArkTS 状态管理的关键。

6.4 Polygon 坐标原点

Polygon.points() 的坐标相对于 Polygon 组件自身的左上角,而非父容器。.position() 中的偏移量以父容器坐标系为准。


七、扩展与展望

7.1 可做的改进方向

  1. 自动位置适配:根据屏幕边缘距离自动选择弹出方向。
  2. 延迟显示/隐藏:添加 showDelay/hideDelay 防止快速划过时闪烁。
  3. 主题定制:通过 @Prop 暴露更多样式参数。
  4. 触屏支持:在移动端用 .onTouch() 替代 .onHover()。
  5. 无障碍:添加屏幕阅读器语义标签。

7.2 Stack 布局的其他应用场景

  • 图片占位 + 加载指示器
  • 角标(Badge)
  • 遮罩层(Overlay)
  • 水印
  • 进度环

7.3 写在最后

从 2024 年 Q4 起,华为要求新上架应用必须使用 HarmonyOS 原生框架开发。这意味着掌握 Stack、Column、Row、RelativeContainer 等核心布局方式将成为鸿蒙开发者的基本功,构建可复用的自定义组件库则成为核心竞争力。

回顾本文的核心要点:Stack 容器通过层叠机制实现了 Tooltip 的浮层效果,@Prop 装饰器保障了父子组件间的状态同步,Polygon 组件以矢量方式绘制了精致的三角箭头,getUIContext().animateTo() 则赋予了交互以生命力。这四种技术组合起来,构成了一个完整、优雅、高性能的 Tooltip 组件方案。

希望通过本文,读者能深入理解 ArkTS 声明式 UI 的核心设计理念:状态驱动视图、组合优于继承、链式属性配置。这些理念将贯穿于所有 HarmonyOS NEXT 原生应用开发的始终。掌握这些基础但强大的布局工具,是构建高质量原生应用的第一步。

更多推荐