制作按钮水波纹效果
对于水波纹按钮大家都不陌生,它其实是手机原生自带的一个效果,但是对于web端是没有此效果的,接下来我们就来探讨如何去模拟一个这样的效果。
我们来看效果图
要完成以上效果首先我们的思路是通过点击外层触发点击事件,收集点击次数然后创建与点击次数相同的水波纹动画组件。然后每当水波纹组件动画执行完之后去销毁当前这个水波纹组件。
第一步我们先去制作这个水波分动画组件
// wave.vue
<template>
<transition name="wave" @after-enter="end">
<span v-show="animating" class="ripple-wave"></span>
</transition>
</template>
<script>
export default {
data () {
return {
animating: true
}
},
props: {
waveClasses: {
type: String,
default: null
},
waveStyles: {
type: String,
default: null
}
},
methods: {
// 动画结束
end () {
this.animating = null;
this.$emit('animating-end')
}
}
}
</script>
<style lang="scss" scoped>
.ripple-wave {
position: absolute;
z-index: 1;
pointer-events: none;
background: currentColor;
border-radius: 50%;
opacity: 0;
transform: scale(2) translateZ(0);
width: 100%;
height: 100%;
~ *:not(.ripple-wave) {
position: relative;
z-index: 2;
}
}
.wave-enter-active {
transition: 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: opacity, transform;
will-change: opacity, transform;
}
.wave-enter {
opacity: 0.26;
transform: scale(0.26) translateZ(0);
}
</style>
以上主要是使用了transition
组件去包裹一个标签,然后每当transition
组件动画执行完之后去调用end
方法向外部发送动画结束事件。
然后我们只需要在外部组件去监听到这个事件然后对其进行销毁,就能确保当前组件不会残留在 DOM 节点中了。
接下来我们来看看外部组件我怎么实现对该动画组件进行增加和删除的。
我们先来创建外部组件的一个骨架。
<template>
<div
class="ripple"
@touchstart.passive="touchstart"
@touchmove.passive="touchmove"
@mousedown.passive="mousedown"
>
<slot></slot>
<Wave></Wave>
</div>
</template>
<script>
import Wave from './wave';
export default {
data () {
return {
}
},
methods: {
// 触摸开始
touchstart (event) {
},
// 触摸移动
touchmove (event) {
},
// 鼠标点击
mousedown () {
}
},
components: {
Wave
}
}
</script>
<style lang="scss" scoped>
.ripple {
width: 100%;
height: 100%;
position: relative;
z-index: 10;
overflow: hidden;
-webkit-mask-image: radial-gradient(circle, #fff 100%, #000 100%);
box-sizing: border-box;
}
</style>
以上创建了一个外壳用于包裹水波纹动画组件,然后在上面定义了手指触摸事件和鼠标点击事件,我们先来制作鼠标点击事件。
由于我们制作此组件需要用到requestAnimationFrame
,但是对于IE或其他浏览器可能不支持requestAnimationFrame
所以我们这里引入是一个polyfill
库 raf。
import raf from 'raf';
export default {
data () {
return {
/*
* 事件类型
*
* @type {String}
*/
eventType: null,
/*
* 涟漪数组
*
* @type {Array}
*/
ripples: []
}
},
methods: {
// 鼠标点击
mousedown (event) {
return this.startRipple(event);
},
// 涟漪开始
startRipple ($event) {
console.log($event)
raf(() => {
if (!this.eventType || this.eventType === $event.type) {
//当前元素位置
let size = this.getSize();
// 涟漪位置
let position = null;
// 获取点击的位置
position = this.getHitPosition($event, size);
// 事件类型
this.eventType = $event.type;
this.ripples.push({
// 波浪 style
waveStyles: this.applyStyles(position, size),
uuid: this.uuid()
});
}
});
},
getSize () {
const { offsetWidth, offsetHeight } = this.$el;
return Math.round(Math.max(offsetWidth, offsetHeight));
},
// 获取点击的位置
getHitPosition ($event, elementSize) {
// 元素的大小及其相对于视口的位置。
const rect = this.$el.getBoundingClientRect();
let top = $event.pageY;
let left = $event.pageX;
return {
top: top - rect.top - elementSize / 2 - document.documentElement.scrollTop + 'px',
left: left - rect.left - elementSize / 2 - document.documentElement.scrollLeft + 'px'
}
},
// 样式
applyStyles (position, size) {
size += 'px';
return {
...position,
width: size,
height: size
}
},
uuid () {
return Math.random().toString(36).slice(4);
}
}
}
以上代码我们引入了raf
库,然后定义一个函数startRipple
通过鼠标点击触发,此函数用于创建水波纹组件数量。
- 第一步我们通过判断
eventType
是否是当前点击事件来确定是否创建水波纹组件,防止其他事件误触发,导致创建了水波纹组件。 - 然后定义了函数
getSize
,用于获取到当前点击此组件时的宽度啊和高度,然后返回了最大值,此值正是水波纹组件的最大宽度和高度。 - 然后通过使用
getHitPosition
我们获取到当前鼠标点击的位置,用于水波纹组件在哪个位置开始现实。此函数内部通过当前点击位置pageY
和pageX
减去元素到视口的top
和left
再减去当前元素的最大宽度或最大高度的一半来获取到当前点击的位置top
和left
的值。 - 最后我们通过定义一个
ripples
数组用于收集点击的次数。数组中包含了水波纹组件的style
和key
,其中uuid
是一个随机数。
我们还需要去循环创建水波纹组件。
<template>
<div
:class="['ripple']"
@touchstart.passive="touchstart"
@touchmove.passive="touchmove"
@mousedown.passive="mousedown"
>
<slot></slot>
<Wave
v-for="ripple in ripples"
:key="ripple.uuid"
:style="ripple.waveStyles"
></Wave>
</div>
</template>
这里我们除了创建还差一个函数,就是动画执行完之后需要销毁当前组件,因为如果不销毁该组件,它会一直残留在DOM中。在外层组件中监听水波纹组件动画结束时发送的事件animating-end
,执行销毁组件函数clearWave
。
<template>
<div
:class="['ripple']"
@touchstart.passive="touchstart"
@touchmove.passive="touchmove"
@mousedown.passive="mousedown"
>
<slot></slot>
<Wave
v-for="ripple in ripples"
:key="ripple.uuid"
:style="ripple.waveStyles"
@animating-end="clearWave(ripple.uuid)"
></Wave>
</div>
</template>
export default {
methods: {
// 清除波纹效果
clearWave (uuid) {
if (uuid) {
this.ripples = this.ripples.filter((ripple) => {
return ripple.uuid !== uuid
});
}
else {
this.ripples = [];
}
}
}
}
最后我们继续完成手指触摸事件,其实很简单我们只需要判断只有当手指触摸才触发即可。
export default {
methods: {
// 触摸开始
touchstart (event) {
return this.touchStartCheck(event);
},
// 触摸移动
touchmove (event) {
return this.touchMoveCheck(event);
},
// 检查触摸开始
touchStartCheck ($event) {
this.touchTimeout = window.setTimeout(() => {
this.startRipple($event);
});
}
}
}
到这来就完成了一个简易的水波纹动画组件了。