首页 > 编程笔记

Vue transition-group组件实现列表过渡动画

通过 transition 元素,可以实现单个节点和多个节点在同一时间渲染过渡,同样,也可以使用 transition-group 组件,实现一个列表中多项的过渡。

相对 transition,transition-group 有以下几个特点:
使用 CSS 类名,实现列表的进入/离开过渡,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 引入 Vue.js -->
  <script src="../static/js/Vue.js" type="text/JavaScript"></script>
  <title>Demo vue</title>
</head>
<body>
  <!-- 定义 Vue.js 的视图 -->
  <div id="app">
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <button @click="shuffle">Shuffle</button>
    <transition-group name="list" tag="div">
      <span v-for="item in items" v-bind:key="item" class="list-item">
        {{ item }}
      </span>
    </transition-group>
  </div>

  <script type="text/JavaScript">
    // 创建 Vue.js 对象
    const vm = new Vue({
      el: "#app",
      data: {
        items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
        nextNum: 10
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.items.length);
        },
        add: function () {
          this.items.splice(this.randomIndex(), 0, this.nextNum++);
        },
        remove: function () {
          this.items.splice(this.randomIndex(), 1);
        },
        shuffle: function () {
          this.items = _.shuffle(this.items);
        }
      }
    });
  </script>

  <style>
    .list-item {
      display: inline-block;
      margin-right: 10px;
    }
    .list-enter-active, .list-leave-active {
      transition: all 1s;
    }
    .list-enter, .list-leave-to {
      /* .list-leave-active for below version 2.1.8 */
      opacity: 0;
      transform: translateY(30px);
    }
  </style>
</body>
</html>
通过代码可以了解,列表的过渡实现同单元素的过渡实现基本类似,除了将过渡元素改成了 transition-group 外。

单击上面样例的 Shuffle 案例,会将数字随机打乱,但是很快就替换完成,没有动画的感觉。

在 transition-group 中有个 v-move 属性,能改变定位。开发人员通过给 v-move 属性设置过渡的切换时机和过渡曲线,就可以实现动态的定位过渡。v-move 名称的命名规则同 v-enter、v-enter-active 等属性的规则一样(默认为 v- 前缀,否则就以 transition-group 的 name 属性的值作为前缀)。

在上面样例的 style 中,添加如下代码的 CSS 类,单击 Shuffle 按钮,就可以体会到定位的动态过渡效果,代码如下:
.list-move {
  transition: transform 3s;
}

Vue.js 使用了一个叫作 FLIP 的简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡到新的位置。使用 FLIP 技术,可完成位置完美的平滑过渡,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 引入 Vue.js -->
  <script src="../static/js/Vue.js" type="text/JavaScript"></script>
  <script src="../static/js/lodash.min.js" type="text/JavaScript"></script>
  <title>Demo vue</title>
</head>
<body>
  <!-- 定义 Vue.js 的视图 -->
  <div id="app">
    <button v-on:click="shuffle">Shuffle</button>
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list-complete" tag="p">
      <span v-for="item in items" v-bind:key="item" class="list-complete-item">
        {{ item }}
      </span>
    </transition-group>
  </div>

  <script type="text/JavaScript">
    // 创建 Vue.js 对象
    const vm = new Vue({
      el: "#app",
      data: {
        items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
        nextNum: 10
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.items.length);
        },
        add: function () {
          this.items.splice(this.randomIndex(), 0, this.nextNum++);
        },
        remove: function () {
          this.items.splice(this.randomIndex(), 1);
        },
        shuffle: function () {
          this.items = _.shuffle(this.items);
        }
      }
    });
  </script>

  <style>
    .list-complete-item {
      transition: all 0.5s;
      display: inline-block;
      margin-right: 10px;
    }
    .list-complete-enter, .list-complete-leave-to {
      /* .list-complete-leave-active for below version 2.1.8 */
      opacity: 0;
      transform: translateY(30px);
    }
    .list-complete-leave-active {
      position: absolute;
    }
  </style>
</body>
</html>
需要注意的是使用 FLIP 过渡的元素不能被设置为 display:inline。作为替代方案,可以设置为 display:inline-block 或者放置于 flex 中。

FLIP 不仅可以实现单列过渡,还可以实现多维网格过渡,代码如下:
<!DOCTYPE html>
<html>
<head>
  <title>List Move Transitions Sudoku Example</title>
  <script src="../static/js/Vue.js" type="text/JavaScript"></script>
  <script src="./static/js/lodash.min.js" type="text/JavaScript"></script>
  <style>
    .container {
      display: flex;
      flex-wrap: wrap;
      width: 238px;
      margin-top: 10px;
      /* ... 其他样式 ... */
    }
    .cell {
      display: flex;
      justify-content: space-around;
      align-items: center;
      width: 25px;
      height: 25px;
      border: 1px solid #aaa;
      margin-right: -1px;
      margin-bottom: -1px;
      /* ... 其他样式 ... */
    }
    .cell:nth-child(3n) {
      margin-right: 0;
    }
    .cell:nth-child(27n) {
      margin-bottom: 0;
    }
    .cell-move {
      transition: transform 0.5s;
    }
  </style>
</head>
<body>
  <div id="sudoku-demo" class="demo">
    <h1>Lazy Sudoku</h1>
    <p>Keep hitting the shuffle button until you win.</p>
    <button @click="shuffle">Shuffle</button>
    <transition-group name="cell" tag="div" class="container">
      <div v-for="cell in cells" :key="cell.id" class="cell">
        {{ cell.number }}
      </div>
    </transition-group>
  </div>

  <script>
    new Vue({
      el: "#sudoku-demo",
      data: {
        cells: Array.apply(null, { length: 81 }).map(function(_, index) {
          return {
            id: index,
            number: (index % 9) + 1
          };
        })
      },
      methods: {
        shuffle: function() {
          this.cells = _.shuffle(this.cells);
        }
      }
    });
  </script>
</body>
</html>

通过 data attribute 与 JavaScript 通信,可以实现列表的交错过渡,代码如下:
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="staggered-list-demo">
  <input type="text" v-model="query">
  <transition-group
    name="staggered-fade"
    tag="ul"
    v-bind:class="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave">
    <li v-for="(item, index) in computedList" v-bind:key="item.msg" v-bind:data-index="index">
      {{ item.msg }}
    </li>
  </transition-group>
</div>

new Vue({
  el: '#staggered-list-demo',
  data: {
    query: '',
    list: [
      { msg: 'Bruce Lee' },
      { msg: 'Jackie Chan' },
      { msg: 'Chuck Norris' },
      { msg: 'Jet Li' },
      { msg: 'Kung Fury' }
    ]
  },
  computed: {
    computedList: function() {
      var vm = this;
      return this.list.filter(function(item) {
        return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1;
      });
    }
  },
  methods: {
    beforeEnter: function(el) {
      el.style.opacity = 0;
      el.style.height = 0;
    },
    enter: function(el, done) {
      var delay = el.dataset.index * 150;
      setTimeout(function() {
        Velocity(el, { opacity: 1, height: "1.6em" }, { duration: 300, complete: done });
      }, delay);
    },
    leave: function(el, done) {
      var delay = el.dataset.index * 150;
      setTimeout(function() {
        Velocity(el, { opacity: 0, height: 0 }, { complete: done });
      }, delay);
    }
  }
});

推荐阅读