序章:UI动效第一步——放弃控制权,还是接管一切?

102 阅读5分钟

序章:UI动效第一步——放弃控制权,还是接管一切?

tab.png

关键词:声明式 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)用一棵"控制权决策树"做选型

可以把它当成一个判断流程:

截图_20260119120205.png

结论是:当需求复杂到"吸顶折叠 + 背景变形 + 首项固定 + 居中滚动"这个级别,路线 B 基本是唯一能长期维护的选择。


4)路线 B 的必然结果:将组件拆分为三层闭环

路线定好,接下来要考虑:

  • 位置怎么算?需要知道元素的尺寸(icon 多大、文字多宽)
  • 尺寸怎么定?需要知道样式(颜色、字号、图片)
  • 样式怎么选?需要知道状态(激活/未激活、吸顶/未吸顶、亮色/暗色)

于是自然就会把问题拆成三层:

  1. 状态 → 样式(Material 物料层)
  2. 样式 + 测量 → 位置(Geometry 几何层)
  3. 位置 → 渲染(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)这套闭环带来的"工程收益"是什么?

与其说"代码更优雅",不如用工程语言描述它的收益:

  1. 确定性:端点确定,首帧就准
  2. 可回归:几何输出可以打印、可以对比、可以做边界测试
  3. 可扩展:新增动效/状态,强调的是"加一个输出维度",而不是"改一堆 if/else"
  4. 性能更稳:动画期间主要在 transform,减少 re-layout 风险

6)本系列接下来怎么写(从这棵树往下展开)

后续每篇文章,会围绕这棵决策树展开:

截图_20260119120435.png

如果只记住一句话:

复杂动效要做稳,不要问"哪个容器更好",先问"动画期间谁来决定位置"。


写在最后:这是《复杂tab动效组件架构设计》系列专栏的开篇,后续会逐步展开每个技术点的实现细节。感兴趣的话可以关注专栏。如果你也在做类似的复杂动效,或者对"几何驱动"的架构思路有疑问,欢迎在评论区交流。如果觉得这个系列对你有帮助,也欢迎点赞支持,这会是我继续分享的动力。