前言
vue.js的自定义指令的最大优势就是复用,而waves波纹特效可以说是最适合做自定义指令的特效,因为它可以应用在任何按钮或者页面每一个角落里。
本文参考了vue-element-admin的waves特效。
学习自定义指令
可以看我写的《通俗学习Vue.js自定义指令》
然后咱们先做一个不借助自定义指令的DEMO。
按钮waves特效DEMO
准备按钮
准备一个button,用div模拟一个:<div style="width: 100px; height: 50px; line-height: 50px; text-align: center; border: 1px solid #999;" @click="waveMe">我是按钮</div>
waveMe方法的第一部分代码
上面绑定了一个click事件,现在准备waveMe方法,第一部分代码如下,这部分做的事情是:
- 让按钮relative,这样给按钮插入子元素之后,子元素可以将按钮作为定位祖先。子元素就是产生波纹特效的元素。
- 给按钮append这个子元素,子元素的class是
waves-ripple
。 - 子元素的宽高取按钮的最长的一边(例子里是宽最长),这样子元素放大的时候才能充满这个按钮。
const target = e.target
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
准备CSS
waveMe方法先看上面第一部分,然后看看waves-ripple
类是怎么回事:
.waves-ripple
要绝对定位、圆形、设一个Alpha透明度很透明的黑色背景、先缩小到消失、opacity则完全不透明。opacity是在Alpha透明度的基础上再做透明度计算,也就是乘法。.waves-ripple.z-active
是说,一旦加上.z-active
,希望实现:用1.2秒时间opacity从1到0,用0.6秒时间从0到放大2倍,两个特效是同时开始的。视觉效果就是:不到0.6秒的时间里,波纹就已经充满了按钮,因为0.6秒的时候已经2倍大了,充满按钮之后还有不到一秒的时间会看到波纹慢慢变淡直到消失。
其实波纹特效本身是没有什么规范的,差不多就可以。
.waves-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
pointer-events: none;
user-select: none;
transform: scale(0);
opacity: 1;
}
.waves-ripple.z-active {
opacity: 0;
transform: scale(2);
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
}
waveMe方法的第二部分代码
水波的动画上面已经准备好了,现在确定水波的坐标。
这里考虑两种不同的需求,一种是一定要从按钮中心点开始水波扩散,另一种是从真正的点击位置进行鼠标扩散。
const type = 'center'
switch (type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
break
default:
ripple.style.top =
e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop + 'px'
ripple.style.left =
e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft + 'px'
ripple.className = 'waves-ripple z-active'
center情况
我解释一下ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
。
这里面rect.height
就是按钮高度,除以2就是半高,ripple.offsetHeight
是水波元素的高度,除以2也是元素的半高。怎么理解?我说一种通俗的理解方法:
假如水波元素没有高度,那么,水波元素的top就真的是按钮的半高就可以了。
假如水波元素有10像素高度,那么,top如果还是按钮半高,那么水波元素的中心点就不再是按钮的中心点,而是靠下了,靠下了5像素。所以,水波元素高度如果是N,被挤下去的距离就是N/2,那么top就要向上修正N/2,也就是减去N/2。这么理解就可以了。
其他情况,也就是从点击位置发散水波
以这句代码为例解释一下:
ripple.style.top =
e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop + 'px'
e.pageY
:鼠标相对于文档坐标(注意,不是相对于浏览器窗口)的纵坐标
rect.top
:按钮左上角相对于浏览器窗口的纵坐标
ripple.offsetHeight / 2
:水波元素的半高
document.documentElement.scrollTop
:滚动条的纵坐标,其实意思就是“页面当前可视部分的上沿,距离页面顶部的距离”
这一连串相减,怎么理解:
先考虑“鼠标相对于文档坐标”减去“滚动条的纵坐标”,等于鼠标相对于浏览器视口的纵坐标,再减去“按钮左上角相对于浏览器窗口的纵坐标”,等于按钮左上角到鼠标的纵距离,再减水波元素的半高,道理跟上面的center场景一样,因为水波有高度,是为了抵消高度带来的偏差,最后等于水波元素的top值。
改写成局部自定义指令
<div v-wave:[wavePoint] style="...">我是按钮</div>
data() {
return {
wavePoint: 'hit' // 或 'center'
};
},
directives: {
wave: {
bind(el, binding) {
el.addEventListener('click', function(e) {
// 贴 waveMe 的代码段
// 注意一处改动 const type = binding.arg
})
}
}
}
改写成全局自定义指令
Vue.directive('wave', {
bind(el, binding) {
// 同上,贴 waveMe 的代码段
}
})
改写成Vue.js的插件
Vue.js的插件编写模式有多种,这里介绍比较流行的一种,做法是在src/directive/wave/中创建三个文件,分别是index.js、wave.css、wave.js:
- 创建src/directive/wave/index.js
这是入口文件,负责引入wave.js,并且定义install函数。if (window.Vue)
这一句是为了兼容Vue的UMD版本,UMD版本说穿了就是用<script>
标签引入Vue,这样Vue是window的一个属性,这时候需要手动Vue.use(install);
来使用插件。
// eslint-disable-line
这句注释是写给ESLint插件看的,意思是让ESLint插件不要管这一行,如果不加这句注释,那么会报错:'Vue' is not defined (no-undef)
。
如果你不用考虑UMD方式,那么这句if不需要加。
import wave from './wave'
const install = function(Vue) {
Vue.directive('wave', wave)
}
if (window.Vue) {
window.wave = wave
Vue.use(install); // eslint-disable-line
}
wave.install = install
export default wave
- 创建wave.css
这个不多说,上文有贴过代码。
- 创建wave.js
import './wave.css'
function handleClick(el, binding) {
// 逻辑代码,上文有贴
}
export default {
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false)
}
}
使用插件
在某个.vue文件中写入:
import wave from '@/directive/wave'
export default {
directives: { wave },
}
谢谢阅读。