用途
这条经验用于指导 Compose 开发中一个常见但不易察觉的错误:为那些自身已经负责动画的组件属性再套一层外部动画。它帮助开发者识别哪些参数应当传入原始目标值,哪些参数才适合用 animateFloatAsState 之类的补间平滑。
主要能力与核心观点
有些组件接收的属性由组件内部动画驱动:它们持有自己的方向性 spec(directional spec)并自行驱动数值。此时正确的输入是“目的地”,而不是通往目的地的平滑路径。把平滑后的值交给它,并不是“更顺滑”,而是每一帧都在下达一条新指令。
文中示例使用原始 0f/1f 翻转,刻意不使用 animateFloatAsState:
// as written — a raw 0f/1f flip, deliberately not animateFloatAsState
val targetAmplitude = if (isPlaying && !isInteracting) 1f else 0f
陷阱机制
组件的更新守卫会丢弃动画进行中到达的目标,因此第一个目标胜出。外部补间每秒送来约六十个略有不同的值。第一帧会启动一次朝向“非常接近旧值”的内部动画(例如 0.97 而不是 0),此后每个更小的目标都会在该动画仍在进行时被吞掉。可见结果是属性冻结在接近起点的位置——例如音频暂停后,波纹仍停在接近满幅处。整个过程不会报错,而且在真机上比在预览中更容易复现。
正确做法
在两个端点之间做硬切换就是修复方案,而且只触发一次重组。新值让调用方重组一次,新的 lambda 使节点的绘制缓存失效,组件自己的方向性 spec 会以设计者选定的时序渲染变化——那个时序通常正是你原本想手工实现的东西。
在包装任何东西之前先识别形态。两个信号:参数文档说明该值是“被动画”而非“是”某个值;组件没有为它暴露 animationSpec。如果你无法给它一个 spec,说明它自己拥有一个——那么外部动画就是在竞争,而不是协作。
按组件要求传入 lambda。这类参数通常是 () -> Float,以便组件在不重组的情况下读取。把值读入局部变量再闭包捕获仍然可以——每次重组产生新 lambda 才是使绘制缓存失效的关键——但把 lambda 提升到带错误 key 的 remember 中,会从另一侧重新引入陈旧目标问题。
组合端点,不要平滑它们。把条件(playing && !scrubbing)合并为单个布尔值,再把这个布尔值映射到两个常量,可保持输入为阶跃函数。任何产生中间值的算术——分数、淡入淡出乘数、缓动曲线——都会让你回到“目标流”的情形。
用 lambda 自身的参数做门控,而不是用你自己保存的副本。这类参数通常会被传入组件当前正在绘制的值——因此一个必须在零处消失的装饰应读取该参数({ p -> if (p > 0f) target else 0f }),而不是你上次计算出的分数。在拖动(scrub)过程中两者不同,读取自己的副本会重新引入组件内部动画本要避免的冲突。
无效做法
缩短自己的 tween 没有帮助:它改变的是第一个目标何时落地,而不是其余目标是否被丢弃。对自己拥有的 Animatable 使用 snapTo 也没有帮助;关键在于不再拥有该值,而不是更快到达终点。
适用场景
同样的陷阱存在于每个“内部 Animatable + 守卫”的组件中:进度指示器、展开式表面、形变容器形状、不确定加载器。当组件行为在第一次变化后看起来“卡住”时,先怀疑所有权,再怀疑自己的状态。
验证方式
枚举设计系统产物中自己拥有动画的组件。只读、无需构建;每个命中都是一个其动画参数必须以原始值传入的组件:
JAR=$(find ~/.gradle/caches/modules-2 -name '*material3*.jar' ! -name '*sources*' | sort -V | tail -1)
echo "$JAR"; D=$(mktemp -d); unzip -oq "$JAR" -d "$D"
for C in "$D"/androidx/compose/material3/*Kt.class; do B=$(basename "$C" .class)
javap -p -cp "$D" "androidx.compose.material3.$B" 2>/dev/null \
| grep "private static final androidx.compose.animation.core.AnimationSpec" | sed "s|^|$B: |"
done
一对方向性 spec(一个递增、一个递减)是最强信号:组件正在按方向选择 spec,而只有它拥有该过渡时才能这样做。sort -V 选取的是最新的缓存产物,不一定是你的构建实际解析到的那个——请阅读回显的路径。
列出每个看起来像动画参数的绑定及其接收组件——第 1 步决定其中哪些重要:
grep -rnE --include='*.kt' --exclude-dir=build -B1 "^ +(amplitude|progress) = " .
同一个参数名在一个组件上安全、在另一个上不安全:普通进度指示器不会出现在第 1 步的普查中,因此给它喂 animateFloatAsState 值是平滑跳动位置的常规做法——而那个绑定在这个列表里看起来与危险的那个一模一样。
手动验证:触发过渡,然后立即再次触发,然后放置几秒后再触发一次。预先动画的输入通常只生效一次——孤立的那第三次尝试才会暴露问题。
限制
资料未提供该条目的作者、发布时间、许可协议或官方文档链接等元信息。