首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KuiklyUI 动画链路解析:属性变化如何带上动画

KuiklyUI 动画链路解析:属性变化如何带上动画

原创
作者头像
骑猪耍太极
发布2026-07-15 23:49:44
发布2026-07-15 23:49:44
170
举报

前面的文章讲过响应式、布局和 Bridge。状态变化后,KuiklyUI 会重新执行相关 attr 块,把新的属性值下发到 Render。默认情况下,属性会立即生效。

动画系统处理的是另一种属性变化:目标值已经确定,但界面不应该瞬间跳过去,需要在一段时间内过渡。比如透明度从 0 到 1,位置从上方滑入,缩放从 1 到 1.2。

这篇文章只看主流程:Kotlin 侧怎么描述动画,属性变化时怎么把动画带上,最后 Render 侧怎么执行。那些更细的分支,比如复杂 completion、旧 API 兼容、平台内部动画类,不在这里展开。


一、为什么不逐帧下发

最直接的动画做法,是 Kotlin 每一帧都算一个新值,然后通过 Bridge 发给 Render:

代码语言:bash
复制
每帧计算一次属性值
  ↓
每帧 SET_VIEW_PROP 或 SET_RENDER_VIEW_FRAME
  ↓
Render 每帧应用新值

这个方案能跑,但成本高。一个动画一秒大概 60 帧,页面上只要有几个动画,Bridge 调用量就会持续增加。更重要的是,各个平台本来就有自己的动画能力,Kotlin 没必要绕过它们去逐帧驱动。

KuiklyUI 选择的是另一条路:Kotlin 只下发动画描述和目标值,插值和逐帧渲染交给 Render 背后的平台动画系统。

动画系统:Kotlin 描述,Render 执行
动画系统:Kotlin 描述,Render 执行

图里可以先抓住一个点:Kotlin 不负责每一帧,它只负责告诉 Render“这次属性变化要带动画”。Render 收到这个信息后,再交给平台动画系统执行。这里的 Render 是跑在各原生平台上的渲染层,平台动画系统则是各平台自带的动画能力,比如 Android 的属性动画、iOS 的 Core Animation。后面提到执行动画的一侧,说的都是它们。

落到 Bridge 指令上,大概是三步:

代码语言:bash
复制
SET_VIEW_PROP("animation", animationDesc)
SET_VIEW_PROP("transform", targetValue)
SET_VIEW_PROP("animation", "")

这里的 animation 不是一个长期样式。它更像一次临时开关:先打开动画上下文,中间下发目标属性,最后关掉上下文。


二、Animation 只描述动画参数

业务代码里常见的动画写法是这样:

代码语言:kotlin
复制
Animation.easeInOut(0.3f)
Animation.springEaseOut(0.5f, damping = 0.7f, velocity = 1f)

这些对象本身不会播放动画。它们只是把一段动画需要的信息收起来,比如动画类型、缓动函数、时长、弹簧参数、延迟、是否循环,以及一个可选的 key。

最后跨 Bridge 传递时,这些信息会被序列化成一段用空格分隔的短字符串。比如 easeInOut(0.3f) 作用在 scale 上,序列化出来大概是 0 3 0.3 0 0 0 0 scale,依次是动画类型、缓动函数、时长、弹簧阻尼、弹簧速度、延迟、是否循环,最后跟一个 key。后面 Bridge 指令里再看到这串数字,指的就是它。Render 侧解析这段字符串,再映射到平台自己的动画能力。

所以可以把 Animation 理解成一张说明书:Kotlin 写说明书,Render 按说明书驱动平台动画执行。Kotlin 不关心 Android 最后用哪个动画类,也不关心 iOS 怎么配置 Core Animation。


三、声明式动画的主流程

声明式动画通常写在 attr 块里:

代码语言:kotlin
复制
attr {
    animate(Animation.easeInOut(0.3f), ctx.scale)
    transform(scale = Scale(ctx.scale, ctx.scale))
}

这段代码里有两个动作。

animate(...) 表示:如果这次变化和 ctx.scale 有关,就使用这段动画。transform(...) 表示:最终目标值是新的 scale。

也就是说,animate 不是直接改 UI 的地方,它只是登记动画;改变 UI 目标值的是后面的 transform

问题在于,框架怎么知道 animatectx.scale 有关?这里借用了响应式系统。attr 执行时,读到 ctx.scale 会留下一个内部 key;后面 ctx.scale 被写入时,也会带着这个 key 触发重新执行。动画系统就用这个 key 把“登记动画”和“状态变化”对应起来。

声明式动画绑定响应式字段
声明式动画绑定响应式字段

这张图用 scale1.0 变成 1.5 做例子。首次执行 attr 时,框架先记住一件事:scale 变化时,要使用 easeInOut(0.3) 这段动画。后面业务把 ctx.scale 改成 1.5attr 重新执行,框架就能找回这段动画,同时拿到新的目标值 scale(1.5)

所以声明式动画的重点在于:状态变化时,框架要能找到对应动画和目标值。下一步就是把这两样东西一起发给 Render。


四、目标属性如何带上动画

先看最常见的普通属性,比如 transformopacitybackgroundColor。这些属性可以直接下发给 Render,不需要等待布局计算。

动画要做的事情很简单:在目标属性前后包一层 animation

animation 属性包裹与 Render 提交
animation 属性包裹与 Render 提交

图里的前后两个 animation 指令是上下文开关。非空字符串表示开始收集动画操作,空字符串表示提交并清空。中间的目标属性才是要变化的内容。

换成指令就是:

代码语言:bash
复制
SET_VIEW_PROP("animation", "0 3 0.3 0 0 0 0 scale")
SET_VIEW_PROP("transform", "scale(1.5, 1.5)")
SET_VIEW_PROP("animation", "")

Render 收到第一条 animation 后,会进入动画上下文。接下来收到 transform,如果这个属性支持动画,就先记录成一次动画操作。等收到最后的空 animation,Render 再把这一批动画操作提交给平台动画系统。

这个设计的好处是边界很清楚。动画只影响被包住的那一组属性;上下文结束后,后续普通属性更新不会被上一段动画带走。


五、布局动画多了一步 frame

普通属性比较好理解,因为目标值就是业务写出来的值。布局属性会多一步。

比如 widthheightmargin 变化后,业务代码写的是布局约束,不是最终 frame。最终要下发到 Render 的 frame,要等 FlexLayout 计算完才知道。

代码语言:bash
复制
observable 写入
  ↓
attr 重新执行,布局属性变化
  ↓
FlexNode 标脏
  ↓
LayoutImpl 算出新 frame
  ↓
SET_RENDER_VIEW_FRAME 下发

这里有一个时间差:动画在 attr 执行时已经登记了,但目标 frame 要等布局计算之后才出现。

所以布局动画只需要记住一件事:哪段动画是给布局变化用的。等 frame 算出来,再用这段动画包住 SET_RENDER_VIEW_FRAME

换句话说,普通属性包的是 SET_VIEW_PROP,布局属性包的是 SET_RENDER_VIEW_FRAME。两者的思想一样,差别只是目标值出现的时间不同。


六、命令式动画只是换入口

前面讲的是声明式动画:状态变化触发 attr 重新执行,框架根据响应式字段找回动画。

还有一种场景,不想为了播放一次动画专门定义状态字段,只想在点击、滑动、页面出现时临时播放一次。这时会用 animateToAttr

代码语言:kotlin
复制
view.animateToAttr(Animation.springEaseOut(0.5f, 0.7f, 1f)) {
    transform(translate = Translate(0f, 100f))
    backgroundColor(Color.RED)
}

它和声明式动画的差别只在入口。声明式动画由状态变化触发,命令式动画由一次函数调用触发。进入属性下发阶段后,两者都复用同一套机制:打开 animation 上下文,下发目标属性,再关闭上下文。

如果命令式动画需要 completion,框架也不会把 Kotlin 函数直接传给 Render。它会生成一个 key,Render 侧动画完成后把 key 带回来,Kotlin 再用这个 key 找到对应回调执行。这个细节知道即可,不影响主流程理解。


七、这套设计的取舍

把动画交给平台动画系统执行,收益首先体现在 Bridge 压力上。Kotlin 只下发动画描述和目标值,不逐帧更新。平台动画系统可以自己调度帧、做合成和硬件加速。

代价也很清楚。Animation 只能描述各平台都比较容易承接的一组能力,比如透明度、位移、缩放、背景色、frame。复杂 keyframe、自定义曲线、动画中途精确控制、跨平台完全一致的 spring 物理效果,没有放在这套轻量协议的主路径里。

所以它适合业务 UI 里的常规过渡。页面进入、按钮反馈、展开收起、尺寸变化,这些场景更需要稳定和低成本,不需要把每个平台动画能力都抽象成一套大而全的 DSL。


总结

KuiklyUI 动画系统的主线可以压缩成四步:

代码语言:bash
复制
状态变化
  ↓
attr 重新执行,找到动画和目标值
  ↓
Bridge 用 animation 包住目标属性
  ↓
Render 驱动平台动画执行

理解这条主线就够了。Animation 负责描述动画,animate 负责把动画和状态变化关联起来,animation 属性负责临时打开和关闭动画上下文,Render 背后的平台动画系统负责实际执行。

布局动画和命令式动画都没有脱离这条主线。布局动画只是目标值要等 frame 计算出来;命令式动画只是触发入口从状态变化换成了一次函数调用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么不逐帧下发
  • 二、Animation 只描述动画参数
  • 三、声明式动画的主流程
  • 四、目标属性如何带上动画
  • 五、布局动画多了一步 frame
  • 六、命令式动画只是换入口
  • 七、这套设计的取舍
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档