整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CSS 如何让auto height完美支持过渡动画?

所周知,高度在设置成auto关键词时是不会触发transition过渡动画的,下面是伪代码

div{
  height: 0;
  transition: 1s
}
.wrap:hover div{
  height: auto
}


效果如下

如果希望展开时有过渡动画,例如这样

通常是借助 JS 动态去获取元素的高度(还有些麻烦的,需要渲染后才能知道高度)。其实CSS 也有一个巧用max-height适配动态高度的解决方案,下面是伪代码

div{
  max-height: 0;
  transition: 1s
}
.wrap :hover div{
  max-height: 800px /*大概的值,需要超过元素高度*/
}


有兴趣的可以参考这篇文章:CSS 奇技淫巧:动态高度过渡动画,但是有一个局限性,高度差异越大,过渡效果越糟糕,假设元素真实高度只有 100px,如果 max-height为800px,那只有前1/8有动画,就像这样

那么,有没有更好的方式来解决这个问题呢?

当然也是有的,这次就来介绍一个全新的方式来实现动态高度过渡,一起看看吧

一、grid 布局中的 fr 单位

grid布局中有一个全新的fr单位,用于定义网格轨道大小的弹性系数。grid布局比较复杂,三言两语不可能说清楚,有兴趣的可以参考grid相关教程,例如

  • 张鑫旭老师的写给自己看的display: grid布局教程
  • 大漠老师的现代 Web 布局

这里简单介绍一下fr单位的用途,比如有这样一个布局

<div class="grid">
  <span class="item">1fr</span>
  <span class="item">1fr</span>
  <span class="item">1fr</span>
</div>


关键样式如下

.grid{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}


可以得到这样的效果

这里的repeat(3, 1fr)其实就是1fr 1fr 1fr的简写,表示 3 等分剩余空间。还可以设置在垂直方向上

.grid{
  grid-template-rows: repeat(3, 1fr);
}


效果如下

也可以改变各自的分配比例

.grid{
  grid-template-rows: 1fr 2fr 1fr;
}


效果如下

现在来看一种特殊情况,还可以将分配比例设置为0fr

.grid{
  grid-template-rows: 0fr 2fr 1fr;
}


效果如下

是不是有点奇怪,0fr怎么和1fr占比相同呢?

其实这是由grid的最小尺寸规则决定的,此时的最小高度是min-content,也就是由内部文本决定的。如果没有文字,0fr自然就不占空间了,下面是去除文字后的效果

如果想保留文字并且不占空间怎么办呢?可以直接手动设置最小尺寸

span{
  min-height: 0
}


这样0fr也会不占空间

还可以借助超出隐藏,彻底隐藏子内容

.grid{
  overflow: hidden;
}
span{
  min-height: 0
}


效果如下

应该还是比较容易理解吧,那么和动画有啥关系呢?接着往下看

二、grid 中的过渡动画

有同学可能纳闷了,为啥要折腾这个0fr呢?下面就来揭晓

如果重新设置1fr,子内容又会重新出现

.grid{
  grid-template-rows: 1fr 2fr 1fr;
}


下面重点来了,grid中的fr单位也是支持过渡动画的(0fr=>1fr )

.grid{
  grid-template-rows: 1fr 2fr 1fr;
  transition: .3s
}


效果如下

由于高度是由内部文本撑开的,也就是高度不确定,而0fr到1fr的过渡变化,相当于实现了 高度不固定的过渡动画

进一步精简一下,可以实现这样的效果

这就是高度不固定动画的雏形了,换个文本多一点也完美支持

完整 demo可以查看以下任意链接

  • CSS auto height transition (codepen.io)
  • CSS auto height transition (runjs.work)

三、自适应高度动画的两个实例

现在根据上面的原理来实现两个实例。

首先来看文章最开头的示例,HTML 结构是这样的

<div class="wrap">
  <button class="trigger">鼠标放上来试试</button>
  <div class="grid">
    <div><p>欢迎关注前端侦探,这里有一些有趣的、你可能不知道的HTML、CSS、JS小技巧技巧,比如这篇文章,如何让 auto height 支持过渡动画?一起看看吧</p></div>
  </div>
</div>


简单修饰一下,应该比较容易,可以得到这样的效果

然后通过上面的技巧将下拉内容隐藏起来,关键样式如下

.grid{
  display: grid;
  grid-template-rows: 0fr;
  transition: .3s;
  overflow: hidden;
}
.grid>div{
  min-height: 0;
}


然后通过hover触发显示,也就是改变grid-template-rows

.wrap:hover .grid{
  grid-template-rows: 1fr;
}


这样就实现了不定高度的过渡动画

完整 demo可以查看以下任意链接

  • CSS auto height drop (codepen.io)
  • CSS auto height drop (runjs.work)

如果仅仅是悬浮窗口,由于是绝对定位,不会影响其他布局,其实是可以用transform scale 进行缩放的,再来看另外一个更加实用的例子,常见的菜单展开收起效果,就像这样

可以看到,在展开的同时,下方的元素也被挤压下去了,这样效果更加自然,也是transform实现不了的,这里的切换是通过:checked实现的,关键代码如下

<input hidden type="checkbox" id="s1" checked />
<label for="s1">工作台</label>
<div class="sub">
  <ul>
    <li>项目列表</li>
    <li>数据配置器</li>
  </ul>
</div>


ul{
  min-height: 0;
}
.sub {
  display: grid;
  grid-template-rows: 0fr;
  transition: 0.3s;
  overflow: hidden;
}
:checked ~ .sub {
  grid-template-rows: 1fr;
}


完整 demo可以查看以下任意链接

  • CSS auto height menu (codepen.io)
  • CSS auto height menu (runjs.work)

四、注意事项和一些局限性

下面是一些注意事项。

这里的动画源于grid-template-rows的变化,也就是0fr到1fr

**注意,注意,注意,**这里的0fr必须是0fr,不能是0或者0px,必须是fr单位

下面是改为40px的效果(动画丢失)

再者,0fr也不支持calc计算,直接被认为不合法

这意味着,例如你希望默认有一个固定高度(非0),然后展开到自适应高度,这种方法是无法实现过渡动画的,略遗憾

五、最后总结一下

最后再来回顾一下实现关键过程

.grid{
  display: grid;
  grid-template-rows: 0fr;
  transition: .3s;
  overflow: hidden;
}
.grid>div{
  min-height: 0;
}
.wrap:hover .grid{
  grid-template-rows: 1fr;
}


主要是利用了grid弹性布局可以实现过渡动画的特点,下面总结一些实现要点

  1. 高度在设置成auto关键词时不会触发transition过渡动画
  2. grid布局中的fr单位,可以用于定义网格轨道大小的弹性系数
  3. grid布局的尺寸计算规则是由最小高度决定的,默认是min-content,也就是由内部文本决定的,可以通过手动设置min-height来实现0fr
  4. grid布局也支持过渡动画(0fr=>1fr ),这样就实现高度不固定的过渡动画
  5. 要使过渡动画生效,必须是fr单位,其他单位不行,也不能通过calc计算
  6. 这种方法只能实现初始高度为0到自适应高度的过渡变化,略微遗憾

最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤


作者:XboxYan
链接:https://juejin.cn/post/7196843994030342200

ss过渡

首先看一下这个版本的vue css过渡和动画的应用方式:

<p class="msg" v-if="show" v-transition="expand">Hello!</p>
<p class="animated" v-if="show" v-transition="bounce">Look at me!</p>
.msg {
    transition: all .3s ease;
    height: 30px;
    padding: 10px;
    background-color: #eee;
    overflow: hidden;
}
.msg.expand-enter, .msg.expand-leave {
    height: 0;
    padding: 0 10px;
    opacity: 0;
}

.animated {
    display: inline-block;
}

.animated.bounce-enter {
    animation: bounce-in .5s;
}

.animated.bounce-leave {
    animation: bounce-out .5s;
}

@keyframes bounce-in {
    0% {
        transform: scale(0);
    }

    50% {
        transform: scale(1.5);
    }

    100% {
        transform: scale(1);
    }
}

@keyframes bounce-out {
    0% {
        transform: scale(1);
    }

    50% {
        transform: scale(1.5);
    }

    100% {
        transform: scale(0);
    }
}

可以看到也是通过指令的方式,这个版本只有支持两个类,一个是进入的时候添加的v-enter,另一个是离开时候添加的v-leave

先看一下这个指令:

module.exports = {
    isLiteral: true,// 为true不会创建watcher实例
    bind: function () {
        this.update(this.expression)
    },
    update: function (id) {
        var vm = this.el.__vue__ || this.vm
        this.el.__v_trans = {
            id: id,
            // 这个版本的vue可以使用transitions选项来定义JavaScript动画
            fns: vm.$options.transitions[id]
        }
    }
}

这个指令不会创建watcher,因为指令的值要么是css的类名,要么是JavaScript动画选项的名称,都不需要进行观察。指令绑定时所做的事情就是给el元素添加了个自定义属性,保存了表达式的值,这里是expandJavaScript动画函数,这里是undefined

要触发动画需要修改if指令show的值,假设开始是false,我们把它改成true,这会触发if指令的update方法,根据第三篇vue0.11版本源码阅读系列三:指令编译最后部分对if指令过程的解析我们知道在进入时调用了transition.blockAppend(frag, this.end, vm),在离开时调用了transition.blockRemove(this.start, this.end, this.vm),这里显然会调用blockAppend

// block是包含了if指令绑定元素的代码片段
// target是一个注释节点,在if指令绑定元素所在的位置
exports.blockAppend = function (block, target, vm) {
    // 代码片段的子节点
    var nodes = _.toArray(block.childNodes)
    for (var i = 0, l = nodes.length; i < l; i++) {
        apply(nodes[i], 1, function () {
            _.before(nodes[i], target)
        }, vm)
    }
}

遍历元素调用apply方法:

var apply = exports.apply = function (el, direction, op, vm, cb) {
    var transData = el.__v_trans
    if (
        !transData ||// 没有过渡数据
        !vm._isCompiled ||// 当前实例没有调用过$mount方法插入到页面
        (vm.$parent && !vm.$parent._isCompiled)// 父组件没有插入到页面
    ) {// 上述情况不需要动画,直接跳过
        op()
        if (cb) cb()
        return
    }
    var jsTransition = transData.fns
    // JavaScript动画,下一小节再看
    if (jsTransition) {
        applyJSTransition(
            el,
            direction,
            op,
            transData,
            jsTransition,
            vm,
            cb
        )
    } else if (
        _.transitionEndEvent &&
        // 页面不可见的话不进行过渡
        !(doc && doc.hidden)
    ) {
        // css
        applyCSSTransition(
            el,
            direction,
            op,
            transData,
            cb
        )
    } else {
        // 不需要应用过渡
        op()
        if (cb) cb()
    }
}

这个方法会判断是应用JavaScript动画还是css动画,并分发给不同的函数处理。函数套娃,又套到了applyCSSTransition方法:

module.exports = function (el, direction, op, data, cb) {
    var prefix = data.id || 'v'// 此处是expand
    var enterClass = prefix + '-enter'// expand-enter
    var leaveClass = prefix + '-leave'// expand-leave
    if (direction > 0) { // 进入
        // 给元素添加进入的类名
        addClass(el, enterClass)
        // op就是_.before(nodes[i], target)操作,这一步会把元素添加到页面上
        op()
        push(el, direction, null, enterClass, cb)
    } else { // 离开
        // 给元素添加离开的类名
        addClass(el, leaveClass)
        push(el, direction, op, leaveClass, cb)
    }
}

可以看到进入和离开的操作是有区别的,本次我们是把show的值改成true,所以会走direction > 0的分支,先给元素添加进入的类名,然后再把元素实际插入到页面上,最后调用push方法;

如果是离开的话会先给元素添加离开的类名,然后调用push方法;

看一下push方法:

var queue = []
var queued = false
function push (el, dir, op, cls, cb) {
    queue.push({
        el  : el,
        dir : dir,
        cb  : cb,
        cls : cls,
        op  : op
    })
    if (!queued) {
        queued = true
        _.nextTick(flush)
    }
}

把本次的任务添加到队列,注册了个异步回调在下一帧执行,关于nextTick的详细分析请前往vue0.11版本源码阅读系列五:批量更新是怎么做的。

addClassop都是同步任务,会立即执行,如果此刻有多个被这个if指令控制的元素都会被依次添加到队列里,结果就是这些元素都会被添加到页面上,但是因为我们给进入的样式设置的是height: 0;opacity: 0;,所以是看不见的,这些同步任务执行完后才会去异步队列里把注册的flush方法拉出来执行:

function flush () {
  // 这个方法用来触发强制回流,确保我们添加的expand-enter样式能生效,不过我试过不回流也能生效
  var f = document.documentElement.offsetHeight
  queue.forEach(run)
  queue = []
  queued = false
}

flush方法遍历刚才添加到queue里的任务对象调用run方法,因为进行了异步批量更新,所以同一时刻有多个元素动画也只会触发一次回流:

function run (job) {
    var el = job.el
    var data = el.__v_trans
    var cls = job.cls
    var cb = job.cb
    var op = job.op
    // getTransitionType方法用来获取是transition过渡还是animation动画,原理是判断元素的style对象或者getComputedStyle()方法获取的样式对象里的transitionDuration或animationDuration属性是否存在以及是否为0s
    var transitionType = getTransitionType(el, data, cls)
    if (job.dir > 0) { // 进入
        if (transitionType === 1) {// transition过渡
            // 因为v-enter的样式是隐藏元素的样式,另外因为给元素设置了transition: all .3s ease,所以只要把这个类删除了自然就会应用过渡效果
            removeClass(el, cls)
            // 存在回调时才需要监听transitionend事件
            if (cb) setupTransitionCb(_.transitionEndEvent)
        } else if (transitionType === 2) {// animation动画
            // animation动画只要添加了v-enter类自行就会触发,需要做的只是监听animationend事件在动画结束后把这个类删除
            setupTransitionCb(_.animationEndEvent, function () {
                removeClass(el, cls)
            })
        } else {
            // 没有过渡
            removeClass(el, cls)
            if (cb) cb()
        }
    } else { // 离开
        // 离开动画很简单,两者都是只要添加了v-leave类就可以触发动画
        // 要做的只是在监听动画结束的事件把元素从页面删除和把类名从元素上删除
        if (transitionType) {
            var event = transitionType === 1
            ? _.transitionEndEvent
            : _.animationEndEvent
            setupTransitionCb(event, function () {
                op()
                removeClass(el, cls)
            })
        } else {
            op()
            removeClass(el, cls)
            if (cb) cb()
        }
    }
}

现在看一下当把show的值由true改成false时调用的blockRemove方法:

// start和end是两个注释节点,包围了该if指令控制的所有元素
exports.blockRemove = function (start, end, vm) {
    var node = start.nextSibling
    var next
    while (node !== end) {
        next = node.nextSibling
        apply(el, -1, function () {
            _.remove(el)
        }, vm, cb)
        node = next
    }
}

遍历元素同样调用apply方法,只不过参数传了-1代表是离开。

到这里可以总结一下vuecss过渡:

1.进入

先给元素添加v-enter类,然后把元素插入到页面,最后创建一个任务添加到队列,如果有多个元素的话会一次性全部完成,然后在下一帧来执行刚才添加的任务:

1.1css过渡

v-enter类名里的样式一般是用来隐藏元素的,比如把元素的宽高设为0、透明度设为0等等,反正让人看不见就对了,要触发动画需要把这个类名删除了,所以这里的任务就是移除元素的v-enter类名,然后浏览器会自己应用过渡效果。

1.2css动画

animation不一样,v-enter类的样式一般是定义animation的属性值,比如:animation: bounce-out .5s;,只要添加了这个类名,就会开始动画,所以这里的任务是监听动画结束事件来移除元素的v-enter类名。

2.离开

css过渡和动画在离开时是一样的,都是给元素添加一个v-leave类就可以了,v-leave类要设置的样式一般和v-enter是一样的,除非进出效果就是要不一样,否则都是要让元素不可见,然后添加一个任务,因为样式上不可见了但元素实际上还是在页面上,所以最后的任务就是监听动画结束事件把元素真正的从页面上移除,当然,相应的v-leave类也是要 从元素上移除的。

JavaScript动画

在这个版本要使用JavaScript进行动画过渡需要使用声明过渡选项:

Vue.transition('fade', {
  beforeEnter: function (el) {
    // 元素插入文档之前调用,比如提取把元素变成不可见,否则会有闪屏的问题
  },
  enter: function (el, done) {
    // 元素已经插入到DOM,动画完成后需要手动调用done方法
    $(el)
      .css('opacity', 0)
      .animate({ opacity: 1 }, 1000, done)
    // 返回一个函数当动画取消时被调用
    return function () {
      $(el).stop()
    }
  },
  leave: function (el, done) {
    $(el).animate({ opacity: 0 }, 1000, done)
    return function () {
      $(el).stop()
    }
  }
})

就是定义三个钩子函数,定义了JavaScript过渡选项,在transition指令的update方法就能根据表达式获取到,这样就会走到上述apply方法里的jsTransition分支,调用applyJSTransition方法:

module.exports = function (el, direction, op, data, def, vm, cb) {
    if (data.cancel) {
        data.cancel()
        data.cancel = null
    }
    if (direction > 0) { // 进入
        // 调用beforeEnter钩子
        if (def.beforeEnter) {
            def.beforeEnter.call(vm, el)
        }
        op()// 把元素插入到页面dom
        // 调用enter钩子
        if (def.enter) {
            data.cancel = def.enter.call(vm, el, function () {
                data.cancel = null
                if (cb) cb()
            })
        } else if (cb) {
            cb()
        }
    } else { // 离开
        // 调用leave钩子
        if (def.leave) {
            data.cancel = def.leave.call(vm, el, function () {
                data.cancel = null
                // 离开动画结束了从页面移除元素
                op()
                if (cb) cb()
            })
        } else {
            op()
            if (cb) cb()
        }
    }
}

css过渡相比,JavaScript过渡很简单,进入过渡就是在元素实际插入到页面前执行以下你的初始化方法,然后把元素插入到页面,接下来调用enter钩子随你怎么让元素运动,动画结束后再调一下vue注入的方法告诉vue动画结束了,离开过渡先调一下你的离开钩子,在你的动画结束后再把元素从页面上删除,逻辑很简单。

TML&CSS核心基础

//xia栽ke:chaoxingit.com/552

HTML的优势:

  1. 易学易用: HTML的语法相对简单,容易学习和理解,即使是初学者也能快速上手。
  2. 标准化: HTML是一个被广泛接受和支持的标准,确保了网页在不同浏览器和设备上的一致性显示。
  3. 语义化: HTML提供了丰富的标签,能够清晰描述文档的结构和内容,有助于搜索引擎优化(SEO)和可访问性。
  4. 跨平台性: 可在几乎所有操作系统和设备上运行,使网页能够被广泛访问。

CSS的优势:

  1. 样式分离: CSS允许将样式与内容分离,使得更改样式不影响内容结构,提高了代码的可维护性和可复用性。
  2. 样式控制: CSS提供了丰富的样式控制,可以改变文本样式、布局、颜色等,使网页更具吸引力和易读性。
  3. 响应式设计: 可通过CSS媒体查询和布局技术实现响应式设计,使网页能够适应不同尺寸和设备,提升用户体验。
  4. 兼容性: CSS也是一个标准化的技术,确保网页在不同浏览器和设备上显示一致性。
  5. 动画与交互: CSS提供了动画和过渡效果的支持,使得网页能够实现更丰富的交互和视觉效果,增强用户体验。