过渡(1):元素/组件过渡和动画

自定义过渡的类名

另一种情况和Vue过渡执行过程见初始渲染

new Vue({
    el: '#app-2',
    data: {
        taxiCalled: false
    }
})
<style>
.slideInRight {
    transform: translateX(300px);
}
.go {
    transition: all 2s ease-out;
}
</style>
<div id="app-2">
    <button @click="taxiCalled = true">
        Call a cab
    </button>
    <transition enter-class="slideInRight" enter-active-class="go">
        <p v-if="taxiCalled">?</p>
    </transition>
</div>

clipboard.png

这个过程到底发生了什么?

当点击按钮时,texiCalled被设置成true,并且taxi插入页面。实际上在完成这些动作之前,Vue读取了指定的类enter-class(这里为slideInRight)并把它应用于taxi的外包元素p,然后指定类enter-active-class的情况是一样的。

enter-class在第一帧之后就被移除了,类enter-active-class也在动画结束时被移除。

这种创建动画的方式成为FLIP

  • First(F):动画第一帧的状态;这里既是taxi在屏幕的右边开始。
  • Last(L): 动画最后帧的状态;这里taxi在屏幕的最左边作为结束。
  • Invert(I): 使用transformopacity在第一帧和最后帧两个状态之间扭转,这里使用translateX(300px)使taxi在两个状态间产生300px的位移。
  • Play(p): 为我们设置的属性创建过渡,这里在2s内使taxi从右到左产生了300px的位移。

在钩子中使用Velocity

new Vue({
    el: '#app-3',
    data: {
        taxiCalled: false
    },
    methods: {
        enter(el, done) {
            Velocity(el,
                { opacity: [1, 0], translateX: ["0px", "200px"] },
                { duration: 2000, easing: "ease-out", complete: done })
        },
        leave(el, done) {
            Velocity(el,
                { opacity: [0, 1], 'font-size': ['0.1em', '1em'] },
                { duration: 200, complete: done })
        }
    }
})
<div id="app-3">
    <button @click="taxiCalled = true">
        Call a cab
    </button>
    <button @click="taxiCalled = false">
        Cancel
    </button>
    <transition @enter="enter" @leave="leave" :css="false">
        <p v-if="taxiCalled">?</p>
    </transition>
</div>

clipboard.png

抓取enterleaveappear三种情况的时间点,每种情况上都定义了四种事件,总计12种事件(见API)。在这12种事件绑定钩子函数,这些函数可以配合CSS模式使用,也可以单独使用。这里我们在钩子函数中使用velocity脚本动画引擎,单独完成动画配置。

在这些钩子中不一定非要使用 velocity,可以使用任何库。

上面代码中:css="false"是告诉Vue,我们关闭CSS的处理,这里可以节省点CPU时间,将跳过所有与CSS动画相关代码,单纯的使用Javascript动画。

我们这里分别在@enter@leave上绑定了钩子函数,他们将在taxi被插入和离开时执行。函数的第一个参数为外包标签(这里是<p>),第二个参数done必须写,尽管你可能不去用它。这是因为用Javascript代替CSS,Vue无法识别动画什么时候完成,这里Vue会认为这个离开的动画在@leave事件之前就完成了,所以执行leave没有动画渲染。

关于velocity,这种类型的API,我们称为forcefeeding,我们只要往它的实例中填写数据,不管他内部如何运行。(具体查看velocity

初始渲染

<transition>使用appear特性,在组件第一次被插入时执行相关的转换。
使用它会给人带来一种页面很快的加载大量元素的感觉(错觉)。

这里还使用了<transition>name特性,可以使用自定义的类名('自定义过渡的类名'的相似写法),
name-enter-activename-enter等。
而使用这种自定义类名是一种好习惯。

<div id="app-4">
    <transition appear name="flip">
        <!-- 指定宽高因为是引用的图片,不知道尺寸 -->
        <img src="https://b-ssl.duitang.com/uploads/item/201602/20/20160220213530_Z4reH.thumb.700_0.jpeg" style="width:300px;height:300px">
    </transition>
    <p>在组件第一次插入文档时执行相关的过渡</p>
</div>
<script>
    new Vue({
        el: '#app-4'
    })
</script>
img {
    float: left;
    padding: 5px
}
.flip-enter-active {
    transition: all 5s cubic-bezier(0.55, 0.085, 0.68, 0.53);
}
.flip-enter {
    transform: scaleY(0) translateZ(0);
    opacity: 0;
}

clipboard.png

以上整个过程如下:
Vue发现appear特性,开始查找<transition>标签中的JavaScript钩子或指定的CSS类名。之后如果有一个name被指定,就根据这个name查到过渡的入口:mySpecifiedName-entermySpecifiedName-enter-active等。
如果以上过程失败,就会找默认的v-enterv-enter-active等。

两个元素之间的过渡

new Vue({
    el:'#app-5',
    data:{
        kisses: 0
    }
})
#app-5 button span {
    color: rgb(255, 0, 140);
}
#app-5 p {
    margin: 0;
    position: absolute;
    font-size: 3em;
}
.fade-enter-active {
    transition: opacity 5s
}
.fade-leave-active {
    transition: opacity 5s;
    opacity: 0
}
.fade-enter {
    opacity: 0
}
<div id="app-5">
    <button @click="kisses++">
        <span>?</span>Kiss!</button>
    <transition name="fade">
        <p v-if="kisses < 3">? frog</p>
        <p v-if="kisses >= 3">? princess</p>
    </transition>
</div>

clipboard.png

结果发现切换后没任何过渡效果,原因是什么呢?
原来是,Vue会启动自身的优化系统,发现两个元素一模一样,就是内容不同,因此当切换时,只做了内容的替换,标签<p></p>部分并没有被替换,因此没有过渡的效果。
我们可以为需要过渡的元素增加key属性,让Vue识别青蛙与公主两个不同的元素。如下:

<transition name="fade">
    <p v-if="kisses < 1" key="frog">? frog</p>
    <p v-if="kisses >= 1" key="princess">? princess</p>
</transition>

clipboard.png

过渡效果正常了。

最佳实践,在元素上使用 key,尤其是当元素间具有不同的语义时。

多个元素之间的过渡和过渡模式

在有两个以上元素时,你可能这么做:

<transition name="fade">
    <p v-if="kisses < 2" key="frog">? frog</p>
    <p v-else-if="kisses >= 2 && kisses <=5" key="princess">? princess</p>
    <p v-else key="santa">? santa</p>
</transition>

更好的方法是我们可以根据已有的数据动态的处理多元素过渡。

new Vue({
    el: '#app-6',
    data: {
        kisses: 0,
        kindOfTransformation: 'fade',
        transformationMode: 'in-out'
    },
    computed: {
        transformation() {
            if (this.kisses < 3) {
                return 'frog'
            }
            if (this.kisses >= 3 && this.kisses <= 5) {
                return 'princess'
            }
            if (this.kisses > 5) {
                this.kindOfTransformation = 'zoom'
                this.transformationMode = 'out-in'
                return 'santa'
            }
        },
        emoji() {
            switch (this.transformation) {
                case 'frog':
                    return '?'
                case 'princess':
                    return '?'
                case 'santa':
                    return '?'
            }
        }
    }
})
<div id="app-6">
    <button @click="kisses++"><span>?</span>Kiss!</button>
    <transition :name="kindOfTransformation" :mode="transformationMode">
        <p :key="transformation">{{emoji}} {{transformation}}</p>
    </transition>
</div>

以上过渡的namemode以及元素的key和内容,都将根据实例数据与计算属性进行动态的绑定。
这样做更加的灵活,并且可以为不同的元素应用不同的过渡(namemode的不同)。

这里的mode有三种情况:

  • 不设置 旧元素离开过渡效果和新元素的进入过渡效果同时发生
  • in-out 新元素进入过渡先进行,完毕后旧元素的离开过渡效果再发生 如下面公主先进入,青蛙后消失
  • out-in 旧元素离开过渡先进行,完毕后新元素的进入过渡效果再发生 如下面公主先消失,圣诞老人后进入

clipboard.png

再来看一个过渡模式的例子。

new Vue({
    el: '#app-7',
    data: {
        product: 0,
        products: ['?umbrella', '?computer', '?ball', '?camera']
    }
})
#app-7 {
    margin-left:300px;
}

#app-7 p {
    position: absolute;
    margin: 0;
    font-size: 3em;
}
.slide-enter-active {
    transition: transform .5s
}
.slide-leave-active {
    transition: transform .5s;
    transform: translateX(-300px);
}
.slide-enter {
    transform: translateX(300px)
}
<div id="app-7" style="margin-bottom:100px;">
    <button @click="product++">next</button>
    <transition name="slide" >
        <p :key="products[product % 4]">{{products[product % 4]}}</p>
    </transition>
</div>

clipboard.png

这似乎没什么问题。那么现在修改下CSS,去掉绝对定位。

#app-7 p {
    /* position: absolute; */
    margin: 0;
    font-size: 3em;
}

再来看看结果。

clipboard.png

似乎是有点不对劲,为什么会这样呢?

clipboard.png

看看过渡执行时的DOM,发现前后两个元素的过渡是同时进行,这是Vue的默认情况,即两个<p></p>同时存在,如果不使用绝对定位,那么上一个就会把下一个的位置挤掉。

这下过渡模式mode就派上用处了,我们为过渡添加mode属性为out-in,旧先出新后进。

<transition name="slide" mode="out-in">
    <p :key="products[product % 4]">{{products[product % 4]}}</p>
</transition>

clipboard.png

列表过渡

列表过渡的情况比较复杂。一个问题一个问题看吧。

<transition>中有多个并列的元素时,我们又没有使用v-if|else指令作用其上时,会警告我们使用<transition-group>标签代替它。
一组列表的过渡效果,由<transition-group>包围,有几点比单元素过渡特殊的。

  • <transition-group>上设置tag属性外包多个元素,如<transition-group tag="p">
  • 在内部以v-for把元素渲染成列表形式
  • 每个内部的元素需要使用:key="data"标记以与它的同胞们作区分
  • 过渡的状态不止于进入/离开,多了一个移动,使用name-move来定义类名(后面详细解释)。

看个普通示例。

new Vue({
    el: '#app-1',
    data: {
        items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
        nextNum: 10
    },
    methods: {
        ramdomIndex: function () {
            return Math.floor(Math.random() * this.items.length)
        },
        add: function () {
            this.items.splice(this.ramdomIndex(), 0, this.nextNum++)
        },
        remove: function () {
            this.items.splice(this.ramdomIndex(), 1)
        }
    }
})
.num-list-item {
    margin-right: 10px;
}
.num-list-enter,
.num-list-leave-to {
    opacity: 0;
    transform: translateY(30px);
}
.num-list-enter-active,
.num-list-leave-active {
    transition: all 2s;
}
<div id="app-1">
    <button @click="add">添加</button>
    <button @click="remove">移除</button>
    <transition-group name="num-list" tag="p">
        <span v-for="item in items" :key="item" class="num-list-item">
            {{item}}
        </span>
    </transition-group>
</div>

clipboard.png

很明显只有透明度的过渡,而Y轴(30px)的转换过渡没有成功,这是因为<span>为一个inline元素,没有这种转换过渡的功能,因此我们需要把它切换成inline-block元素。

.num-list-item {
    /*把span切换成inline-block*/
    display:inline-block;
    margin-right: 10px;
}

clipboard.png

仔细观察下还能发现个问题,其他元素因为进入/离开的那个元素,会发生位置的变化,其他元素这种移动变换没有过渡的效果,这不是我们想要的。

现在就轮到使用name-move为其他元素在此时添加移动过渡效果,很简单的做个修改:

.num-list-move,/* 为其他受到影响的元素添加移动过渡效果*/
.num-list-enter-active,
.num-list-leave-active {
    transition: all 2s;
}

clipboard.png

观察文档结构,当进入/离开发生时,会给受影响的元素添加移动过渡类名num-list-move

clipboard.png

进入过渡时没问题了,其他元素平滑的向右位移,但是离开过渡时其他受影响元素的移动还是没有过渡效果,这是因为定位问题,离开的元素要有2秒消失,默认的static定位要在2秒后离开元素才会腾出空位让给后面的元素,而此时此刻,移动过渡的时效2秒已经过去了,因此后面元素才会很突兀的补位。
那么,我们可以在离开状态name-leave-active上使用absolute让元素脱离正常的文档流,那么一发生离开,后面的元素就可以开始正常的移动过渡了。如下修改:

/* 在此离开过渡的状态类名添加absolute定位,以受影响元素正常的使用平滑过渡 */
.num-list-leave-active {
    position: absolute;
}

clipboard.png

总结下,就是当使用行内元素时,使用位置转换的过渡需要把其设置为 inline-block,否则位置转换没有效果。在离开/进入过渡时,受影响的其他元素应该使用移动过渡 name-move为期定义移动过渡。还需要在离开过渡状态类中 name-leave-active设置离开过渡元素的定位为 absolute使其脱离正常的文档流,以不妨碍其他元素的移动过渡。

列表过渡的另一种写法

观察其上CSS代码,可以发现<span>进入/离开/移动过渡定义的过渡都一样,即:

.num-list-move,
.num-list-enter-active,
.num-list-leave-active {
    transition: all 2s;
} 

因此可以去掉这些过渡状态类名,之间写在<span>的样式里就可以了。

.num-list-item {
    display:inline-block;
    margin-right: 10px;
    /* 代替 进入/离开/移动过渡状态类 */
    transition: all 2s;
}
.num-list-enter,
.num-list-leave-to {
    opacity: 0;
    transform: translateY(30px);
}
/* 
.num-list-move,
.num-list-enter-active,
.num-list-leave-active {
    transition: all 2s;
} 
*/
/* 在此离开过渡的状态类名添加absolute定位,以受影响元素正常的使用平滑过渡 */
.num-list-leave-active {
    position: absolute;
}

其他都原封不动,效果和第一种一样。

为移动过渡添加功能

在第一种写法的基础上添加以下功能,再次理解移动过渡。
js中导入lodash库,使用shuffle方法重新排列数组items

methods: {
    ramdomIndex: function () {
        return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
        this.items.splice(this.ramdomIndex(), 0, this.nextNum++)
    },
    remove: function () {
        this.items.splice(this.ramdomIndex(), 1)
    },
    //使用`shuffle`方法重新排列数组`items`。
    shuffle: function () {
        this.items = _.shuffle(this.items)
    }
} 

页面添加一个按钮

<div id="app-1">
    <button @click="add">添加</button>
    <button @click="remove">移除</button>
    <button @click="shuffle">重排列</button>
    <transition-group name="num-list" tag="p">
        <span v-for="item in items" :key="item" class="num-list

-item">
            {{item}}
        </span>
    </transition-group>
</div>

clipboard.png

列表过渡的其他示例

一个汽车站:

new Vue({
    el: '#app-2',
    data: {
        buses: [1, 2, 3, 4, 5],
        nextBus: 6
    },
    mounted() {
        setInterval(() => {
            const headOrTail = () => Math.random() >= 0.5
            if (headOrTail()) {
                this.buses.push(this.nextBus)
                this.nextBus += 1
            } else {
                this.buses.splice(0, 1)
            }
        }, 2000)
    }
})
.station-bus {
    display: inline-block;
    margin-left: 10px;
    font-size: 2em;
}
.station-enter {
    opacity: 0;
    transform: translateX(30px);
}
.station-leave-to {
    opacity: 0;
    transform: translateX(-30px);
}
.station-move,
.station-enter-active,
.station-leave-active {
    transition: all 2s;
}
.station-leave-active {
    position: absolute;
}
<div id="app-2">
    <h3>公交车站</h3>
    <transition-group tag="p" name="station">
        <span v-for="bus in buses" :key="bus" class="station-bus">?</span>
    </transition-group>
    {{buses}}
</div>

clipboard.png

在元素插入时的钩子上定义一个timer,每隔两秒一辆车进入或离开,为它们设置进入/离开过渡,和其他受影响车辆的移动过渡。

使用组件包装可重用的过渡

如果想要在我们的站点的各处重用一种过渡,把它包装进一个组件是个好方法。
要在站点上展示/隐藏一些缩略的文章,我们可以编写一个过渡组件,然后为不同的缩略文章添加这个过渡组件使其拥有过渡效果。

Vue.component('puff', {
    functional: true,
    render: function (createElement, context) {
        var data = {
            props: {
                'enter-active-class': 'magictime puffIn',
                'leave-active-class': 'magictime puffOut'
            }
        }
        return createElement('transition', data, context.children)
    }
})
new Vue({
    el: '#app-3',
    data: {
        showRecipe: false,
        showNews: false
    }
})
<link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/magic/1.1.0/magic.min.css">
<div id="app-3">
    <button @click="showRecipe = !showRecipe">
        Recipe
    </button>
    <button @click="showNews= !showNews">
        Breaking News
    </button>
    <puff>
        <article v-if="showRecipe" class="card">
            <h3>
                过渡和动画
            </h3>
            <p>
                自定义过渡的类名 在钩子中使用Velocity 两个元素之间的过渡 ...
            </p>
        </article>
    </puff>
    <puff>
        <article v-if="showNews" class="card">
            <h3>
                今日头条
            </h3>
            <p>
                2017、2018面试分享(js面试题记录)记得点赞分享哦;让更多的人看到~~
            </p>
        </article>
    </puff>
</div>

clipboard.png

这里使用了一个magicCSS动画库和函数式组件(todo)
定义一个全局的函数式组件。在其render选项中定义函数并返回一个可重用的元素<puff>,在内部通过magic将进入/离开的过渡效果添加到<puff>的属性上。在页面需要的地方包裹该<puff>元素即可。

动态过渡

响应是Vue永恒的主体,因此过渡和它的属性都可以是动态的。这样我们可以控制在特定的位置与时间使用特定的过渡。

多个元素之间的过渡和过渡模式中我们已经展示了动态过渡,对于不同的<p>元素,我们使用了不同的过渡效果和模式。

猜你喜欢

转载自www.cnblogs.com/jlfw/p/11967353.html