一,过渡动画的实现
1,无过渡,直接变化
<template>
<div class="testBox">
<div class="testCon" :style="{ width: width + 'px' }"></div>
<div class="testBtn" @click="clickBtn">点击按钮</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const width = ref(10);
const clickBtn = () => (width.value += 100);
</script>
<style lang="scss" scoped>
.testBox {
overflow: hidden;
height: 100vh;
padding: 100px 50px;
box-sizing: border-box;
.testCon {
height: 200px;
background: pink;
}
.testBtn {
margin-top: 10px;
height: 60px;
line-height: 60px;
border: 1px solid #35aeff;
background: #35aeff;
border-radius: 10px;
}
}
</style>
为了好讲述,只看一次点击。也就是开始时width:10px,结束时width:110px。在没有设置过渡的时候,变化是突兀的,生硬的。如下图:
2,设置过渡,设置时间和速率
过渡的实现。可以理解为:记录起始状态,然后添加一个从开始到结束的时间和速度。
主要就是利用css3的transition来设置。需要监听谁的属性变化,就写在谁的样式中:
.testCon {
height: 200px;
background: pink;
transition: width 2s linear;//监听这个元素的width属性的变化,
}
于是就会监听这个width属性,从开始到结束的变化,会被拉长到2s来线性变化。
当想要监听该元素所有元素的变动的时候,只要把属性改成all即可:
<template>
<div class="testBox">
<div :class="['testCon', btnClick ? 'btnClick' : '']"></div>
<div class="testBtn" @click="clickBtn">点击按钮</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const btnClick = ref(false);
const clickBtn = () => {
btnClick.value = true;
};
</script>
<style lang="scss" scoped>
.testBox {
overflow: hidden;
height: 100vh;
padding: 100px 50px;
box-sizing: border-box;
.testCon {
width: 100px;
height: 100px;
background: pink;
}
.btnClick {
height: 200px;
width: 200px;
background: red;
transition: all 2s linear;
}
.testBtn {
margin-top: 10px;
height: 60px;
line-height: 60px;
border: 1px solid #35aeff;
background: #35aeff;
border-radius: 10px;
}
}
</style>
主要是从这个样式:
.testCon {
width: 100px;
height: 100px;
background: pink;
}
变成:
.btnClick {
height: 200px;
width: 200px;
background: red;
transition: all 2s linear;
}
这样一来,就会给该元素所有变动的属性增加过渡变化的效果:
也就是说,过渡主要是transition这个css样式产生的效果,
transition :transition-property(执行变换的属性) || transition-duration(执行变换的时间) || transition-timing-function(执行变换的速率) || transition-delay(延迟变换的时间);
transition-property(执行变换的属性)
transition-property | 可取的值 |
none | 没有属性会获得过渡效果 |
all | 所有属性都将获得过渡效果,也是其默认值 |
property | CSS 属性名称列表,逗号隔开 |
transition-duration(执行变换的时间
过渡的持续时间,取值time为数值,单位为s(秒)或者ms(毫秒),其默认值是0,也就是变换时是即时的。
transition-timing-function(执行变换的速率)
值 | 描述 |
linear | 相同速度开始至结束 |
ease | 慢速开始,然后变快,然后慢速结束 |
ease-in | 慢速开始 |
ease-out | 慢速结束 |
ease-in-out | 慢速开始和结束 |
cubic-bezier(n,n,n,n) | 在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值。 |
transition-delay(延迟变换的时间)
默认是0,也就是立即执行,若有设置值,则是延迟执行过渡。
需要注意的是,不是所有的CSS属性都支持transition,可以支持的具体的效果可以看这里。
二,animation 和 keyframe 的组合实现动画
过渡动画,只能定义首尾两个状态。而animation可以自定义多个帧的状态之间的过渡效果。从而实现真正的动画。
1,animation的取值
值 | 描述 |
animation-name | 规定需要绑定到选择器的 keyframe 名称 |
animation-duration | 规定完成动画所花费的时间,以秒或毫秒计 |
animation-timing-function | 规定动画的速度曲线 |
animation-delay | 规定在动画开始之前的延迟,默认值为0 |
animation-iteration-count | 规定动画应该播放的次数,默认值为1 |
animation-direction | 规定是否应该轮流反向播放动画,默认值是正向 |
2,keyframe
@keyframes 就是让程序员设置一定序列的动画帧,然后提供给animation使用的。
使用方法:
当使用百分比用法时:
@keyframes 动画名字{
0% { top: 0; left: 0px}
50% { top: 30px; left: 20px; }
100% { top: 0; left: 30px;}
}
使用的方法如下:
<template>
<div class="testBox">
<div class="testCon"></div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped>
.testBox {
overflow: hidden;
height: 100vh;
padding: 100px 50px;
box-sizing: border-box;
.testCon {
animation: testAni 3s ease-in-out infinite;
}
}
@keyframes testAni {
0% {
width: 50px;
height: 50px;
background: #cd4a48;
border-radius: 50px;
}
50% {
width: 100px;
height: 100px;
background: #e72365;
border-radius: 0;
}
100% {
width: 50px;
height: 50px;
background: #04aef1;
border-radius: 50px;
}
}
</style>
实现的效果:
当使用from-to用法时:
@keyframes preloader {
from {
transform: rotate(0deg);//其实这里写的就是支持过渡动画的属性啦
}
to {
transform: rotate(360deg);
}
}
不同于transition,使用animation自动触发,无需事件触发。
三,transform变换
在制作动画的时候,常常要使用到属性变化。transform就是变形,主要包括旋转rotate、扭曲skew、缩放scale和移动translate以及矩阵变形matrix。
transform: none || transform-functions
none:表示不进么变换;transform-function表示一个或多个变换函数,用空格分开。
transform-function变换函数有以下几种:
1,旋转rotate
通过指定的角度参数对原元素指定一个2D rotation(2D 旋转),需先有transform-origin属性的定义(默认dom元素的中心)。transform-origin定义的是旋转的基点,其中angle是指旋转角度,如果设置的值为正数表示顺时针旋转,如果设置的值为负数,表示逆时针旋转。 如:transform:rotate(30deg):
<template>
<div class="testBox">
<div class="testCon"></div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped>
.testBox {
overflow: hidden;
height: 100vh;
padding: 100px 50px;
box-sizing: border-box;
.testCon {
width: 100px;
height: 100px;
animation: testAni 3s ease-in-out infinite;
}
}
@keyframes testAni {
0% {
background: #04aef1;
border-radius: 50px;
}
50% {
background: #e72365;
border-radius: 0;
transform: rotate(180deg);
}
100% {
background: #04aef1;
border-radius: 50px;
}
}
</style>
2,移动translate
移动translate我们分为三种情况:translate(x,y)水平方向和垂直方向同时移动(也就是X轴和Y轴同时移动);translateX(x)仅水平方向移动(X轴移动);translateY(Y)仅垂直方向移动(Y轴移动),如果是负值,则是反方向移动,当然,也可以用transform-origin来控制基准。
@keyframes testAni {
0% {
background: #04aef1;
border-radius: 50px;
}
50% {
background: #e72365;
border-radius: 0;
transform: rotate(180deg);
transform: translate(50px, 50px);
}
100% {
background: #04aef1;
border-radius: 50px;
}
}
实现的便是这样的效果:
3,scale缩放
scale(x,y)使元素水平方向和垂直方向同时缩放(也就是X轴和Y轴同时缩放);scaleX(x)元素仅水平方向缩放(X轴缩放);scaleY(y)元素仅垂直方向缩放(Y轴缩放),但它们具有相同的缩放中心点和基数,其中心点就是元素的中心位置,缩放基数为1,如果其值大于1元素就放大,反之其值小于1,元素缩小。同样的,它可以用transform-origin来控制基准。
50% {
background: #e72365;
border-radius: 0;
transform: scale(1.5, 2);
}
4,扭曲skew
扭曲skew和translate、scale一样同样具有三种情况:skew(x,y)使元素在水平和垂直方向同时扭曲(X轴和Y轴同时按一定的角度值进行扭曲变形);skewX(x)仅使元素在水平方向扭曲变形(X轴扭曲变形);skewY(y)仅使元素在垂直方向扭曲变形(Y轴扭曲变形)。
50% {
background: #e72365;
border-radius: 0;
transform: skewX(30deg);
}
单个的动画看起来有些死板,但是多种结合在一起,就会有神效:
<div id="preloader">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
#preloader {
position: relative;
width: 42px;
height: 42px;
animation: preloader 5s infinite linear;
}
#preloader span {
width: 20px;
height: 20px;
position: absolute;
background: red;
// display: block;
animation: preloader_6_span 1s infinite linear;
}
#preloader span:nth-child(1) {
background: #2ecc71;
}
#preloader span:nth-child(2) {
left: 22px;
background: #9b59b6;
animation-delay: 0.2s; //延迟0.2s后开始动画
}
#preloader span:nth-child(3) {
top: 22px;
background: #3498db;
animation-delay: 0.4s; //延迟0.4s后开始动画
}
#preloader span:nth-child(4) {
top: 22px;
left: 22px;
background: #f1c40f;
animation-delay: 0.6s; //延迟0.6s后开始动画
}
@keyframes preloader {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes preloader_6_span {
0% {
transform: scale(1);
}
50% {
transform: scale(0.5);
}
100% {
transform: scale(1);
}
}
四,vue中的动画效果
1,普通的过渡动画
Vue 3 中提供了一些动画的封装,使用内置的 transition 组件来控制组件的动画。
按照官网的说法,就是被transition包裹的组件,会有这六种class的切换:
1,v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
2,v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
3,v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
4,v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
5,v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
6,v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
具体 class 的名字,Vue 的官网有一个图给出了很好的解释,图里的 v-enter-from 中的 v,就是我们设置的 name 属性。
实际上,这个过程就是过渡动画的体现。当元素进入的那一帧,会添加v-enter-from的class,设置了初始状态。而v-enter-to则是元素开始进入到完成会添加的class,用来设置元素过渡的结束状态。v-enter-active则是整个过渡时间内会存在的class。
也就是上面那张图实际上是这样的:
于是,v-enter-active和v-leave-active就可以被用来定义进入过渡的过程时间,延迟和曲线函数。因为这两个类在过渡的时间内一直存在,过渡完成后才删除。
而v-enter-from和v-leave-to则可以用来设置动画的开始和结束。
另外两个可以不使用,而用元素自身的class替代:
<template>
<div id="demo">
<button @click="data.noActivated = !data.noActivated">Toggle</button>
<transition name="fade">
<p v-if="data.noActivated" class="test">hello</p>
</transition>
</div>
</template>
<script setup>
import { reactive } from "vue";
const data = reactive({
noActivated: false
});
</script>
<style lang="scss" scoped>
.test {
color: green;
opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
transition: all 5s ease;
}
.fade-enter-from,
.fade-leave-to {
color: black;
opacity: 0;
}
</style>
实现的效果:
也就是在元素渲染的那段动画中:
1,fade-enter-from设置了初始状态
2,fade-enter-active设置了过渡的动画和时间
3,元素本身的class:test充当设置了最终状态。
从而实现了开始渲染的过渡动画效果。
而在元素移除的那段动画中:
1,元素本身的class:test充当设置了初始状态。
2,fade-leave-active设置了过渡的动画和时间
3,fade-leave-to设置了最终状态。
2,列表的进入/离开过渡动画
transition 元素作为单个元素/组件的过渡效果。transition 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。它的这六个class就是放在这个子元素上的。所以列表渲染就不用这个,而是使用transition-grou来包裹,至于六种class则是添加在item上。用法和上文说的一样,我习惯用元素本身的class,来取代v-enter-to和v-leave-from。所以没设置这两个。
<template>
<div id="demo">
<button @click="clickBtn">增加按钮</button>
<transition-group name="fade" tag="ul">
<li v-for="(item, index) in data.testList" :key="index" class="test">
{{ item.content }}
</li>
</transition-group>
</div>
</template>
<script setup>
import { reactive } from "vue";
const data = reactive({
testList: [
{
content: "第一个"
}
]
});
function clickBtn() {
data.testList.push({
content: "下一个"
});
}
</script>
<style lang="scss" scoped>
.test {
color: red;
font-size: 18px;
}
.fade-enter-active,
.fade-leave-active {
transition: all 2s ease;
}
.fade-enter-from,
.fade-leave-to {
color: black;
opacity: 0;
transform: translateX(30px);
}
</style>
3,js动画配合transition
<template>
<div class="title">
<input type="text" @keypress.enter="add" v-model="title" />
<button @click="clear">清理</button>
</div>
<div class="content">
<transition-group name="flip-list" tag="ul">
<li v-for="(item, index) in todoList" :key="item.content">
<input type="checkbox" v-model="item.done" />
<span :class="[item.done ? 'inactive' : 'active']">
{{ item.content }}
</span>
<span class="remove-btn" @click="removeTodo($event, index)">❌</span>
</li>
</transition-group>
</div>
<span class="dustbin">🗑</span>
<div class="animate-wrap">
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
>
<div class="animate" v-show="animate.show">📋</div>
</transition>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
//把这个useTodo解耦出来
function useTodos() {
let title = ref("");
let todoList = ref([
{
content: "五湖四海皆一望",
done: false
},
{
content: "千江有水千江月",
done: true
}
]);
function add() {
const obj = {
content: title.value,
done: false
};
todoList.value.push(obj);
title.value = "";
}
function clear() {
todoList.value = todoList.value.filter((v) => !v.done);
}
return { title, todoList, add, clear };
}
//其实你可以把组件内部的任何一段代码,从组件文件里抽离出一个独立的文件进行维护。
//再在这个组件中使用,于是这些东西就不依赖于this上下文了,这里再解构赋值出来
let { title, todoList, add, clear } = useTodos();
const { animate, beforeEnter, enter, afterEnter, removeTodo } = useAnimation();
function useAnimation() {
let animate = reactive({
show: false,
el: null
});
const dustbin = {
el: null,
pos: [],
init(queryStr) {
this.el = document.querySelector(queryStr);
this.getPos();
},
getPos() {
const { left, top } = this.el.getBoundingClientRect();
this.pos[0] = left;
this.pos[1] = top;
}
};
function beforeEnter(el) {
let dom = animate.el;
let rect = dom.getBoundingClientRect();
const aniEl = document.querySelector(".animate");
//动画元素 调整到dustbin的位置,也可以css直接写精准位置
aniEl.style.left = `${dustbin.pos[0]}px`;
aniEl.style.top = `${dustbin.pos[1]}px`;
//计算并赋值偏移量
let dx = dustbin.pos[0] - rect.left;
let dy = dustbin.pos[1] - rect.top;
el.style.transform = `translate(-${dx}px, ${dy * -1}px)`;
}
function enter(el, done) {
document.body.offsetHeight;
el.style.transform = `translate(0,0)`;
el.addEventListener("transitionend", done);
}
function afterEnter(el) {
animate.show = false;
el.style.display = "none";
}
function removeTodo(e, i) {
animate.el = e.target;
animate.show = true;
todoList.value.splice(i, 1);
dustbin.init(".dustbin");
}
return { animate, beforeEnter, enter, afterEnter, removeTodo };
}
</script>
<style lang="scss" scoped>
.active {
color: v-bind(color);
}
.flip-list-move {
transition: transform 0.8s ease;
}
.flip-list-enter-active,
.flip-list-leave-active {
transition: all 1s ease;
}
.flip-list-enter-from,
.flip-list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.dustbin {
position: absolute;
top: 0;
right: 0;
}
.animate-wrap .animate {
position: fixed;
right: 10px;
top: 10px;
z-index: 100;
transition: all 0.5s linear;
}
</style>
实现的效果: