ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] > [animating-vue](https://hub.fastgit.org/Code-Pop/animating-vue) > [vu 3 - 过渡的 class 名更改](https://vue3js.cn/docs/zh/guide/migration/transition.html) # 为什么需要动画 您可能听说过 Web 动画可以改善 Vue 应用程序的用户体验。但是,在 Vue 中该如何实现呢? 在学习如何在Vue中制作动画之前,让我们了解一下为什么要这么做,以及探索如何使用动画来改善用户体验。 ## 直击焦点 在当今世界,人类的头脑不断受到信息的轰炸。我发送电子邮件, 接收短信, 滚动信息源... 我们的注意力被引向的无穷无尽的方向。因此,当用户使用你的应用时,他们的大脑很可能已经处于一天中处理的信息的漩涡中。作为用户界面构建者,我们的工作是快速引导和指导用户的注意力,指导他们如何有效地使用我们的应用程序。 我们可以利用动画的力量来集中用户的注意力。一旦拥有它,我们就可以把它引导到我们想要去的地方。 ## 激发行动 当用户到达您的应用程序时,您希望他们做的第一件事是什么? 它可能是您希望他们阅读的一行文字,或者是被点击的一个按钮……您希望他们采取的第一步。 通过有效利用动画,您可以消除干扰并激发特定的动作。 在此示例中,我们希望激发的行为是用户单击按钮。 通过使用运动(motion),可以集中用户的注意力,消除一些关于应该如何与我们的应用互动方面的困惑。 动作(motion)之所以能如此有效地吸引注意力,是因为人类的一种原始本能。无论是为了捕食猎物,还是为了避免成为其他动物的猎物,对运动的视觉敏感性都是人类大脑的核心过程,帮助我们在这个星球上生存和进化。 作为开发人员,我们可以利用对视觉运动的敏感性,将用户的注意力吸引到我们希望他们关注的元素上,以便他们更有可能采取我们希望他们去操作的行为。 但是,强大的力量伴随着巨大的责任,因此我们应该用一种自然的方式来模拟运动(motion)。 ## 创建流程 如果我开始告诉你要进行动画处理的下一个原因是什么,最后我要告诉你们,在银河系中有1000亿颗恒星,这就叫做*不合理的推论(non sequitur)*。结束的地方突然从我开始的地方移开了。 不幸的是,在设计糟糕的 Web 界面中,不合理的推论是非常普遍的。会看起来像什么?你是否曾经点击过一个按钮或链接,而你刚刚看到的所有东西都在一瞬间消失,然后被一个全新的页面所替代?这可能会令人迷惑,并导致你的用户不得不在每次类似的事情发生时重新去定位到你的网站。久而久之,这会导致用户的认知疲劳。 我们利用动画的,可以让用户在浏览应用时创建一个无缝的流程,而不需要去破坏上下文。可以将元素变转换为另一个元素,以一种自然的方式在页面和组件之间转换。 我们可以向用户显示他们刚打开的菜单去了哪里,通过动画到把它设置为折叠的状态。可以使用动画在应用中创建一种物理空间和时间感,其中元素在可预测的持续时间内在位置和状态之间转换。如果这个方法实现得当,用户就会对我们的应用中的 “世界” 有一种熟悉感。他们会知道这些事物在哪些位置,他们会更好地记住如何找到他们需要的东西,当他们需要它的时候,他们知道应用是如何运作的,这样他们就不会迷失方向,感到沮丧,或筋疲力尽。 相反,他们会很高兴,这就是我认为我们应该对界面进行动画处理的最后一个主要原因。 ## 喜悦和惊喜 随着应用程序的数量无穷无尽,其中许多都是相当常规和沉闷的,而我们的用户将欣赏到美妙的触感,这些触感使您的应用更加有趣并且易于使用。如果你的用户开心了,你就得到了一个粉丝。 这些深思熟虑的触感可以是任何东西,从一个定制的加载旋转器(loading spinner),到一个超级简洁和有用的表单,它减轻了填写一大堆字段的负担,到一个按钮,它传达了一个成功或错误的状态。 如果做得好,这些令人愉快的触感将使你的应用更加令人难忘,可以使你从竞争对手中脱颖而出,甚至可以帮助你更长时间地吸引用户的注意力。但是有个警告:这方面很容易走极端和动画太多。动画就像在菜肴上添加香料。太少了,菜就没有味道了;太多了,菜就毁了。像任何一顿美餐一样,一个有效的动画界面是需要去平衡的。 ## 小结 在下面的课程中,我们将看到几种利用 Vue 创建动画的方法,这些动画几乎可以被任何 web 程序使用。虽然在 Vue 中有很多种动画的方法,而且它可以变得相当高级和复杂,但是本课程的目标并不是向您展示许多选项中的每一个。目的是为您提供向 Vue 应用添加简单,实用的动画所需的工具和见识,这些动画可指导您集中注意力,激发动作,创造流畅感以及带来惊喜。 # 转换(Transitions) 我们理解了动画界面的价值,可以开始编码了。但是首先,需要了解 Vue 的转换组件是如何工作的,然后我们将构建第一个简单的转换动画。 ## Vue 转换 有人会说转换和动画是绝对不同的东西。但是为了这门课程的缘故,我们来实现一个过渡,作为一个简单的动画。 转换正如它听起来的那样。就是某些事情从一个阶段过渡到另一个阶段。从关闭屏幕到打开屏幕,从这里到那里,从打开到关闭,从可见到不可见,等等。因此,转场可以为用户提供关于事物如何变化的可视化反馈。 在 Vue 中,可以使用内置的`transition`组件,作为一个包装器,它为我们提供可以在转换的生命周期中挂钩的类。接下来,看看如何利用这些类来定义进入和离开转换。 ### 默认样式 在使用 Vue 的`transition`组件设计转换时,我们首先要问自己这个问题:**默认样式应该是什么**?换句话说,当元素 / 组件没有转换时,它应该如何显示? 为了更清楚地说明这一点,让我们来看一个简单的例子,我们希望一个框在屏幕上进行切换,并在切换过程中淡化其可见性。 它的默认样式应该是什么?当它没有过渡的时候,它应该如何表现?在这种情况下,我们希望它默认是`opacity: 1`(不透明度:1),因为随着可见性的淡入和淡出,这是我们要转换的样式。 你可能知道一个 HTML 元素的默认样式已经是`opacity: 1`所以在这种情况下,我们不需要为我们正在转换的元素定义一个默认样式,因为`opacity: 1`已经被浏览器定义了。实际上,不需要定义默认样式,因为该样式已经是元素默认的显示方式。想象一下,如果你将一个元素的刻度从 `10%` 转换到 `100%` ,一个元素的刻度在默认情况下已经是 `100%` 了,所以我们不需要再次明确地定义它。 注意事项,我们需要设置一个默认样式,该样式与浏览器默认为元素设置样式的方式不同,但我们稍后会介绍。 一旦我们清楚了元素(或组件)的默认样式是什么,我们就可以使用`transition`换组件的内置类来设计转换到或离开默认样式。换句话说:我们可以构建进入和离开时的转换。 ### 进入转换(过渡) 在设计转换时,我们应该问自己的下一个问题是:起始的样式应该是什么? 因为我们想要方块从不可见到可见,我们需要从`opacity: 0`开始.。我们将这个样式放在`v-enter`类中,以设置转换的**开始**样式。 ~~~css .v-enter { /* starting style */ opacity: 0; } ~~~ 现在,当元素进入 DOM 时,它开始时完全不可见,然后从样式 (`0`) 过渡到我们的默认样式 (`1`)。这就引出了我们在定义过渡时的下一个问题:激活时样式应该是什么? 在这种情况下,我们需要知道:过渡(transition)需要多长时间?在过渡期间,我们应该加快还是放慢过渡进程?我们使用`v-enter-active`类来定义过渡处于激活状态时的行为,并指定其持续时间和 easing 等内容。如果你对 easing 函数还不熟悉,你可以在这里[了解更多](https://www.w3schools.com/cssref/css3_pr_transition-timing-function.asp)。 ~~~css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } ~~~ 在这里,我们指定我们正在过渡的`opacity`属性,并将转换的持续时间设置为持续`2s`,并给它一个 [`ease-in`](https://cubic-bezier.com/#.42,0,1,1) 曲线,这意味着它将淡入慢慢变快,因为它接近 1。 ## 离开转换 方块是在屏幕上,我们如何过渡,如果关闭?在进入转换中,我们需要定义开始状态,即`opacity: 0`;对于离开转换,我们需要定义结束状态,这就引出了下一个问题:结束样式应该是什么? 因为需要从可见 (`1`) 过渡到 不可见 (`0`) ,所以在 `v-leave-to` 类中设置了这种结束样式。 ~~~css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } .v-leave-to { /* ending style */ opacity: 0; } ~~~ 现在,方块 将从默认状态 `1` 过渡到结束状态`0`。这就引出了我们在定义转变时的最后一个主要问题:激活离开的样式应该是什么? 与进入转换类似,我们在`v-leave-active`类中定义这个转换,设置它的持续时间,easing 等等。 ~~~css .v-enter { /* starting style */ opacity: 0; } .v-enter-active { /* active entering style */ transition: opacity 2s ease-in; } .v-leave-active { /* active leaving style */ transition: opacity 2s ease-out; } .v-leave-to { /* ending style */ opacity: 0; } ~~~ 现在我们了解了 Vue 转换类的机制,接下来让我们使用 Vue 的`transition`组件来构建我们的第一个实际转换。 ## 一个简单的过渡 每当 DOM 中突然出现一些东西时,我们的用户可能会有点迷惑,他们可能不会立即知道屏幕上发生了什么变化。解决这个问题的一个简单方法是随着时间的推移将元素*淡入*视图,为它们提供有关正在发生变化的上下文。让我们从前面的`opacity`例子中获取一些概念,并在我们的例子中实现一个简单的淡入过渡。 假设我们有一个模态对话框(modal)。它可以是登录模式、配置模式等等。当点击`Open`按钮时,模态渐入。然后模态本身有一个`Close`按钮,点击这个按钮后,模态消失。 *src/views/Home. vue*: ``` <template> <div> <button @click="toggleModal">Open</button> <div v-if="isOpen" class="modal"> <button @click="toggleModal">Close</button> </div> </div> </template> <script> export default { data() { return { isOpen: false } }, methods: { toggleModal() { this.isOpen = !this.isOpen } } } </script> ``` 有一个 `isOpen` data 属性,当 `toggleModal` 方法运行时,我们在 `true` 和 `false` 之间切换。因为我们的模态 div 中有`v-if="isOpen`,所以每当按下打开和关闭按钮时,模态就会出现和消失。 通过在`transition`组件中包裹该模态,我们可以在它打开和关闭时为它创建转换。 *src/views/Home. vue*: ``` <template> <div> <button @click="toggleModal">Open</button> <transition name="fade"> // <-- named transition <div v-if="isOpen" class="modal"> <button @click="toggleModal">Close</button> </div> </transition> </div> </template> ``` ### 命名转换 注意,这里使用了 `name` 属性来转换的命名为 `fade`。这允许我们用这个名字来设置转换的类名(`fade-enter` 而不是`v-enter`)。命名的过渡帮助我们保持应用程序的规模,并使我们的过渡更可重复使用。我们可能希望在整个应用程序中的其他元素上使用这个`fade`过渡,这就是为什么我们应该根据转换的作用而不是它们的目标元素来命名转换的原因。我们可以给这个过渡模态命名,但是这个名字只是描述了这个特定的用例,我们可能想`fade`非模态的内容。 ## 进入转换 现在我们可以使用命名的转换类来创建**进入**转换,这需要定义开始样式。 还记得我们那些问题吗? 开始的样式应该是什么? *src/views/Home. vue*: ```css .fade-enter { /* starting style */ opacity: 0; } ``` 激活进入的样式应该是什么? *src/views/Home. vue*: ```css .fade-enter { /* starting style */ opacity: 0; } .fade-enter-active { /* entering style */ transition: opacity .5s ease-out; } ``` 在`.fade-enter-active`内定义了CSS `transition`的行为方式,指定了我们`transition`的属性(`opacity`)转换的持续时间 (`.5s`)和 timing 函数(`ease-out`)。 ## 离开转换 现在我们的**进入**转换已经建立,我们可以创建**离开**转换。 介绍的样式应该是什么? *src/views/Home. vue*: ~~~css .fade-leave-to { /* ending style */ opacity: 0; } ~~~ 激活的离开样式应该是什么? *src/views/Home. vue*: ~~~css .fade-leave-active { /* leaving style */ transition: opacity .5s ease-out; } ~~~ 正如您所看到的,过渡的**结束**样式(`.fade-leave-to`) 的`opacity`为`0`,**离开**样式为(`.fade-leave-active`)包含与我们的进入转换相同的 CSS 转换。因为这部分是一样的,所以可以这样压缩样式: *src/views/Home. vue*: ~~~css .fade-enter { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity .5s ease-out; } .fade-leave-to { opacity: 0; } ~~~ ## 额外的过渡类 如果查看 Vue 的 Enter/Leave 转换的[文档](https://vuejs.org/v2/guide/transitions.html),您还会发现 `v-enter-to` 和 `v-leave` 类。在本课中我们没有讨论它们的原因是,我们正在过渡到的样式(`opacity: 1`)已经是元素的默认样式。我们正在从不透明状态过渡到其他状态(`opacity: 1`)也是如此。这就是为什么我们不需要在 `v-enter-to`或 `v-leave`中明确设置不透明度为 1 。只有当您正在过渡到(`v-enter-to`)或者离开(`v-leave`)的样式与元素的默认样式不同时,或者当您遇到浏览器兼容性问题时,您才需要使用这些类,可能会对您有用。 ## 小结 在本课中,我们讨论了转换/过渡的本质,探索了 Vue 的`transition`组件及其内置类的机制,然后构建了我们的第一个简单转换,使用这些问题来指导我们的决策: * 默认样式应该是什么? * 开始样式应该是什么? * 激活进入的样式应该是什么? * 结束的样式应该是什么? * 激活离开的样式应该是什么? 在下一课中,我们将研究使用这些相同的概念来使用 Vue Router 创建页面转换。 # 页面转换 了解了如何使用 Vue 的`transition`组件来创建我们自己的转换,接下来将这个概念应用到单页面应用中的一个常见用例:**页面转换(page transitions)**。 > 首要条件:假设您以及具备了 Vue Router 的工作知识。 ## Vue Router + transition 当用户浏览我们的应用程序时,我们可以通过使用页面转换使一个视图替换另一个视图的方式变得平滑。看看我们可以多快地在页面之间(例如路由级别的组件)重用上一节课的`fade`过渡。 目前,在我们的示例应用程序中,有两个路由:重新命名的`modal`和`about`组件。如果我们打开 App.vue 文件,我们可以看到每个路由以及有了一个`router-link`,以及它下面的`router-view`组件。`router-view`实际上只是一个动态组件,会被`modal`和`about`组件所取代,所以可以用 Vue 的`transition`组件来包装`router-view`,并在这里使用`fade`转换来淡出我们路由到的页面组件。 *src/App. vue*: ~~~ <template> <div id="app"> <div id="nav"> <router-link to="/">Modal</router-link> | <router-link to="/about">About</router-link> </div> <transition name="fade"> <router-view /> </transition> </div> </template> ~~~ 因为 `fade`转换还只存在于 `Modal.vue` 中,所以需要把它移动到 `App.vue` 的样式中,这样就可以在整个应用中使用它。 ## 转换模式 目前,这种转换发生的方式是,页面内容从淡入淡出(fades out)的同时,新页面就会淡入(fades in),然后内容跳入到一个合适的位置,转换就会显得杂乱。我们可以通过使用 Vue 的**转换模式(transition modes)**来解决这个问题,其中包括: * `in-out`:新元素转入(transitions in),然后当完成时,当前元素转出(transitions out) * `out-in`:当前元素转出,然后当完成时,新元素向内转入 * (默认情况下,当前元素和新元素将同时进出) Let’s use the`out-in`mode to smooth out our`fade`transition. We do that by specifying it within the`transition`component’s`mode`attribute, like so: 让我们使用`out-in`模式来平滑`fade`转换。我们通过在`转换`组件的 `mode` 属性中指定它来实现,如下所示: *src/App.vue*: ~~~ <transition name="fade" mode="out-in"> <router-view /> </transition> ~~~ 现在我们可以看到,页面转换更加流畅了。在一个页面淡出后,新的页面将会淡入。 ## 另一个页面转换 看到实现页面转换有多简单之后,希望你可以感到放松,但是你可能会对我们仅仅重复使用已经建立的`fade`转换感到有点失望。现在让我们来创建一个更加细致的页面转换/过渡。 一种常见页面相互转换的方式是水平滚动。例如,您可能会在博客或照片库中看到这种转变。我们可以通过在将页面组件动画消失和转换到屏幕上时平移`X`位置来实现此效果(除了像以前一样淡化`opacity`)。 To start, when a component transforms*onto*the screen, we want it to come in a bit from the right. So we’ll set the**starting**state of the**entering**transition`10px`over in the`X`direction, like so: 首先,当一个组件`转换到`屏幕上时,我们希望它稍微从右边进来一点。因此,我们将在`X`方向上设置**进入**过渡的**开始**状态`10px`,如下所示: *src/App.vue*: ```css .slide-fade-enter { transform: translateX(10px); opacity: 0; /* still fading opacity */ } ``` 注意一下,如何将`opacity`设置为 `0`。 When the component performs its**leaving**transition, where do we want it to \*\*\*\*leave**to**? We can do the inverse of our entering transition, and make it transition`-10px`to the left as it fades away. We’ll define that like so: 当组件执行**离开**转换时,我们希望它离开到哪里?我们可以对**进入**转换做相反的操作,使它向左转换`-10px`,随着淡出消失。我们这样定义它: *src/App.vue*: ```css .slide-fade-leave-to { transform: translateX(-10px); opacity: 0; } ``` 最后,我们希望这种过渡在发生时如何表现? 它应该持续多长时间,并且应该具有缓动功能(easing function)吗? 我们将在`.slide-fade-enter-active`和`.slide-fade-leave-active`类中设置那 **激活(active)** 设置,让我们: *src/App.vue*: ```css .slide-fade-enter { transform: translateX(10px); opacity: 0; } .slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.2s ease; } .slide-fade-leave-to { transform: translateX(-10px); opacity: 0; } ``` 现在,当我们在浏览器中进行测试时,我们看到我们的页面组件正在淡出,并且以很好的水平滚动的方式显示。这里仍然在`transition`换组件上使用`mode="out-in"`来平滑这个转换。 ## 另一个用例 值得注意的是,我们并不局限于只在页面之间路由时才使用这种转换。可能你希望在使用 `is` 属性的组件之间进行转换。在这种情况下可以使用这种转换。 ## 小结 在本课中,使用 Vue 的`transition`组件在用户浏览应用时创建平滑的页面转换是多么简单,以及转换模式如何帮助我们修改转换的行为。在下面,我们将学习如何使用 Vue 的`transition-group`组件来转换多个元素 / 组件的组。 # Group Transitions 到目前为止,我们已经学习了如何在DOM 界面上内外转换单个组件 / 元素;并将这个概念应用于 Vue Router,在路由级页面组件之间进行转换。但是,如果我们有一组组件 / 元素,我们想对它们应用相同的转换呢? 例如,每次向列表添加新项时,我们可能希望该项以特定的方式转换到(transition into)该组。或者我们可能有一组元素,我们希望以与更新过滤器相同的方式重新排序。为了实现这种基于组的转换行为,可以利用 Vue 的内置组件的能力,它的功能与组件类似,但是是用于一组组件 / 元素的。 让我们通过一个简单的例子来理解它是如何工作的。 ## 一个简单的例子 在列表中显示数据是非常常见的,所以让我们从这里开始。毕竟,列表只是一组元素。假设我们有一个联系人列表,当我们向列表中添加一个新联系人时,我们希望它以特定的方式转换到列表中(到组中) *src/views/List. vue*: ``` <template> <div> <input v-model="newContact" placeholder="Name" type="text" /> <button @click="addContact">Add Contact</button> <ul> <li v-for="(contact, index) in contacts" :key="index"> {{ contact }} </li> <ul> </div> </template> <script> export default { data() { return { newContact: "", contacts: [ "Beau Thabeast", "Cindy Rella", "Alice Wunderlind" ] } }, methods: { addContact() { this.contacts.push(this.newContact) this.newContact = "" } } } </script> ``` 如您所见,我们的起始代码只包含一个模板,该模板显示来自数据的联系人列表,这数据是可以添加到的数组。因此,每次向该组添加新的列表项时都希望可以进行转换,那么简单地将`ul`换成**过渡组组件**,如下所示: *src/views/List. vue*: ~~~ <transition-group tag="ul"> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ~~~ 注意,这里添加一个标记属性并将其设置为`ul`,这是因为转换组(transition-group)会渲染一个包装了 组本身的元素。虽然 `transition-group` 默认渲染的元素是`span`,但是我们可以将其更改为其他元素,比如`div`,或者是对应当前的示例:ul。您可以将标记看作是用来包装一组子元素的标记,因为这基本上就是转换组渲染时所发生的事情。 > 注意:`transition`组件不渲染元素;所以这种行为只属于转换组特有的。 转换组还不会做任何事情,因为就像转换组件(transition component)一样,我们将给它指定我们想要应用于组中每个成员的转换名称。创建一个转换,并将其命名为 “slide-up”。 *src/views/List.vue*: ~~~ <transition-group name="slide-up" tag="ul"> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ~~~ 然后在`App.vue`文件的样式部分,创建 slide-up 转换。 *src/App.vue*: ```css .slide-up-enter { transform: translateY(10px); /* start 10px down*/ opacity: 0; } .slide-up-enter-active { transition: all 0.2s ease; } ``` 在这个简单的示例中,我们只是将元素转换到列表中,而不是从列表中移出,因此我们可以删除 leave 相关的转换类。 现在,当我们向联系人列表添加新成员时,我们将看到它们上滑到( slide-up)列表中。 如果你想知道为什么我们使用`10px`而不是`-10px`从页面的下方开始,这是一个很好的问题!记住,网页的坐标从页面左上角的`(0,0)`开始。所以沿着页面向下,实际上是沿着 y 坐标方向 “向上”。 ## 在初始渲染时触发过渡 如果刷新页面,会注意到列表看起来像是在滑入适当的位置。因为整个路由级别的组件都是这样过渡的(请记住:我们添加了滑动淡入作为页面过渡)。可是我们特别希望在初次渲染页面(以及列表)时,列表会向上滑动,可以通过使用`appear`属性来实现,如下所示: *src/views/List.vue*: ``` <transition-group name="slide-up" tag="ul" appear> <li v-for="contact in contacts" :key="contact"> {{ contact }} </li> </transition-group> ``` 出现属性是我们告诉转换组的方式,我们不仅希望每次添加组中的新成员时都执行幻灯片向上转换,还希望首次呈现整个组 (整个 ul) 时执行幻灯片向上转换。 现在,当我们刷新页面并初始加载组件时,我们将看到这些列表项滑动到位(slide-up into place)。 > 注意:出现`appear`属性也可以用在转换组件上,它专属于转换组的属性。 ## 在组内移动项目 如果我们要添加一个特性来对列表进行排序,比如字母排序方法,那么当列表中的条目移动到新的位置时会发生什么情况呢? 例如,如果我们的代码有一个排序按钮: *src/views/List.vue*: ``` <template> ... <button @click="sortContacts">Sort</button> ... </template> ``` 一种新的字母排序方法: *src/views/List.vue*: ```js methods: { ... sortContacts() { this.contacts = this.contacts.sort() } } ``` 当我们点击排序按钮,我们会看到我们的列表项目只是有点突然吸附到他们的新位置。如果我们能够微调列表中的项目在排序时的移动方式,就会更好一点。我们可以使用`v-move`过渡类来调整元素在组中改变位置时的移动方式。 让我们添加`v-move`到我们的滑动向上的过渡样式。注意`v-`前缀需要被转换的名称所替换的。 `src/App.vue`: ```css .slide-up-enter { transform: translateX(10px); opacity: 0; } .slide-up-enter-active { transition: all 0.2s ease; } /* v-move => slide-up-move */ .slide-up-move { transition: transform 0.5s ease-out; } ``` 如果在浏览器中查看,会看到组的成员正在按照这些样式移动。如果我们想要加快或者减慢持续时间,或者改变缓和函数,可以根据自己的喜好调整`move`样式。 如果您想知道`v-move`在底层是如何工作的,它利用了FLIP转换。 ## 小结 到这里结束了对组过渡基础的探索。 该主题涉及面很广,因为可以采用多种方式对数据(以及显示该数据的元素/组件)进行分组,并且向这些组应用转换的方法也很多。但是有了这些基础知识,我希望当你的应用程序调用它们时,你能有信心开始编写你自己的代码。 在下一课中,将深入研究 JavaScript 动画。 # JavaScript Hooks + Velocity 迄今为止,我们一直在学习 Vue 的转换和转换组组件,这些组件为我们构建输入和输出转换提供了必要的类。然而,当我们的需求变得更加独特或复杂时,我们需要开始使用 JavaScript 来构建我们的动画。 在本课中,我们将研究 Vue 的 JavaScript Hooks,并将其与一个名为 Velocity.js 的有用的动画库结合起来,以构建一些更强大的动画。 ## JavaScript Hooks 什么是 JavaScript Hooks?它们类似于 Vue 的生命周期钩子(`beforeCreate`、 `created`等)。它们可以看作是 Vue 转换(Vue transition)生命周期的一系列阶段。 下面是它们的列表: before-enter enter after-enter before-leave enter-cancelled leave after-leave leave-cancelled 如您所见,在进入和离开转换期间都会调用一些钩子。当它们被调用时,我们可以让它们触发方法,这些方法会包含更复杂的行为,而这些是使用 CSS 是无法做到的。 `` <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" > <!-- ... --> </transition> ``` 了解了有哪些钩子可用,接着就构建第一个基于 javascript 的 Vue 转换。 ## 拉抽屉式转换 抽屉过渡 web 应用程序的一个共同特征是抽屉式(drawer-type)组件,它可以滑出以显示用户配置文件、导航菜单或仪表板等内容。在我们的开始代码里面,你会看到我们现在有了一个`Drawer.vue`文件,它包含一个用户配置文件,点击一下按钮就可以在屏幕上弹出和关闭。 借助 JavaScript hook 和 Velocity 动画库,我们将把它变成一个从浏览器左侧滑出的 “抽屉(drawer)”。但首先,让我们检查一下起始代码: *Drawer.vue*: ``` <template> <div> <button @click="isOpen = !isOpen"> My Profile </button> <div v-if="isOpen" class="drawer"> <img src="../assets/avatar.png" alt="avatar" /> <div></div> <div></div> <div></div> <div></div> </div> </div> </template> <script> export default { data() { return { isOpen: false } } } </script> ``` 按钮用来切换`isOpen`布尔值,这决定了我们是否显示我们的抽屉。在这之下,我们有一些被限定作用域的样式: ``` <style scoped> img { height: 2.5em; width: 2.5em; border-radius: 50%; } .drawer { display: flex; flex-direction: column; align-items: center; width: 12em; height: 20em; border-radius: 1%; background-color: #e0e0e0; box-shadow: 0.08em 0.03em 0.4em #ababab; padding-top: 0.7em; } .drawer div { height: 3.5em; width: 95%; margin-top: 0.6em; background-color: #f0f0f0; border: 0.02em solid #ababab; border-radius: 1%; } </style> ``` 接着第一步是在转换组件中包装抽屉,并添加我们将要使用的钩子。 ``` <template> <div> <button @click="isOpen = !isOpen"> My Profile </button> <transition @before-enter="beforeEnter" @enter="enter" @leave="leave" :css="false" > <div v-if="isOpen" class="drawer"> <img src="../assets/avatar.png" alt="avatar" /> <div></div> <div></div> <div></div> <div></div> </div> </transition> </div> </template> ``` 正如您将看到的,我们需要利用:`before-enter()`、`enter()`和`leave()`,它们将触发指定的方法。我们需要这些方法做什么呢? 至于`beforeEnter()`,设置在抽屉进入界面之前设置它的初始样式。 使用`enter()`方法,设置抽屉进入时的样式。 `leave()`方法,设置抽屉在离开界面时转换到的样式。 > 注意:我们使用`:css="false"`来告诉 Vue 它不需要处理这里的转换类,因为我们依赖于 JavaScript 钩子。 首先需要导入 Velocity。查看`package.json`,会看到这个库已经安装在项目中了(所以只需运行`npm i velocity-animate --save-prod`)。 *Drawer.vue*: ```js <script> import Velocity from 'velocity-animate' export default { data() { return { isOpen: false } }, methods: { beforeEnter(el) { // we'll place our starting style here }, enter(el, done) { // style to transition to on enter }, leave(el, done) { // style to transition away to on leave } } } </script> ``` 注意我们的每个方法都有`el`参数。用来访问正在转换的 DOM 元素,该例子中就是我们的`div.drawer`。 那么,我们希望我们的抽屉如何开始呢?开始不希望它是可见的,所以它需要`opacity:0`,宽度也要`width: 0`,我们可以通过转换它的宽度向外扩展。将这些样式添加到`beforeEnter()`中: *Drawer.vue*: ```js beforeEnter(el) { el.style.opacity = 0 el.style.width = '0em' }, ``` 现在在 enter () 中,我们可以告诉这个方法将抽屉打开并给它一个宽度。我们将使用 Velocity () 方法来动画我们的`opacity`和`width`。 *Drawer.vue*: ```js enter(el, done) { Velocity( el, // element to animate { opacity: 1, width: '12em' }, // new style rules { duration: 1000, easing: 'easeOutCubic', complete: done } // define how transition happens and complete it ) } ``` 让我们分析一下这段代码。我们向`Velocity()`方法传递了一些参数,首先传入需要的元素(el)进行动画处理,然后给它传入一个会应用于该元素的新样式规则的对象。最后,我们传入一些规则来定义转换的持续时间,缓和函数:`easeOutCubic`,还有`done`,这是一个当 velocity 动画完成时将要运行的方法。`done`让 Vue 知道这个钩子已经完成,它可以继续它的生命周期中的下一个钩子。 > 注意:我们已经使用了`easeOutCubic`宽松函数,可以使用的众多函数之一。 我们可以感谢 Velocity。但是,如果您使用此参考,请知道通过 Velocity 无法使用`Back`,`Elastic`和`Bounce`功能。 ## 离开样式 现在我们已经定义了抽屉应该如何进入,让我们来定义它应该如何离开`leave()`。 *Drawer.vue*: ```js leave(el, done) { Velocity( el, { opacity: 0, width: '0em' }, { duration: 500, easing: 'easeInCubic', complete: done } ) } ``` 在这里,我们再次使用`Velocity()`方法,传递`el`进行动画处理,告诉它返回到我们最初使用的样式(不可见且没有宽度) ,并且我们告诉它如何转换,设置持续时间和缓和函数,然后我们传递`done`,这样 Vue 就知道钩子什么时候完成了。 Note: It may seem odd to you that we’re using easeInCubic on our leave transition and easeOutCubic on our enter transition. But the “In” and “Out” only refer to the curve of the ease itself. The “In” and “Out” don’t correlate to how you should use them with an enter vs leave transitions. The best way to pick an easing function is to simply try them out and see which feels most natural for the transition you’re building. > 注意:在**离开**转换中使用`easeInCubic`,在**进入**转换中使用`easeOutCubic`,这对你来说可能有点奇怪。但是 “In” 和 “Out” 只是指 ease 本身的曲线。“In” 和 “Out” 与输入和输出转换时应该如何使用它们并不相关。选择一个缓和功能的最好方法就是简单地尝试它们,看看哪个对于你正在构建的过渡来说是最自然的。 现在,当我们在浏览器中检查时,我们会看到用户配置文件抽屉在我们的界面中滑动得很好。 ## Velocity 的强大 你可能会认为所有这些都是过度消耗,因为我们没有使用 Velocity 来完成 CSS 本身不能完成的任务(除了一些特殊的缓解功能)。那是因为开始总是很简单的。Velocity 还有更多我们尚未使用的特性,比如弹簧物理(spring physics),它允许我们在界面中创建弹性 / 快速 / 弹性(springy/snappy/bouncy)运动。 为了了解弹簧物理的实际作用,让我们用一个具有 2 项的数组代替`enter ()`转换中的`easing`函数: *Drawer.vue*: ```js enter(el, done) { Velocity( el, { opacity: 1, width: '12em' }, { duration: 1000, easing: [60, 10], complete: done } // now with spring physics ) } ``` 数组中的这些数字是什么?第一个 (60) 是张力的量,第二个 (10) 是这个弹簧函数的摩擦量。我们可以调整这些来实现动态效果。例如,摩擦力越小,结束振动的速度就越快。和缓和函数一样,这些数字也是你想要玩弄的东西,直到你找到正确的组合,就会有很自然的效果。 现在,在浏览器中查看,会看到抽屉在打开时稍稍弹开。好玩! ## 另一个例子:卡片 To see another example of spring physics in action, in combination with the transition-group component, check out the Cards.vue component in this lesson’s ending code. You’ll see that the same concepts we just learned can be applied to another use case, where we might want cards to spring into view when a user loads the page. They could display anything from user reviews to pricing or product options… you name it. 要查看实际的弹簧物理示例,并结合过渡组组件,可以在下面查看`Cards.vue`组件代码。可知刚刚学到的相同概念可以应用于另一个用例,在该用例中,希望用户加载页面时将卡片显示出来。这里可以显示任何东西,从用户评论到价格或产品选择……你能想到的。 *Cards.vue*: ```js <template> <transition-group appear @before-enter="beforeEnter" @enter="enter" :css="false" > <div class="card" v-for="card in cards" :key="card.id"> <p>{{ card.title }}</p> </div> </transition-group> </template> <script> import Velocity from 'velocity-animate' export default { data() { return { cards: [ { title: 'Could contain anything', id: 123 }, { title: 'Endless possibilities', id: 456 } ] } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.width = '0em' }, enter(el, done) { Velocity( el, { opacity: 1, width: '12em', rotateZ: '3deg' }, { duration: 1000, easing: [70, 8], complete: done } ) } } } </script> <style scoped> .card { height: 4em; width: 12em; border-radius: 1%; background-color: #e0e0e0; box-shadow: 0.08em 0.03em 0.4em #ababab; padding-top: 1em; } </style> ``` ## 小结 我们学习了如何通过使用 javascript 钩子从第三方库触发动画逻辑,在接口中构建更复杂的动画。Velocity 比我们在这个简短的课程中提到的要强大得多,所以我推荐您继续使用它提供的功能,尽可能地使用它。 接下来。我们将研究状态转换(State Transitions)以及一个更强大的动画库: GSAP。 # 介绍GSAP 在上一课中,我们学习了如何结合使用 JavaScript Hooks 和第三方动画库 (Velocity.js) 来构建自定义动画。在本课中,我们将看到另一个不同的动画库如何做到这一点的示例,称为 GreenSock 动画平台(GSAP)。然后,通过 GSAP 用多种方式使我们的界面动画化。 ## 什么是 GSAP? 如果你观察一下 web 动画社区的领先开发人员正在使用的工具,你会发现 GSAP 是他们的首选工具。该库是一个健壮的、高性能的库,使您能够动画任何 JavaScript 可以触及的内容,并且它默认消除了浏览器之间的不一致。 在本课中,我们将关注 GSAP 库的 TweenMax 部分。就上下文而言,TweenMax 是 GSAP 中基本的 TweenLite 工具的一个功能更加全面的版本。虽然它比它的小兄弟要大,但是 TweenMax 更加强大,由于它附带的插件,比如 CSS Plugin,它可以为我们处理一些任务。由于 TweenMax 具有有用的插件,因此可以更轻松地开始学习GSAP,但是如果您担心大小,我建议您在项目中使用配对的TweenLite版本。 ## 第一个 GSAP 动画 开始使用 TweenMax,先看一个简单的例子,这与上一课类似,当相应的 JavaScript hook 被触发时,我们通过我们的方法运行动画。 下面是我们的起始代码: *src/views/Simple.vue*: ```css .card { display: block; margin: 0 auto 0 auto; height: 6.5em; width: 6.5em; border-radius: 1%; background-color: #16c0b0; box-shadow: 0.08em 0.03em 0.4em #ababab; } ``` 我们有一个包装 card div 的转换组件,该组件将在加载该组件时显示。 我们使用`before-enter`并输入 javascript 钩子以相同的名称触发方法,并将`false`绑定到`:css`,因此 Vue 无需担心添加/删除过渡类、 现在从 GSAP 导入 TweenMax,把它安装到演示项目中。您可以通过从终端运行`npm i gsap`自己安装它。 导入了 TweenMax,可以继续使用我们的方法。首先,定义在 card 进入我们的界面之前,它应该具有的样式。我们将在`beforeEnter`方法中设置它。 *src/views/Simple.vue*: ```js methods: { beforeEnter(el) { el.style.opacity = 0 el.style.transform = ‘scale(0,0)’ }, ``` 我们告诉它从不可见开始,其`x`和`y`值的缩放为`0`。在`enter`方法中,将使用 TweenMax`to`实现动画到一个全尺寸可见的 card 。 *src/views/Simple.vue*: ```js enter(el, done) { TweenMax.to(el, 1, { opacity: 1, scale: 1, onComplete: done }) } ``` 与前面编写的 Velocity 方法的方式相似。这里使用了`TweenMax.to()`方法,并向其传递一些参数。`To()`方法用于定义我们希望动画转到哪里,而不是从哪里开始。那么我们希望卡片(card)的样式过渡到什么样子呢?我们在这个方法的参数中进行了设置。 第一个参数(`el`)告诉 TweenMax 要动画哪个元素。(这与 Velocity 完全相同)。第二个参数是动画持续的时间。第三个参数是一个对象,为 TweenMax 提供了结束样式,或者动画样式。正如你看到的,我们正在使 card 可见 (`opacity:1`) 和完全缩放到 100% 的`scale:1`。与 Velocity 类似,TweenMax 也有一个属性,可以在该属性中传入一个方法,以便在动画完成时运行。因此,当这个转换完成时,我们传入`done`以运行(`onComplete`)。作为提醒,`done`让 Vue 知道转换生命周期中的这一步已经完成,因此 Vue 可以继续下一步。 如果我们在浏览器中运行这个程序,我们会看到当卡片出现时,它从不可见变为可见,而它的比例从 0% 增加到 100% 。 很好。继续使用 TweenMax 来构建一个更复杂的动画。 ## Staggering Elements 当元素进入 web 界面时,它们交错进入适当的位置是很常见的。您可以想象通过 API 调用显示在卡中的数据列表,然后每张卡交错排列。我们可以使用 TweenMax 轻松完成此操作。 在开始编码之前值得注意的是,当我们构建这样的基于第三方的 JavaScript 动画时,并不总是需要依赖使用转换 / 转换组组件及其 JavaScript Hooks。有许多方法可以实现这个目标,使用组件的生命周期方法就是其中之一。所以在这个例子中,我们将依赖组件的`mounted`钩子。让我们来看一下代码: *src/views/Stagger.vue*: ```css <template> <div id="container"> <div v-for="card in cards" :key="card.id" class="card"></div> </div> </template> ... <style scoped> #container { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-evenly; } .card { height: 6.5em; width: 6.5em; border-radius: 1%; background-color: #16c0b0; box-shadow: 0.08em 0.03em 0.4em #ababab; padding-top: 1em; margin-top: 0.5em; margin-right: 0.5em; } </style> ``` 正如您看到的,我们正在使用`v-for`创建一个基于数据的卡片列表。如果我们在浏览器中查看这个组件,那么卡片就已经在它们的位置上了。但是在这个示例中,我们希望它们在组件挂载时错开到位,所以让我们设置它们。TweenMax 已经被导入,因此可以立即开始使用它。 这就是错开代码的样子: *src/views/Stagger.vue*: ```js mounted() { TweenMax.staggerFrom(’.card’, 0.5, { opacity: 0, y: 200 }, 0.1) } ``` 正如您想,`staggerFrom()`方法使我们能够通过传入指令来将元素错开到位,这些指令告诉元素在哪里以及如何开始错开进程。所以,我们告诉 TweenMax 要动画的元素是什么(`.card`)。给它一个持续时间(`0.5`) ,然后传入过渡指令,告诉元素开始时不可见(`opacity:0`) ,在`y`方向向下 200 像素。最后的附加参数(`0.1`)是每个元素之间的延迟,这意味着每个动画卡之间将有 0.1 秒的暂停。没有这个暂停,所有 card 都会在同一时间进来。 现在,如果我们在浏览器中查看,我们会看到它们正如我们所希望的那样缓慢地进入。 要让它更好一点,可以用下面的方法来动画它的比例: 现在我们有了适当的位置,我们可以添加到该动画中以使其更加美观。假设我们还想在卡片错开时为比例设置动画。 *src/views/Stagger.vue*: ```js mounted() { TweenMax.staggerFrom(’.card’, 0.5, { opacity: 0, y: 200, scale: 0 }, 0.1) } ``` 现在当元素交错放置时,会将它们的大小从 0 扩展到 1(`0%`到`100%`)。太棒了! ## 小结 我们已经学会了如何使用另一个行业标准的基于 JavaScript 的动画库 GSAP 来构建动画,它们以两种不同的方式发生:当 JavaScript Hooks 触发转换组件时,以及当我们的组件的生命周期方法触发时。 接下来,我们继续使用 GSAP,并根据变化的状态进行动画处理。 # State with GSAP web 应用程序中,显示不断变化的数据的情况并不少见。 无论是在线游戏中玩家的实时得分,还是公司或财务统计数据,了解如何以一种不错的方式显示这种随时变化的状态都是有帮助的。 在本节中,我们将研究使用 GSAP 实现这一目标。 在底层,GSAP 会为我们执行“补间(tweens)”。 但这实际上是什么意思? 补间是中间的简称,它是生成将在动画的开始和结束之间显示的帧的过程。 例如,介于 1 和 10 之间的值是:2、3、4、5、6、7、8 和 9(以及它们的小数分别为:2.123 …)。 在从 0% 缩放到 100% 的情况下,介于两者之间的值将大于 0 且小于 100。因此,通过使用GSAP,我们可以自然地和高效地在值之间进行补间。 让我们看一个示例,其中我们有一些随时变化的状态,并将该状态显示为条形图。 这对于实时记分牌之类的东西很有用,或者对于任何显示实时不断变化的数据的地方都很有用。 *src/views/State.vue*: ``` <template> <div> <div :style="{ width: number + 'px' }" class="bar"> <span>{{ number }}</span> </div> </div> </template> <script> export default { data() { return { number: 0 } }, methods: { randomNumber() { this.number = Math.floor(Math.random() * (800 - 0)) } }, created() { setInterval(this.randomNumber, 1500) } } </script> <style scoped> .bar { padding: 5px; background-color: #2c3e50; border: 1px #16c0b0 solid; min-width: 20px; } .bar span { color: white; } </style> ``` 为了创建一些虚拟数据,在创建这个组件时,我们使用`setInterval`每 1500 毫秒运行一次 `randomNumber` 方法。`randomNumber` 方法用一个新的值(范围从 0-800)更新我们的`number`数据,我们的模板使用这个总是变化的数字通过样式绑定设置 div 的宽度`:style="{ width: number + ‘px’ }"`,在 `span` 中显示`number`值本身。 按照原样,我们的条形宽度会随着数字的变化从一个值跳到另一个值,并且它显示的`number`只会更新为新的`number`。 但是,如果我们的标尺平稳地增长并缩小到新的宽度值会不会很好? 同时,它显示的`number`值也可以补间。 让我们开始: 现在我们可以使用它来生成旧数字和新数字之间的值。要做到这一点,我们将使用`watch`。如果您以前没有使用过这个选项,那么这是一个内置的 Vue 组件选项,它允许我们监听一个反应值,当这个值发生变化时,我们可以使用新的和 / 或旧的值执行操作。所以需要监听`number`值。 *src/views/State.vue*: ``` watch: { number (newValue){ //now what? } } ``` 创建了一个监听器,它的名字与它正在观察的数据值((`number`)相同,传递了`newValue`。在继续之前,让我们创建另一个名为 `tweenedNumber` 的数据值。你马上就会明白为什么了。 *src/views/State.vue*: ``` data() { return { number: 0, tweenedNumber: 0 } }, ``` 现在有了 `number` 和 `tweenedNumber` 数据值,可以在监听器中使用 `gsap.to()` 方法使用它们。在这种情况下,我们不需要 GSAP 直接动画一个元素,我们希望它动画我们的数据。 因此,不需要传递 GSAP 元素来动画,而是传递要动画的数据: *src/views/State.vue*: ``` watch: { number( newValue ) { gsap.to(this.$data, {}) } }, ``` 使用`this.$data`传入组件的`data`对象数据。现在 GSAP 可以访问我们的数据,并且可以更新它的属性。现在我们可以给这个动画一个对象,包括有关如何为数据设置动画的说明。 我们将给它一个 1 秒的持续时间,一个 ease ,并指定我们想要动画的数据值`newValue`。 *src/views/State.vue*: ``` watch: { number (newValue) { gsap.to ( this.$data, { duration: 1, ease: ‘circ.out’ tweenedNumber: newValue }) } }, ``` 请注意,我们是如何告诉 gsap 更新数据上的 `tweenedNumber` 属性的 (在`this.$data`上)并将其动画化为 `newValue`。根据 `gsap.to() `方法的特性,它将通过将其补间或补间到新值来动画该值,而不是直接跳转到该值。 换句话说,不是从`1`直接跳到`5`,而是通过从`1`开始逐渐变高并逐渐接近直到达到`5`,而是创建一条平滑的路径,通过从`1`开始,逐渐地越来越高,越来越近,直到`5`。因为在值之间有了这些,所以可以使用它们来平滑模板中发生的事情。 不要将`bar`样式绑定到数字,而是将其绑定到`tweenedNumber`上,并同时显示该数字。 宽度现在将基于这些增量补间的值而扩大和缩小。 您还会注意到,由于我们现在显示的是tweenedNumber,因此我们看到了所有的小数。 虽然这些有助于平滑补间,但它们看起来并不漂亮,所以我们只需在显示范围值的位置添加`.toFixed(0)`即可进行清理。现在,我们将只显示整数,而不显示小数部分。 *src/views/State.vue* 最终代码: ```js <template> <div> <div :style="{ width: tweenedNumber + 'px' }" class="bar"> <span>{{ tweenedNumber.toFixed(0) }}</span> </div> </div> </template> <script> import gsap from 'gsap' export default { data() { return { number: 0, tweenedNumber: 0 } }, watch: { number(newValue) { gsap.to(this.$data, { duration: 1, ease: 'circ.out', tweenedNumber: newValue }) } }, methods: { randomNumber() { this.number = Math.floor(Math.random() * (800 - 0)) } }, created() { setInterval(this.randomNumber, 1500) } } </script> <style scoped> .bar { padding: 5px; background-color: #2c3e50; border: 1px #16c0b0 solid; min-width: 20px; } .bar span { color: white; } </style> ``` ## 小结 在本课程中,我们学习了补间的概念,以及如何使用GSAP在应用程序中更改状态之间创建平滑的过渡。 在下一课中,我们将学习如何使用GSAP的时间轴来创建包含多个动画的序列。 # GSAP 时间表 使用 GSAP 时,动画通常会开始增加复杂性。 有时候,简单的补间是不够的,我们需要创建多个动画,这些动画可以按顺序一起工作。 为此,我们可以使用 GSAP 的时间轴功能,该功能允许我们将一组补间链接在一起以创建更复杂的动画,这些动画可以一起工作。 ## 第一个 Timeline 从我们的开始代码中的 `Timeline.vue` 组件开始。 *src/views/Timeline. vue*: ``` <template> <div> <img class="runner first" src="../assets/runner.png" alt="runner" /> <img class="runner second" src="../assets/runner.png" alt="runner" /> <img class="runner third" src="../assets/runner.png" alt="runner" /> </div> </template> ... <style scoped> .runner { display: block; height: 5em; width: 5em; margin-top: 1.5em; } </style> ``` 在模板中,我们有三张跑步者的图片。过一会儿,我们将让它们每个都 “跑” 到浏览器的右侧。因为已经将 gsap 导入到这个组件中,所以我们只需要初始化我们的时间线,这将在挂载的钩子中完成。 在这里,我们还将为每个参赛者添加一个 `tween` 到时间线上。让我们从第一名开始; 在这里,使用 `to` 方法。这个方法允许我们创建一个 `tween` 并将其应用到目标元素 (我们正在动画的元素)。在本例中,我们使用类名`.first`对元素进行动画处理。告诉该元素在`2`秒钟的时间内“向右”运行 700 像素,并为该运动提供一个简单的“ expo.out”。 *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline() tl.to('.first', { x: 700, duration: 2, ease: 'expo.out' }) } ``` 如果我们在浏览器中查看,我们会看到右边 700px 的跑步者 “赛跑”。到目前为止还不错,现在让我们再创建两个到 tweens 中。 *src/views/Timeline. vue*: ``` mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to('.first', { x: 700, duration: 2, ease: 'expo.out' }) tl.to('.second', { x: 700, duration: 2, ease: 'expo.out' }) tl.to('.third', { x: 700, duration: 2, ease: 'expo.out' }) } ``` 现在,当这个组件被安装时,每个参赛者向右 “赛跑” 700 像素。照这样看来,我们的选手一个接一个地 “起跑”。在第一名运动员到达目的地后,第二名运动员出发,然后是第三名。然而,如果我们想让我们的运动员同时起飞呢?或者,或者如果我们希望第二名选手在第一名选手出发后半秒开始 ## 位置参数 我们可以通过使用位置参数来更改时间轴内动画的触发时间。通过此参数,我们可以定义补间在时间轴的序列中何时发生,并且有几种方法可以设置该位置。 ### 绝对位置 设置补间在时间轴内的位置的一种方法是设置补间的绝对位置。 这意味着我们提供了一个整数值,它表示从动画开始到现在的秒数。 因此,如果我们希望动画在时间轴上发射半秒,则可以将该动画的位置值设置为0.5,如下所示: *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to(’.first’, { x: 200, duration: 2, ease: ‘expo.out’ }) tl.to(’.second’, { x: 400, duration: 2, ease: ‘expo.out’ }, 0.5) // now with an absolute position tl.to(’.third’, { x: 600, duration: 2, ease: ‘expo.out’ }) } ``` 现在,第二名选手将开始“跑步” 0.5秒。 ### 相对位置 有时,如果相对于其他动画设置动画的位置,对我们来说可能更有用。 要设置相对位置,我们使用 `-=.75` 或 `+=.75`之类的字符串来设置动画补间(`-=`)之前或之后(`+=`)补间一个`.75`秒的开始 。 因此,让我们在时间轴的第二个补间中添加一个相对位置。 *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to(’.first’, { x: 200, duration: 2, ease: ‘expo.out’ }) tl.to(’.second’, { x: 400, duration: 2, ease: ‘expo.out’ }, '-=.75') tl.to(’.third’, { x: 600, duration: 2, ease: ‘expo.out’ }) } ``` ![](https://img.kancloud.cn/eb/b9/ebb9a187a366716e6170784490228f7a_397x216.png) 现在第二个动画将在第一个动画结束前 0.75 秒开始。 ### 开始 / 结束位置 如果我们希望第二个动画与第一个动画同时开始,您可能会想着将第二个动画的位置参数改为 `-=2`。 *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' }) tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '-=2') tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }) } ``` 虽然可行,但是如果以后需要更改第一个动画的持续时间呢?例如,如果第一个动画的持续时间改变为`4`,那么我们的第二个动画的位置只会向后移动到第一个动画开始的一半,因为 `4-2=2`。如果我们想让第一个和第二个补间同时触发,不管第一个运动员的持续时间有多长,我们可以通过第三种设置位置的方法来实现: 当我们把第二个选手的位置设置为`"<"`时,它就会在第一个动画开始时开始。`">"` 告诉它会在第一个动画结束时开始。 *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' }) tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '<') // starts when first tween starts tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }, '>' ) // starts when second tween starts, which is when first tween starts } ``` 在上面的代码中,所有三个跑步者将同时开始。为什么?嗯,我们告诉第三名选手在第二名选手开始时开始,第二名选手在第一名选手开始时开始。 ![](https://img.kancloud.cn/5f/96/5f96896d658b2cff0606120b7bf69a96_236x217.png) 我们也可以添加这些,使它们相对。例如,你认为 `<0.5` 的位置会怎样? *src/views/Timeline. vue*: ```js mounted() { let tl = gsap.timeline({ repeat: -1, repeatDelay: 1 }) tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' }) tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '<0.5') // What will this do? tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' } ) } ``` 如果你猜测第二个动画会在第一个动画开始后半秒启动,同时启动第三个动画,那么你是正确的。 如您所见,在时间轴上放置动画的方式有很多,包括更多可以在此处查看的方式。 ## 时间轴持续 通过这样一个简单的时间轴,我们可以很容易地计算出时间轴的总持续时间。然而,在更为复杂的时间线中,我们可以运行 `tl.duration()` ,这将合计所有补间的各个时长,并为我们返回时间轴的总时长。 在确定补间在时间轴序列中的位置时,这可能会有所帮助。 ## 重复 / 循环时间轴 你可能需要重复甚至循环一个时间线。我们可以通过在创建时间轴时传入一个对象来轻松实现这一点,如下所示: ``` gsap.timeline({ repeat: 2 }) ``` 现在,我们的时间轴将重复两次。如果我们想让它在循环中无限地重复,我们会使用 -1。 ``` gsap.timeline({ repeat: -1 }) ``` 还可以在下一次重复之前添加延迟。 ``` gsap.timeline({ repeat: -1, repeatDelay: 1 }) ``` 现在这个时间轴将等待 1 秒钟再重新开始。 ## 小结 让我们来看看在这一课中,我们看到了 gsap 时间轴如何允许我们对多个动画进行排序,并使用`position`参数在它们之间添加或删除空间。 在最后的课程中,我们将了解如何通过使用嵌套的时间轴来创建更灵活,可伸缩的动画。 # 嵌套的时间轴 有时候单一的时间轴 对于更复杂的动画来说是不够的。接下来,将学习如何通过在一个主时间轴线中嵌套多个时间轴来创建更具可伸缩性、模块化和可重用的动画。 打开 `Master.vue` 组件。它目前只有一个时间轴,所以我们将添加一个新的时间轴,看看我们如何在一个新创建的主时间轴中嵌套两个时间轴。 *src/views/Master.vue*: ``` <template> <div> <div id="container"> <img class="paws first" src="../assets/paws.png" alt="fox-paws" /> <img class="paws second" src="../assets/paws.png" alt="fox-paws" /> <img class="paws third" src="../assets/paws.png" alt="fox-paws" /> <img class="paws fourth" src="../assets/paws.png" alt="fox-paws" /> <img id="fox" src="../assets/fox.png" alt="fox-logo" /> </div> </div> </template> ... <style scoped> #container { display: flex; flex-direction: row; justify-content: center; margin-top: 5em; } #fox { height: 8em; width: 8em; opacity: 0; filter: blur(2px); } .paws { transform: scale(0); width: 2.5em; height: 2.5em; margin-top: 50px; margin-right: 0.8em; opacity: 0; } button { margin-top: 5em; } </style> ``` 我们来分析一下。在我们的模板中,我们有几张图片。其中四只是爪子,另一只是狐狸。在`mounted`下,我们有一个 GSAP 时间轴,每个爪子都有一个动画,通知它在`0.5`秒的持续时间内逐渐淡入(`opacity:1`)并放大(`scale:1`),并具有弹性。注意爪子的位置是 `<.3`,这意味着它们在动画结束前的 0.3 秒内反弹。 从概念上讲,这里没有我们在前一个示例中介绍的新东西。但是,如果我们的动画需要变得越来越复杂怎么办。 例如,我们可能还需要向狐狸添加动画。 在这种情况下,如果我们创建专门用于对基于爪子的动画进行排序的时间线,以及为基于狐狸的动画进行排序的时间线,则可能会有所帮助。 然后,我们可以将它们都添加或嵌套到主时间轴中。 我们可以使用一些方法来创建这些时间线,现在让我们把以爪子为基础的时间线重构为一种方法。 Now we have a method that we can call, which will create our timeline and return it. In a moment, you’ll see how we can make this timeline available to a master timeline, but first let’s make a method that creates a new timeline for a fox-based animation. 现在我们有了一个可以调用的方法,它将创建我们的时间线并返回它。稍后,您将看到我们如何使这个时间线可用于主时间线,但首先让我们提供一个为基于狐狸的动画创建新时间轴的方法。 *src/views/Master.vue*: ```js foxTL() { let tl = gsap.timeline() tl.to('#fox', { opacity: 1, filter: 'blur(0)', scale: 1, duration: 0.4, ease: 'slow' }) return tl } ``` 现在我们有一个单独的时间线来放入我们目前所有的基于 fox 的动画,我们可以在将来添加这些动画。 > 旁注:因为我们的时间轴正在淡出不透明性和模糊的狐狸,我们只需要添加一些样式到狐狸的CSS,如下: *src/views/Master.vue*: ```css #fox { height: 8em; width: 8em; opacity: 0; filter: blur(2px); } ``` 这样,我们的狐狸将开始隐形和模糊,因此,当它动画进入视野,它不再模糊成为了焦点。 ## 创建主时间轴 现在我们有了两种方法,每种方法都创建自己的时间轴,我们可以创建一个主时间轴并将这些时间轴添加到其中。 我们将在导入语句下方初始化主时间轴: *src/views/Master.vue*: ``` … import gsap from 'gsap' let masterTL = gsap.timeline() export default { … ``` 虽然我们可以像以前一样使用我们的`mounted`挂钩,但是我想展示如何添加交互性来触发主时间线,因此让我们添加一个新的`play`方法,当运行时,它将把我们的其他时间轴添加到主时间轴,然后`play()`它。 *src/views/Master.vue*: ```js … methods: { play () { masterTL.add(this.pawsTL()) masterTL.add(this.foxTL()) masterTL.play() }, … ``` 现在需要一个按钮来触发这个方法,现在把它添加到模板中: *src/views/Master.vue*: ``` <template> <div> <div id="container"> <img class="paws first" src="../assets/paws.png" alt="fox-paws" /> <img class="paws second" src="../assets/paws.png" alt="fox-paws" /> <img class="paws third" src="../assets/paws.png" alt="fox-paws" /> <img class="paws fourth" src="../assets/paws.png" alt="fox-paws" /> <img id="fox" src="../assets/fox.png" alt="fox-logo" /> </div> <button @click="play">Play</button> </div> </template> ``` 现在有了一个可以点击的按钮了,它会把嵌套的爪子时间轴和狐狸时间轴添加到我们的主时间线上,然后播放整个时间轴: *src/views/Master.vue*: ``` <template> <div> <div id="container"> <img class="paws first" src="../assets/paws.png" alt="fox-paws" /> <img class="paws second" src="../assets/paws.png" alt="fox-paws" /> <img class="paws third" src="../assets/paws.png" alt="fox-paws" /> <img class="paws fourth" src="../assets/paws.png" alt="fox-paws" /> <img id="fox" src="../assets/fox.png" alt="fox-logo" /> </div> <button @click="play">Play</button> </div> </template> <script> import gsap from 'gsap' let masterTL = gsap.timeline() export default { methods: { play() { masterTL.add(this.pawsTL()) masterTL.add(this.foxTL()) masterTL.play() }, pawsTL() { let tl = gsap.timeline() tl.to('.first', { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }) tl.to( '.second', { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }, '<.3' ) tl.to( '.third', { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }, '<.3' ) tl.to( '.fourth', { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }, '<.3' ) return tl }, foxTL() { let tl = gsap.timeline() tl.to('#fox', { opacity: 1, filter: 'blur(0)', scale: 1, duration: 0.4, ease: 'slow' }) return tl } } } </script> <style scoped> #container { display: flex; flex-direction: row; justify-content: center; margin-top: 5em; } #fox { height: 8em; width: 8em; opacity: 0; filter: blur(2px); } .paws { transform: scale(0); width: 2.5em; height: 2.5em; margin-top: 50px; margin-right: 0.8em; opacity: 0; } button { margin-top: 5em; } </style> ``` ## 为啥不使用 SVGs 呢? 您可能已经注意到,在这些例子中,为了简化这些概念的教学,我们只是使用 PNG。 在一个广泛应用于多种设备尺寸的生产级应用程序中,你会希望使用 SVG(可缩放矢量图形)。 使用 SVG 是一个非常深入和广泛的主题,我们没有时间在本课中讨论,但是在 Real World Vue 课程中涉及到了它。 # 小结 让我们在这一课中,我们学习了如何在时间轴内嵌入时间轴,以便在我们的应用中创建更加模块化和可伸缩的动画。恭喜你完成动画制作。希望你现在感觉更自信,更有能力为 Vue 应用引入更多的动作、流动和视觉创意。 # 参考 [VueMastery - Animating Vue](https://coursehunters.online/t/vuemastery-animating-vue/2548) [Vue 中的动画效果](https://www.cnblogs.com/skyflask/p/10990482.html)