经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
Vue 不定高展开动效原理详解
来源:jb51  时间:2022/6/20 12:21:58  对本文有异议

使用场景

在大多数 APP 中,都有问答模块,类似于下面这种(bilibili 为例):

问答模块的静态页面开发并不复杂,也没有特殊的交互。唯一有一点难度应该是回答部分的展开特效。

  • 展开时,需要从上往下将回答部分的 div 慢慢撑开,上面的箭头也要有旋转的特效。
  • 收回时,需要从下往上将回答部分的 div 慢慢缩小,上面的箭头也要有旋转的特效。

对于一般的展开、隐藏特效,只需要在对应元素的 height 上面增加过渡效果即可。但问题是: 不知道对应的 div 的高度,其高度是内部的元素自动撑开的,此时直接在 height 属性上面添加过渡效果会失效(后面会说明原因)。

对于箭头的旋转,则只需要在箭头元素的 transform 上面增加过渡效果,然后让其旋转 180 度(rotateZ(180deg))即可,这个比较好实现。

背景

今天做需求时,正好需要做这种特效。也就是上面的第一张图。先介绍一下列表的数据结构和其 DOM 结构。

列表数据结构如下:

  1. // Item 表示问答的每一项
  2. interface Item {
  3. Q: string; // 问题
  4. A: string; // 回答
  5. show: boolean; // 是否展示
  6. }
  7.  
  8. // List 表示问答列表
  9. type List = Item[];

项目中并未使用 TypeScript,这里用 interface 是为了方便理解。

DOM 结构(Vue 版本)如下:

  1. <div class="qa panel">
  2. <div class="qa__title">
  3. 常见问题
  4. </div>
  5. <div class="list-qa">
  6. <div
  7. v-for="(item, ind) in qaList"
  8. :key="ind"
  9. class="list-qa__item"
  10. >
  11. <div class="list-qa__question">
  12. <span>{{ item.Q }}</span>
  13. <span class="list-qa__question__arrow" />
  14. </div>
  15. <span class="list-qa__answer">
  16. {{ item.A }}
  17. </span>
  18. </div>
  19. </div>
  20. </div>

上面的结构简化了一些交互逻辑和展示逻辑,默认问答列表的每一项都会展示。最外层包裹了一层 div,上面是标题,下面是问答列表,问答列表的每一项包括问题、箭头 icon 和答案。

实现

因为项目使用的框架是 Vue,所以以 Vue 为例,来分析一下如何实现它,以及其实现的原理。

回答是否展示,可以用一个变量控制,这里是 item 的 show 属性。使用 v-show 实现,因为用户可能会多次点击箭头,导致回答频繁地展示或隐藏。

  1. <div
  2. v-for="(item, ind) in list"
  3. :key="ind"
  4. class="list-item"
  5. >
  6. <!-- 。。。省略不相关元素。。。 -->
  7. <span
  8. v-show="item.show"
  9. class="list-answer"
  10. >
  11. {{ item.A }}
  12. </span>
  13. </div>

transition 组件

在 Vue 中,可以使用 transition 组件来为元素添加动态效果。transition 组件让我们可以为使用条件渲染(v-if、v-show)的元素添加进入、离开时的过渡效果。

  1. <div id="demo">
  2. <button v-on:click="show = !show">
  3. Toggle
  4. </button>
  5. <transition name="fade">
  6. <p v-if="show">hello</p>
  7. </transition>
  8. </div>
  1. .fade-enter-active, .fade-leave-active {
  2. transition: opacity .5s;
  3. }
  4. .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  5. opacity: 0;
  6. }

这样在 name 为 fade 的 transition 组件包裹的 p 标签展示和隐藏时,会有一个 0.5s 的淡入淡出效果。

过渡效果原理

在展示时,p 标签的 opacity(透明度)会从 0(.fade-enter 类选择器中设定的值)开始增加,经过 0.5s 之后,增加至 opacity: 1(元素默认的透明度 opacity 为 1)。

在隐藏时,p 标签的 opacity(透明度)会从 1 开始减少,经过 0.5s 之后,减少至 opacity: 0.fade-leave-to 类选择器中设定的值)。

这样就实现了淡入淡出效果。

同样的,如果我们想让一个元素展示时高度从 0 开始增加,经过某一个时间,达到具体的值;隐藏时高度从该具体值开始减少,经过某一个时间,达到 0。这样就能实现我们前面需要的效果。

我们可以用 css transition 为某一个元素设置过渡效果,过渡效果作用在这个元素的某个属性上、过渡效果的时长等。

  1. .box {
  2. transition: height 1s;
  3. }

上面代表为 class 为 box 的元素设置了过渡效果,作用在它的 height 属性上面,过渡效果的时长为 1s。当该元素的高度从某一个值变化到另一个值时,就会有一个长为 1s 的过渡效果。

过渡效果的本质是: 当作用的属性的值变化时,并不会立即从一个值变为另一个值,而是在变化的过程中,将中间状态呈现出来。

例如:设置了过渡效果的元素的高度(height)从 0 变化到 100px 时,并不是直接从 0 变化到 100px 的,其变化过程是一个连续的状态,从 0 到 1px,从 1px 到 2px······直到 100px。把中间的高度展现出来,就可以让用户看到过渡效果。

再例如:设置了过渡效果的元素的透明度(opacity)从 0 变化到 1 时,并不是直接从 0 变化到 1 的,其变化过程也是一个连续的状态,从 0 到 0.1,从 0.1 到 0.2······直到 opacity 为1。这样用户就可以看到一个元素从透明状态逐渐变得清晰。当然,并不一定就是从 0 变化到 0.1,然后从 0.1 变化到 0.2,这个过程是一个连续的过程,它的值在慢慢增加,增量是多少并不重要。

需要实现过渡效果,就需要一个起始态和一个终止态,浏览器能够从起始态逐步过渡到终止态。也就是从起始态到终止态之间的部分是连续的,是可以计算的,这样浏览器才能把中间的状态给我们呈现出来。

再回到之前的问题:不知道 div 的高度,其高度是内部的元素自动撑开的,此时直接在 height 属性上面添加过渡效果会失效。

为什么会失效?为什么会失效?为什么会失效?

对于一个 div,如果它的高度是由子元素撑开的,那么它的 css 样式 height 属性的值为 auto。从 0 变到 auto,或者从 auto 变到 0,其中间状态都是不可计算的,浏览器没发给我们展示出中间状态,所以我们看不到过渡效果。

既然从 0 变到 auto,或者从 auto 变到 0,中间状态无法计算,那我们可以显式地告诉浏览器一个数值,应该从 0 变到多少,或者从多少变到 0,让浏览器可以计算出中间状态,这样不就能看到过渡效果了吗?

解决

当展开时,起始态为 0,我们通过 getComputedStyle(element).height 得到元素的具体高度 x(终止态)。给元素设置 transition 属性,然后将元素的高度从 0 变到 x,这样就能实现展开的动效了。

  1. <transition
  2. name="slide"
  3. @before-enter="beforeEnter"
  4. @enter="enter"
  5. @after-enter="afterEnter"
  6. @before-leave="beforeLeave"
  7. @leave="leave"
  8. @after-leave="afterLeave"
  9. >
  10. <span
  11. v-show="item.show"
  12. class="list-answer"
  13. >{{ item.A }}</span>
  14. </transition>
  1. beforeLeave(el) {
  2. // 给元素设置过渡效果
  3. el.style.transition = '0.3s height ease-in-out';
  4. // 高度变化时,让其内容隐藏
  5. el.style.overflow = 'hidden';
  6. },
  7. leave(el) {
  8. el.style.height = 'auto';
  9. // 设置高度为具体的值
  10. el.style.height = window.getComputedStyle(el).height;
  11. // 强制浏览器回流,否则浏览器会合并两次元素的高度更改(回流重绘的知识)
  12. el.offsetHeight;
  13. el.style.height = '0px';
  14. },
  15. afterLeave(el) {
  16. // el.style.height = null;
  17. // 收尾工作,展示完过渡效果之后,设为原来的值
  18. el.style.transition = '';
  19. el.style.overflow = 'visible';
  20. },

这里需要给元素设置 overflow: hidden,在元素高度小于内部内容的高度时,才会隐藏内容。

同样地,隐藏时先通过 getComputedStyle(element).height 得到元素的具体高度 x(起始态),给元素设置 transition 属性,然后将元素的高度从 x 变到 0,这样就能实现隐藏的动效了。

  1. beforeEnter(el) {
  2. // 给元素设置过渡效果
  3. el.style.transition = '0.3s height ease-in-out';
  4. // 高度变化时,让其内容隐藏
  5. el.style.overflow = 'hidden';
  6. },
  7. enter(el) {
  8. el.style.height = 'auto';
  9. // 保存元素原来的高度
  10. const endWidth = window.getComputedStyle(el).height;
  11. el.style.height = '0px';
  12. // 强制浏览器回流,否则浏览器会合并两次元素的高度更改(回流重绘的知识)
  13. el.offsetHeight;
  14. el.style.height = endWidth;
  15. },
  16. afterEnter(el) {
  17. // el.style.height = null;
  18. // 收尾工作,展示完过渡效果之后,设为原来的值
  19. el.style.transition = '';
  20. el.style.overflow = 'visible';
  21. },

箭头的旋转动效就比较简单了。先设置过渡效果,然后只需要在点击箭头的时候,动态为这个元素添加一个类名,让其旋转属性生效(rotateZ(180deg));当再一次点击的时候,去掉这个类名就好了。

  1. <span
  2. class="list-question__arrow"
  3. :class="{'list-question__rotate-arrow': !item.show}"
  4. @click="onClickPromblem(ind)"
  5. />
  1. onClickPromblem(index) {
  2. const qaItem = this.qaList[index];
  3. this.$set(qaItem, 'show', !qaItem.show);
  4. },
  1. list-question__arrow {
  2. width: 12px;
  3. height: 12px;
  4. background: url(https://xxx.com/arrowup.png) center no-repeat;
  5. transition: transform .4s;
  6. }
  7. list-question__rotate-arrow {
  8. transform: rotateZ(180deg);
  9. transition: transform .4s;
  10. }

到此这篇关于Vue 不定高展开动效原理详解的文章就介绍到这了,更多相关Vue 不定高展开 内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号