vue3学习 --- 动画

2,944 阅读7分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验

  • React框架本身并没有提供任何动画相关的API,所以在React中使用过渡动画我们需要使用一个第三方库 react-transition-group
  • Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果

过渡动画

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

<template>
  <div>
    <button @click="isShow = !isShow">show/hidden</button>

    <!--
        在整个动画的执行阶段,只是定义了开始状态(开始帧)和结束状态(结束帧)
        然后设置了开始帧和结束帧之间相互转换时候的过渡动效
        所以整个动画可以被认为是一个过渡动画
    -->
    <transition name="fade">
      <h2 v-if="isShow">message</h2>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
/*
  在实际执行动画的时候,vue会自动为我们添加上对应的动画效果
  .[transtiton组件上设置的name值]-[样式后缀]
*/
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/*
  因为默认情况下, 这部分的样式就是浏览器的默认设置的样式,
  所以这部分的样式可以省略不进行任何的编写
*/
/*
  .fade-leave-from,
  .fade-enter-to {
    opacity: 1;
  }
*/
  
.fade-enter-active,
.fade-leave-active {
  transition: opacity 2s ease;
}
</style>

class的name命名规则如下:

  • 如果我们使用的是一个没有name的transition,那么所有的class是以 v- 作为默认前缀
  • 如果我们添加了一个name属性,比如 <transtion name="foo">,那么所有的class会以 foo- 开头

过渡动画class

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理

  • 自动嗅探目标元素是否应用了CSS过渡或者动画,如果有,那么在恰当的时机添加/删除 CSS类名
  • 如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用
  • 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行
样式名生效时机失效时机说明
v-enter-from在元素被插入之前生效在元素被插入之后的下一帧移除定义进入过渡的开始状态
v-enter-to在元素被插入之后下一帧生效
(与此同时 v-enter-from 被移除)
在过渡/ 动画完成之后移除定义进入过渡的结束状态
v-enter-active在整个进入过渡的阶段中应用,
在元素被插入之前生效
过渡/动画完成之后移除定义进入过渡生效时的状态
这个类可以被用来定义进入过渡的过程时间,延迟和曲线函
v-leave-from在离开过渡被触发时立刻生效下一帧被移除定义离开过渡的开始状
v-leave-to在整个离开过渡的阶段中应用,
在离开过渡被触发时立刻生效
在过渡/动画完成之后移除离开过渡的结束状态
这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数
v-leave-active过渡/动画完成之后移除在离开过渡被触发之后下一帧生效
(与此同时 v-leave-from 被删除)
在过渡/ 动画完成之后移除定义离开过渡生效时的状态

IcSyMT.png

animate动画

<template>
  <div>
    <!-- 这里包裹div是为了和下面的h2换行显示 -->
    <div>
      <button @click="isShow = !isShow">show/hidden</button>
    </div>

    <transition name="fade">
      <!--
        这里的h2必须设置为行内元素或行内块元素
        默认是块元素,那么设置的样式作用在的是h2这一整行
        而内容是显示不全一整行的,所以动画的效果会有对应的偏差
      -->
      <h2 v-if="isShow">message</h2>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
h2 {
  display: inline-block;
}

.fade-enter-active {
  animation: bounce 1s ease;
}

.fade-leave-active {
  animation: bounce 1s ease reverse;
}

/* 定义帧动画 */
@keyframes bounce {
  0% {
    transform: scale(0);
  }

  50% {
    transform: scale(1.2);
  }

  100% {
    transform: scale(1);
  }
}
</style>

动画属性

type

Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则

如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听

但是如果我们同时使用了过渡和动画,在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束

在这种情况下,我们可以设置 type 属性为 animation 或者 transition 来明确的告知Vue监听的类型

推荐: 不推荐将animation和transition动画一起使用

如果必须一起使用,也应该尽可能的将两种动画的执行时间都设置为一样的

<transition name="fade" type="animation">
  <h2 v-if="isShow">message</h2>
</transition>
duration

我们也可以显示的来指定过渡的时间,通过 duration 属性

单位是ms --- 如果设置了duration 那么在css样式中设置的动画执行时间将会被覆盖

duration可以设置两种类型的值

  • number类型:同时设置进入和离开的过渡时间
<transition name="fade" :duration="3000">
  <h2 v-if="isShow">message</h2>
</transition>
  • object类型:分别设置进入和离开的过渡时间
<transition name="fade" :duration="{enter: 1000, leave: 2000}">
  <h2 v-if="isShow">message</h2>
</transition>
mode

默认情况下进入和离开动画是同时发生的

此时在两个元素之间切换的时候存在的问题

我们不希望同时执行进入和离开动画,那么我们需要设置transition的过渡模式

  • in-out: 新元素先进行过渡,完成之后当前元素过渡离开
  • out-in: 当前元素先进行过渡,完成之后新元素过渡进入
<transition name="fade" mode="out-in">
  <h2 v-if="isShow">message</h2>
  <h2 v-else>msg</h2>
</transition>
appear

默认情况下,首次渲染的时候是没有动画的

如果我们希望给他添加上去动画,那么就可以增加另外一个属性 appear

<!-- appear的值是一个boolean值,默认是false -->
<!-- 所以 :appear="true" 可以简写为 appear -->
<transition name="fade" mode="out-in" appear>
  <Home v-if="isShow" />
  <About v-else />
</transition>

animate.css

如果我们手动一个个来编写这些动画,那么效率是比较低的,所以在开发中我们可能会引用一些第三方库的动画库,比如animate.css

Animate.css是一个已经准备好的、跨平台的动画库为我们的web项目,对于强调、主页、滑动、注意力引导 非常有用

如何使用Animate库呢?

  • 第一步:需要安装animate.css库
  • 第二步:导入animate.css库的样式
  • 第三步:使用animation动画或者animate提供的类

main.js

import { createApp } from 'vue'
import App from './App.vue'

// 全局引入动画相关的样式
import 'animate.css'

createApp(App).mount('#app')

App.vue

<template>
  <div>
    <div>
      <button @click="isShow = !isShow">show/hidden</button>
    </div>

    <transition name="fade" mode="out-in">
      <Home v-if="isShow" />
      <About v-else />
    </transition>
  </div>
</template>

<script>
import About from './components/About.vue'
import Home from './components/Home.vue'

export default {
  name: 'App',

  components: {
    Home,
    About
  },

  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
h2 {
  display: inline-block;
}

.fade-enter-active {
  /* 这里的样式名可以去animate.css的官网去取  */
  animation: bounceInDown 1s ease;
}

.fade-leave-active {
  animation: zoomOut 1s ease;
}
</style>

我们还可以通过以下 attribute 来自定义过渡类名

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css. 结合使用十 分有用。

<template>
  <div>
    <div>
      <button @click="isShow = !isShow">show/hidden</button>
    </div>

    <!-- animate__animated是animate.css所设置的公共样式 -->
    <transition
      mode="out-in"
      enter-active-class="animate__animated animate__fadeIn"
      leave-active-class="animate__animated animate__fadeOut"
    >
      <Home v-if="isShow" />
      <About v-else />
    </transition>
  </div>
</template>

<script>
import About from './components/About.vue'
import Home from './components/Home.vue'

export default {
  name: 'App',

  components: {
    Home,
    About
  },

  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
h2 {
  display: inline-block;
}

.fade-enter-active {
  animation: bounceInDown 1s ease;
}

.fade-leave-active {
  animation: zoomOut 1s ease;
}
</style>

gsap

在使用动画之前,我们先来看一下transition组件给我们提供的JavaScript钩子,这些钩子可以帮助我们监听动画执行到什么阶段了

Icr9jh.png

注意:

  1. 当我们使用JavaScript来执行过渡动画时,需要进行 done 回调

  2. 添加 :css="false",也会让 Vue 会跳过 CSS 的检测

    也就是vue不会再进行对于动画的css样式的设置和检测

    除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响

  3. 每一个钩子函数都有一个参数el,表示的是当前执行动画的元素

  4. 对于enterleave构造函数,有两个参数:

    • 参数1 ---> el即执行动画的元素

    • 参数2 ---> done 是一个函数,需要显示调用,以告知vue,对应的动画生命周期函数动效已经执行完毕

某些情况下我们希望通过JavaScript来实现一些动画的效果,这个时候我们可以选择使用gsap库来完成。

什么是gsap呢

  • GSAP是The GreenSock Animation Platform(GreenSock动画平台)的缩写
  • 它可以通过JavaScript为CSS属性、SVG、Canvas等设置动画,并且是浏览器兼容的

这个库应该如何使用呢

  • 第一步: 需要安装gsap库
  • 第二步: 导入gsap库
  • 第三步: 使用对应的api即可
<template>
  <div>
    <div>
      <button @click="isShow = !isShow">show/hidden</button>
    </div>

    <transition
      mode="out-in"
      @enter="handleEnter"
      @leave="handleLeave"
      :css="false"
    >
      <Home v-if="isShow" />
      <About v-else />
    </transition>
  </div>
</template>

<script>
import About from './components/About.vue'
import Home from './components/Home.vue'
import gsap from 'gsap'

export default {
  name: 'App',

  components: {
    Home,
    About
  },

  data() {
    return {
      isShow: true
    }
  },

  methods: {
    // enter 和 leave回调中,都有两个参数
    // 参数1: 执行回调的元素
    // 参数2: 手动告诉vue动画执行完毕了,否则对应动画将一直执行下去,会影响后续动画的执行
    handleEnter(el, done) {
      // from函数表示的是以什么样的状态去还原到初始状态
      gsap.from(el, {
        scale: 1.2,
        opacity: 0,
        onComplete: done
      })
    },

    handleLeave(el, done) {
      // to表示的是初始状态需要变化成什么样的状态
      gsap.to(el, {
        scale: 1.2,
        opacity: 0,
        onComplete: done
      })
    }
  }
}
</script>

<style scoped>
h2 {
  display: inline-block;
}

.fade-enter-active {
  animation: bounceInDown 1s ease;
}

.fade-leave-active {
  animation: zoomOut 1s ease;
}
</style>

gsap实现数字变化

在一些项目中,我们会见到数字快速变化的动画效果,这个动画可以很容易通过gsap来实

<template>
  <div style="display: inline-block">
    <!--
      每次变化的时候,默认存在小数点的变化
      所以使用toFixed来去除小数点,进行取整操作
    -->
    <div>{{ counter.toFixed(0) }}</div>
    <button @click="increment">+ 100</button>
  </div>
</template>

<script>
import gsap from 'gsap'

export default {
  name: 'Home',

  data() {
    return {
      counter: 0
    }
  },

  methods: {
    increment() {
      // to方法的第一个参数是一个普通对象
      // 增长100的值所需要使用的时间是2s
      // 变化是couter + 100
      // gsap发现变化的是数字的时候,会自动执行数字的递增或递减效果
      gsap.to(this, { duration: 2, counter: this.counter + 100 })
    }
  }
}
</script>

列表操作

之前的所有过渡动画,我们只要是针对单个元素或者组件

  • 要么是单个节点
  • 要么是同一时间渲染多个节点中的一个

那么如果希望渲染的是一个列表,并且该列表中添加删除数据也希望有动画执行呢?

这个时候我们要使用 <transition-group> 组件来完成;

使用<transition-group> 有如下的特点:

  • 默认情况下,它不会渲染一个元素的包裹器,但是你可以指定一个元素并以 tag attribute 进行渲染
  • 过渡模式(in-out 和 out-in)不可用,因为我们不再相互切换特有的元素
  • 内部元素总是需要提供唯一的 key attribute值,用以标识动画具体需要添加在哪一个元素上
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身

对于transaction-group的样式设置方式和对应的动画生命周期函数 和transaction组件是一致的

基本使用

案例:

  1. 一列数字,可以继续添加或者删除数字
  2. 在添加和删除数字的过程中,对添加的或者移除的数字添加动画

IcmGRi.png

<template>
<div>
  <button @click="add">add</button>
  <button @click="remove">remove</button>
  <button @click="shuffle">shuffle</button>

  <transition-group tag="ul" name="foo">
    <li v-for="item in list" :key="item">
      {{ item }}
    </li>
  </transition-group>
</div>
</template>

<script>
import _ from 'lodash'

export default {
name: 'App',

data() {
  return {
    list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    length: 10
  }
},

methods: {
  add() {
    this.list.splice(this.getRandomIndex(), 0, this.length++)
  },

  remove() {
    this.list.splice(this.getRandomIndex(), 1)
  },

  shuffle() {
    // 洗牌
    this.list = _.shuffle(this.list)
  },

  getRandomIndex() {
    return Math.floor(Math.random() * this.list.length) + 1
  }
}
}
</script>

<style scoped>
ul,
li {
  list-style: none;
  margin: 0;
  padding: 0;
}

ul {
  /*
  	不应该设置布局为flex布局,因为flex布局会有自己的特殊的渲染方式
  	所以在进行动画的时候,可能会受到flex布局特殊渲染方式的影响
    从而导致动画的执行可能存在问题
  */
  /* display: flex; */
  margin-top: 10px;
}

li {
  margin-right: 10px;
  display: inline-block;
}

.foo-enter-from,
.foo-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.foo-enter-active,
.foo-leave-active {
  transition: all 1s ease;
}

.foo-leave-active {
  /*
    默认情况下,在移除元素的时候,后边的元素无法进行左移动画
    因为会被还没有被及时移除的元素阻挡,需要等待元素移除后在位移
		此时动画已经指向时间完毕了,所以会出现闪跳的效果
    所以此时可以给离开的元素设置定位,让其脱离标准流,
    此时就不会阻挡后续元素的左移操作
  	从而让位移动画和移除动画同时执行
  */
  
  position: absolute;
}


/*
  v-move 是 transition-group特有的样式
  默认情况下,新增的或者删除的节点是有动画的,但是对于哪些其他需要移动的节点是没有动画的
  所以需要加上这个样式类,用于在插入和移除的时候,添加前后样式的位移动画
  这个样式, 它会在元素改变位置的过程中应用
  像之前的名字一样,我们可以通过name来自定义前缀
*/
.foo-move {
  /* 只要设置过渡属性即可,其余的属性,vue会自动进行相应的计算 */
  transition: transform 1s ease;
}
</style>

交错过渡案例

所谓交错过渡动画效果,就是多个元素在执行对应动画的时候,并不是一起执行的,而是一个接着一个进行执行的

IcwUar.png

<template>
  <div>
    <input type="text" v-model="search">
    <transition-group
      tag="ul"
      name="list"
      @before-enter="beforeEnter"
      @enter="handleEnter"
      @leave="handleLeave"
      :css="false"
    >
      <!-- 
				因为enter和leave方法是绑定在li
				设置自定义属性,以便于在js代码中可以获取到li的索引值 
      -->
      <li v-for="(user, index) in showUsers" :key="user" :data-index="index">{{ user }}</li>
    </transition-group>
  </div>
</template>

<script>
import gsap from 'gsap'

export default {
  name: 'App',

  data() {
    return {
      search: '',
      users: ['Bruce Lee','Jackie Chan','Chuck Norris','Jet Li','Kung Fury']
    }
  },

  computed: {
    showUsers() {
      return this.users.filter(user => user.includes(this.search))
    }
  },

  methods: {
    // 样式初始化
    beforeEnter(el) {
      el.style.height = '0px'
      el.style.opacity = '0'
    },

    handleEnter(el, done) {
      // 我们来通过gsap的延迟delay属性,实现一个交替消失的动画
      gsap.to(el, {
        height: '1.5em',
        opacity: 1,
        delay: el.dataset.index * 0.1, // 获取索引,从而实现delay值依次递增的效果
        onComplete: done
      })
    },

    handleLeave(el, done) {
      gsap.to(el, {
        height: 0,
        opacity: 0,
        delay: el.dataset.index * 0.1,
        onComplete: done
      })
    }
  }
}
</script>