水波纹效果的实现

水波纹效果的实现

前言

在学 Android 开发的时候,一直很喜欢 Material Design,经常翻看官方的指南。其中有个很出众的 UI 效果 – 水波纹。Android 开发者对这个效果应当是不陌生的,可以说这是 Material Design 的一大特性了吧。

个人觉得这个设计的动效还是很赞的,于是就想在前端简陋地实现一下。

实现

内容本体和水波纹效果分开

若一个内容(div, button 等)想加入水波纹的效果,只需加个classripple-effect。如果不分开的话,每个想实现水波纹效果的地方都必须加上相同的水波纹 View,再说了,水波纹应是广泛通用的。所以应该把水波纹提取出来,单独作为一个 class 来使用。

内容本体

为了实现水波纹叠加在内容本体之上,水波纹应该是绝对定位的,而绝对定位是相对于最近一个使用了定位的父节点来说的,所以内容本体用到了定位position: relative

其实 Android 里面是有两种水波纹效果的

  • 有边界

    android-ripple

  • 无边界

    no-bound

    可以看到无边界的水波纹都延伸到了上方的TextView

那么对于有边界的水波纹:overflow: hidden,而无边界的水波纹则是overflow: visible

综上,用于内容本体的 CSS 应该是:

1
2
3
4
5
.ripple-effect {
position: relative;
overflow: hidden; /*有边界的水波纹*/
overflow: visible; /*无边界的水波纹*/
}

水波纹的实现

其实水波纹只是一个 View 从小放大的效果而己,即由scale(0)scale(?)

首先,这个 View 是圆形的,所以是border-radius: 100%。然后为了不遮挡下面的内容,水波纹的背景色应有一定的透明度,例如background: rgba(0, 0, 0, 0.25)

即:

1
2
3
4
5
6
7
.ripple {
position: absolute;
background: rgba(0, 0, 0, 0.25);
border-radius: 100%;
transform: scale(0);
pointer-events: none;
}

水波纹的大小

可以将水波纹的大小设为内容本体长和宽中的最大值,但这就存在一个问题,如果点击位置不在内容本体的中心,那么扩散之后的水波纹并不能覆盖整个内容本体。最差的情况是,如果点击位置在内容本体里的最远端,那么水波纹至多只能覆盖内容本体的一半。既然如此,只需将水波纹的扩散设为scale(2)

水波纹的位置

上面也说了,水波纹应该是绝对定位的,所以position: absolute。但仅仅这一行,只能将水波纹置于父节点内部的左上角,即水波纹的左上角点与内容本体的左上角点重合。这并不能实现在点击时由鼠标点击的位置触发水波纹,所以要将水波纹的中心移到鼠标点击的位置。

先来看看几个概念的示意图:

  • 红点

    点击位置

  • pageX, pageY

    点击位置相对于整个网页的坐标。

  • left, top

    getBoundingClientRect().leftgetBoundingClientRect().top,表示内容本体相对于浏览器窗口的坐标。

  • scrollLeft, scrollTop

    document.body.scrollLeftdocument.body.scrollTop,表示网页的滚动距离。

显然,通过pageY - top - scrollTop可以得到点击位置相对于内容本体的 Y 坐标,同理,可以通过pageX - left - scrollLeft可以得到点击位置相对于内容本体的 X 点击位置相对于整个网页的坐标。

知道点击位置相对于内容本体的坐标之后,就可以轻易计算出将水波纹中心移到点击位置需要移动的距离:

  • Y 方向上需要移动的距离:pageY - top - scrollTop - offsetHeight / 2

  • X 方向上需要移动的距离:pageX - left - scrollLeft - offsetWidth / 2

其中,offsetHeightoffsetWidth分别指水波纹的高和宽。

水波纹的扩散

想要动态修改 CSS,方便的做法是增删类,所以这里写一个 CSS 类show,里面加上动画,包括:

  • scale(0)scale(2)

  • opacity: 1opacity: 0

即:

1
2
3
4
5
6
7
8
9
10
.ripple.show {
animation: ripple 0.75s ease-out;
}
@keyframes ripple {
to {
transform: scale(2);
opacity: 0;
}
}

当点击事件触发时,先移除show类,再添加回show类,这样就可以达到每次点击都有水波纹扩散的效果。

具体 JavaScript 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var addRippleEffect = function (e) {
var target = e.target;
var rect = target.getBoundingClientRect();
var ripple = target.querySelector('.ripple');
if (!ripple) { // 首次添加水波纹
ripple = document.createElement('span');
ripple.className = 'ripple';
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
target.appendChild(ripple); // 添加水波纹子节点
}
ripple.classList.remove('show'); // 移除类 show
var top = e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop;
var left = e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft;
ripple.style.top = top + 'px';
ripple.style.left = left + 'px';
ripple.classList.add('show'); // 添加类 show
return false;
}

在页面结构加载完之后,就可以遍历所有带ripple-effect的节点,逐个将上面的函数添加到点击事件中去。

效果

我的博客有这种水波纹的效果,例如:

总结

初涉前端,感觉上,一些效果的实现比在 Android 上实现要来得简单一些。但我知道,UI 和交互是灵感迸发的结果,在各种炫酷特效前面,很多时候只能默默奉上自己的膝盖,随后百思,而终不得其解。