57436787_p0

在 css3.0 中 flex 突出一个莽。解决了css多年的尴尬症–人们能把人送上太空,却依然无法在页面上做出垂直居中这种简单的事。而需要各种 hack 简直让人爆炸。而现在有了 flex 想水平居中就水平居中,想垂直居中就垂直居中。

说起 flex 就不得不提到阮一峰的教程,里面的内容简洁明了,非常值得一读。

本篇文章不会详细解释每个属性如何使用,仅仅是我个人的一些理解而不是教程。

flex 前言

flex 实际上解决了最核心的两个需求,这是一个非常平常的需求。令人惊讶的是人们却到现在才实现了他。

  1. 控制子元素在以父元素的宽为轴的水平方向上的位置
  2. 控制子元素在以父元素的高为轴的垂直方向上的位置

这两点看起来见简单得不行。但是在没有 flex 的时代,我们就是没有很好的方法解决他。

第一点我们习惯用margin: 0 auto解决。这能让计算机自动计算外边距,以达到水平居中的效果。但是有这么几个问题:

  • 父元素要求有确定宽度
  • 这个方法只针对单个子元素居中,无法直接处理多个子元素居中问题。
  • 这个方法实际上无法处理元素靠左靠右的问题。比如我想要子元素向右对齐

第二点由于历史原因,甚至没有像第一点那样简陋的解决方法,光是单独垂直居中就能单独写一大篇文章了。这里不再叙述。

知道了最核心的两点需求后。我们就不难理解 flex 为什么是不像 float 那样 right or left 明了,而是依赖了水平的主轴(main axis)和垂直的交叉轴(cross axis)这两个概念。表述一个平面坐标需要两个轴,这连小学生都知道了。

如图所示,其中主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。

至于为什么不用 x y 轴,left or right这概念。原因一个是不够清晰,容易混淆,一个是不够抽象,不够 dry。

xy 轴 or 主轴交叉轴?

如上文所述 flex 中所用的概念是主轴交叉轴,而不是比较直观的xy轴。这个让我在一开始稍微有点迷惑,不过了解了flex的各个属性后,就豁然开朗了。

flex-direction

所谓的主轴交叉轴跟 flex-direction 属性相关。flex-direction 属性决定主轴的方向,即项目的排列方向。
其值包括:

  • row (default) 水平方向,从左到右。
  • row-reverse 水平方向,从右到左,row 的逆序。
  • column 垂直方向,从上到下
  • column-reverse 垂直方向,从下到上 ,column 的逆序。

可以看到,所谓的主轴并不限定就是水平方向,也有可能是垂直方向。你可以在通过flex-direction属性决定主轴是哪一个。

这时候 flex 其他的属性就针对 flex-firection 定好的主轴交叉轴进行计算处理。

如果我们套用 x y 轴的概念,你就会发现我们需要重复四套对应不同方向而相同概念的属性。

前面我说过 xy 轴的概念不够抽象,在这里就可见端倪了。当我们希望用 xy 轴来尽可能简洁的描述同样的事物时,你会发现重复无法避免,而且会有所混淆。

而将它归纳为主轴,交叉轴的概念时,我们只需要一套属性,外加一个指明方向的属性 flex-firection,实际上是把flex-direction对应的四种模式归纳为统一的概念。

为什么在交叉轴上可以有align-self 而在主轴上没有对应的属性?

在阐述这个问题前,简单的回顾一下align-self

align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

回到我们的问题上,有这个问题原因是我在使用flex有时希望主轴上的第一个或者最后一个子元素可以像设置了 space-bewteen 那样靠向两端,而其他的就保持居中。 当然,并没有这样属性。然而在交叉轴上却有align-self这就让我很好奇了。为什么在交叉轴上可以有align-self 而在主轴上没有对应的属性?

原因在于 align-itemsjustify-content 的作用域不一样。

justify-content 确定主轴上的一个或者多个元素之间如何对齐,作用于父元素
align-items 确定每个元素在交叉轴上对齐哪条线,作用于子元素。

举个例子

当我们定义justify-content:space-around,每个元素之间等大间隔,而这个间隔是如何计算出来的?

间隔宽度=(父元素的宽度-各个子元素之间的和)/间隔数

可以看到每个元素最终的所在位子是相互影响的,每个元素更宽一些或者更高一些都会有所不同。我们并不能覆盖单个子元素而不影响其他的子元素。

而当我们定义 align-items:center 时就有所不同了。
它实际上是令到每个子元素的中线都对齐交叉轴的中点,某个子元素更宽一些或者更高一些也并不会影响其他子元素的中线对齐交叉轴的中点。上面提到align-self可以让某个子元素单独覆盖align-items的值,其实就是换了一个对齐的点,由于其子元素的不相关性,这对其他元素没有任何副作用。

综上所述,主轴与交叉轴不同的计算方式导致了只有交叉轴上有单独调整的可能。

当然,回到我最先的问题,假如我想要的第一个元素在主轴上对齐flex-start,而其他继续居中。一个简单的方法就是把需要居中的元素包多一层像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
.box{
display:flex;
justify-content:flex-start;
}
.content{
display:flex;
justify-content:center;
}
</style>

<div class="box">
<h1 class="children"></h1>
<section class="content">
<p class="children"></p>
<p class="children"></p>
</section>
</div>

flexbox 如何实现自适应侧边居中对齐?

大概来说就是这种布局

这种布局的一种应用场景就是form表单的 label跟input的排列方式
左边是label,右边是input框

其中,这个布局有以下的一些特性

  1. 左侧的紫色部分长度是不确定,绿色部分固定宽
  2. 紫色与绿色对齐的轴线由紫色部分最长的一段决定,其他较短紫色部分靠右对齐轴线

嗯,看到难题所在了吗
我们还是先上代码吧

1
2
3
4
5
6
7
8
// 以jade pug 代码为例
section.form
.row
.left
.right
.row
.left
.right
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.form{
display: block;
width:900px;
height:600px;
padding: 0 20px;
background: #fff;
}
.row{
margin-top: 20px;
display:flex;
justify-content: flex-start;
}
.left{
background: #3a44cc;
width: 200px;
height: 45px;
margin-right: 30px;
}
.right{
background: #006600;
width: 400px;
height: 45px;
}
.other-width{
width:250px;
}

可以看到在left等宽的时候,这样已经达到了我们上面图示的效果,然而我们的希望是不等宽。

1
2
3
4
5
6
7
section.form
.row
.left
.right
.row
.left.other-width //这时给第二行的left增加一个宽度
.right

这时我们就可以看到justify-content在默认flex-start值下,两行完全靠左对齐。并不是我们想要的效果。
这时我做了几个尝试。

left 增加父元素元素定宽

像这样

1
2
3
4
5
6
.row
.left-outer
.left
.right


同时添加样式让.left 向右对齐

1
2
3
4
5
6
.left-outer{
width:250px;
display:flex;
justify-content:flex-end;
}

嗯,看起来不错。然而有一个致命的问题是——左侧的元素宽是固定的,而且为了让.left正常显示,必须是定为最大的可能宽度,即使当前页面的.left 元素其实没有这么宽(实际中,宽度最大的可能出现在另一个页面上,我猜你不会希望为此单独设定[2,NaN]次宽度吧?)。这个时候就比较尴尬了,为了配合这个宽度,在不足宽度的页面上,所有的left元素都会向右偏移

像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
.left-outer{
width:300px;
display:flex;
justify-content:flex-end;
}

// 为了让效果更明显....
.left{
width:100px;
}
.other-width{
width:200px;
}

结果是整个页面像是硬生生向右偏移了100px。不行呀。

先分列,再分行

接着我尝试了这个方法

1
2
3
4
5
6
7
section.form
.left-outer
.left
.left.outher-width
right-outer
.right //这时给第二行的left增加一个宽度
.right

直接划分成两列

1
2
3
4
5
6
7
8
9
10
11
12
13
14

.form{
display: flex;
width:900px;
height:600px;
padding: 0 20px;
background: #fff;
}
.left-outer ,.right-outer{
width:min-content;
display:flex;
flex-direction:column;
}

上面的 left-outer 是为让子元素竖向排列,同时min-content的值则是让父元素依照最大不断行内容定宽度,这样一来left-outer的 width 就自然等于.left 子元素最大宽度。当然这里不这样设定他也会是同样效果。

终于让两边对齐了。而且不会像想上一个方法,产生不必要的偏移。看起来也很好的自适应不同的left宽度,可喜可贺。

然而……

  1. 首先,我们把一个应当具有关联的row切断了。如果他们没有明显的关联这样未尝不可。想象一下,假如产品要求在这个row下面添加一个横线分割不同的row,我们该怎么办?
  2. 此外,我们必须维护left-outer与right-outer元素,维护他们与他们的子元素的关系,两边子元素之间的对应关系。
  3. 总结必须要有三点,我想我上面两点的意思是这样不够 dry

逆转裁判出6啦

所以说到底要如何保留row的设计的情况下完成这个事情…

先让我们回到原点

1
2
3
4
5
6
7
section.form
.row
.left
.right
.row
.left
.right

为什么在一开始的这个情况下,无法轴线对齐?

这是因为我们的 .left 元素 宽度不一致,而轴线距离左侧的长度正好是这个宽度,说到底上面两个方法都是为了解决这个问题。难道就没有更好的方法了吗?有,
虽然它一开始就在那里但是我们都没有留意到…… right元素是定宽的。

瞎扯淡啥呢 right元素是定宽的又怎么样?不关事啊=。=

不,听我慢慢到来。

我们一开始这样设定.row

1
2
3
4
5
.row{

display:flex;
justify-content: flex-start;
}

justify-content 为flex-start,center的时候想想都知道不符合我们的需求。但是当justify-content的时候呢?
这有什么嘛,不就是向右对齐吗,不也不符…..欸!!!!

仔细想想由于right元素是等宽的,而当我们取flex-end右对齐的时候,所有right元素是对齐,假如我们从右往左看,我们的轴线因为是从右方算起都间隔了一个right元素的宽度,是完全对齐在一条线上的。当然,left的元素也靠右对齐了

除了整个form元素是靠右的之外,我们已经完美的达到了对齐轴线的问题。是不是很有趣?
接下来我们只要解决靠右的问题就好了。这个解决方法要比上面的直观的多。

首先为什么会靠右到右边缘?flex布局下的元素不设定宽度的话,会自动占满宽度。
这里我们没有设定row的宽度,所以计算为父元素的宽,因此当flex-end的起始位子在右侧边缘。

这个时候,我们就不用在在row上纠结了,就让它占满父元素的宽度就好了。

1
2
3
4
5
6
7
8
section.form
.outer
.row
.left
.right
.row
.left.width250
.right
1
2
3
4
5

.outer{
width:min-content;
}

可以看到,已经成了,可喜可贺。可是这里到底又是什么回事!!!

这里我直接给了多一重的元素,还记得min-content的意义么?定义宽度为子元素的最大不断行宽度。但是row不是等于父元素的宽度吗?死循环?当然不是。宽度会尽可能的小。

接下来的话有点绕
实际上,由于left跟right元素也是有宽度的,父元素的宽度不会小于left和right宽的之和。

如果不同row的子元素宽度之和不相同(就像我们希望的那样)。

我们的父元素在min-content控制下必取最大的行宽。

由上可知,父元素的宽度必定大于等于任意行的行宽。

然后 row 元素的父元素有足够的宽度,row默认100%父元素宽度

结果,所有的row等宽。并且自适应于最长的一个row宽度

f.width=row.width=max(left.width+right.width)

对齐轴线依然有效,而且整个outer元素默认行为的靠左

这不就是我们想要的么?

p.s 这个方法不适合左右都是不定宽的情况