MDUI 的两代水波纹
在一个论坛上点了下按钮,觉得那圈水波纹很有 Chrome 的味道:快点是一圈浅浅的光,长按是一团慢慢洇开的深色。顺着找下去是 MDUI v1。这篇把 MDUI v1 和 v2 的水波纹拆开对照:分几个阶段、各多少毫秒、为什么长按会更深,附一个可以直接上手按的复刻。
在 玲珑论坛 上随手点了一下按钮,第一反应是「这很 Chrome」:快点一下,是一圈很浅、很快的光;按住不放,是一团更深的颜色慢慢洇开,松手才收掉。扒了它的样式表,按钮上挂的是 .mdui-ripple,来自 MDUI v1。
先按一下试试。左边是 v1,右边是 v2,各试两种按法:快点一下,再按住一两秒。
系统开了「减少动态效果」,演示已停用。
这里没有加载 MDUI 本体:v1 的 CSS 有 325 KB,还带着给 body 定字体和底色这类全局规则,挂进来会改到全站样式。所以只把水波纹这一件事按源码抄了过来,数值和出处在下面。
MDUI 是什么
MDUI 是 zdhxiong 做的开源前端框架,MIT 协议,官网 mdui.org,源码在 GitHub。它的目标是把 Google 的 Material Design 做成国内开发者顺手的写法。两个大版本差别很大:
| v1 | v2 | |
|---|---|---|
| 本文对照的版本 | 1.0.2 | 2.1.5 |
| 设计规范 | Material Design 2 | Material Design 3(Material You) |
| 写法 | CSS 类名,加一个自带的类 jQuery 小库 | Web Components,组件是 <mdui-button> 这类自定义标签 |
| 水波纹 | 给任意元素加 .mdui-ripple 类 | 封装在各组件的 shadow DOM 里 |
水波纹在两个版本里都只是一个小功能,不是独立的库。
v1:按下慢,松手快
v1 的水波纹是一个 JS 插进元素里的圆形 div,靠 CSS transition 动起来,JS 负责在不同时刻换类名。一共三个阶段:
| 阶段 | 类名 | 时长 | 做什么 | 透明度 |
|---|---|---|---|---|
| 按下 | .mdui-ripple-wave | 1400ms,ease | 从 scale(0) 长到 scale(1) | 1 |
| 松手 | 加 -fill | 300ms | 把还没长完的部分一口气补满 | 降到 0.35 |
| 之后 | 加 -out | 600ms | 淡出,然后删掉节点 | 0 |
圆本身的颜色是 rgba(0, 0, 0, 0.1),深色主题和有底色的按钮上换成 rgba(255, 255, 255, 0.3)。
长按更深并不是单独做的效果,它和快点是同一个圆,差别只在什么时候松手:
| 按法 | 松手那一刻 | 看到的样子 |
|---|---|---|
| 快点一下 | 圆还没怎么长,就被切到 300ms 补满,透明度同时降到 0.35 | 实际只剩 10% × 0.35 = 3.5% 的灰,又浅又快 |
| 按住不放 | 一直停在按下阶段,透明度保持 1,按 1400ms 的节奏慢慢长满 | 完整的 10%,又深又慢 |
说到底就是「按下慢、松手快」两个时长,加上「松手变淡」这一步。反馈跟着手指按住的时长变化,这大概就是它显得有分量的原因。上面演示里勾上「慢放 4 倍」,三个阶段看得更清楚。
圆从哪儿长、长多大
// mdui@1.0.2 dist/js/mdui.js 的 show()
var diameter = Math.max(Math.pow(Math.pow(height, 2) + Math.pow(width, 2), 0.5), 48);
var translate = "translate3d(" + (-center.x + width / 2) + "px," +
(-center.y + height / 2) + "px, 0) scale(1)";直径取元素的对角线(最小 48px),圆心放在手指落点,长大的同时往元素中心平移。所以不管从哪个角按下去,长满时都正好盖住整个元素,不会漏出一角。
什么时候开始、什么时候结束
| 输入 | 开始 | 结束 |
|---|---|---|
| 鼠标 | mousedown 立刻开始 | mouseup、mouseleave,以及 mousemove |
| 触摸 | touchstart 之后等 200ms 才开始 | touchend、touchcancel、touchmove |
两个细节:
1、鼠标只要一动就算松手。所以用鼠标长按时手要稳,稍微滑一下波纹就会进入补满和淡出。
2、触摸那 200ms 是为了区分「点」和「滑」:这期间手指一动就判成滚动页面,干脆不出波纹;200ms 内就抬起的,补出一圈再收。
源码注释里写着这部分参考了 Waves 和 Framework7 的 fast-clicks。
v2:225ms 就长满
v2 跟着 Material Design 3 改了节奏。波纹换成 CSS keyframes,封装在每个组件的 shadow DOM 里:
| 阶段 | 时长 | 做什么 | 透明度 |
|---|---|---|---|
| 按下 | 225ms,缓动 cubic-bezier(0.2, 0, 0, 1) | 从 scale(0.4) 长到 scale(1) | 75ms 内淡入到 0.12 |
| 按住 | 不限 | 定格,不再变化 | 0.12 |
| 松手 | 150ms | 淡出;如果还没长满,先等它长满再淡出 | 0 |
几何部分和 v1 一字不差:直径同样取对角线、至少 48px,同样往中心平移。v2 的 startPress 里算直径和位移的那两行,写法和 v1 完全一样。
变的是事件和时序:
| v1 | v2 | |
|---|---|---|
| 事件 | mousedown / touchstart | Pointer Events |
| 触摸延迟 | 200ms | 70ms |
| 鼠标移动 | 算松手 | 不算,移出元素才算 |
v2 还多了一层「状态层」:盖在整个元素上的一层纯色,和波纹叠在一起用。
| 状态 | 透明度 |
|---|---|
| 鼠标悬停 | 0.08 |
| 键盘聚焦 | 0.12 |
| 拖动 | 0.16 |
| 按下(波纹) | 0.12 |
悬停只认真鼠标,触摸不会触发。
两版并排看
| v1 | v2 | |
|---|---|---|
| 长大 | 1400ms,从 0 开始 | 225ms,从 0.4 倍开始 |
| 按住时 | 10%,而且一直在长 | 12%,225ms 后定格 |
| 松手 | 300ms 补满、降到 35%,再 600ms 淡出 | 150ms 淡出 |
| 快点和长按 | 差别明显:一个浅而快,一个深而慢 | 几乎没差别,都是同一圈 |
| 悬停 | 没有 | 8% 状态层 |
| 驱动方式 | CSS transition + JS 换类名 | CSS keyframes + animationend 判断长没长满 |
v2 更干脆:按一下就是固定的一下反馈,悬停交给状态层去表达。v1 把按下和松手拆成两个速度,反馈会跟着你按住多久而变化。我想要的「长按更深更慢」只有 v1 有。
自己写一个 v1 风格的
不需要引入 MDUI,二三十行就够。CSS:
.ripple {
position: relative;
overflow: hidden;
}
.ripple-wave {
position: absolute;
border-radius: 50%;
pointer-events: none;
background: rgba(0, 0, 0, 0.1);
transform: scale(0);
transition: transform 1.4s ease, opacity 1.4s ease;
}
.ripple-wave.fill {
opacity: 0.35;
transition-duration: 0.3s;
}
.ripple-wave.out {
opacity: 0;
transition-duration: 0.6s;
}JS:
document.addEventListener('pointerdown', (e) => {
const host = e.target.closest('.ripple');
if (!host) return;
const r = host.getBoundingClientRect();
const x = e.clientX - r.left;
const y = e.clientY - r.top;
const d = Math.max(Math.hypot(r.width, r.height), 48);
const to = `translate(${r.width / 2 - x}px, ${r.height / 2 - y}px)`;
const wave = document.createElement('span');
wave.className = 'ripple-wave';
Object.assign(wave.style, {
width: `${d}px`, height: `${d}px`,
left: `${x - d / 2}px`, top: `${y - d / 2}px`,
});
host.prepend(wave);
wave.offsetWidth; // 让 scale(0) 先生效,否则没有起点可过渡
wave.style.transform = `${to} scale(1)`;
const release = () => {
wave.classList.add('fill');
wave.style.transform = `${to} scale(1.01)`;
setTimeout(() => {
wave.classList.add('out');
setTimeout(() => wave.remove(), 600);
}, 300);
window.removeEventListener('pointerup', release);
window.removeEventListener('pointercancel', release);
};
window.addEventListener('pointerup', release);
window.addEventListener('pointercancel', release);
});真要放进站里,还得补上 prefers-reduced-motion:开了「减少动态效果」的读者,就别再给他们插这个圆了。
评论
评论加载中……