• 技术文章 >web前端 >Vue.js

    实例详解,带你玩转 Vue 动画

    青灯夜游青灯夜游2022-09-09 20:39:43转载704
    Vue为我们提供了很多动画接口方便我们实现动画效果,过渡动画可以实现一些简单的动画,如果一个动画包含几个简单的动画这个时候就需要使用动画钩子了。本文列出了几个例子,都是日常开发中经常遇到的。

    大前端零基础入门到就业:进入学习

    在开发 ToC 项目中,需要用到很多动画,比如常见的路由动画,弹出框动画以及一些业务动画等,本文对学过的动画做了一个总结,方便日后复习,如果对您有所帮助不甚荣幸。

    首先我们来回顾下 Vue 提供了那些动画,帮助我们快速实现想要的动画效果。【相关推荐:vuejs视频教程

    Vue 动画

    CSS 过渡

    以显示和隐藏动画为例:

    <div id="demo">
      <button v-on:click="show = !show">
        Toggle
      </button>
      <transition name="fade">
        <p v-if="show">hello</p>
      </transition>
    </div>

    从隐藏到显示

    当动画开始的时候,P 标签还是隐藏的,此时 Vue 会给 P 加上两个class:

    .fade-enter {
       opacity: 0;
    }
    .fade-enter-active {
       transition: opacity 0.5s;
    }

    1.png

    当动画开始后,会移除.fade-enter(在元素被插入之前生效,在元素被插入之后的下一帧移除),这个时候 P 标签的 opacity就恢复到 1,即显示,这个时候就会触发transition , 检测到opacity的变化,就会产生动画。当动画结束后,会移除 Vue 加上的 class(v-enter-to, v-enter-active)。

    2.png

    上面这个过程是怎么实现的呢?它主要用到了requestAnimationFrame这个api,我们自己可以实现一个简易版的动画,当生成下一帧的时候添加或删除某个类,从而形成动画效果。

    <!DOCTYPE html>
    <html>
      <head>
        <title>Document</title>
        <style type="text/css">
          .box {
            width: 100px;
            height: 100px;
            background-color: red;
          }
          .enter {
            opacity: 0;
          }
          .mov {
            transition: opacity 5s linear;
          }
        </style>
      </head>
      <body>
        <div id="box" class="box mov enter"></div>
        <script>
          var box = document.getElementById('box')
          // 第一帧之后执行
          requestAnimationFrame(function() {
            box.setAttribute('class', 'box mov')
          })
        </script>
      </body>
    </html>

    当生成下一帧的时候,会移除enter这个class,那么 div 就会显示出来,就会触发transition产生动画效果。

    从显示到隐藏

    当隐藏的时候也产生动画,如下图:

    3.png

    .fade-leave-to {
       opacity: 0;
    }
    .fade-leave-active {
       transition: opacity 0.5s;
    }

    刚开始 P 标签是显示的,因为此时fade-leave (在离开过渡被触发时立刻生效,下一帧被移除) 的样式是 opacity 是 1,执行到第二帧的时候加上fade-leave-to(在离开过渡被触发之后下一帧生效 ,与此同时 fade-leave 被删除),此时opacity 是 0,既然发生了属性的变化,transition就会监听到,从而形成动画。

    这样显示和隐藏就形成了一个完整的动画。

    原理是当你通过点击事件改变css属性,比如opacity时,transition会检测到这个变化,从而形成动画

    CSS 动画

    CSS 动画原理同 CSS 过渡类似,区别是在动画中 v-enter 类名在节点插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除。

    <style>
      .bounce-enter-active {
        animation: bounce-in 3s;
      }
      .bounce-leave-active {
        animation: bounce-in 3s reverse;
      }
      @keyframes bounce-in {
        0% {
          transform: scale(0);
        }
        50% {
          transform: scale(1.5);
        }
        100% {
          transform: scale(1);
        }
      }
    </style>
    
    <div id="demo">
      <button @click="show = !show">Toggle show</button>
      <transition name="bounce">
        <p v-if="show">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
          facilisis enim libero, at lacinia diam fermentum id. Pellentesque
          habitant morbi tristique senectus et netus.
        </p>
      </transition>
    </div>

    当我们点击改变showfalse时,会在 P 标签上添加.bounce-leave-active这个class,这个类就会执行animation 动画,当动画执行完成后删除.bounce-leave-active

    JavaScript 钩子动画

    <!DOCTYPE html>
    <html>
      <head>
        ...
        <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
      </head>
      <body>
        <div id="demo">
          <button @click="show = !show">
            Toggle
          </button>
          <transition
            v-on:before-enter="beforeEnter"
            v-on:enter="enter"
            v-on:leave="leave"
            v-bind:css="false"
          >
            <p v-if="show">
              Demo
            </p>
          </transition>
        </div>
    
        <script>
          new Vue({
            el: '#demo',
            data: {
              show: true
            },
            methods: {
              beforeEnter: function(el) {
                el.style.opacity = 0
                el.style.transformOrigin = 'left'
              },
              enter: function(el, done) {
                Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 1000 })
                Velocity(el, { fontSize: '1em' }, { complete: done })
              },
              leave: function(el, done) {
                Velocity(
                  el,
                  { translateX: '15px', rotateZ: '50deg' },
                  { duration: 600 }
                )
                Velocity(
                  el,
                  {
                    rotateZ: '45deg',
                    translateY: '30px',
                    translateX: '30px',
                    opacity: 0
                  },
                  { complete: done }
                )
              }
            }
          })
        </script>
      </body>
    </html>

    Javascript 钩子动画一般用在比较复杂的动画上,而不是简单的过渡动画。后面我们会用很大的篇幅通过几个例子来说明用 Javascript 钩子动画是如何完成复杂动画的。

    初始渲染的过渡

    因为CSS过渡动画需要有个触发条件,比如opacity必须有一个变化,如果没有变化就不会触发。那么,可以通过 appear attribute 设置节点在初始渲染的过渡:

    <transition appear>
        <!-- ... -->
    </transition>

    CSS 动画(animation)则不需要触发条件。

    多个元素的过渡

    一旦涉及到多个元素的过渡,那么就会出现旧元素和新元素进出的先后问题。<transition> 的默认行为是进入和离开同时发生,但是这样就会产生一些不协调的效果,所以 Vue 提供了过渡模式

    <transition name="fade" mode="out-in">
        <!-- ... the buttons ... -->
    </transition>

    但是这两个模式并不能完全满足实际需要,实际上我们可以定制我们要想的先后效果,比如后台管理系统中有一个面包屑导航栏,当改变路由的时候需要更改面包屑里面的内容,那么这个更改的动画可以让旧的元素向左滑出,新的元素从右边滑入。

    <div id="demo">
      <div>
        <button @click="show = !show">
          Toggle
        </button>
      </div>
    
      <transition name="fade">
        // 一定要设置key
        <div class="cls" v-if="show" key="1">
          if Demo
        </div>
        <div class="cls" v-else key="2">else demo</div>
      </transition>
    </div>
    
    <style>
      .cls {
        display: inline-block;
      }
      .fade-enter-active,
      .fade-leave-active {
        transition: all 1s;
        // 这个定位设置很关键
        position: absolute;
      }
    
      .fade-enter {
        opacity: 0;
        transform: translateX(30px);
      }
    
      .fade-leave-to {
        opacity: 0;
        transform: translateX(-30px);
      }
    </style>

    4.gif

    当有相同标签名的元素切换时,需要通过 key attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 transition 组件中的多个元素设置 key 是一个更好的实践。

    多个组件的过渡

    多个组件的过渡简单很多 - 我们不需要使用 key attribute。相反,我们只需要使用动态组件

    <transition name="component-fade" mode="out-in">
        <component v-bind:is="view"></component>
    </transition>
    
    new Vue({
      el: '#transition-components-demo',
      data: {
        view: 'v-a'
      },
      components: {
        'v-a': {
          template: '<div>Component A</div>'
        },
        'v-b': {
          template: '<div>Component B</div>'
        }
      }
    })
    
    .component-fade-enter-active, .component-fade-leave-active {
      transition: opacity .3s ease;
    }
    .component-fade-enter, .component-fade-leave-to {
      opacity: 0;
    }

    列表过渡

    上面讲的动画都是针对单个节点,或者同一时间渲染多个节点中的一个,那么怎么同时渲染整个列表,比如使用 v-for

    在这种场景中,使用 <transition-group> 组件,这个组件的几个特点:

    后面我们会通过一个例子演示如何使用<transition-group>

    案例

    路由动画

    在后台管理系统中,当路由变化时,对应的组件内容也会发生变化,当在变化时加上一个动画,让整个页面效果更加自然。

    <transition name="fade-transform" mode="out-in">
      // 这里加了key
      <router-view :key="key">
    </transition>
    
    computed: {
      key() {
          return this.$route.path
      }
    }
    
    .fade-transform-leave-active,
    .fade-transform-enter-active {
        transition: all 0.5s;
    }
    
    .fade-transform-enter {
        opacity: 0;
        transform: translateX(-30px)
    }
    
    .fade-transform-leave-to {
        opacity: 0;
        transform: translateX(30px)
    }

    H5 页面弹框从底部弹出动画

    在 H5 页面开发中,一个常用的功能是点击一个按钮,隐藏的内容从屏幕底部弹出,同时弹出的时候有个动画效果,一般是缓慢上升。

    <div id="list-demo">
      <transition name="list-fade">
        // 外面一层遮罩
        <div class="playlist" v-show="showFlag" @click="hide">
          // 这里面才是内容
          <div class="list-wrapper"></div>
        </div>
      </transition>
    
      <div @click="show" class="add">
        Add
      </div>
    </div>
    
    <style>
      .add {
        position: fixed;
        bottom: 0;
        left: 0;
        text-align: center;
        line-height: 40px;
      }
    
      .playlist {
        position: fixed;
        z-index: 200;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.3);
      }
      .list-wrapper {
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 400px;
        background-color: #333;
      }
      
      // 针对最外面一层的遮罩
      .list-fade-enter-active,
      .list-fade-leave-active {
        transition: opacity 0.3s;
      }
      // 针对类为list-wrapper的内容
      .list-fade-enter-active .list-wrapper,
      .list-fade-leave-active .list-wrapper {
        transition: all 0.3s;
      }
      
      .list-fade-enter,
      .list-fade-leave-to {
        opacity: 0;
      }
      
      // 最开始内容是隐藏的,所以translate3d(0, 100%, 0)
      .list-fade-enter .list-wrapper,
      .list-fade-leave-to .list-wrapper {
        transform: translate3d(0, 100%, 0);
      }
    </style>

    这个动画有两层,一层是最外层的内容,另一层是最里面部分的内容,效果如下:

    5.gif

    列表删除动画

    在 H5 页面开发中,如果在一个列表页中删除其中一个子项,要求有一个删除动画效果。

    <div id="list-demo">
      <transition-group ref="list" name="list" tag="ul">
        <li :key="item.id" class="item" v-for="(item, index) in arr">
          <span class="text" v-html="item.name"></span>
          <span class="delete" @click="deleteOne(item, index)">
            delete
          </span>
        </li>
      </transition-group>
    </div>
    
    .item {
      height: 40px;
    }
    .list-enter-active,
    .list-leave-active {
      transition: all 0.1s
    }
    .list-enter,
    .list-leave-to {
      height: 0
    }

    复杂动画-案例1

    看下面的图片,这个动画该怎么实现呢?

    6.gif

    一般复杂的动画,并不能用简单的 css 过渡或者 css 动画能实现的,需要使用 javascript钩子动画实现。

    针对上面图片的动画进行拆解:

    代码结构:

    <div>
        <transition
          name="normal"
          @enter="enter"
          @after-enter="afterEnter"
          @leave="leave"
          @after-leave="afterLeave"
        >
          <div class="normal-player" v-show="fullScreen">
            <div class="top">
              // 顶部区域...
            </div>
            <div
              class="middle"
              @touchstart.prevent="middleTouchStart"
              @touchmove.prevent="middleTouchMove"
              @touchend.prevent="middleTouchEnd"
            >
              // 中间区域...   
            </div>
            <div class="bottom">
              // 底部区域...
            </div>
          </div>
        </transition>
        <transition name="mini">
          <div class="mini-player" v-show="!fullScreen" @click="open">
             // 内容区域...  
          </div>
        </transition>
    </div>

    实现第一个动画效果

    // 这是stylus的写法
    .normal-enter-active,
    .normal-leave-active
      transition: all 0.4s
      .top,
      .bottom
        // 通过这个白塞尔曲线,使得动画有个回弹的效果
        transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)
    .normal-enter,
    .normal-leave-to
      opacity: 0
      .top
        // 从上往下滑入
        transform: translate3d(0, -100px, 0)
      .bottom
        // 从下往上滑入
        transform: translate3d(0, 100px, 0)

    通过第一章节部分的学习,看懂这段动画代码应该不难。

    实现第二个动画效果

    要实现这个动画效果,必须要计算左下角的圆到中心部分的圆的 x 轴和 y 轴方向上的距离,因为H5页面在不同的手机屏幕下,这个距离是不同的,所以一开始就不能写死,只能通过 javascript 去动态的获取。

    // 计算从小圆中心到大圆中心的距离以及缩放比例
    _getPosAndScale() {
      const targetWidth = 40
      const paddingLeft = 40
      const paddingBottom = 30
      const paddingTop = 80
      const width = window.innerWidth * 0.8
      const scale = targetWidth / width
      const x = -(window.innerWidth / 2 - paddingLeft)
      const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
      return {
        x,
        y,
        scale
      }
    }

    这段代码细节可以不用看,只需要知道它是计算左下角小圆的原心到中心部分圆的原心的距离(x, y),以及根据圆的直径获取放大缩小倍数(scale)。

    // 这个库可以让我们使用js来创建一个keyframe的动画,为什么要用js来生成呢?这是因为有些变化的属性需要动态的计算,而不是一开始就定好了
    import animations from 'create-keyframe-animation'
    
    
    // 动画钩子
    // done:当动画执行完后执行done函数,然后跳到afterEnter钩子函数
    enter(el, done) {
      const { x, y, scale } = this._getPosAndScale()
      // 对于大圆来说,进入的时机就是从小圆到小圆
      let animation = {
        0: {
          // 一开始大圆相对于小圆的位置,所以x为负数,y为整数
          transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`
        },
        // scale: 1.1 这样圆就有个放大后变回原样的效果
        60: {
          transform: 'translate3d(0, 0, 0) scale(1.1)'
        },
        100: {
          transform: 'translate3d(0, 0, 0) scale(1)'
        }
      }
      // 设置animation
      animations.registerAnimation({
        name: 'move',
        animation,
        presets: {
          duration: 400,
          easing: 'linear'
        }
      })
      // 往dom上加上这个animation,并执行动画
      animations.runAnimation(this.$refs.cdWrapper, 'move', done)
    },
    // 动画结束之后把样式置为空
    afterEnter() {
      animations.unregisterAnimation('move')
      this.$refs.cdWrapper.style.animation = ''
    },
    leave(el, done) {
      this.$refs.cdWrapper.style.transition = 'all 0.4s'
      const { x, y, scale } = this._getPosAndScale()
      this.$refs.cdWrapper.style[
        transform
      ] = `translate3d(${x}px,${y}px,0) scale(${scale})`
      // 这样写的目的是如果没有监听到动画结束的事件,那么我们自己就写一个定时器,400ms后执行done函数
      const timer = setTimeout(done, 400)
      // 监听动画结束
      this.$refs.cdWrapper.addEventListener('transitionend', () => {
        clearTimeout(timer)
        done()
      })
    }

    这段代码的效果就是大圆的动画效果。当点击小圆的时候,大圆开始进入,进入的过程就是动画的过程。当点击向下的箭头,大圆将消失,消失的过程就是大圆退出的动画过程。

    虽然有点复杂,但是也不难看懂,以后我们对于复杂动画可以模仿上面的代码。

    那小圆的动画呢?它非常简单,就是一个显示隐藏的动画:

    .mini-enter-active,
    .mini-leave-active
      transition: all 0.4s
    .mini-enter,
    .mini-leave-to
      opacity: 0

    至此,就完成小圆和大圆的联动动画,整体效果还是很惊艳的。

    实现第三个动画

    // 模板部分
    <div class="cd" ref="imageWrapper">
      <img
        ref="image"
        :class="cdCls"
        class="image"
        :src="currentSong.image"
      />
    </div>
    
    // 逻辑部分
    // 通过事件来控制playing的值,然后改变img标签的class,从而是动画停止和展示
    cdCls() {
       return this.playing ? 'play' : 'play pause'
    }
    
    // css部分
    .play
      animation: rotate 20s linear infinite
    .pause
      animation-play-state: paused
      
    @keyframes rotate
      0%
        transform: rotate(0)
      100%
        transform: rotate(360deg)

    复杂动画-案例2

    7.gif

    首先对动画进行拆解:

    实现第一个动画

    通过上面的学习,对这一个动画的实现应该不难。代码如下:

    <div class="cartcontrol">
      // 小圆 - 
      <transition name="move">
        <div class="cart-decrease" v-show="food.count>0" @click.stop="decrease">
          <span class="inner"> - </span>
        </div>
      </transition>
      <div class="cart-count" v-show="food.count>0">{{food.count}}</div>
      // 小圆 + 
      <div class="cart-add" @click.stop="add"> + </div>
    </div>
    
    .move-enter-active, &.move-leave-active
      transition: all 0.4s linear
    .move-enter, &.move-leave-active
      // 外层动画是从右往左运动
      opacity: 0
      transform: translate3d(24px, 0, 0)
      .inner
        // 内层动画是旋转180°
        transform: rotate(180deg)

    实现第二个动画

    <div class="ball-container">
      <div v-for="(ball,index) in balls" :key="index">
        <transition
          @before-enter="beforeDrop"
          @enter="dropping"
          @after-enter="afterDrop">
          <div class="ball" v-show="ball.show">
            <div class="inner inner-hook"></div>
          </div>
        </transition>
      </div>
    </div>
    
    <script>
    function createBalls() {
      let balls = []
      for (let i = 0; i < BALL_LEN; i++) {
        balls.push({ show: false })
      }
      return balls
    }
    
    data() {
      return {
        balls: createBalls()
      }
    },  
    </script>

    这里创建是10个小球,小球开始的状态都是隐藏的。

    // 点击加号调用这个函数,同时把加号的dom传递,这样就能知道小球运动的起点位置
    onAdd(target) {
      // shopCart就是图中底部组件,执行drop函数
      this.$refs.shopCart.drop(target)
    },
    
    // 把加号对应的dom传入,并绑定到小球el属性上
    drop(el) {
      for (let i = 0; i < this.balls.length; i++) {
        const ball = this.balls[i]
        if (!ball.show) {
          ball.show = true
          ball.el = el
          // dropBalls表示正在下落的小球,因为当快速点击时,会触发多个小球下落
          this.dropBalls.push(ball)
          return
        }
      }
    },

    因为小球的ball.show为true,那么就会触发对应的动画钩子函数,首先触发beforeDrop:

    beforeDrop(el) {
      // 取出最后一个小球
      const ball = this.dropBalls[this.dropBalls.length - 1]
      // 获取小球的起点位置,就是在哪个地方点击的加号按钮
      const rect = ball.el.getBoundingClientRect()
      const x = rect.left - 32
      const y = -(window.innerHeight - rect.top - 22)
      // 设置小球的位置,把小球设置到点击加号按钮的那个地方
      el.style.display = ''
      // 外层动画,向下
      el.style.transform = el.style.webkitTransform = `translate3d(0,${y}px,0)`
      const inner = el.getElementsByClassName(innerClsHook)[0]
      // 内层动画向左
      inner.style.transform = inner.style.webkitTransform = `translate3d(${x}px,0,0)`
    }

    接着执行enter事件函数dropping:

    dropping(el, done) {
      // 触发浏览器重绘,把beforeDrop事件中设置的小球位置从底部位置移动到点击加号的位置,这样小球就会从上面往下面落下
      this._reflow = document.body.offsetHeight
      // 设置小球落下的终点位置
      el.style.transform = el.style.webkitTransform = `translate3d(0,0,0)`
      const inner = el.getElementsByClassName(innerClsHook)[0]
      inner.style.transform = inner.style.webkitTransform = `translate3d(0,0,0)`
      // 监听动画结束
      el.addEventListener('transitionend', done)
    }

    最后执行after-enter事件函数afterDrop:

    afterDrop(el) {
      // 取出第一个小球,设置属性show为false,同时要设置el.style.display = 'none'
      const ball = this.dropBalls.shift()
      if (ball) {
        ball.show = false
        el.style.display = 'none'
      }
    }

    我们来梳理下流程:

    .ball
      position: fixed
      left: 32px
      bottom: 22px
      z-index: 200
      transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
      .inner
        width: 16px
        height: 16px
        border-radius: 50%
        background: $color-blue
        transition: all 0.4s linear

    所以函数的执行顺序是:drop -> before-enter -> enter -> after-enter -> drop -> before-enter -> enter -> after-enter...,这样就形成了在页面中同时出现多个小球的动画。

    注意:当我们设置show的属性为false就可以了,但是代码中同时也设置了el.style.display = 'none',如果不设置这个小球消失有一个延迟。

    好了,整个 Vue 动画到此就结束了,动画其实是一个比较难的功能,特别是复杂动画。通过上面两个复杂动画可以给我们做一个借鉴,相信你也能写出自己想要的动画效果。

    (学习视频分享:web前端开发编程基础视频

    以上就是实例详解,带你玩转 Vue 动画的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    自己动手写 PHP MVC 框架:点击学习

    快速了解MVC架构、了解框架底层运行原理

    专题推荐:vue3 Vue vue.js
    上一篇:聊聊怎么用Vue3构建Web Components 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• 快速了解Vue3的setup执行时机(附代码示例)• 聊聊vite+vue3.0+ts中如何封装axios?• 详解vue3中reactive和ref的区别(源码解析)• 一文详解Vue3项目中怎么引入 SVG 图标• 聊聊怎么用Vue3构建Web Components
    1/1

    PHP中文网