vue3学习随便记13

过渡与动画

进入过渡和离开过渡

在插入、更新或从DOM移除项时,Vue提供了多种方法实现转换效果:

单个元素或组件的过渡

Vue提供了 transition 封装组件(内置组件),在下列情形下,可以给任何元素或者组件添加进入或离开过渡效果:

  • 使用 v-if 条件渲染
  • 使用 v-show 条件展示
  • 动态组件
  • 组件根节点

下面是一个条件渲染淡入淡出的例子

    <style>
        .fade-enter-active,
        .fade-leave-active {
            transition: opacity 0.5s ease;
        }

        .fade-enter-from,
        .fade-leave-to {
            opacity: 0;
        }
    </style>

    <div id="app">
        <button @click="show = !show">切换</button>
        <transition name="fade">
            <p v-if="show">你好</p>
        </transition>
    </div>
    <script>
        const { createApp } = Vue
        const app = createApp({
            data() {
                return {
                    show: true
                }
            }
        })
        const vm = app.mount('#app')
    </script>

 我们<p v-if="...">...</p> 条件渲染元素封装在 transition 内置组件中,当插入或删除该元素时,Vue将做以下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加或删除 CSS 类名
  2. 如果过渡组件提供了 JS 钩子函数,这些钩子将在恰当的时机被调用
  3. 如果既没有 JS钩子 也没有 CSS 过渡或动画,DOM插入或删除在下一帧立即执行

我们的例子中是上述情形1,内置组件 transition 本身不会渲染新的 DOM,它是把合适的 CSS类应用到了被包裹的元素(或组件这样的自定义元素)上。transition组件name属性为fade,从而自动拓展为 .fade-enter-from.fade-enter-active.fade-enter-to.fade-leave-from.fade-leave-active.fade-leave-to 等6个 CSS过渡类(如果name属性值没有指定,则其值为v)。实现进入过渡是设置好前三个CSS类,而离开过渡是后三个CSS类。

 (上图引用自 Vue官网)

CSS动画也是使用 transition 组件,只是设置CSS类时,不是设置 transition,而是设置 animation,并且 v-enter-from 类在节点插入 DOM 后不会立刻移除,直到 animationend 事件触发时才移除。

前面说的6个CSS过渡类的名字是可以自定义的,方法是在 transition 组件的属性 enter-from-classenter-active-classenter-to-classleave-from-classleave-active-classleave-to-class 中进行覆盖。这么做的好处是可以把 Vue 的过渡机制和一些现成的 CSS动画库结合起来,自己就不用费神编写过渡类的CSS了,例如,在前面的代码中我们可以引入 Animate.css

<link
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
  rel="stylesheet"
  type="text/css"
/>

然后,去掉自己编写的 <style> 内的4个类,将 transition 组件改成

        <transition name="custom-classes-transition" 
            enter-active-class="animate__animated animate__tada"
            leave-active-class="animate__animated animate__bounceOutRight">
            <p v-if="show">你好</p>
        </transition>

这样就能完成过渡或动画效果了。

transition组件的 type属性可以区分使用的是动画还是过渡,duration属性可以自定义过渡持续的时间。transition组件还提供了 before-enter、enter、after-enter……等JS钩子,可以把transition组件的 css属性设置为 false(:css="false"),不使用CSS过渡类,然后在钩子中实现 JS动画(不过 enterleave 钩子中必须使用 done 回调),调用 GreenSock 那样的动画库会省事。

初始渲染的过渡

给 transition 组件设置 appear属性(默认值false)就能获得初始渲染过渡效果

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

多个元素之间的过渡

transition组件包裹的,可以是 v-if...v-else-if....v-else 那样的多个中选一个的元素,也可以是动态可变的一个元素(使用:key属性)。在多个元素的过渡切换中,我们有时希望新元素先进行进入过渡,完成之后老元素过渡离开(称为in-out模式),更多时候希望老元素先进行离开过渡,完成之后新元素过渡进入(称为out-in模式),我们可以用 transition组件的 mode属性指定要使用哪一种模式。

多个组件之间的过渡

组件之间的过渡更简单,只要让 transition组件包裹一个动态组件(使用<component>:is属性)

我们把以前的动态组件例子(3个tab页)改一改

        <transition name="fade" mode="out-in">
            <keep-alive>
                <component :is="currentTabComponent" class="tab"></component>
            </keep-alive>
        </transition>

然后添加相关的过渡类

        .fade-enter-active,
        .fade-leave-active {
            transition: opacity 0.3s ease;
        }

        .fade-enter-from,
        .fade-leave-to {
            opacity: 0;
        }

这样就能实现多个组件的过渡了。(注意,keep-alive也要包裹在transition组件内,因为缓存的东西要一块儿过渡切换)

列表过渡

前面提到的过渡,或者是单个节点,或者是多个节点,但每次只渲染一个。这里需要考虑同时渲染整个列表的情形,这也是 <transition-group> 组件的用武之地,<transition-group>有下述特点:

  • 默认情况下,它不会渲染包裹的一个元素,但你可以用tag属性指定渲染一个元素
  • 过渡模式不可用,因为这里没有切换特定的一个的概念了
  • 内部元素总是需要提供唯一的 key 属性值进行标识
  • CSS过渡类应用在内部的元素中,而不是这个组(容器)本身

列表的进入/离开过渡

<body>
    <div id="app">
        <button @click="add">添加</button>
        <button @click="remove">移除</button>
        <transition-group name="list" tag="p">
            <span v-for="item in items" :key="item" class="list-item">
                {{ item }}
            </span>
        </transition-group>
    </div>
    <script>
        const { createApp } = Vue
        const app = createApp({
            data() {
                return {
                    items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
                    nextNum: 10
                }
            },
            methods: {
                randomIndex() {
                    return Math.floor(Math.random() * this.items.length)
                },
                add() {
                    this.items.splice(this.randomIndex(), 0, this.nextNum++)
                },
                remove() {
                    this.items.splice(this.randomIndex(), 1)
                }
            }
        })
        const vm = app.mount('#app')
    </script>

列表 name 属性 list,tag属性值p表示指定渲染一个<p>作为容器,里面的每个 span 都包含了 :key 属性唯一识别,列表的进入和离开过渡通过CSS指定:

        .list-item {
            display: inline-block;
            margin-right: 10px;
        }

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

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

这个例子中,新增的元素进入,或者被删的元素离开,都非常平滑,但插入或者移除时,周围元素的移动显得突兀,这涉及列表的移动过渡

列表的移动过渡

<transition-group> 组件除了进入和离开的一些CSS类(.XXX-enter-from.XXX-enter-active.XXX-enter-to.XXX-leave-from.XXX-leave-active.XXX-leave-to),还有 .XXX-move 类,它会应用在元素改变定位的过程中。(前缀XXX可以通过name属性定义,也可以通过 move-class 属性手动设置)

修改CSS,使得插入和移除时,左右移动都是平滑的

        .list-item {
            display: inline-block;
            margin-right: 10px;
            transition: all 0.8s ease;  /* 移除时平滑移动 */
        }

        /* .list-enter-active,
        .list-leave-active {
            transition: all 1s ease;
        } */
        .list-leave-active {
            position: absolute;   /* 移除时平滑移动 */
        }

        .list-enter-from,
        .list-leave-to {
            opacity: 0;
            transform: translateY(30px);
        }
        .list-move {
            transition: transform 0.8s ease; /* 插入时平滑移动  */
        }

官网还给出列表中块元素 shuffle 时的平滑移动例子  (列表过渡 | Vue.js

列表的交错过渡

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>


    <div id="app">
        <input v-model="query" />
        <transition-group
          name="staggered-fade"
          tag="ul"
          :css="false"
          @before-enter="beforeEnter"
          @enter="enter"
          @leave="leave"
        >
          <li
            v-for="(item, index) in computedList"
            :key="item.msg"
            :data-index="index"
          >
            {{ item.msg }}
          </li>
        </transition-group>
    </div>
    <script>
        const { createApp } = Vue
        const app = createApp({
            data() {
                return {
                    query: '',
                    list: [
                        { msg: '张三' },
                        { msg: '李四' },
                        { msg: '王五' },
                        { msg: '李六' },
                        { msg: '张七' }
                    ]
                }
            },
            computed: {
                computedList() {
                    var vm = this
                    return this.list.filter(item => {
                        return item.msg.indexOf(vm.query) !== -1
                    })
                }
            },
            methods: {
                beforeEnter(el) {
                    el.style.opacity = 0
                    el.style.height = 0
                },
                enter(el, done) {
                    gsap.to(el, {
                        opacity: 1,
                        height: '1.6em',
                        delay: el.dataset.index * 0.15,
                        onComplete: done
                    })
                },
                leave(el, done) {
                    gsap.to(el, {
                        opacity: 0, 
                        height: 0,
                        delay: el.dataset.index * 0.15,
                        onComplete: done
                    })
                }
            }
        })
        const vm = app.mount('#app')
    </script>

计算属性 computedList 是依赖 query的,所以,(双向绑定的)query 输入值一改,computedList 就重新计算,列表就刷新了。交错过渡效果中,使用了强悍的  gsap JS动画库(https://greensock.com/)。

可复用的过渡

Vue的组件系统可以实现过渡效果的复用,也就是把过渡效果做成组件。要创建一个可以复用的过渡组件,只要将 <transition> 或 <transition-group> 作为根组件,然后将任何子组件放置其中就可以了。我们把上述例子改一改,把过渡效果做成 <my-transition-group> 组件:

    <div id="app">
        <input v-model="query" />
        <my-transition-group>
            <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index">
                {{ item.msg }}
            </li>
        </my-transition-group>
    </div>
    <script>
        const { createApp } = Vue
        const app = createApp({
            data() {
                return {
                    query: '',
                    list: [
                        { msg: '张三' },
                        { msg: '李四' },
                        { msg: '王五' },
                        { msg: '李六' },
                        { msg: '张七' }
                    ]
                }
            },
            computed: {
                computedList() {
                    var vm = this
                    return this.list.filter(item => {
                        return item.msg.indexOf(vm.query) !== -1
                    })
                }
            },
        })
        app.component('my-transition-group', {
            template: '\
            <transition-group\
                name="staggered-fade"\
                tag="ul"\
                :css="false"\
                @before-enter="beforeEnter"\
                @enter="enter"\
                @leave="leave"\
            >\
                <slot></slot>\
            </transition-group>\
            ',
            methods: {
                beforeEnter(el) {
                    el.style.opacity = 0
                    el.style.height = 0
                },
                enter(el, done) {
                    gsap.to(el, {
                        opacity: 1,
                        height: '1.6em',
                        delay: el.dataset.index * 0.15,
                        onComplete: done
                    })
                },
                leave(el, done) {
                    gsap.to(el, {
                        opacity: 0, 
                        height: 0,
                        delay: el.dataset.index * 0.15,
                        onComplete: done
                    })
                }
            }
        })
        const vm = app.mount('#app')
    </script>

上面的代码中,组件 <my-transition-group> 模板中把 <transition-group> 作为根,用插槽 <slot> 来容纳子组件。对于可复用的过渡组件,使用函数式组件更合适:

这部分还没搞定(test42.html)

动态过渡

 

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>


    <div id="app">
        淡入:<input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
        淡出:<input type="range" v-model="fadeOutDuration" min="0" :max="maxFadeDuration" />
        <transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
            <p v-if="show">你好</p>
        </transition>
        <button v-if="stop" @click="stop = false; show = false">
            动画开始
        </button>
        <button v-else @click="stop = true">停止动画!</button>
    </div>
    <script>
        const { createApp } = Vue
        const app = createApp({
            data() {
                return {
                    show: true,
                    fadeInDuration: 1000,
                    fadeOutDuration: 1000,
                    maxFadeDuration: 1500,
                    stop: true
                }
            },
            mounted() {
                this.show = false
            },
            methods: {
                beforeEnter(el) {
                    el.style.opacity = 0
                },
                enter(el, done) {
                    var vm = this
                    Velocity(
                        el,
                        { opacity: 1 },
                        {
                            duration: this.fadeInDuration,
                            complete: function () {
                                done()
                                if (!vm.stop) vm.show = false
                            }
                        }
                    )
                },
                leave(el, done) {
                    var vm = this
                    Velocity(
                        el,
                        { opacity: 0 },
                        {
                            duration: this.fadeOutDuration,
                            complete: function () {
                                done()
                                vm.show = true
                            }
                        }
                    )
                }
            }
        })
        const vm = app.mount('#app')
    </script>

动态过渡的一般用法是某个 attribute (最基础的是 name属性) 绑定动态的值,而上面的代码是利用事件钩子和 velocity JS动画库来实现的淡入淡出。

状态过渡

状态动画与侦听器

通过侦听器(watcher)能监听到任何数值 property 的更新,即监听vm的数据变量的变化。下面的例子,用侦听器监听 vm.number 的变化,把 tweenedNumber 动画方式修改为新值,而显示的 animatedNumber 是依赖 tweenedNumber 的计算属性,从而数值的状态变化就产生动画效果。

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

    <div id="app">
        <input v-model.number="number" type="number" step="20" />
        <p>{{ animatedNumber }}</p>
    </div>
    <script>
        const App = {
            data() {
                return {
                    number: 0,
                    tweenedNumber: 0
                }
            },
            computed: {
                animatedNumber() {
                    return this.tweenedNumber.toFixed(0)
                }
            },
            watch: {
                number(newValue) {
                    gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue })
                }
            }
        }
        Vue.createApp(App).mount('#app')
    </script>

动态状态过渡

下面的例子,用SVG 绘制多边形,侦听器监听多边形有关变量,更新时用了状态过渡,实现了动态的过渡效果。

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>
    <div id="app">
        <svg width="200" height="200">
            <polygon :points="points"></polygon>
            <circle cx="100" cy="100" r="90"></circle>
        </svg>
        <label>边数: {{ sides }}</label>
        <input type="range" min="3" max="500" v-model.number="sides" />
        <label>最小半径: {{ minRadius }}%</label>
        <input type="range" min="0" max="90" v-model.number="minRadius" />
        <label>更新间隔: {{ updateInterval }} 毫秒</label>
        <input type="range" min="10" max="2000" v-model.number="updateInterval" />
    </div>
    <script>
        const defaultSides = 10;
        const stats = Array.apply(null, { length: defaultSides }).map(() => 100);

        const App = {
            data() {
                return {
                    stats: stats,
                    points: generatePoints(stats),
                    sides: defaultSides,
                    minRadius: 50,
                    interval: null,
                    updateInterval: 500
                };
            },
            watch: {
                sides(newSides, oldSides) {
                    var sidesDifference = newSides - oldSides;
                    if (sidesDifference > 0) {
                        for (var i = 1; i <= sidesDifference; i++) {
                            this.stats.push(this.newRandomValue());
                        }
                    } else {
                        var absoluteSidesDifference = Math.abs(sidesDifference);
                        for (var i = 1; i <= absoluteSidesDifference; i++) {
                            this.stats.shift();
                        }
                    }
                },
                stats(newStats) {
                    gsap.to(this.$data, this.updateInterval / 1000, {
                        points: generatePoints(newStats)
                    });
                },
                updateInterval() {
                    this.resetInterval();
                }
            },
            mounted() {
                this.resetInterval();
            },
            methods: {
                randomizeStats() {
                    var vm = this;
                    this.stats = this.stats.map(() => vm.newRandomValue());
                },
                newRandomValue() {
                    return Math.ceil(this.minRadius + Math.random() * (100 - this.minRadius));
                },
                resetInterval() {
                    var vm = this;
                    clearInterval(this.interval);
                    this.randomizeStats();
                    this.interval = setInterval(() => {
                        vm.randomizeStats();
                    }, this.updateInterval);
                }
            }
        }
        Vue.createApp(App).mount('#app')

        function valueToPoint(value, index, total) {
            var x = 0;
            var y = -value * 0.9;
            var angle = ((Math.PI * 2) / total) * index;
            var cos = Math.cos(angle);
            var sin = Math.sin(angle);
            var tx = x * cos - y * sin + 100;
            var ty = x * sin + y * cos + 100;
            return { x: tx, y: ty };
        }

        function generatePoints(stats) {
            var total = stats.length;
            return stats.map(function (stat, index) {
                    var point = valueToPoint(stat, index, total);
                    return point.x + "," + point.y;
                }).join(" ");
        }
    </script>

把过渡放到组件里

 前面的整数动画效果是针对一个数字的,如果页面有多个数字,都去管理状态,会非常复杂,幸好很多的动画效果都可以提取到专门的子组件中,用组件复用来实现动画效果复用。

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

    <div id="app">
        <input v-model.number="firstNumber" type="number" step="20" /> +
        <input v-model.number="secondNumber" type="number" step="20" /> = {{ result }}
        <p>
            <animated-integer :value="firstNumber"></animated-integer> +
            <animated-integer :value="secondNumber"></animated-integer> =
            <animated-integer :value="result"></animated-integer>
        </p>
    </div>
    <script>
        const App = {
            data() {
                return {
                    firstNumber: 20,
                    secondNumber: 40
                }
            },
            computed: {
                result() {
                    return this.firstNumber + this.secondNumber
                }
            }
        }
        const app = Vue.createApp(App)
        app.component('animated-integer', {
            template: '<span>{{ fullValue }}</span>',
            props: {
                value: {
                    type: Number,
                    required: true
                }
            },
            data() {
                return {
                    tweeningValue: 0
                }
            },
            computed: {
                fullValue() {
                    return Math.floor(this.tweeningValue)
                }
            },
            methods: {
                tween(newValue, oldValue) {
                    gsap.to(this.$data, {
                        duration: 0.5,
                        tweeningValue: newValue,
                        ease: 'sine'
                    })
                }
            },
            watch: {
                value(newValue, oldValue) {
                    this.tween(newValue, oldValue)
                }
            },
            mounted() {
                this.tween(this.value, 0)
            }
        })
        app.mount('#app')
    </script>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值