vue 開發波紋點擊特效組件
最近在使用 vue2 做一個新的 material ui 庫,波紋點擊效果在 material design 中被多次使用到,于是決定把它封裝成一個公共的組件,使用時直接調用就好啦。
開發之前的思考
常見的波紋點擊效果的實現方式是監聽元素的 mousedown 事件,在元素內部創建一個 波紋元素 ,并調整元素的 transform: scale(0); 到 transform: scale(1); , 通過計算點擊的位置來設置 波紋元素 的大小和位置,以達到波紋擴散的效果。
我將組件分為兩個部分, circleRipple.vue 和 TouchRipple.vue 各自實現不同的功能
-
circleRipple.vue 波紋擴散組件,完成波紋擴散的效果
-
TouchRipple.vue 監聽 mouse 和 touch 相關事件,控制 circleRipple 的顯示,位置。
circleRipple.vue
circleRipple 需要完成波紋擴展的效果,而且可以從外部控制它的大小和位置, 所以利用 vue 的 transition 動畫完成效果, 提供 mergeStyle 、 color 、 opacity 參數來從外部控制它的樣式。實現代碼如下。
<template>
<transition name="mu-ripple">
<div class="mu-circle-ripple" :style="styles"></div>
</transition>
</template>
<script>
import {merge} from '../utils'
export default {
props: {
mergeStyle: {
type: Object,
default () {
return {}
}
},
color: {
type: String,
default: ''
},
opacity: {
type: Number
}
},
computed: {
styles () {
return merge({}, {color: this.color, opacity: this.opacity}, this.mergeStyle)
}
}
}
</script>
<style lang="less">
@import "../styles/import.less";
.mu-circle-ripple{
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
pointer-events: none;
user-select: none;
border-radius: 50%;
background-color: currentColor;
background-clip: padding-box;
opacity: 0.1;
}
.mu-ripple-enter-active, .mu-ripple-leave-active{
transition: transform 1s @easeOutFunction, opacity 2s @easeOutFunction;
}
.mu-ripple-enter {
transform: scale(0);
}
.mu-ripple-leave-active{
opacity: 0 !important;
}
</style>
vue2 對于動畫方面做了比較大的修改,除了把指令換成組件外,它還可以完成更復雜的動畫效果,具體可以看這里 vue2 transition
TouchRipple.vue
TouchRipple 需要控制 circleRipple 的顯示。完成以下內容:
-
監聽 mouse 和 touch 相關事件, 控制 circleRipple 的顯示。
-
通過點擊事件 event 對象, 計算出 circleRipple 的大小和位置
-
如果頻繁點擊可能出現多個 circleRipple
首先,基本模板 + 數據模型
<template>
<!--最外層用div包裹-->
<div @mousedown="handleMouseDown" @mouseup="end()" @mouseleave="end()" @touchstart="handleTouchStart" @touchend="end()" @touchcancel="end()">
<!--外層包裹防止波紋溢出-->
<div :style="style" ref="holder">
<!--多個波紋用 v-for 控制-->
<circle-ripple :key="ripple.key" :color="ripple.color" :opacity="ripple.opacity" :merge-style="ripple.style" v-for="ripple in ripples"></circle-ripple>
</div>
<!--利用slot分發實際內容-->
<slot></slot>
</div>
</template>
<script>
import circleRipple from './circleRipple'
export default {
props: {
// 是否從中間擴散,設為false會從點擊處擴散
centerRipple: {
type: Boolean,
default: true
},
// 外層包裹的樣式
style: {
type: Object,
default () {
return {
height: '100%',
width: '100%',
position: 'absolute',
top: '0',
left: '0',
overflow: 'hidden'
}
}
},
// 波紋顏色
color: {
type: String,
default: ''
},
// 波紋透明度
opacity: {
type: Number
}
},
data () {
return {
nextKey: 0, // 記錄下一個波紋元素的key值, 相當于uuid,不設置的話會使動畫失效
ripples: [] // 波紋元素參數數組
}
},
mounted () {
this.ignoreNextMouseDown = false // 防止既有 touch 又有 mouse點擊的情況
},
methods: {
start (event, isRippleTouchGenerated) {
// 開始波紋效果
},
end () {
// 結束波紋效果
},
handleMouseDown (event) {
// 監聽 鼠標單擊
},
handleTouchStart (event) {
// 監聽 touchstart 方法
}
},
components: {
'circle-ripple': circleRipple
}
}
</script>
開始和結束波紋效果
增加一個波紋元素只需要在 ripple 增加一個 object 即可,不同的是當需要從點擊處擴展時,需要計算一下波紋元素的大小和位置。
{
// isRippleTouchGenerated 是否是touch 事件開始的
start (event, isRippleTouchGenerated) {
// 過濾 touchstart 和 mousedown 同時存在的情況
if (this.ignoreNextMouseDown && !isRippleTouchGenerated) {
this.ignoreNextMouseDown = false
return
}
// 添加一個 波紋元素組件
this.ripples.push({
key: this.nextKey++,
color: this.color,
opacity: this.opacity,
style: this.centerRipple ? {} : this.getRippleStyle(event) // 不是從中心擴展的需要計算波紋元素的位置
})
this.ignoreNextMouseDown = isRippleTouchGenerated
},
end () {
if (this.ripples.length === 0) return
this.ripples.splice(0, 1) // 刪除一個波紋元素
this.stopListeningForScrollAbort() // 結束 touch 滾動的處理
}
}
因為 vue2 基于 Virtual DOM 的, 所以如果沒有 key 在增加一個元素又同時刪除一個元素的時候,dom tree并沒有發生變化,是不會產生動畫效果的。
監聽 mousedown 和 touchstart
mousedown 和 touchstart 處理上會有所不同,但都是用來啟動波紋效果的, touch涉及到多點點擊的問題,我們一般取第一個即可。
{
handleMouseDown (event) {
// 只監聽鼠標左鍵的點擊
if (event.button === 0) {
this.start(event, false)
}
},
handleTouchStart (event) {
event.stopPropagation() // 防止多個波紋點擊組件嵌套
if (event.touches) {
this.startListeningForScrollAbort(event) // 啟動 touchmove 觸發滾動處理
this.startTime = Date.now()
}
this.start(event.touches[0], true)
}
}
touchmove控制
當發生touchMove事件是需要判斷是否,移動的距離和時間,然后結束小波紋點擊小姑
{
// touchmove 結束波紋控制
stopListeningForScrollAbort () {
if (!this.handleMove) this.handleMove = this.handleTouchMove.bind(this)
document.body.removeEventListener('touchmove', this.handleMove, false)
},
startListeningForScrollAbort (event) {
this.firstTouchY = event.touches[0].clientY
this.firstTouchX = event.touches[0].clientX
document.body.addEventListener('touchmove', this.handleMove, false)
},
handleTouchMove (event) {
const timeSinceStart = Math.abs(Date.now() - this.startTime)
if (timeSinceStart > 300) {
this.stopListeningForScrollAbort()
return
}
const deltaY = Math.abs(event.touches[0].clientY - this.firstTouchY)
const deltaX = Math.abs(event.touches[0].clientX - this.firstTouchX)
// 滑動范圍在 > 6px 結束波紋點擊效果
if (deltaY > 6 || deltaX > 6) this.end()
}
}
計算波紋的位置和大小
需要從點擊處擴散的波紋效果,需要計算波紋元素的大小和位置
{
getRippleStyle (event) {
let holder = this.$refs.holder
// 這個方法返回一個矩形對象,包含四個屬性:left、top、right和bottom。分別表示元素各邊與頁面上邊和左邊的距離。
let rect = holder.getBoundingClientRect()
// 獲取點擊點的位置
let x = event.offsetX
let y
if (x !== undefined) {
y = event.offsetY
} else {
x = event.clientX - rect.left
y = event.clientY - rect.top
}
// 獲取最大邊長
let max
if (rect.width === rect.height) {
max = rect.width * 1.412
} else {
max = Math.sqrt(
(rect.width * rect.width) + (rect.height * rect.height)
)
}
const dim = (max * 2) + 'px'
return {
width: dim,
height: dim,
// 通過margin控制波紋中心點和點擊點一致
'margin-left': -max + x + 'px',
'margin-top': -max + y + 'px'
}
}
}
使用
由于 touchRipple 內部都是 position:absolute 布局,使用時,需要在外部加上 position:relative
// listItem.vue
<a :href="href" @mouseenter="hover = true" @mouseleave="hover = false" @touchend="hover = false"
@touchcancel="hover = false" class="mu-item-wrapper" :class="{'hover': hover}">
<touch-ripple class="mu-item" :class="{'mu-item-link': link}" :center-ripple="false">
<div class="mu-item-media">
<slot name="media"></slot>
</div>
<div class="mu-item-content">
// ...
</div>
</touch-ripple>
</a>
<style>
.mu-item-wrapper {
display: block;
color: inherit;
position: relative;
}
</style>
來自:https://segmentfault.com/a/1190000006931367