
June 29, 2026 · 8:15 AM
UI 动效 005:Wheel Picker,为什么一列选项像滚筒一样转
这期拆解 Wheel Picker / 滚轮选择器:它适合日期、时间、身高等连续离散值选择,核心是中心吸附、圆柱透视和释放后的惯性对齐。文中用自制 GIF 和 Web 伪代码说明怎么做出滚轮感。
这类选择器最容易被误解:手指只是上下拖一列文字,屏幕却让你觉得那列文字绕着一个看不见的圆柱在转。它不是普通列表,也不是 Slot Machine 的结果揭晓动画;它的重点是让用户在小空间里连续扫过一组相邻值,并在松手后停到最容易确认的中间行。
本期拆的是 Wheel Picker / Picker Wheel / 滚轮选择器。Apple 对 picker 的基本定义是:它展示一组或多组可滚动的离散值列表,供用户从中选择。1 这句话很短,但已经把它和普通下拉菜单分开了:滚轮选择器适合「相邻值之间有顺序」的场景,比如日期、时间、身高、体重、倒计时分钟数。

它适合解决什么问题
Wheel Picker 的好处不是「酷」,而是节省空间。一个时间选择器如果直接摊开 24 个小时和 60 个分钟值,页面马上变成表格;如果只显示当前值,用户又很难判断前后相邻值。滚轮把这两个需求折中:中间行给明确结果,上下几行保留上下文。
可以优先考虑它的场景:
- 值之间有自然顺序:日期、时间、年龄、身高、金额区间。
- 用户通常只会微调,不会从 1 一路找 999。
- 每个选项很短,最好是数字、月份、星期、简短标签。
- 页面空间有限,但用户需要看到相邻值。
不适合的场景也很明确:国家/城市、联系人、商品型号这类长列表别硬塞进滚轮。用户一旦需要搜索、分组或看完整文本,滚轮就会变成一个漂亮但折磨人的控件。
为什么它看起来像在转
普通纵向列表的核心是
translateY:整列上下移动,每个项目的形状不变。Wheel Picker 多做了一层「圆柱错觉」。越靠近中间的项目越正、越清晰;越靠近上下边缘,文字越小、越淡,甚至带一点倾斜。Web 里常见做法是给容器设置透视,再让每一行围绕 X 轴旋转。MDN 对
perspective() 的解释是:它设置观看者到 z=0 平面的距离,让二维界面按三维空间的方式呈现透视效果。2 rotateX() 则是在水平轴周围旋转元素。3 这两个东西合在一起,就能让「上下滑动」看起来像「绕轴转动」。| 层 | 负责什么 | 做错会怎样 |
|---|---|---|
| 遮罩窗口 | 只让中间几行可见 | 信息太多,选中值不突出 |
| 中心高亮 | 告诉用户当前选中行 | 用户不知道松手后选了哪一项 |
| 透视与旋转 | 让上下行像绕圆柱运动 | 看起来只是普通列表上下平移 |
| 松手吸附 | 停到最近的合法值 | 文字卡在两行之间,读数不可信 |
实现时先抓住三个变量
别一上来就写一堆动画库参数。滚轮选择器最小模型只需要三个状态:
selectedIndex:当前选中的值下标。dragOffset:手指拖动造成的临时位移。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,而是让「连续浏览」和「明确选择」同时成立:拖动时像滚筒,松手后像齿轮卡进槽里。只要中心吸附、上下文和减速节奏三件事稳住,它就会比普通下拉更适合小范围的连续值选择。

Add more perspectives or context around this Post.