300 lines
12 KiB
Vue
300 lines
12 KiB
Vue
<template>
|
||
<view class="like-button">
|
||
<!-- #ifdef APP-NVUE -->
|
||
<!-- 在NVUE环境下使用list容器显示动画元素 -->
|
||
<list class="animate-wrap">
|
||
<!-- 循环渲染点赞动画元素 -->
|
||
<cell class="a-img" v-for="(item,index) in viewList" :key="item.elId" :ref="item.elId" :style="{ 'right': propSite.x || propSite[0] + 'rpx', 'bottom': propSite.y || propSite[1] + 'rpx'}">
|
||
<image :style="{ 'width': propImgWidth + 'rpx', 'height': propImgHeight + 'rpx'}" mode="widthFix" :src="item.src" :animation="item.animation"></image>
|
||
</cell>
|
||
</list>
|
||
<!-- #endif -->
|
||
<!-- #ifndef APP-NVUE -->
|
||
<!-- 在非NVUE环境下使用view容器显示动画元素 -->
|
||
<view class="animate-wrap">
|
||
<!-- 循环渲染点赞动画元素 -->
|
||
<view class="a-img" v-for="(item,index) in viewList" :key="item.elId" :ref="item.elId" :style="{ 'right': propSite.x || propSite[0] + 'rpx', 'bottom': propSite.y || propSite[1] + 'rpx'}">
|
||
<image :style="{'width': propImgWidth + 'rpx', 'height': propImgHeight + 'rpx'}" mode="widthFix" :src="item.src" :animation="item.animation"></image>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<!-- 点赞按钮区域 -->
|
||
<view class="on-button">
|
||
<view ref="likeBtn" id="like-btn" class="el-like-btn" @tap="handleClick">
|
||
<slot></slot>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 点赞动画按钮组件
|
||
* 实现点击按钮后产生飞出图标动画的效果,常用于直播场景中的点赞功能
|
||
*/
|
||
export default {
|
||
props: {
|
||
/**
|
||
* 显示图标路径数组
|
||
* @type {Array}
|
||
* @default []
|
||
*/
|
||
propShowImgs: { // 显示图标路径
|
||
type: Array,
|
||
default: () => {
|
||
return []
|
||
}
|
||
},
|
||
/**
|
||
* 动画持续时间(毫秒)
|
||
* @type {Number}
|
||
* @default 5000
|
||
*/
|
||
propDuration: { // 动画效果时间
|
||
type: Number,
|
||
default: 5000
|
||
},
|
||
/**
|
||
* X轴方向随机幅度范围
|
||
* @type {Number}
|
||
* @default 50
|
||
*/
|
||
propRange: { // x 间隔幅度
|
||
type: Number,
|
||
default: 50
|
||
},
|
||
/**
|
||
* 图标飞行最大高度
|
||
* @type {Number}
|
||
* @default 200
|
||
*/
|
||
propHigh: { // 图标高度
|
||
type: Number,
|
||
default: 200
|
||
},
|
||
/**
|
||
* 组件宽度
|
||
* @type {Number|String}
|
||
* @default 52
|
||
*/
|
||
propWidth: { // 图标宽度
|
||
type: Number || String,
|
||
default: 52
|
||
},
|
||
/**
|
||
* 组件高度
|
||
* @type {Number|String}
|
||
* @default 52
|
||
*/
|
||
propHeight: { // 图标高度
|
||
type: Number || String,
|
||
default: 52
|
||
},
|
||
/**
|
||
* 图标宽度
|
||
* @type {Number|String}
|
||
* @default 52
|
||
*/
|
||
propImgWidth: { // 图标宽度
|
||
type: Number || String,
|
||
default: 52
|
||
},
|
||
/**
|
||
* 图标高度
|
||
* @type {Number|String}
|
||
* @default 52
|
||
*/
|
||
propImgHeight: { // 图标高度
|
||
type: Number || String,
|
||
default: 52
|
||
},
|
||
/**
|
||
* 点击节流时间(毫秒),防止过快点击
|
||
* @type {Number}
|
||
* @default 200
|
||
*/
|
||
propThrottle: { // 点击节流 ms
|
||
type: Number,
|
||
default: 200
|
||
},
|
||
/**
|
||
* 图标出现位置坐标
|
||
* 可以是[x, y]数组或{x, y}对象格式
|
||
* @type {Array|Object}
|
||
* @default [122, 95]
|
||
*/
|
||
propSite: { // x y 坐标 [x<Number>, y<Number>]
|
||
type: Array || Object,
|
||
default: () => {
|
||
return [122, 95] || {
|
||
x: 122,
|
||
y: 95
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* 是否启用缩放效果,可以设置为数字指定缩放倍数
|
||
* @type {Number|Boolean}
|
||
* @default false
|
||
*/
|
||
propLarge: { // 是否缩放冒泡
|
||
type: [Number, Boolean],
|
||
default: false
|
||
},
|
||
/**
|
||
* 是否单独移除动画元素(true时逐个删除,false时统一删除)
|
||
* @type {Boolean}
|
||
* @default true
|
||
*/
|
||
propAlone: {
|
||
type: Boolean,
|
||
default: true
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
viewList: [], // 渲染元素列表
|
||
elId: 0, // 元素渲染ID计数器
|
||
oldTime: 0, // 上次点击时间戳,用于节流
|
||
timer: null, // 定时器引用
|
||
waitDeleteIndex: 0 // 等待删除的元素索引计数
|
||
}
|
||
},
|
||
watch: {},
|
||
methods: {
|
||
/**
|
||
* 处理点击事件,创建并播放点赞动画
|
||
* @param {Event} e 点击事件对象
|
||
*/
|
||
handleClick(e) {
|
||
// 函数节流,防止过快点击
|
||
let interval = e.timeStamp - this.oldTime
|
||
if (interval < this.propThrottle) return null;
|
||
this.oldTime = e.timeStamp
|
||
let animation = {}
|
||
// 创建animate配置
|
||
// #ifdef APP-NVUE
|
||
animation = weex.requireModule('animation')
|
||
// #endif
|
||
// 随机选择一个图标
|
||
let randomImg = Math.floor(Math.random() * this.propShowImgs.length)
|
||
// 构造动画元素数据
|
||
let _item = {
|
||
elId: 'el_likeicon_' + this.elId, // 生成元素ref
|
||
src: this.propShowImgs[randomImg], // 随机图标
|
||
animation: animation, // 每个盒子动画
|
||
x: Math.ceil(Math.random() * this.propRange), // 方向间距
|
||
q: Math.floor(Math.random() * 2), // 随机方向
|
||
}
|
||
// 计算随机飞行方向和距离
|
||
let _abs = ['-', '']
|
||
let _dirX = Number(_abs[_item.q] + _item.x) // 随机的方向和间距
|
||
let _dirY = this.propHigh - Math.random() * 10
|
||
// 添加到渲染列表
|
||
this.elId++
|
||
this.viewList.push(_item)
|
||
// #ifndef APP-NVUE
|
||
// 非NVUE环境下创建动画实例
|
||
_item.animation = uni.createAnimation({
|
||
duration: this.propDuration,
|
||
timingFunction: 'ease-out',
|
||
})
|
||
// 设置动画结束后的处理逻辑
|
||
setTimeout(() => {
|
||
// 完成后事件回调
|
||
this.$emit('finished')
|
||
// 根据propAlone属性决定逐个删除还是一起删除
|
||
if (this.propAlone) {
|
||
this.waitDeleteIndex++
|
||
this.onThrottle(this.deleteView, this.propDuration)
|
||
return null;
|
||
} else {
|
||
// 完成动画后在n秒后清空
|
||
clearTimeout(this.timer)
|
||
this.timer = setTimeout(() => {
|
||
this.viewList = []
|
||
}, this.propDuration)
|
||
return null;
|
||
}
|
||
}, this.propDuration)
|
||
// #endif
|
||
// 执行动画
|
||
setTimeout(() => {
|
||
this.$nextTick(() => {
|
||
// 确定缩放倍数
|
||
let _n = 1
|
||
if (this.propLarge) _n = typeof(this.propLarge) === 'number' ? this.propLarge : 2;
|
||
// #ifndef APP-NVUE
|
||
// 非NVUE环境下的动画实现
|
||
_item.animation.translateY(-_dirY).translateX(_dirX).scale(_n, _n).opacity(0).step()
|
||
_item.animation = _item.animation.export()
|
||
// #endif
|
||
// #ifdef APP-NVUE
|
||
// NVUE环境下的动画实现
|
||
let el = this.$refs[_item.elId][0];
|
||
clearTimeout(this.timer)
|
||
_item.animation.transition(el, {
|
||
styles: {
|
||
transform: `translate(${_dirX}rpx, -${_dirY}rpx) scale(${_n}, ${_n}])`,
|
||
transformOrigin: 'center center',
|
||
opacity: 0
|
||
},
|
||
duration: this.propDuration, // ms
|
||
timingFunction: 'ease-out',
|
||
delay: 0 // ms
|
||
}, () => {
|
||
// 完成后事件回调
|
||
this.$emit('finished')
|
||
// 根据propAlone属性决定逐个删除还是一起删除
|
||
if (this.propAlone) {
|
||
this.waitDeleteIndex++
|
||
this.onThrottle(this.deleteView, this.propDuration)
|
||
return null
|
||
} else {
|
||
// 完成动画后在n秒后清空
|
||
clearTimeout(this.timer)
|
||
this.timer = setTimeout(() => {
|
||
this.viewList = []
|
||
}, this.duration)
|
||
}
|
||
})
|
||
// #endif
|
||
})
|
||
}, 0)
|
||
// 点击立即触发组件事件
|
||
this.$emit('handleClick', this.elId)
|
||
},
|
||
/**
|
||
* 删除已完成动画的视图元素
|
||
*/
|
||
deleteView() {
|
||
this.viewList.splice(0, this.waitDeleteIndex)
|
||
this.waitDeleteIndex = 0
|
||
},
|
||
/**
|
||
* 节流函数,控制函数执行频率
|
||
* @param {Function} fn 需要节流的函数
|
||
* @param {Number} delay 延迟时间(毫秒)
|
||
*/
|
||
onThrottle(fn, delay) {
|
||
let verifi = true
|
||
return (function() {
|
||
if (!verifi) return false;
|
||
verifi = false
|
||
setTimeout(() => {
|
||
fn()
|
||
verifi = true
|
||
}, delay)
|
||
})()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 动画元素固定定位样式 */
|
||
.a-img {
|
||
position: absolute;
|
||
}
|
||
</style> |