【共创季稿事节】鸿蒙原生 ArkTS 布局深度解析:用 Stack 容器实现 Tooltip 工具提示
鸿蒙原生 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 等装饰器。其核心理念有三点:
- 状态驱动视图:UI 是状态(
@State)的函数,状态变化时框架自动重绘。 - 组合优于继承:自定义组件如积木般自由组合。
- 链式属性配置:组件通过链式调用配置样式、布局和事件。
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 条件渲染的两种模式
.visibility(condition ? Visible : Hidden)—— 组件始终在 DOM 树中,适用有过渡动画的场景。if (condition) { ... }—— condition 为 false 时完全移除 DOM 树,性能更优但不支持transition动画。
选择建议:需要过渡动画时用 visibility,追求极致性能时用 if。
5.3 动画性能优化
getUIContext().animateTo() 的工作原理:
- 在回调中修改状态变量。
- 框架记录状态变更前后的 UI 树差异(diff)。
- 在指定
duration内以指定curve插值过渡。 - 仅对有变化的属性执行动画,不触发全量重绘。
5.4 与 CSS Tooltip 对比
对于从 Web 前端转来的开发者,ArkTS 的 Tooltip 实现方式与 CSS 有显著差异:
| 维度 | Web CSS Tooltip | ArkTS Stack Tooltip |
|---|---|---|
| 布局 | position:absolute + z-index | Stack 层叠 + .align() |
| 显隐 | display: none/block | visibility / if 条件渲染 |
| 动画 | CSS transition | .transition() + animateTo() |
| 箭头 | CSS border 技巧 / ::before | Polygon 矢量绘制 |
| 状态管理 | 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 可做的改进方向
- 自动位置适配:根据屏幕边缘距离自动选择弹出方向。
- 延迟显示/隐藏:添加
showDelay/hideDelay防止快速划过时闪烁。 - 主题定制:通过
@Prop暴露更多样式参数。 - 触屏支持:在移动端用
.onTouch()替代.onHover()。 - 无障碍:添加屏幕阅读器语义标签。
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 原生应用开发的始终。掌握这些基础但强大的布局工具,是构建高质量原生应用的第一步。
更多推荐

所有评论(0)