MDUI 的两代水波纹

在一个论坛上点了下按钮,觉得那圈水波纹很有 Chrome 的味道:快点是一圈浅浅的光,长按是一团慢慢洇开的深色。顺着找下去是 MDUI v1。这篇把 MDUI v1 和 v2 的水波纹拆开对照:分几个阶段、各多少毫秒、为什么长按会更深,附一个可以直接上手按的复刻。

在 玲珑论坛 上随手点了一下按钮,第一反应是「这很 Chrome」:快点一下,是一圈很浅、很快的光;按住不放,是一团更深的颜色慢慢洇开,松手才收掉。扒了它的样式表,按钮上挂的是 .mdui-ripple,来自 MDUI v1。

先按一下试试。左边是 v1,右边是 v2,各试两种按法:快点一下,再按住一两秒。

各试两下:快点一下,再按住一两秒
v1 · Material Design 2
按下 1400ms 慢慢长,松手 300ms 补满
v2 · Material Design 3
225ms 就长满,然后定格

系统开了「减少动态效果」,演示已停用。

按两版源码里的数值复刻,按钮用的是本站的样式

这里没有加载 MDUI 本体:v1 的 CSS 有 325 KB,还带着给 body 定字体和底色这类全局规则,挂进来会改到全站样式。所以只把水波纹这一件事按源码抄了过来,数值和出处在下面。

MDUI 是什么

MDUI 是 zdhxiong 做的开源前端框架,MIT 协议,官网 mdui.org,源码在 GitHub。它的目标是把 Google 的 Material Design 做成国内开发者顺手的写法。两个大版本差别很大:

v1v2
本文对照的版本1.0.22.1.5
设计规范Material Design 2Material Design 3(Material You)
写法CSS 类名,加一个自带的类 jQuery 小库Web Components,组件是 <mdui-button> 这类自定义标签
水波纹给任意元素加 .mdui-ripple 类封装在各组件的 shadow DOM 里

水波纹在两个版本里都只是一个小功能,不是独立的库。

v1:按下慢,松手快

v1 的水波纹是一个 JS 插进元素里的圆形 div,靠 CSS transition 动起来,JS 负责在不同时刻换类名。一共三个阶段:

阶段类名时长做什么透明度
按下.mdui-ripple-wave1400ms,ease从 scale(0) 长到 scale(1)1
松手加 -fill300ms把还没长完的部分一口气补满降到 0.35
之后加 -out600ms淡出,然后删掉节点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 倍」,三个阶段看得更清楚。

圆从哪儿长、长多大

js
// 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 完全一样。

变的是事件和时序:

v1v2
事件mousedown / touchstartPointer Events
触摸延迟200ms70ms
鼠标移动算松手不算,移出元素才算

v2 还多了一层「状态层」:盖在整个元素上的一层纯色,和波纹叠在一起用。

状态透明度
鼠标悬停0.08
键盘聚焦0.12
拖动0.16
按下(波纹)0.12

悬停只认真鼠标,触摸不会触发。

两版并排看

v1v2
长大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:

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:

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:开了「减少动态效果」的读者,就别再给他们插这个圆了。

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置