「原生练手」🌊安卓上的按钮水波涟漪效果

1,257 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

不知道你是否有见过安卓系统上的这种水波纹涟漪特效,就像下图这样:

水波涟漪特效.gif

思考实现原理

那么这样一个小特效要怎么实现呢?不妨先来思考一下以下几个问题:

  • 特效的起点在哪?
  • 特效从整体上看应当是什么形状的?
  • 特效触发到结束,整个特效生效的过程中有什么要注意的吗?

特效的起点

首先要先找到特效触发的起点,直观上看,也就是鼠标点击的位置,但这个位置是相对触发特效元素,也就是图中的按钮而言的,而不是整个视口而言,所以我们不仅仅需要获取到鼠标的坐标,还需要获取到触发特效元素的坐标,计算一个相对坐标才行

特效的形状

这里由于按钮元素设置了overflow: hidden,所以导致看不出整体形状是怎样的,但是其实也不难猜测,特效的形状应当是一个圆形,这从现实生活中的水波纹也可以看出来

如果把overflow: hidden去掉,再看看特效的样子:

取消溢出隐藏.gif

这样是不是更加像水波纹呢?只是为了美观和尽可能还原安卓系统上的水波纹特效,还是需要给父元素设置overflow: hidden

特效生效过程要注意什么?

  1. overflow: hidden应当有还原机制,比如触发特效的元素本身设置的overflow并不是hidden,而是其他值时,如果特效强行改为hidden后就不管了的话,会影响到原本想要的效果,所以应当在特效结束后将overflow恢复到原来的值
  2. 特效应当通过position: absolute绝对定位放到触发特效元素上,这也就意味着,触发特效的元素的position应为relative,但也和overflow类似,应当在特效结束后将其还原回原本的position,否则强行修改之后就不管了的话,会导致意外的bug

实现

HTML

html结构很简单,就是一个按钮元素

<div class="btn ripple">Button</div>

CSS

css比较简单,我只放出水波涟漪特效相关的部分,按钮和整体布局等样式就没必要贴出来了

.ripple-effect {
  position: absolute;
  background-color: white;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(0);
  animation: scale 0.5s ease-out;
}

/* 水波涟漪逐渐放大并慢慢消失 */
@keyframes scale {
  to {
    transform: translate(-50%, -50%) scale(3);
    opacity: 0;
  }
}

JavaScript

根据前面的分析,写出如下代码,注释都已经写明,直接看即可

(() => {
  /** @type { NodeListOf<HTMLElement> } */
  const oRipples = document.querySelectorAll(".ripple");

  const init = () => {
    bindEvent();
  };

  const bindEvent = () => {
    // 遍历所有带有 ripple 特效的元素 -- 添加 ripple 特效
    oRipples.forEach((oRipple) => {
      oRipple.addEventListener("click", rippleEffect);
    });
  };

  /**
   * @description 水波涟漪特效
   * @param {MouseEvent} e
   */
  const rippleEffect = (e) => {
    // 将触发特效的元素的定位设置为 relative -- 这样才能让特效元素定位到触发元素上
    // 要记录原始的 position,在特效结束后还原回去
    const rawPosition = e.target.style.position;
    e.target.style.position = "relative";

    // 将触发特效的元素的 overflow 设置为 hidden 防止水波溢出
    // 先记录原始的 overflow 然后强行改成 hidden 再在特效结束时将其恢复 避免影响原本的元素
    const rawOverflow = e.target.style.overflow;
    e.target.style.overflow = "hidden";

    // 计算水波涟漪特效的起点 -- 即鼠标点击处相对于触发特效元素的坐标
    const [mouseX, mouseY] = [e.clientX, e.clientY];

    // 获取触发水波涟漪特效的元素的位置
    const [elTop, elLeft] = [e.target.offsetTop, e.target.offsetLeft];

    // 计算鼠标点击处相对于触发水波涟漪特效元素的位置
    const [relativeX, relativeY] = [mouseX - elLeft, mouseY - elTop];

    // 在起点处插入一个 DOM 元素 用于形成水波涟漪特效
    const rippleEl = document.createElement("span");
    rippleEl.className = "ripple-effect";

    // 特效元素的位置正是鼠标相对于触发特效元素的位置
    rippleEl.style.left = `${relativeX}px`;
    rippleEl.style.top = `${relativeY}px`;

    // 添加到触发特效的元素中让特效生效
    e.target.appendChild(rippleEl);

    // 特效持续时长为 500ms -- 到时间后将特效元素移除
    setTimeout(() => {
      // 移除特效元素
      rippleEl.remove();

      // 将触发特效元素的 position 样式还原
      e.target.style.position = rawPosition;

      // 将触发特效元素的 overflow 样式还原
      e.target.style.overflow = rawOverflow;
    }, 500);
  };

  init();
})();