最近遇到了一个红包雨的需求,就大概这个样子:
具体效果可以进入拼多多查看。
在实现这个效果之前,我先安利一个我们老大之前写的基于requestAnimationFrame实现的小动画框架 《chito》。https://redmed.github.io/chito/
本文所述的需求均依赖此框架完成。
它可以根据你传入的相关动画参数来为你创建流畅的补间动画。
OK 回归正题。
首先思考几个问题。
1、红包要如何插入进来并展示掉落动画?
思考结果:平常的方式就是创建一个以单个红包为纬度的组件,我需要创建多少个红包,进页面就用v-for来创建多少个dom。但是我觉得以我司运营的套路,这个红包以后肯定会各种复用,我总不能谁复用这个功能都copy一遍我的代码过去吧 那简直太灾难了。
所以我准备将红包的插入方式改为函数调用的方式,就像在日常的后台管理系统中调用Element-UI中的 this.$message.success()一样,调用一次函数,红包就从上往下掉落一次,再根据不同的参数来单独控制每个红包的掉落速度、点击红包后对应的得分,甚至红包的颜色等等各项自定义属性。
2、红包的掉落需要哪些参数?
思考结果:以最最简单的交互来讲,可能只需要掉落速度,从何处掉落(红包掉落动画起始点的X轴),红包数量,红包雨持续的时间,点击红包后的反馈动画。
现在我有两个组件,1、父组件来负责红包的展示,以及红包的掉落的属性(上述那些)。2、子组件就是红包组件。
3、通过函数调用的组件如何编写和调用?
这个貌似官方没有什么对应的文档,我也是从Element-UI的源码中抄过来的。
4、未经允许直接复制本文的同学,我谢谢你。
OK进入开发,首先就是红包组件
首先我要定义一个红包容器,代码十分简单, 再给它一个简简单单的样式
<template>
<div class="box" ref="packet" @touchstart.once="handleClick" v-show="hidden">
</div>
</template>
<style scoped lang="stylus">
* {
user-select: none
outline none
}
.box {
width 0.592rem
height 0.86rem
position absolute
top -1rem
background url("https://coolcdn.igetcool.com/p/2021/1/f611d435ffb8c8c43c6983d807e27a65.png?_296x430.png")
background-repeat no-repeat
background-size contain
transform rotate(0deg)
}
</style>
这样,屏幕上就会出现一个红包,图是我随便找的一个。
红包应该是竖着的,我随便截了个图而已。
红包组件的详细代码,每一行都有注释,我不相信你看不懂😄
<template>
<div class="box" ref="packet" @touchstart.once="handleClick" v-show="hidden">
</div>
</template>
<script>
const {Animation, Clip} = require('chito')
export default {
name: "bonusRain",
data() {
return {
// 代表红包该显示还是不显示
hidden: true,
// 用于存放chito生成的动画实例
animation: null,
// 用于存放从父组件传进来的配置项
options: null,
// 用于记录红包是不是已经掉落
isDropped: false,
// 用于判断红包在掉落的时候是顺时针旋转还是逆时针旋转
rotateComputed: 1
}
},
methods: {
// 点击事件
handleClick(e) {
this.$nextTick(() => {
// 因为每个红包都只能点击一次,所以点击红包后,就让动画停止
this.animation.stop()
// 因为红包点击后也代表了销毁,所以在这里也要调用dropped事件
if (this.isDropped === false) {
this.options.onDropped()
this.isDropped = true
}
// 自定义的click事件,让事件能够分发出去,因为不是直接通过dom的方式向
// 父组件插入的组件,所以不能用$.emit分发事件
if (this.hidden === true) {
this.options.onClick(e)
}
// 点击红包后修改红包样式
this.$refs['packet'].style.transform = 'rotate(0deg)'
this.$refs['packet'].style.background = `url(https://coolcdn.igetcool.com/p/2021/1/c1a3568325f4fa97f843850aaa9713a7.jpg?_500x511.jpg)`
this.$refs['packet'].style.backgroundSize = 'contain'
this.$refs['packet'].style.backgroundRepeat = 'no-repeat'
this.$refs['packet'].style.animation = 'unset'
// 修改完样式总不能直接消失,所以适当给一个延时
setTimeout(() => {
this.hidden = false
}, 500)
})
},
show(obj) {
// 把传进来的配置项赋值给options
this.options = obj
// 自定义了一个beforShow的钩子,这样动画在初始化的时候想执行什么方法也方便
if(this.options.beforeShow){
this.options.beforeShow()
}
// 如果穿进来了一个封面图片,就替换掉当前的红包封面
if(obj.cover){
this.$refs['packet'].style.background = `url(${obj.cover})`
this.$refs['packet'].style.backgroundSize = 'contain'
this.$refs['packet'].style.backgroundRepeat = 'no-repeat'
}
// 创建一个动画剪辑
let clip = new Clip({
// 剪辑持续的时间,如果掉落距离是固定的,那么掉落时间就决定了红包掉落的速度
duration: obj.speed || 2000,
// 剪辑重复一次,因为每个红包都是独立的
repeat: 1
}, {
// 掉落的路线,从 -100开始到屏幕高度
y: [-100, document.documentElement.clientHeight]
})
// 红包在掉落的过程中,每一次运动都会触发clip的update事件,
// 如果你需要一些花里胡哨的效果,可以在这里定义
clip.on('update', (ev) => {
var keyframe = ev.keyframe;
// 因为要操作dom 所以需要用nextTick
this.$nextTick(() => {
// 掉落的过程中动态改变红包的y轴位置
this.$refs['packet'].style.top = keyframe.y + 'px';
// 根据配置项的x轴位置来设置红包的x轴位置
this.$refs['packet'].style.left = obj.xAxis + 'px'
// 红包掉落的时候让它旋转起来
this.$refs['packet'].style.transform = `rotate(` + (ev.progress * 180 * this.rotateComputed) +`deg)`
})
});
// 创建Animation实例
this.animation = new Animation();
// 把创建的剪辑添加到Animation实例中
this.animation.addClip([clip]);
},
start(){
// 还在奇怪为什么我rotateComputed给了个数字1么?
// 这里就告诉你,如果是1它就顺时针旋转,-1就是逆时针旋转
this.rotateComputed = (Math.random() * 10) > 5 ? 1:-1
// 让动画开始播放
this.animation.start()
// 动画完成播放的事件
this.animation.on('complete', () => {
// 动画结束之后回调onDropped事件
if (this.isDropped === false) {
this.options.onDropped()
// 动画完成后,把isDropped的值修改为是
// 代表红包已经掉落,理论上没什么用,但是玩意需要父组件做判断呢?
this.isDropped = true
}
// 动画播放完成后,隐藏红包
this.hidden = false
})
}
}
}
</script>
<style scoped lang="stylus">
* {
user-select: none
outline none
}
.box {
width 0.592rem
height 0.86rem
position absolute
top -1rem
background url("https://coolcdn.igetcool.com/p/2021/1/f611d435ffb8c8c43c6983d807e27a65.png?_296x430.png")
background-repeat no-repeat
background-size contain
transform rotate(0deg)
}
</style>
因为要通过函数调用的方式调用组件,所以还需要写一个插件。
源码如下:
// 引入Vue
import Vue from 'vue'
// 引入红包组件
import bonusItem from './bonusItem.vue';
// 红包实例
let packet;
// 组件挂载
function createItem(args) {
// 用vue渲染红包组件并挂载
const vnode = new Vue({
render: h => h(bonusItem)
}).$mount()
// 将组件添加到body上
document.body.appendChild(vnode.$el)
// 返回当前组件的实例
return vnode.$children[0]
}
export function showPacket(args) {
// 创建组件
packet = createItem(args)
// 将组件实例暴露出去
return packet
}
export default showPacket
这样,我们就可以通过调用函数的方式动态插入组件了
OK,接下来就是父组件的调用,完整代码如下:
<template>
<div class="container">
分数{{point}}
<br>
<p v-if="game">游戏结束</p>
<br>
<button @click="start">开始游戏</button>
</div>
</template>
<script>
// 引入红包组件
import showPacket from './bonusItem.js'
export default {
name: "test",
data(){
return {
// 倒计时
time: 10,
// 红包数量
itemCount: 1,
// 红包实例存储栈
dropStack: [],
// 分数记录
point: 0,
// 已经销毁的红包数量
dropped: 0,
// 游戏状态
game:false,
}
},
// 进页面的时候,让页面高度固定为100vh并且不能滚动
beforeCreate() {
document.body.style.maxHeight = '100vh'
document.body.style.overflow = 'hidden'
},
// 离开页面之前 恢复,避免不影响其它页面
beforeDestroy() {
document.body.style.maxHeight = 'unset'
document.body.style.overflow = 'unset'
},
mounted() {
// 创建红包DOM,并把每次函数执行返回的dom实例放到存储栈中
let arr = []
for(let i in this.itemStack){
let instance = showPacket()
arr.push(instance)
instance.show({
// 遍历x轴,因为x轴是随机生成的,所以红包掉落的起始位置也是随机的
xAxis: this.itemStack[i],
// 红包掉落的速度
speed: 3000,
// 红包点击事件
onClick: () =>{
// 每次点击,分数+1
this.point++
},
// 红包销毁事件
onDropped: () => {
// 销毁数量加1
++this.dropped
// 如果销毁数量等于红包数量,那么游戏停止
if(this.dropped === this.itemCount){
this.game = true
}
}
})
}
// 将创建的红包实例存入栈中
this.dropStack = arr
},
methods: {
// 红包雨开始
start(){
this.game = false;
// 遍历红包栈,并根据事件平均分配掉落的时机
for(let i in this.dropStack){
setTimeout(() => {
this.dropStack[i].start()
// 假设红包雨持续的事件是10秒,红包数量为20个,那么每个红包掉落的时机就是
// 当前遍历的索引 * (10秒 * 1000毫秒 / 红包数量)
// 等于每 i * 10000毫秒 / 20 掉落一个。
// 这里不懂的可以问我
}, i * (this.time * 1000 / this.itemCount))
}
},
},
computed: {
// 按照红包数量生成对应的X轴随机数
itemStack(){
let arr = []
for(let i = 0; i < this.itemCount; i++){
// 保证红包的x轴在固定的范围内,根据实际需求控制
arr.push(Math.floor(Math.random() * ( 300 - 20) + 20))
}
return arr
}
}
}
</script>
<style scoped lang="stylus">
.container {
background-size contain
background-repeat no-repeat
height 100vh
overflow hidden
color #000
font-size 0.2rem
}
</style>
最终的效果如下:
因为没有工具录制gif 所以截了个示例图,快去自己试试吧。
搬运本文,请注明原文地址,谢谢。