前言

不知道你们有没有使用过 Material UI。这是一个 React UI 组件库,它实现了 Google 的 Material Design。

Material Design 设计规范中包含了很多关于点击的涟漪效果,类似于一块石头跌落水中所产生的波浪效果。

以下是效果图

速度放慢之后的效果:

我们把 overflow: hidden 去掉之后:

本文就以 Material Design 中的涟漪效果作为目标,来使用原生的 JavaScript、CSS、HTML 来实现此效果。

分析

通过观察,我们可以发现点击的涟漪效果是在鼠标点击的点开始以一个正圆往外扩散。当圆形扩散到正好能将 Button 全部包围住的时候停止,在扩散的过程中颜色逐渐变浅直到消失,并且此效果可以叠加。

长按效果也是一个圆往外扩散,只不过是在长按结束之后,圆才会消失。

除了鼠标点击效果外,还有键盘焦点事件的效果。当使用键盘的 Tab 键切换到按钮上的时候,会有一个抖动的效果,类似于呼吸效果。

我们提炼出几个比较关键的点:

  • 从鼠标点击的位置开始扩散;
  • 是一个正圆形;
  • 圆形扩散到正好能将 Button 全部包围住的时候停止;
  • 长按效果;
  • 效果叠加;

第一点,我们可以通过 JavaScript 计算当前鼠标的坐标信息;第三点,获取 Button 四个顶点的坐标信息,再选一个距离鼠标最远的点,以它们的距离作为半径来画一个圆;第五点,每一个效果都是一个 dom 元素,每点击一次就追加一个 dom 元素,在动画结束的时候,移除此 dom;

实现

创建一个 index.html 文件,包含以下内容;

<!-- index.html -->
<style>
  @import 'button.css';
  @import 'ripple.css';
</style>

<button class="button-root" id="ripple-example-button" type="button">
  Button
  <!-- 用来装涟漪效果的 DOM 的容器 -->
  <span class="ripple-root"></span>
</button>

创建 button.css 和 ripple.css,分别是 Button 的基础样式和涟漪效果的样式。

/* button.css */
.button-root {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 16px;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.75;
  min-width: 64px;
  margin: 0;
  border-radius: 4px;
  border: 1px solid rgba(25, 118, 210, 0.5);
  cursor: pointer;
  box-sizing: border-box;
  outline: none;
  appearance: none;
  user-select: none;
  color: #1976d2;
  background-color: transparent;
  transition-property: background-color, color, box-shadow, border-color;
  transition-duration: 0.25s;
}

.button-root:hover {
  background-color: rgba(25, 118, 210, 0.04);
  border: 1px solid #1976d2;
}
/* ripple.css */
@keyframes enterKeyframe {
  0% {
    transform: scale(0);
    opacity: 0.1;
  }

  100% {
    transform: scale(1);
    opacity: 0.3;
  }
}

@keyframes exitKeyframe {
  0% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

@keyframes pulsateKeyframe {
  0% {
    transform: scale(0.9);
  }

  50% {
    transform: scale(0.8);
  }

  100% {
    transform: scale(0.9);
  }
}

.ripple-root {
  display: block;
  position: absolute;
  overflow: hidden;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  background-color: transparent;
  z-index: 0;
  border-radius: inherit;
}

.ripple-root > .ripple-child {
  position: absolute;
  display: block;
  opacity: 0;
}

.ripple-root > .ripple-child.enter {
  opacity: 0.3;
  transform: scale(1);
  animation: enterKeyframe 550ms ease-in-out;
}

.ripple-root > .ripple-child > .ripple-child-child {
  opacity: 1;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: currentColor;
}

.ripple-root > .ripple-child.exit > .ripple-child-child {
  opacity: 0;
  animation: exitKeyframe 550ms ease-in-out;
}

.ripple-root > .ripple-child.pulsate > .ripple-child-child {
  position: absolute;
  left: 0;
  top: 0;
  animation: pulsateKeyframe 2500ms ease-in-out 200ms infinite;
}

开始写 JavaScript。创建一个 ripple.apis.js 文件,编写 startRipple 函数。该函数首先要获取 Button 的位置信息和宽高。

// ripple.apis.js
export function startRipple(event) {
  const { currentTarget: container } = event
  const { left, top, width, height } = container.getBoundingClientRect()
}

接着计算开始扩散的位置。

// ripple.apis.js
export function startRipple(event) {
  // ...
  // 效果开始的坐标(相对于 Button)
  let rippleX, rippleY
  // 鼠标当前的坐标
  let clientX = 0, clientY = 0
 
 /**
  * 涟漪效果是否从节点的中心扩散,否则从鼠标点击的位置开始扩散
  * 使用 Tab 键移动焦点的时候,从节点的中心扩散
  */
  let center = false
  let isFocusVisible = false
 
  if (container.matches(':focus-visible')) {
    center = isFocusVisible = true
  } else {
    clientX = event.clientX
    clientY = event.clientY
  }
 
  rippleX = center ? width / 2 : clientX - left
  rippleY = center ? height / 2 : clientY - top
}

通过勾股定理,构造一个能正好包围当前元素的圆。

// ripple.apis.js
export function startRipple(event) {
  // ...
  // 从鼠标点击的中心位置,构造一个能正好包围当前元素的圆
  const sizeX = Math.max(width - rippleX, rippleX) * 2
  const sizeY = Math.max(height - rippleY, rippleY) * 2
  const diagonal = Math.sqrt(sizeX ** 2   sizeY ** 2)
}

再创建一个 createRippleChild 函数,用来创建涟漪效果的 DOM,并且使用一个全局变量来保存已经创建的 DOM。

// ripple.apis.js
const rippleChildren = []

/**
 * 创建以下结构并返回:
 * <span class="ripple-child enter">
 *   <span class="ripple-child-child"></span>
 * </span>
 */
function createRippleChild(rect) {
  const rippleChild = document.createElement('span')
  rippleChild.classList.add('ripple-child', 'enter')
  const rippleChildChild = document.createElement('span')
  rippleChildChild.classList.add('ripple-child-child')
  rippleChild.appendChild(rippleChildChild)

  const { height, left, top, width } = rect
  rippleChild.style.height = height
  rippleChild.style.width = width
  rippleChild.style.top = top
  rippleChild.style.left = left

  rippleChildren.push(rippleChild)

  return rippleChild
}

回到 startRipple 函数,使用刚才创建的 createRippleChild 函数。

// ripple.apis.js
export function startRipple(event) {
  // ...
  const rippleChild = createRippleChild({
    width: `${diagonal}px`,
    height: `${diagonal}px`,
    left: `${-diagonal / 2   rippleX}px`,
    top: `${-diagonal / 2   rippleY}px`,
  })
  if (isFocusVisible) {
    rippleChild.classList.add('pulsate')
  }
  const rippleRoot = container.querySelector(':scope > .ripple-root')
  rippleRoot.appendChild(rippleChild)
}

完成了 startRipple 函数之后,我们再创建一个 stopRipple 函数。该函数中,从 rippleChildren 取出最早创建的 DOM,添加一个动画结束的监听事件,在动画结束的时候,删除该 DOM。

// ripple.apis.js
export function stopRipple() {
  const rippleChild = rippleChildren.shift()

  if (!rippleChild) return

  rippleChild.addEventListener('animationend', (event) => {
    if (event.animationName === 'exitKeyframe') {
      rippleChild.remove()
    }
  })
  rippleChild.classList.add('exit')
}

此时,我们已经完成了大部分的代码,接下来就是给 Button 绑定事件的时候了。在 index.html 文件中添加以下代码:

<!-- index.html -->
<style>
  @import 'button.css';
  @import 'ripple.css';
</style>

<script type="module">
  import { startRipple, stopRipple } from 'ripple.apis.js'

  const button = document.querySelector('#ripple-example-button')

  button.addEventListener('mousedown', startRipple)
  button.addEventListener('focus', startRipple)
  button.addEventListener('mouseup', stopRipple)
  button.addEventListener('mouseleave', stopRipple)
  button.addEventListener('blur', stopRipple)
</script>

<button class="button-root" id="ripple-example-button" type="button">
  Button
  <!-- 用来装涟漪效果的 DOM 的容器 -->
  <span class="ripple-root"></span>
</button>

我们完成了所有的功能!完整的代码在此仓库中。

也可以直接在 CodeSandbox 中编辑

总结

到此这篇关于使用JavaScript实现按钮的涟漪效果的文章就介绍到这了,更多相关js实现按钮涟漪效果内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

使用JavaScript实现按钮的涟漪效果实例代码的更多相关文章

  1. html5 拖拽及用 js 实现拖拽功能的示例代码

    这篇文章主要介绍了html5 拖拽及用 js 实现拖拽,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. HTML5 input新增type属性color颜色拾取器的实例代码

    type 属性规定 input 元素的类型。本文较详细的给大家介绍了HTML5 input新增type属性color颜色拾取器的实例代码,感兴趣的朋友跟随脚本之家小编一起看看吧

  3. amazeui模态框弹出后立马消失并刷新页面

    这篇文章主要介绍了amazeui模态框弹出后立马消失并刷新页面,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. 移动HTML5前端框架—MUI的使用

    这篇文章主要介绍了移动HTML5前端框架—MUI的使用的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. amaze ui 的使用详细教程

    这篇文章主要介绍了amaze ui 的使用详细教程,本文通过多种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. AmazeUI 模态窗口的实现代码

    这篇文章主要介绍了AmazeUI 模态窗口的实现代码,代码简单易懂,非常不错,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  7. ios – UIPopoverController出现在错误的位置

    所以我花了一些时间寻找答案,但到目前为止还没有找到任何答案.我正在尝试从UIInputAccessoryView上的按钮呈现弹出窗口.UIBarButtonItem我想显示popover来自定制视图,所以我可以使用图像.我创建这样的按钮:当需要显示popover时,我这样做:但我得到的是:弹出窗口看起来很好,但它应该出现在第一个按钮上时出现在第二个按钮上.然后我发现了这个问题:UIBarButto

  8. ios – 关闭UIBarButtonItem上的突出显示

    我正在尝试使用UIBarButtonItem在我的UIToolbar上添加标题.我使用简单的风格,看起来很好,但我似乎无法让它停止突出显示触摸.“突出显示时触摸”选项不适用于条形按钮项目.有没有快速简便的方法来做到这一点?

  9. 以编程方式调整iOS中的按钮大小

    我正在使用XCode4.6.1并开发iOS6.我在故事板上添加了一个按钮.我在我的实现文件ViewController.m中创建了一个插座:我尝试按如下所示更改按钮b1的属性(在同一个文件中:ViewController.m):当我在模拟器中运行应用程序时,按钮的alpha成功设置为0.5.但是,按钮的位置和大小不会改变.我尝试了各种方法来实现它.然而似乎没有任何作用.我想知道我做错了什么.我对O

  10. 如何在iOS / Swift的顶部导航栏中添加“继续”按钮

    我想在导航栏的右侧添加一个“继续”按钮.如何实现这一目标?我一直在尝试使用UIBarButtonItem上的一些方法,但无法使其正常工作.我迄今为止的最大努力是:但我在第一行遇到错误.它不喜欢“style”参数.我也试过了但没有运气.仍然停留在样式参数上.有任何想法吗?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部