序章:UI动效第一步——放弃控制权,还是接管一切?
关键词:声明式 UI / ArkUI / 复杂动效 / 吸顶折叠 / 预计算 / transform / 架构分层
说明:本文阐述的"控制权决策树"和"三层架构"思路是通用的,适用于任何声明式 UI 框架。但本系列后续文章中的具体实现细节和代码示例均基于 ArkUI(ArkTS)。如果你使用的是 React、Flutter、SwiftUI 等其他框架,核心思路仍然适用,只需将具体的 API 调用替换为对应框架的等价实现。
这不是一篇"某个 API 怎么用"的文章,而是在做一个复杂 Tab 导航组件时,最先需要进行的一次设计规划:到底让谁来决定布局?
很多同学做动效会先冲进代码:先把 UI 摆出来,再一点点补动画。
但如果你的需求里同时出现这些关键词:
- 吸顶折叠(展开态/折叠态两套视觉)
- icon+文字横纵切换
- 背景形状跟随激活项移动,且折叠后形态变化
- 首项固定 + 边缘遮挡策略
- 横向滚动居中
- 亮暗色、多位置、多业务形态
那么建议先停一下。
因为这种组件最容易走向一种"工程债务":
- 靠系统布局先把效果凑出来
- 然后为了补齐动效又不断加 Stack、加中间层、加 onAreaChange 兜底
- 最后会发现:不是某个 bug 难修,而是整体路线选错了
这篇序章,先把"路线选择"讲清楚:通过一棵控制权决策树,可以推导出一整套架构(Material → Geometry → Render)。后续每一篇文章,都只是这棵树的自然展开。
1)第一性问题:动画期间,谁来决定"位置"?
可以把方案分成两大类,不以具体容器命名,而以"控制权归属"命名:
路线 A:布局系统驱动(Layout-driven)
典型表现:
- 用
listDirection/ Flex 方向切换 / 双模板切换 等,让框架在不同状态下自动重排
开发者提供的是"规则",位置是布局算法的输出。
路线 B:几何驱动(Geometry-driven)
典型表现:
- 子元素用
position({x:0,y:0})脱离布局流 - 自己计算两态的
(x,y,width,height) - 动画期间主要改变
translate/scale/opacity(渲染变换)
开发者提供的是"结果",框架只负责渲染。
2)为什么"横纵切换"是架构的关键分岔口?
因为横纵切换表面是"icon 和文字怎么排",但本质是:
是否允许布局系统在动画期间持续参与。
一旦选择路线 A(布局系统驱动),后面很多能力会被锁上限:
- 端点不确定:位置往往要等布局结束才知道
- 轨迹不可控:想做分段、延迟、overshoot 会非常别扭
- re-layout 干扰:动画过程中改 width/margin/direction,重排会打断补间,导致抖动/跳帧风险
- 后续复杂需求很难叠:背景形状变化、首项固定遮挡、滚动居中,会变成补丁叠补丁
而一旦选择路线 B(几何驱动),就必须承担相应的代价:
- 必须建立"几何计算层"(要算坐标,要测文本,要缓存)
但回报也非常明确:
- 首帧确定性:动画开始前端点已确定
- 轨迹可控:所有元素可用同一时间轴插值
- 动效稳定:动画主要在 transform,不被重排打断
- 复杂能力自然打开:背景图层化、targetScrollOffset 预计算、首项遮挡状态机,都变成"顺手的扩展"
3)用一棵"控制权决策树"做选型
可以把它当成一个判断流程:
结论是:当需求复杂到"吸顶折叠 + 背景变形 + 首项固定 + 居中滚动"这个级别,路线 B 基本是唯一能长期维护的选择。
4)路线 B 的必然结果:将组件拆分为三层闭环
路线定好,接下来要考虑:
- 位置怎么算?需要知道元素的尺寸(icon 多大、文字多宽)
- 尺寸怎么定?需要知道样式(颜色、字号、图片)
- 样式怎么选?需要知道状态(激活/未激活、吸顶/未吸顶、亮色/暗色)
于是自然就会把问题拆成三层:
- 状态 → 样式(Material 物料层)
- 样式 + 测量 → 位置(Geometry 几何层)
- 位置 → 渲染(Render 渲染层)
这样每一层都有明确的输入和输出,复杂度被隔离了。
flowchart LR
subgraph 输入
S[State<br/>选中/吸顶/主题/位置]
end
subgraph 三层架构
M[Material 物料层<br/>颜色/字号/图片/基础尺寸]
G[Geometry 几何层<br/>x/y/width/height/bgX]
R[Render 渲染层<br/>position/translate/scale]
end
subgraph 输出
U[UI 最终呈现]
end
S --> M
M --> G
G --> R
R --> U
style M fill:#E6E6FA
style G fill:#98FB98
style R fill:#87CEEB
4.1 Material(物料层):状态 → 颜色/字号/图片/基础尺寸
- 解决的问题:状态组合爆炸(激活/吸顶/亮暗/位置/业务)
- 方法:预计算查表缓存
- 典型实现:
StyleManager
4.2 Geometry(几何层):物料 + 测量 → 坐标/宽高/滚动目标
- 解决的问题:两态几何(展开/折叠)必须在动画前确定
- 方法:集中计算 + 缓存输出
- 典型实现:
GeometryCalculator
4.3 Render(渲染层):消费几何 → position/translate/scale/visibility
- 解决的问题:让 UI 代码变薄、变稳定
- 方法:Modifier 只负责"把结果贴上去"
- 典型实现:
Modifiers(icon/text/bg/scroll 等 modifier)
5)这套闭环带来的"工程收益"是什么?
与其说"代码更优雅",不如用工程语言描述它的收益:
- 确定性:端点确定,首帧就准
- 可回归:几何输出可以打印、可以对比、可以做边界测试
- 可扩展:新增动效/状态,强调的是"加一个输出维度",而不是"改一堆 if/else"
- 性能更稳:动画期间主要在 transform,减少 re-layout 风险
6)本系列接下来怎么写(从这棵树往下展开)
后续每篇文章,会围绕这棵决策树展开:
如果只记住一句话:
复杂动效要做稳,不要问"哪个容器更好",先问"动画期间谁来决定位置"。
写在最后:这是《复杂tab动效组件架构设计》系列专栏的开篇,后续会逐步展开每个技术点的实现细节。感兴趣的话可以关注专栏。如果你也在做类似的复杂动效,或者对"几何驱动"的架构思路有疑问,欢迎在评论区交流。如果觉得这个系列对你有帮助,也欢迎点赞支持,这会是我继续分享的动力。