防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。 防止微信内出现全文翻译提示。因此在这里放一个充满中文的分区。
2024年3月16日@Aragakey.

转盘交互动画:以关键参数,细化我们的感受

2023 朋友圈广告年度评选 中,设计师与我实现了一个顺逆时针、无限旋转的转盘交互动画。我们可以在这里直接使用手指、鼠标或触控板体验一下:

左右滑动体验

在这篇文章中,我们将通过文字和交互式 demo 穿插的方式,一起思考和体验:有哪些关键参数影响着最终结果。我们不需要记住,甚至不需要理解这些参数,更重要的是,我们能够细化对交互动画的感受,体会“对”或“不对”之间的具体差异。

在这个交互体验中,有 4 个问题可以被关注:

  1. 手指是横向移动的,而转盘是旋转的,这两者的变化关系是什么?
  2. 松开手指后,应该停在哪?
  3. 停到那个地方,要花多久?
  4. 是否需要在目标位置加回弹?回弹应该是什么样的?

横移和旋转是什么关系?

转盘要跟着手指旋转,一种做法是通过手指初始位置的 [x1, y1] 与移动过程中的实时位置 [x2, y2] 计算出夹角角度。这里为了方便,我直接使用了 x 轴的位移量去映射角度的变化。因此,我需要一个比例系数 x:deg

  1. 如果值太大,转盘会变得 迟钝
  2. 如果值太小,转盘会变得 灵敏
  3. 目标:要保证 跟手

我最终设置的值为 40,即 40px 的位移量对应 1deg 的旋转角度。下面是一个初始设置为 80 的例子,我们可以体验到不跟手的迟钝感,也可以在右上方对该参数进行调节:

左右滑动体验

松开手指后,应该停在哪?

在松开手指的瞬间,就会拿到移动速度 velocity,这是一个实时计算的值,是不需要、也无法调整的值。接着,我指定了一个 power 系数,乘以 velocity 以得到松手后走的距离。改变这个值,就能调整距离的远近。

velocity = Δx / Δtime
amplitude = power * velocity
velocity = Δx / Δtime
amplitude = power * velocity
  1. 如果值太大,松手后走的距离 太远
  2. 如果值太小,松手后走的距离 太近
  3. 目标:要兼顾 逐张浏览快速浏览 的体验。

我最终设置的值为 0.35。下面是一个初始设置为 1.2 的例子,我们可以体验到松手后走的距离较远,也可以在右上方对该参数进行调节:

左右滑动体验

停到那个地方,要花多久?

有办法调整最终停在哪里了,下一个问题是需要多久。这里我们可能会想到,设置一个 duration 的参数。只要改变 duration,再配合上一个贝塞尔曲线,就能控制动画停下来的整体表现了。这种常规的思路其实也大致行得通。

麻烦的是,为了让衰减效果尽可能自然,就需要对不同的距离设置不同的 duration,而且——按照我的经验——还需要对应地调整贝塞尔曲线。这种做法无法穷尽所有的距离,我们只能对距离设置几个范围,尽可能地调优,以达到看起来自然的结果。

重点是我们要清楚,这是一种本末倒置,因为:

时间是物体运动的结果,而非原因。

这里,实际上我需要的是很简单的衰减函数:

f(x) = (amplitude - amplitude * e) ^ (-x / timeConstant)
f(x) = (amplitude - amplitude * e) ^ (-x / timeConstant)

这个函数描述了一个随着时间 x 的增加而逐渐衰减,最后接近目标值的效果。它由两个关键参数组成:振幅(amplitude)和时间常数(timeConstant)。

  1. 振幅 amplitude:表示振动的最大幅度或强度,它实际上就是我上面提到的 amplitude = power * velocity
  2. 时间常数 timeConstant:表示振幅衰减的速度,它决定了振幅随着时间的推移而减小的速度。

假设已经得到 amplitude = 100,当 timeConstant = 180 时,将会得到下面的曲线:

对比看一下 timeConstant = 500 时的曲线:

时间常数 timeConstant 越大,曲线越平缓,衰减越慢。从图形上观察,当 timeConstant = 180 时,动画静止大概要花 600ms;当 timeConstant = 500 时,动画静止大概要花 1200ms(事实上曲线会无限接近极限值,而无法达到极限值)。

重要的是,其实我并不关心动画停止的具体时间。还是那句话,时间是物体运动的结果,而非原因。我只需知道:

  1. 如果值太大,动画会停止得 太慢
  2. 如果值太小,动画会停止得 太快
  3. 目标:要保证停下来的过程既是 优雅 的,同时也足够 高效

我最终设置的值为 180。下面是一个初始设置为 500 的例子,我们可以体验到停止得非常慢,也可以在右上方对该参数进行调节:

左右滑动体验

最后,从衰减函数的构成来看,有一个值也可以变化,但是我并没有提到它。它就是底数 e。作为指数函数的底数,其也会影响振幅的衰减速度,当底数越大时,函数的值会越快地接近极限值。从图形上来看,整个曲线就会越“陡峭”。

这个底数,一般就设置为自然常数 e。它有一个特殊的性质,它的导数(斜率)与该点的函数值相等。这里不展开讨论。

是否需要回弹?

转盘停到目标位置前,我们加入了一个回弹的效果。最开始我没有考虑要加入,因为这是一个无限旋转的转盘,不应该存在任何边界,也就不应该有任何回弹。我们可以在下面体验一下没有回弹的样子:

左右滑动体验

在体验了这一版之后,设计师认为好像缺了一点“确认感”。我想,这是因为虽然转盘确实减速停止到了目标位置,但是没有更多反馈告诉用户“我已经停下来了,你可以继续操作了”。加入回弹效果,相当于强化了这种反馈,告诉用户“我已经抵达了这次的目标位置”。UI 通过更强的反馈,更加直截地和用户进行沟通。

在我看来,回弹效果是加强反馈的一种方式。有没有其他方式加强反馈呢?我试过在手指按下时,所有卡片缩小到 95%,松手后再放大。效果如下:

左右滑动体验

嗯,好像是那么回事,不仅对停止做了反馈,且手指按下时也有了更强的反馈。似乎也不错,不过我最后还是决定使用了用户更加熟悉的回弹效果。

回弹效果的实现依靠的是一个简单的弹簧动画。如果我们熟悉弹簧动画的原理就会知道,一般来说调节这两个参数,就可以得到不同的回弹效果:弹性系数 stiffness 和阻尼系数 damping。我并不打算在这里详细讲解。

这两个值和动画表现的关系不是线性的,没有办法用很简单的线性关系来描述。它们是相辅相成的,就和现实生活中的弹簧一样,我们可以这样理解它们:

  1. 弹性系数 stiffness 越大,弹簧就会越有劲,动画的表现就会越 活泼,同时也可能越 生硬
  2. 阻尼系数 damping 越大,弹簧就越难运动,动画的表现就会越 平缓
  3. 目标:要保证回弹足够 柔和,同时要控制总的动画时间不会过长。

我最终设置的值分别为 8030。下面是一个初始设置为 1807 的例子,由于阻尼系数太小,我们可以体验到夸张的多段回弹,也可以在右上方对这两个参数进行调节:

左右滑动体验

结语

通过这篇文章我们看到,转盘动画的背后,存在着数个关键参数。理解这些参数,不(仅)是为了理解实现原理本身,而是为了在改变这些参数的过程中,寻求简单的“对”或“不对”之间的具体差异,更是为了细化对交互动画的具体感受。

Analyzing and making sense of design details beyond just "it feels nice" helps nurture taste, amplify level of execution, and grow appreciation for how hard the pursuit of excellence is. —— Invisible Details of Interaction Design

一个简单的交互动画背后,存在着数个关键参数,它们就是所谓的“隐形细节”。