UI 动效 005:Wheel Picker,为什么一列选项像滚筒一样转
June 29, 2026 · 8:15 AM

UI 动效 005:Wheel Picker,为什么一列选项像滚筒一样转

这期拆解 Wheel Picker / 滚轮选择器:它适合日期、时间、身高等连续离散值选择,核心是中心吸附、圆柱透视和释放后的惯性对齐。文中用自制 GIF 和 Web 伪代码说明怎么做出滚轮感。

这类选择器最容易被误解:手指只是上下拖一列文字,屏幕却让你觉得那列文字绕着一个看不见的圆柱在转。它不是普通列表,也不是 Slot Machine 的结果揭晓动画;它的重点是让用户在小空间里连续扫过一组相邻值,并在松手后停到最容易确认的中间行。
本期拆的是 Wheel Picker / Picker Wheel / 滚轮选择器。Apple 对 picker 的基本定义是:它展示一组或多组可滚动的离散值列表,供用户从中选择。1 这句话很短,但已经把它和普通下拉菜单分开了:滚轮选择器适合「相邻值之间有顺序」的场景,比如日期、时间、身高、体重、倒计时分钟数。
滚轮选择器机制演示
滚轮选择器机制演示
自制 GIF 演示:中间高亮行吸附到选中值,上下选项缩小淡出,非真实产品截图。

它适合解决什么问题

Wheel Picker 的好处不是「酷」,而是节省空间。一个时间选择器如果直接摊开 24 个小时和 60 个分钟值,页面马上变成表格;如果只显示当前值,用户又很难判断前后相邻值。滚轮把这两个需求折中:中间行给明确结果,上下几行保留上下文。
可以优先考虑它的场景:
  • 值之间有自然顺序:日期、时间、年龄、身高、金额区间。
  • 用户通常只会微调,不会从 1 一路找 999。
  • 每个选项很短,最好是数字、月份、星期、简短标签。
  • 页面空间有限,但用户需要看到相邻值。
不适合的场景也很明确:国家/城市、联系人、商品型号这类长列表别硬塞进滚轮。用户一旦需要搜索、分组或看完整文本,滚轮就会变成一个漂亮但折磨人的控件。

为什么它看起来像在转

普通纵向列表的核心是 translateY:整列上下移动,每个项目的形状不变。Wheel Picker 多做了一层「圆柱错觉」。越靠近中间的项目越正、越清晰;越靠近上下边缘,文字越小、越淡,甚至带一点倾斜。
Web 里常见做法是给容器设置透视,再让每一行围绕 X 轴旋转。MDN 对 perspective() 的解释是:它设置观看者到 z=0 平面的距离,让二维界面按三维空间的方式呈现透视效果。2 rotateX() 则是在水平轴周围旋转元素。3 这两个东西合在一起,就能让「上下滑动」看起来像「绕轴转动」。
负责什么做错会怎样
遮罩窗口只让中间几行可见信息太多,选中值不突出
中心高亮告诉用户当前选中行用户不知道松手后选了哪一项
透视与旋转让上下行像绕圆柱运动看起来只是普通列表上下平移
松手吸附停到最近的合法值文字卡在两行之间,读数不可信

实现时先抓住三个变量

别一上来就写一堆动画库参数。滚轮选择器最小模型只需要三个状态:
  1. selectedIndex:当前选中的值下标。
  2. dragOffset:手指拖动造成的临时位移。
  3. velocity:松手瞬间的速度,用来决定要不要再多滚一小段。
渲染时,不直接问「第 i 行应该放在哪里」,而是先算它离当前中心有多远:
const rawIndex = selectedIndex - dragOffset / itemHeight;
const distance = i - rawIndex;
distance = 0 的项目在中间,distance = -1 在上一行,distance = 1 在下一行。后面所有视觉变化都从这个距离推出来。
const angle = clamp(distance * 18, -72, 72); // 每格 18°,上下限防止翻面
const rad = angle * Math.PI / 180;

const y = Math.sin(rad) * radius;
const scale = 0.72 + 0.28 * Math.cos(rad);
const opacity = 0.25 + 0.75 * Math.max(0, Math.cos(rad));
这段代码故意保留了一个近似:它不追求真实三维圆柱,只追求用户能读懂的滚轮感。真正的产品里还会根据字体、行高、设备尺寸调参数。只要中间行稳定、上下行渐隐,用户就会接受这个错觉。

松手之后,别让它停在半格

Wheel Picker 最怕停在两行之间。拖动过程中可以连续移动,松手之后必须吸附到某个整数下标。
function onRelease(velocity) {
  const projected = rawIndex + velocity * 0.18;
  const target = clamp(Math.round(projected), 0, values.length - 1);

animateTo(target, {
    duration: 260,
    easing: 'cubic-bezier(.2, .9, .2, 1)'
  });
}
这里的 projected 是惯性预测:手指滑得快,滚轮可以多走一两格;手指慢慢放开,就停在最近的一格。transition-timing-function 负责控制过渡中间值如何计算,MDN 也把它描述为设置加速度曲线,让速度能在动画持续时间内变化。4 滚轮常用的节奏是前半段快,后半段明显减速,最后轻轻贴到中心线。
有些团队会加一点弹性,但别太像橡皮筋。Wheel Picker 的反馈应该是「机械地对齐」,不是「弹来弹去」。如果弹性太大,用户会怀疑最终值是不是还没停稳。

参数怎么调才顺眼

我会先从这组参数起步,再按设备和字体微调:
  • 可见行数:5 到 7 行。少于 5 行,上下文太少;多于 7 行,中心值被淹没。
  • 中心行高度:44 到 56 px。触控设备上太矮会影响可点区域。
  • 每格旋转角:14° 到 20°。角度太小不像滚轮,太大又像文字在翻倒。
  • 惯性系数:先小后大调。宁可少滚一格,也别让用户一滑就飞出去。
  • 渐隐范围:上下边缘要淡出,但不能淡到用户看不见相邻值。
还有一个细节:中心高亮不要只靠颜色。最好有一条浅色背景或上下边界线。这样在深色模式、低亮度屏幕、色弱用户那里,选中行也能被确认。

可访问性:要给「少动」模式留退路

滚轮动效有方向、有惯性、有透视,长时间快速滚动会让一部分人不舒服。MDN 在 transition-timing-function 的可访问性说明里提到,动画能帮助用户理解界面变化,但也可能触发前庭障碍、偏头痛等问题,并建议提供暂停或减少动画的机制。4
所以实现时可以给 prefers-reduced-motion 留一条更朴素的路径:关闭 3D 倾斜,保留中心吸附和淡入淡出。用户仍然能选值,只是少了滚筒错觉。
.wheel {
  perspective: 700px;
  overflow: hidden;
}

.wheel-item {
  transform: translateY(var(--y)) rotateX(var(--angle));
  opacity: var(--opacity);
}

@media (prefers-reduced-motion: reduce) {
  .wheel-item {
    transform: translateY(var(--y));
    transition-duration: 80ms;
  }
}

一句话记住

Wheel Picker 的关键不是把列表做成 3D,而是让「连续浏览」和「明确选择」同时成立:拖动时像滚筒,松手后像齿轮卡进槽里。只要中心吸附、上下文和减速节奏三件事稳住,它就会比普通下拉更适合小范围的连续值选择。

Related content

Add more perspectives or context around this Post.

  • Sign in to comment.