Skip to content

使css高度auto支持过渡动画

前言

在平时的开发中,我们经常遇见各种展开与收起的场景,例如子菜单,手风琴,提示框,下拉框的等,这种效果通常都是利用transition过渡高度实现的,但众所周知,heightwidth要支持transition过渡动画必须设置成具体的数值,要想让高不定或者设置成auto 的元素支持过渡动画,我收集了三种实现方法:

max-height过渡

其一是设置最大或高度,将max-height 设置成一个元素高度基本不可能达到的值,然后transition过渡max-height;

效果如下:

使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画

这样实现的过渡缺点是很明显的:由于过渡过程是按照最大高度过渡的,假如max-height 为1000px,元素实际高度为300px并且过渡时长为1s时,元素在0.3s的时候就已经展开完成了,这就显得展开过程很快,而当收回的时候,点击按钮会等待0.7s元素才开始动画,有很明显的延迟。

配合js实现过渡

其二实现方式是配合js,在元素展开状态改变后元素还没来得及渲染时,获取元素高度,并设置给元素从而实现动画

使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
点击查看源码
vue
<script setup lang="ts">
import {ref} from "vue";

const boxRef = ref();
const isExpand = ref(false);
const duration = 200;
const toggle = () => { 
  isExpand.value = !isExpand.value;
  if (isExpand.value) {
    boxRef.value.style.height = 'auto';
    requestAnimationFrame(() => {
      //在设置height:auto后下一帧获取元素高度
      const height = boxRef.value.getBoundingClientRect().height || 0;
      //先将元素高度设置为0,再在下一帧设置为上一步获取的高度,这样可以实现0-height的transition
      boxRef.value.style.height = '0px';
      requestAnimationFrame(() => {
        boxRef.value.style.height = height + 'px';
        //在动画结束后,再将元素高度设置为auto
        setTimeout(() => {
          boxRef.value.style.height = 'auto';
        }, duration);
      })
    })
  } else {
    //先获取元素高度,再在下一帧设置为0,这样可以实现height-0的transition
    const height = boxRef.value.getBoundingClientRect().height || 0;
    boxRef.value.style.height = height + 'px';
    requestAnimationFrame(() => {
      boxRef.value.style.height = '0px';
    })
  }
}
</script>

<template>
  <el-button type="primary" class="button" @click="toggle">点击{{isExpand?'收起':'展开'}}</el-button>

  <div class="box" ref="boxRef" :style="{'transition-duration':duration+'ms'}">
    <div class="content">
      <div v-for="item in 10">使css高度auto支持过渡动画</div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.button {
  margin-bottom: 20px;
}

.box {
  border-radius: 5px;
  background-color: rgba(0, 0, 0, 0.5);
  color: #fff;
  transition: height 0.5s;
  overflow: hidden;
  height: 0;

  .content {
    padding: 10px;
  }
}
</style>

通过js配合transition成功的实现了元素不定高度的过渡动画,但是可以看到,整个实现过程是很复杂的,而且必须完全理解动画实现的每一个步骤才能实现。

利用grid布局中的fr单位实现过渡

有没有一种不用js存粹使用css的实现方法呢?还真有,便是接下来介绍的使用gird布局中的fr实现过渡。

gird布局中有一个全新的单位fr,用于定义网格轨道大小的弹性系数。grid布局比较复杂,三言两语是说不清楚的,有兴趣的同学可自行查阅相关文档,但在这里我们仅仅需要知道的是gird的基础排列方式以及fr单位的作用。transition只能过渡设置具体数值的属性,而fr 单位也是一个具体的数值,并且它的作用最直观的体现就是能影响元素的宽高,所以在此我们可以利用fr来实现动画的过渡。

首先我们用grid编写出这样一个布局:

主要代码如下:

html
<div class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>
css
.grid{
  display: grid;
  height: 300px;
  grid-template-rows: 1fr 1fr 1fr;
}

这里的grid-template-rows表示布局中元素行方向上排列方式,属性值1fr 1fr 1fr表示每一个item占有1等分剩余高度空间,也就是每个item分得100px。

grid-template-rows设置为1fr 2fr 1fr时,效果如下:

grid-template-rows设置为1fr 0fr 1fr时,效果如下:

可以看到,中间的item设置为2fr时,元素的高度为150px,也就是父元素.grid高度的一半,这表示该元素分得的剩余高度为2 / (1 + 2 + 1) = 0.5,当设置为0fr的时候,元素高度变成了0,分得的剩余空间0 / (1 + 0 + 1)也等于0。

此时,将grid-template-rows设置上过渡,并加上展开与收起状态控制:

css
.grid{
  transition: grid-template-rows 0.3s;
}

中间的item像一张大口张开闭合,元素从1fr0fr实现了过渡。

再将上下两个item去掉,将父元素grid-template-rows改为1fr并不设置高度,就实现了元素的展开与收起

使css高度auto支持过渡动画

为什么不起作用呢?其实这是由grid的最小尺寸规则决定的,此时的最小高度是min-content,也就是由内部文本决定的,而父元素能撑开的高度也是文本高度,所以就没有出现收缩效果,这时我们可以将item的overflow设置为hidden,超出部分隐藏,使得最小高度为0,保险起见,父元素overflow也设置为hidden,这样就能使父元素收回到0。

使css高度auto支持过渡动画

gird还有grid-template-columns属性,表示设置列方向上子元素剩余空间,按照上面的方法,我们可以实现宽度的收缩与展开:

使css高度auto支持过渡动画

细心的同学可能会发现,上面的例子还是有些不足的:border在收起后并没有被收进去,宽度的过渡会使文字在过渡过程中不断重新换行,我的解决方案是,再将item包裹一层元素,border设置到item元素上,不设置到这层元素,文字包裹进一层文字层包裹层,文字包裹层设置固定宽度(好像这又回到最初的亚子,其实也可以宽度直接固定数值过渡,我在这里为方便便顺便实现了),padding值也设置到文字包裹层,便可同时实现宽高的过渡动画。

使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画
使css高度auto支持过渡动画

总结

到此,我便介绍完了三种使css高度auto支持过渡动画的方法,三种方法各有优劣,max-height/max-widht,简单实用但效果不佳,js配合实现可靠兼容性好但过程难懂,代码复杂gridfr只用css实现但grid属性兼容性没那么好,如果不考虑兼容性问题的话,我推荐使用第三种方法,在vue中,也可以将其封装成一个组件,方便调用,这里可以参考我自己封装的一个组件:CollapsibleBox.vue