透过垂直水平居中来理解Flex

733 阅读14分钟

水平垂直居中的方法

方案1:Flex

NSFileHandle_19.png

方案2:Position + Transform

NSFileHandle_14.png

方案3:Grid

NSFileHandle_24.png

方案4:Position + Margin

NSFileHandle_6.png

普通的 position: abosulte 配合 margin: auto 是无法实现垂直水平居中的,之所以例子中可以实现主要是因为激活了 abosulte 的流体性,而激活的方式就是"对立方向发生定位",简单来说就是例子中上右下左都有定位,即使是0,也会激活横向(左、右定位激活)和竖向(上、下定位激活)方向的流体性,至于流体性的其他特性在后续会慢慢道来。

方案5:Table-Cell

NSFileHandle_4.png

方案6:Writing-Mode

NSFileHandle_41.png

兼容性:

NSFileHandle_30.png

知识扩展:

实际上writing-mode这个CSS属性在上古时代就诞生了,IE5.5浏览器就已经支持了(也就是2001年),那为啥一直沉寂了差不多20年呢?

那是因为,在很长一段时间里,FireFox, Chrome这些现代浏览器都不支持writing-mode,writing-mode基本上就是IE浏览器的私有产物,大家对IE一直没啥好感,爱屋及乌的,自然对writing-mode也不待见。

但随着前端技术的发展,大家逐渐摆脱了一叶障目的桎梏,各大现代浏览器纷纷对writing-mode实现了更加标准的支持(主要得益于FireFox浏览器的积极跟进),writing-mode的兼容性已经不成问题了,终于可以发挥它本身的逆天特性了。

特性:

  1. 水平方向也能margin重叠

The bottom margin of an in-flow block-level element always collapses with the top margin of its next in-flow block-level sibling, unless that sibling has clearance.

W3C清清楚楚写的bottom margin和top margin会重叠;然而,这是CSS2文档中的描述,在CSS3的世界中,由于writing-mode的存在,这种说法就不严谨了,应该是对立流方向的margin值会发生重叠。换句话说,如果元素是默认的水平流,则垂直margin会重叠;如果元素是垂直流,则水平margin会重叠。

  1. 可以使用margin:auto实现垂直居中

道理很简单,在块状元素里,margin: 0 auto;可以实现水平居中,现在水平流被writing-mode改为垂直流,那么auto生效的方向自然变为垂直方向,至于为什么块状元素内margin: auto 0;不能实现垂直居中,我们稍后会讲到。

  1. 可以使用text-align:center实现图片垂直居中

  2. 可以使用text-indent实现文字下沉效果

文字下沉.gif

  1. 可以实现全兼容的icon fonts图标的旋转效果

方案7:::after

NSFileHandle_34.png

方案8:::before

NSFileHandle_26.png

最优雅的水平垂直居中的方法

方案9:Flex + Margin

NSFileHandle_8.png

这里的 display: flex 替换成 display: inline-flex | grid | inline-grid 也是可以的。

Flex布局中的margin

不知道大家有没有思考过,为什么在Block元素中,margin: auto;只能实现水平居中,而无非垂直居中呢?

NSFileHandle_39.png

CSS2 Visual formatting model details: 10.3.3

If both margin-left and margin-right are auto, their used values are equal, causing horizontal centring.

CSS2 Visual formatting model details: 10.6.3

If margin-top, or margin-bottom are auto, their used value is 0.

所以,垂直方向的auto的值都为0,这也就解释了为什么不能垂直居中了。

为什么水平方向可以居中但垂直方向的值就是0了呢?

想解释清楚这个问题,就不得不提到CSS中的width/height两个属性了。

我们先来看看W3C上对于这两个属性的定义:

NSFileHandle_33.png

可以看到,width/height的默认值都是auto。

那如果一个不添加任何属性的div会是什么样子呢?

NSFileHandle_23.png

这不由得又引发了我的疑问,为什么相同的默认值最后的表现却不一样呢?

这就涉及到了另一个关键字“auto”。

auto到底做了什么?

NSFileHandle_2.png

width:

  • 对于块元素来说,width 取值为 auto 时,它的width就是其父容器的宽度(类似于100%)

  • 对于内联元素或内联块元素,width 取值为 auto时,它的width就是内容的宽度(即min-content)

延伸:auto 和 100% 的区别

我们总说块级元素的宽度会自动填满父级,这时 width: auto 就相当于 width: 100%,但这俩个属性真的一样吗?

NSFileHandle_16.png

可以看出再子元素没有多余的尺寸样式时,auto 和 100% 表现一致。

于是,我们在子级中加入一个尺寸样式,padding: 0 20px

NSFileHandle_10.png

这时,width: 100% 的子元素边框已经超出父级宽度了,而auto状态下的子级表现依然良好。

这是因为块级元素自带容器的流体性,我们可以想象一下水倒入容器时,只要水足够多就会自然的充满整个容器的宽度。

NSFileHandle.gif

而 width: auto 在块级元素中就具有水一般的原生流动性,这是一种 margin/padding/border/content 内容区域自动分配水平空间的机制。

但是,块级元素一旦被设置了宽度,就打破了这一规则,即“流动性丢失”,表现为浏览器无法根据原生流动性来计算元素的宽度了。

所以,大家在工作中,要记住“无宽度”这条准则,少了代码、计算、维护,何乐不为呢?

height

  • 对于块元素或者内联元素来说,height的默认值是元素内容的高度,当内容为空时,则会显示为height: 0;

  • 对于flex、grid等盒格式化模型除外。

那么,我们现在试着来解释一下为什么水平方向可以但垂直方向的值就是0了呢?

流动性:当元素在该方向上具备流动性,即默认撑满包裹它的父级时,则该方向具有 margin: auto 来分配剩余空间的特性,在 block 下就表现为水平方向的水平居中。

包裹性:当元素在该方向上表现为包裹性,即默认为内容的大小时,则该方向没有可以被分配的剩余空间,在 block 下就表现为垂直方向默认高度为0.

既然auto会根据盒格式化模型来改变auto值的计算方式,那么Flex下的margin又哪些特性呢?

Flex中的margin: auto

还是这个最优雅的垂直居中代码为例

NSFileHandle_8.png

这里要延伸2个概念:

  • FFC(flex formatting context)

  • GFC(grid formatting context)

上述的margin: auto实现垂直居中在这两种和格式化模型下都可以实现垂直居中,即下述dispaly取值范围:


display: flex;

display: inline-flex;

display: grid;

display: inline-grid;

我们今天主要围绕Flex来分析margin的表现,grid布局有机会我们再单独讲一期。

继续查看CSS文档

NSFileHandle_17.png

第二条的大意就是flex格式化上下文中,设置了 margin: auto 的元素,在通过 justify-content 和 align-self 进行对齐之前,任何正处于空闲的空间都会分配到该方向的自动 margin中去,最重要的是垂直方向也会自动去分配这个剩余空间。

注意:如果我们使用了margin: auto;来分配该维度的剩余空间,那对应维度的justify-content 和 align-self这些对齐方式就失效了。

自动margin的神奇效果

首先margin几乎可以实现justify-content 和 align-self对齐方式的每一种效果,这里主要说一下一些实用的布局小技巧。

  1. 居右的登录按钮

NSFileHandle.webp


<ul class="g-nav">

<li>导航A</li>

<li>导航B</li>

<li>导航C</li>

<li>导航D</li>

<li class="g-login">登陆</li>

<li>注册</li>

</ul>


.g-nav {

display: flex;

}

.g-login {

margin-left: auto;

}

  1. 垂直方向上的多行居中

NSFileHandle_2.webp


<div class="g-container">

<p>这是第一行文案</p>

<p>这是第二行文案</p>

<p class="s-third">1、剩余多行文案需要垂直居中剩余空间</p>

<p class="s-forth">2、剩余多行文案需要垂直居中剩余空间</p>

<p>这是最后一行文案</p>

</div>


.g-container {

display: flex;

flex-wrap: wrap;

flex-direction: column;

}

.s-third {

margin-top: auto;

}

.s-forth {

margin-bottom: auto;

}

  1. 实现粘性footer布局

NSFileHandle_1.webp


<div class="g-container">

<div class="g-real-box">

Content

</div>

<div class="g-footer"></div>

</div>


.g-container {

height: 100vh;

display: flex;

flex-direction: column;

}

.g-footer {

margin-top: auto;

flex-shrink: 0;

height: 30px;

background: deeppink;

}

当然在flex下使用justify-content: space-between;也可以很好的解决。

总结:

  • 块格式化上下文中margin-top 和 margin-bottom 的值如果是 auto,则他们的值都为0;

  • flex 格式化上下文中,在通过 justify-content; 和 align-self; 进行对齐之前,任何剩余空间都会分配到该方向的 margin: auto; 中去;

  • 单个方向上的自动 margin 也非常有用,它的计算值为该方向上的剩余空间;

  • 使用了margin: auto;的 flex 子项目,它们父元素设置的 justify-content 以及它们本身的 align-self 将不再生效。

剩余空间和溢出空间

在margin: auto;的案例中,有一个高频词——剩余空间。

要了解Flex中的剩余空间,首先要了解Flex中容器的概念。

NSFileHandle_21.png

剩余空间:子容器在父容器的“主轴”上还有多少空间可以“瓜分”,这个可以被“瓜分”的空间就叫做剩余空间。

NSFileHandle_5.png

既然是子容器瓜分父容器的空间,所以分配的属性一定属于子容器,也就是flex属性。

面试的时候,Flex相关的问题我第一个总会问flex属性的默认值什么?

flex默认值: 0 1 auto;

flex是一个聚合属性,它是由3个属性构成的,所以我们把默认值拆解就会变成flex-grow: 0; flex-shrink: 1; flex-basis: auto;

flex-basis

flex-basis是flex属性的最后一个属性,为什么会第一个讲呢?带着这个疑问我们来了解下flex-basis。

MDN: flex-basis 指定了 flex 元素在主轴方向上的初始大小。如果不使用 box-sizing 改变盒模型的话,那么这个属性就决定了 flex 元素的内容盒(content-box)的尺寸。


.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {

width: 150px;

flex-basis: 300px;

}

NSFileHandle_43.png

可见最后flex-basis覆盖了主轴方向上的width。

Note: 当一个元素同时被设置了 flex-basis (除值为 auto 外) 和 width (或者在 flex-direction: column 情况下设置了height) , flex-basis 具有更高的优先级.

这时,我不禁又产生了疑问,flex-basis: auto; 的表现又是什么样子呢?

于是,我设计了几种在 flex-basis: auto; 情况下的实验:

  1. 无内容 + 无宽度

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {}

NSFileHandle.png

  1. 有内容 + 无宽度

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {}

NSFileHandle_18.png

  1. 有内容 + 有宽度

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {

width: 300px;

}

NSFileHandle_27.png

所以,我们可以得出如下结论:

如果未指定 flex-basis,即 flex-basis: auto;,flex-basis 将回退到 width 属性。如果同时未指定 width 属性,flex-basis 将回退到基于 Flex 项目内容的计算宽度值(computed width),如果改变主轴方向为垂直则宽度 width 属性,将被替换为 height 属性。

得出结论之后,又产生了新的疑问,flex-basis可以覆盖width,那 max-width/min-width 和 !import 之间谁的优先级更大呢?

想知道结果最简单的方式就是继续测试:

  1. max-width

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {

max-width: 150px;

flex-basis: 300px;

}

NSFileHandle_31.png

  1. min-width

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {

min-width: 300px;

flex-basis: 150px;

}

NSFileHandle_9.png

  1. !import

.wrap {

width: 500px;

height: 200px;

display: flex;

justify-content: center;

}

.item {

flex-basis: 300px;

width: 150px !important;

}

NSFileHandle_32.png

其实!import的测试没有必要,稍微思考下就能清楚,import改变的只是同属性的覆盖,并不能提升该属性的优先级。

最终可以得出结论:

flex-basis的优先级:max-width/min-width > flex-basis > width > content-width

知道了优先级我们才可以计算flex盒模型下的容器最终尺寸(final flex-basis),才会进一步的计算出剩余/溢出空间,这就是flex-basis的介绍放在最前面的原因。

flex-grow

flex-grow 定义子容器的瓜分剩余空间的比例,默认为0,即如果存在剩余空间,也不会去瓜分。

NSFileHandle_15.png

现在设置CSS为:


.item-3 {

width: 100px;

flex-grow: 3;

}

.item-4 {

width: 100px;

flex-grow: 1;

}

那么,现在各item的宽度分别为多少?(默认案例都是主轴为row情况下,如果主轴方向改为垂直,则width要替换为height)

计算flex-grow属性下的子容器宽度,就需要知道剩余空间和分配原则,grow是分配主容器的剩余空间

freeSpace = container-size - sum(item-final-flex-basis)

剩余空间 = 容器尺寸 - sum(子级的最终尺寸)

所以得出剩余空间为(500px - 100px * 4 = 100px)

知道剩余空间后,套用宽度计算公式:

width = width + freeSpace * (growRatio / sum(growRatio))

宽度 = 当前宽度 + 剩余空间 * (瓜分系数 / 总瓜分系数)

所以可以得出,item3.width = '175px',item4.width = '125px'

flex-shrink

说完flex-grow就该轮到flex-shrink了。

MDN: flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。

依旧看的云里雾里,不过别急,上面说完了剩余空间,既然shrink是收缩规则,那自然要轮到溢出空间登场了。

NSFileHandle_28.png

现在设置CSS为:


.item-3 {

width: 150px;

flex-shrink: 3;

}

.item-4 {

width: 150px;

flex-shrink: 1;

}

这时我们可以得出溢出空间为:

freeSpace = container-size - sum(item-final-flex-basis)

溢出空间 = 容器尺寸 - sum(子级的最终尺寸)

所以,得出溢出空间为(500 - 4 * 150 = '-100px')

接着,套用宽度计算公式:

width = width + freeSpace * (shrinkRatio / sum(shrinkRatio))

宽度 = 当前宽度 + 溢出空间 * (收缩系数 / 总收缩系数)

注意:我们刚才提到了Flex的默认值是 0 1 auto,这也就意味着item1 和 item2 的 shrinkRatio 也为别为 1, 所以,sum(shrinkRatio) = 6

所以可以得出,item3.width = '100px',item1~4.width = '133.333px'

至此,有没有发现grow和shrink的计算公式几乎是一致的,具体公式总结如下:

空间宽度:freeSpaceWidth = wrapWidth - sum(itemWidth)

子元素宽度:itemWidth = itemWidth + freeSpaceWidth * (ratio / sum(ratio))

注:flex-grow 和 flex-shrink 都不支持负数。

聚合属性flex

现在我们知道了,flex属性默认值是(0 1 auto),也知道了这三个值分别对应哪几个子属性。

那么,我们通常写样式时最常用的 flex: 1; 平均分配宽/高时,到底它的值会是什么呢?

NSFileHandle_40.png

我们可以看到,flex-grow 由0变成了1,另外 flex-basis 也由 auto 变为了 0%,即在没有max/min尺寸属性的限制下,子级的final-flex-basis为0,这时所有 flex: 1 的子级均分剩余空间。

那么,继续测试:

  1. flex: none

NSFileHandle_38.png

  1. flex: 10px

NSFileHandle_1.png

  1. flex: content

NSFileHandle_7.png

  1. flex: 1 2

NSFileHandle_35.png

  1. flex: 1 10px

NSFileHandle_3.png

  1. flex: 1 0 1%

NSFileHandle_42.png

现在总结如下:

NSFileHandle_29.png

至此 flex 属性我们就基本了解了。

Flex中的不常用属性

order

MDN: order 属性规定了弹性容器中的可伸缩项目在布局时的顺序。元素按照 order 属性的值的增序进行布局。拥有相同 order 属性值的元素按照它们在源代码中出现的顺序进行布局,默认值为 0。

NSFileHandle_12.png

NSFileHandle_13.png

思考一下,如果使用vue循环来生成DOM,再通过order来打乱顺序,DOM的index是否会乱序


<div class="wrap">

<div v-for="(i, index) in 5" :key="index" class="item" :class="`item${index}`">

{{`item${index}`}}

<br/>

</div>

</div>

NSFileHandle_37.png

可以看到子级的排序变了,但是index并没有变。

3A61B740-6497-4360-82AF-894899B7E7DC.png

通过Chrome审查DOM,可以发现DOM的结构并没有发生改变,而浏览器显示的排序已经改变,仔细分析一下不难发现,order只是CSS属性,它能改变的只是DOMTree和CSSOM结合后的RenderTree,而我们审查的仅仅是DOMTree,这也解释通了为什么排序乱了,而index分配并没有变化。

该属性的使用场景猜想:利于css进行排序、轮播。

flex-wrap

MDN: flex-wrap 指定 flex 元素单行显示还是多行显示 。如果允许换行,这个属性允许你控制行的堆叠方向。

  • nowrap(默认)

flex 的元素被摆放到到一行,即不换行,这可能导致内容溢出 flex 容器。 子级排列方向与父级定义的 flex-direction 的方向一致。

  • wrap

允许换行,子级元素被打断到多个行中。子级排列方向与父级定义的 flex-direction 的方向一致。

利用这个属性,我们可以很简单的实现瀑布流布局。

NSFileHandle_22.png

  • wrap-reverse

与 wrap 的行为一样,但是排列方向与父级排列方向相反。

  1. wrap-reverse + vue循环 index的分配

NSFileHandle_36.png

  1. wrap-reverse + order

NSFileHandle_13.png

chrome审查元素发现 DOM Tree排序并没有变化

CE62910B-52AB-49BF-AD69-841D61885AEF.png

思考题

  1. 计算每个item宽度?

NSFileHandle_11.png

  1. 瀑布流到底如何布局?

NSFileHandle_22.png

flex基础语法思维导图

Flex布局.png

参考链接