Vue3项目实战之实现物品小球飞入购物车动画
简介
物品小球飞入购物车动画是现在许多项目中都会运用到的一个动画技术,不仅好看还很好玩捏~ 该动画也是本人自己在写项目的时候想要实现它,经过学习,也是成功实现啦!所以,现在该篇文章将会非常详细地带大家实现这个动画效果,以及在实现过程中会遇到的问题。目标效果图如下图所示:
实现思路
通过上面的目标图不难看出,完成该动画我们需要完成这几个任务:
- 选择要加入购物车的商品,点击加入购物车后,该商品上会出现一个有该商品图片的小球。
- 该小球会以曲线的形式从商品位置飞入到购物车的位置
- 在小球飞入到购物车消失后,购物车会有一个弹簧动画效果,且此时购物车物品数量才增加。
好!带着上面这几个任务,我们一起来一一实现目标效果叭~
任务一:点击加入购物车,目标商品上会出现一个有该商品图片的小球
首先,我们要先搞明白这些事儿,第一件事肯定是先把小球绘制出来,那问题是这个小球它该放在哪里呢?好,有人要说了,小球是从商品容器里飞出来的,放商品容器里面。你别说,第一次我就是这么干的,我们看看会出现什么问题, 先给上我的商品图片代码,并且将小球放入小商品容器中:
<div class="bigPic"> <!-- 大图容器 -->
<el-image :src="当前选中商品图片路径" style="height: 100%;"></el-image>
</div>
<div class="samllPic-container"><!-- 小图容器 -->
<!-- v-for渲染每个小图 -->
<div class="samllPic"
v-for="(item,index) in 小图数据数组"
:key="item.Id"
:class="{'active':curIndex === index}"
@click = "curIndex = index"
ref="itemRefsList"
>
<el-image :src="item.小图图片路径" style="height: 100%;"></el-image>
<div class="Ball"></div> <!-- 小球 -->
</div>
</div>
效果如下: 可以看到,我的小图片商品是通过v-for渲染出来的,如果我把小球绘制在小商品容器里面,那么每个小商品容器里面都会有一个小球。好,这里我们先不管,我们继续按这个思路走,接下来就是,我们选择一个小商品加入购物车,然后该小商品容器里面的小球要飞到购物车里面。这个过程,我们需要用到Vue的transiton属性,使用该属性看着难,其实并不难,首先只需用个transiton标签将小球容器包一层,添加appear动画属性和befoeEnter(执行动画开始前要做的事)、afterEnter(执行动画要做的事)事件方法,此时将小球代码修改为:
<transition
appear
@before-appear="beforeEnter"
@after-appear="afterEnter"
>
<div class="Ball"></div> <!-- 小球 -->
</transition>
这时我们来完成一下事件before-appear,动画开始前要执行的方法beforeEnter,我们写一段测试代码:
const beforeEnter = ()=>{
console.log('动画开始了');
}
可以看到打印结果一共有6个,为什么会这样呢?因为有6个小商品容器,所以有6个小球,每个小球都会执行动画,于是就打印了6个结果。那要是容器更多,执行动画次数就更多了,而我们是不是只希望只有我们所选中的那个商品执行动画就够了。 好好好,那我们代码写到这是不是感觉不对劲了,对的,我也是这么觉得的,于是我又另寻它路了。换一种思路,我们是不是可以只创建一个小球,只不过这个小球的位置它是动态变化的,而我们只要保证该小球每次在动画开始前出现在我们选中的商品容器里面就可以了,这样一想,思路是不是又来啦~ 好,又回到开始这个问题,小球该创建在哪里?其实只要不在小商品容器里面创建就好了,因为小球位置是动态变化的,在哪其实已经无所谓了,只要保证它是一个小球就行啦~小球css代码如下:
.Ball{
position: fixed;
width: 30px;
height: 30px;
background-color: #ff0000;
top: 0px;
left: 0px
}
效果如下:
通过给小球添加position: fixed;top: 0px;left: 0px
,此时只有一个小球,让小球相对于页面进行定位,所以现在我们接下来要实现的是选中小商品后点击加入购物车,小球要出现在小商品里面。这个动态改变小球位置的方法我们是不是通过beforeEnter方法来实现就好啦,在每次动画开始前,将小球放到选中小商品里面,所以现在我们是不是得拿到选中小商品得位置属性。听起来有点不好实现,不要慌朋友!其实很简单,我们只需拿到对应的dom结构就好了,有了dom结构位置属性就有了,而在vue3里面通过给dom绑定一个ref属性就可以获得该dom结构,所以我们只需在小容器里面绑定一个ref就ok啦,代码如下:
<div class="bigPic"> <!-- 大图容器 -->
<el-image :src="当前选中商品图片路径" style="height: 100%;"></el-image>
</div>
<div class="samllPic-container"><!-- 小图容器 -->
<!-- v-for渲染每个小图 -->
<div class="samllPic"
v-for="(item,index) in 小图数据数组"
:key="item.Id"
:class="{'active':curIndex === index}"
@click = "curIndex = index"
ref="itemRefsList" //绑定ref获得小容器dom结构
>
<el-image :src="item.小图图片路径" style="height: 100%;"></el-image>
</div>
</div>
需要注意的是,咱们得小容器是v-for遍历渲染的,所以每个容器都会绑定ref属性,于是这里的itemRefsList其实是拿到了6个小容器的dom结构,我们可以打印看看:
const itemRefsList = ref([]) //所有小商品容器的dom结构
onMounted(()=>{
console.log(itemRefsList.value);
})
好,所有小容器的dom结构都拿到了,curIndex
是我们目前选中小商品的小标,所以itemRefsList.value[curIndex.value]
就是我们选中小商品的dom结构了。通过dom结构的getBoundingClientRect().left
和getBoundingClientRect().top
方法便可以获得dom结构到页面左边和顶部的距离。
我们只需把这个距离设置给小球的left
和top
就可以完成小球的动态定位啦~代码如下:
const elLeft = ref(0) //记录当前点击加入购物车的小商品距离网页左边的距离
const elTop = ref(0)//记录当前点击加入购物车的小商品距离网页顶部的距离
const addCart = ()=>{ //点击加入购物车按钮后,获取选中小商品的位置信息
elLeft.value = itemRefsList.value[curIndex.value].getBoundingClientRect().left
elTop.value = itemRefsList.value[curIndex.value].getBoundingClientRect().top
}
给小球动态绑定位置属性
<div :style="ballStyle" class="Ball"></div>
const ballStyle = ref({
left: elLeft.value,
top: elTop.value
})
在动画开始前,将小球移动到选中小容器里面
const beforeEnter = (el)=>{
el.style.transform = `translate3d(${elLeft.value-55}px,${elTop.value-80}px,0)`
console.log('动画开始了');
}
此时效果如下:
会发现,页面一加载动画就开始了,这种情况不是我们想要的,我们想要的是,在我们点击加入购物车后,动画才开始。那为什么一加载页面动画就开始了呢?是因为Ball带有动画属性,只要它被创建出来了就会执行动画。所以,我们只需控制Ball的显示和隐藏,就可以控制动画。控制一个dom的显示隐藏,用一个v-if是不是就可以啦~然后我们通过一个数组showBall动态增加值,每次点击加入购物车,给数组showBall增加一个true,然后在transiton标签中v-for遍历数组showBall,通过Ball的v-if控制小球的显示与隐藏,这么说可能有点不明白,上代码:
<transition
appear
@before-appear="beforeEnter"
@after-appear="afterEnter"
v-for="(item,index) in showBall"
:key="index"
>
//item为true才创建Ball
<div :style="ballStyle" class="Ball" v-if="item"></div>
</transition>
const showBall = ref([])//控制小球显示与隐藏
const addCart = ()=>{ //点击加入购物车按钮后,获取选中小商品的位置信息
showBall.value.push('true')
elLeft.value = itemRefsList.value[curIndex.value].getBoundingClientRect().left
elTop.value = itemRefsList.value[curIndex.value].getBoundingClientRect().top
}
这时效果如下: 当我们选中小商品点击加入购物车后,红色小球成功出现在了选中小容器里面!我们想要的效果是不是成功实现啦~ 哈哈哈哈,还得改成当前商品图片,当前选中容器的下标curIndex都有了,拿到该商品信息不是简简单单嘛~相信小伙伴们都可以的!在Ball里面再加一段代码就好啦!可以给图片加样式,让它变成圆球。效果如下:
<div :style="ballStyle" class="Ball" v-if="item">
<el-image :src="curFood.image"></el-image>
</div>
非常nice!我们的效果是不是成功实现啦~好,现在任务一成功完成!后面几个任务相对就比较简单啦,小伙伴们请继续往下看喔
任务二:将小球以曲线的形式从商品位置飞入到购物车的位置
完成这个任务,无非就是获取到购物车的位置,获取一个dom结构的位置,是不是和上面一样吖,通过ref来绑定dom,然后通过getBoundingClientRect()
就可以获得dom的位置信息啦~这里就不再详细说明了喔。代码如下:
//由于我的购物车dom和商品dom不在一个组件中,所以这里用了props来接收父组件传来的购物车位置属性
const props = defineProps({
CartMessage:{
type:Object,
}
})
const afterEnter = (el)=>{
//购物车到网页左边和顶部的距离
const { left,top } = props.CartMessage
//设置小球移动的位移目标点
el.style.transform = `translate3d(${left - 70}px,${top -80}px,0)`
//增加贝塞尔曲线,让移动路径为曲线
el.style.transition = 'transform .55s cubic-bezier(0.3,-0.25,0.7,-0.15)'
el.style.transition = 'transform .55s linear'
}
现在效果如下:
到这里我们的曲线动画是不是就成功实现啦~就是动画结束了还得让小球隐藏消失,这里我们是不是直接将showBall里面得值全部改成false就好啦,所以我们只需在afterEnter
中再加一段代码就好啦,还有就是小球在移动过程中有个逐渐变小得动画,我们只要给小球再加一个animation
动画就完成啦!代码如下:
控制小球动画结束后消失:
const afterEnter = (el)=>{
//...原有代码
showBall.value = showBall.value.map(item => false)
}
实现小球在移动过程中逐渐变小
.Ball{
position: fixed;
width: 30px;
height: 30px;
border-radius: 15px;
.el-image{
animation: 1s ballScale ease-in-out;
border-radius: 50%;
}
}
@keyframes ballScale {
0%{
transform: scale(1);
}
30%{
transform: scale(0.8);
}
60%{
transform: scale(0.6);
}
90%{
transform: scale(0.4);
}
100%{
transform: scale(0.2);
}
}
效果如下: 到这里我们的任务二是不是也成功实现了呢~👍👍👍
任务三:在小球飞入到购物车消失后,购物车会有一个弹簧动画效果,且此时购物车物品数量才增加
完成这个任务我们其实只需要解决一个问题,就是动画什么时候结束,动画结束了立马调用方法。在transiton标签里有一个事件方法@after-leave,监听动画结束执行方法。所以我们再给transiton标签绑定一个事件@after-leave="handleAnimationEnd"
,在动画结束后会执行handleAnimationEnd
方法,所以我们只需把要做的事都写在该方法里面就可以啦~前面说啦,因为我的购物车dom和商品dom不在一个组件中,所以我这里需要用到defineEmits
,在动画结束后,立马发布一个事件告诉父组件(动画结束了,购物车别睡啦!该动起来啦!)
//发布一个事件AnimationEnd,告诉父组件动画结束了
const emit = defineEmits(['AnimationEnd'])
const handleAnimationEnd = ()=>{
emit('AnimationEnd',true)
//调用将商品加入购物车的方法,动画结束后才将商品数据添加到购物车
addCartData()
}
父组件监听AnimationEnd
事件 @AnimationEnd="AnimationEnd"
,相当于动画结束后父组件执行AnimationEnd
方法
const playStart = ref(false) //控制购物车的弹簧动画效果
//e是子组件传的参数值,这里是true
const AnimationEnd = (e)=>{
playStart.value = e
setTimeout(()=>{
playStart.value = false
},500) //500ms移除购物车的弹簧动画效果
}
//让购物车dom动态绑定弹簧动画类名
<Cart :class="{'playAnimation': playStart}">
购物车弹簧动画类名和实现购物车弹簧动画
.playAnimation{
animation: cartScale 0.5s ease-in-out;
}
//实现购物车弹簧动画
@keyframes cartScale {
0%{
transform: scale(1);
}
20%{
transform: scale(1.2);
}
60%{
transform: scale(0.8);
}
80%{
transform: scale(1.1);
}
100%{
transform: scale(1);
}
}
总结
到这里我们要实现的动画效果就全部实现啦~不知道小伙伴们有没有看懂喔,如果各位小伙伴们有不明白的地方或者更好的解决方法,欢迎来评论喔😘😘😘。要是觉得文章对您有所帮助的话,可以给小米露点个美美的赞鼓励一下嘛🌹🌹🌹,码字不易,后续还会更新一些其他项目功能的代码文章,欢迎各位掘友支持!😭😭😭
转载自:https://juejin.cn/post/7320231441707040768