如何重现 Material Design 按钮的涟漪效果

Avatar of Bret Cameron
Bret Cameron

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

当我第一次发现 Material Design 时,我特别受到其 按钮组件 的启发。它使用涟漪效果以简单优雅的方式为用户提供反馈。

这种效果是如何工作的?Material Design 的按钮不仅仅带有整洁的涟漪动画,而且动画还会根据点击按钮的位置而改变位置。

我们可以达到相同的结果。我们将从使用 ES6+ JavaScript 的简洁解决方案开始,然后查看一些替代方法。

HTML

我们的目标是避免任何多余的 HTML 标记。因此,我们将使用最少的标记

<button>Find out more</button>

按钮样式

我们需要使用 JavaScript 动态设置涟漪的一些元素的样式。但其他所有内容都可以在 CSS 中完成。对于我们的按钮,只需包含两个属性即可。

button {
  position: relative;
  overflow: hidden;
}

使用 position: relative 允许我们在涟漪元素上使用 position: absolute,我们需要控制其位置。同时,overflow: hidden 防止涟漪超出按钮的边缘。其他所有内容都是可选的。但现在,我们的按钮看起来有点过时了。这是一个更现代的起点

/* Roboto is Material's default font */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');

button {
  position: relative;
  overflow: hidden;
  transition: background 400ms;
  color: #fff;
  background-color: #6200ee;
  padding: 1rem 2rem;
  font-family: 'Roboto', sans-serif;
  font-size: 1.5rem;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
  cursor: pointer;
}

涟漪样式

稍后,我们将使用 JavaScript 将涟漪作为带有 .ripple 类的跨度注入到我们的 HTML 中。但在转向 JavaScript 之前,让我们在 CSS 中定义这些涟漪的样式,以便我们随时准备使用它们

span.ripple {
  position: absolute; /* The absolute position we mentioned earlier */
  border-radius: 50%;
  transform: scale(0);
  animation: ripple 600ms linear;
  background-color: rgba(255, 255, 255, 0.7);
}

为了使我们的涟漪呈圆形,我们已将 border-radius 设置为 50%。为了确保每个涟漪都从无到有出现,我们已将默认比例设置为 0。现在,我们将无法看到任何内容,因为我们还没有为 topleftwidthheight 属性提供值;我们很快就会使用 JavaScript 注入这些属性。

至于我们的 CSS,我们需要添加的最后一件事是动画的结束状态

@keyframes ripple {
  to {
    transform: scale(4);
    opacity: 0;
  }
}

注意,我们没有在关键帧中使用 from 关键字定义起始状态?我们可以省略 from,CSS 将根据应用于动画元素的值构建缺失的值。如果显式声明相关值(如 transform: scale(0))或如果它们是默认值(如 opacity: 1),则会发生这种情况。

现在是 JavaScript 的时间了

最后,我们需要 JavaScript 来动态设置涟漪的位置和大小。大小应基于按钮的大小,而位置应基于按钮和光标的位置。

我们将从一个空函数开始,该函数以点击事件作为参数

function createRipple(event) {
  //
}

我们将通过查找事件的 currentTarget 来访问我们的按钮。

const button = event.currentTarget;

接下来,我们将实例化我们的跨度元素,并根据按钮的宽度和高度计算其直径和半径。

const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;

我们现在可以定义涟漪所需的其余属性:lefttopwidthheight

circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple"); 

在将跨度元素添加到 DOM 之前,最好检查是否存在可能来自先前点击的任何剩余涟漪,并在执行下一个涟漪之前将其删除。

const ripple = button.getElementsByClassName("ripple")[0];

if (ripple) {
  ripple.remove();
}

作为最后一步,我们将跨度作为子元素附加到按钮元素,以便将其注入按钮内部。

button.appendChild(circle);

我们的函数完成后,剩下的就是调用它。这可以通过多种方式完成。如果我们想将涟漪添加到页面上的每个按钮,我们可以使用类似以下内容

const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
  button.addEventListener("click", createRipple);
}

现在我们有了工作的涟漪效果!

更进一步

如果我们想更进一步,并将此效果与对按钮位置或大小的其他更改结合起来会怎样?毕竟,能够自定义是我们选择自己重新创建效果的主要优势之一。为了测试扩展我们的函数有多容易,我决定添加一个“磁铁”效果,当光标位于某个区域内时,它会导致我们的按钮向光标移动。

我们需要依赖涟漪函数中定义的一些相同变量。为了避免不必要地重复代码,我们应该将它们存储在某个可供两种方法访问的地方。但我们也应该将共享变量的作用域限制在每个单独的按钮中。实现此目标的一种方法是使用类,如下例所示

由于磁铁效果需要在每次移动时跟踪光标,因此我们不再需要计算光标位置来创建涟漪。相反,我们可以依赖 cursorXcursorY

两个重要的新变量是 magneticPullXmagneticPullY。它们控制磁铁方法在光标之后拉动按钮的强度。因此,当我们定义涟漪的中心时,我们需要调整新按钮的位置(xy)和磁性拉力。

const offsetLeft = this.left + this.x * this.magneticPullX;
const offsetTop = this.top + this.y * this.magneticPullY;

要将这些组合效果应用于我们所有的按钮,我们需要为每个按钮实例化一个新的类实例

const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
  new Button(button);
}

其他技术

当然,这只是实现涟漪效果的一种方法。在 CodePen 上,有很多示例展示了不同的实现方式。以下是我的一些最喜欢的示例。

仅 CSS

如果用户禁用了 JavaScript,我们的涟漪效果没有任何回退。但是,仅使用 CSS 可以接近原始效果,使用 :active 伪类来响应点击。主要限制是涟漪只能从一个位置出现——通常是按钮的中心——而不是响应我们的点击位置。Ben Szabo 的这个例子特别简洁

ES6 之前的 JavaScript

Leandro Parice 的演示类似于我们的实现,但它与早期版本的 JavaScript 兼容:

jQuery

此示例使用 jQuery 来实现涟漪效果。如果您已经将 jQuery 作为依赖项,它可以帮助您节省几行代码。

React

最后,我再举一个例子。虽然可以使用 React 功能(如状态和引用)来帮助创建涟漪效果,但这些并不是严格必要的。涟漪的位置和大小都需要为每次点击计算,因此将这些信息保存在状态中没有任何优势。此外,我们可以从点击事件中访问按钮元素,因此我们也不需要引用。

此 React 示例使用与本文首次实现相同的 createRipple 函数。主要区别在于——作为 Button 组件的方法——我们的函数的作用域限于该组件。此外,onClick 事件侦听器现在是我们 JSX 的一部分