关于grid布局的一些思考
0. 基础概念
0.1. 什么场景
当一个设计者在进行排版初期时,想要对布局进行整体划分
0.2. 什么问题
只能局部设计,无法从宏观角度设计并逐步深入细节。
0.3. 解决方案
通过对布局进行简单的「数据结构」描述,比如「一棵树」描述,生成可视化的布局结果
0.4. 概念阐述
在容器上进行布局轨道的划分,当内容填充进去时会按照轨道排列
具体解释: Basic concepts of grid layout
1. 数据结构
从具象的角度来出发,一般我们的划分一个区域的过程是这样的:
- 先画一个「矩形」
- 将这个「矩形」进行分割,从而形成新的矩形区域
做一次最简单的划分:
- 先画一个「矩形」
1
- 将这个「矩形」分为2个,形成了
1.1
和1.2
- 再将
1.2
分为2个,形成了1.2.1
和1.2.2
那么可以表达为以下结构
1 | 1 |
使用对象和数组进行表达
1 | { |
2. 嵌套关系
采用类似html的方式来进行表达
1 | <area name="1"> |
3. 结构演化
仅凭最基础的数据结构,是无法进行视觉表达的,因为不知道这块区域有多大,也不知道它在哪儿。在视觉上,我们认为的「区域」是有「面积」概念。在这里我们通过「宽度」和「高度」对「区域」进行描述
假设我们做这样一个布局
3.1 宽高结构
1 | { |
按照以上结构,虽然对「面积」有了描述,但是对于「位置」并没有阐释。那我们再引入一个包含x
,y
两个维度的坐标系。
我们用点
的「坐标」来描述一个「矩形」,我们需要四个点
前提是矩形的相邻边都是互相垂直的
在一个平面直角坐标系中,一个点
的描述为
用4个点来表达一个矩形
我们可以认为矩形是4
条线段组成的图形
- 第一条线段:
- 第二条线段:
- 第三条线段:
- 第四条线段:
3.2 坐标结构
1 | { |
全部用点来表达显得非常复杂,考虑在web
前端的场景中,我们来试图简化一下
既然是矩形,其表达可以如下
在浏览器的区域里,我们可以认定原点是在左上角。
- 原点右侧
x+
- 原点左侧
x-
- 原点上方
y-
- 原点下方
y+
原点坐标是(0,0)
,并且位于左上方
那么我们可以只通过4个参数值来表达一个区域:
- x
左上原点x坐标
即
- y
左上原点y坐标
即
- width
矩形宽度,实际是右下点的x坐标
即
- height
矩形高度,实际是右下点的y坐标
即
3.3 宽高和坐标
1 | { |
到这一步,我们发现这其实就是「绝对定位」,那么其缺点也显而易见了。就是每一块区域都是独立的,相互之间的关系需要你严格设定。
大多数的设计师在布局的时候都是先画一个区域,然后再画一个区域,逐个进行调整。然后带来的问题就是,设计师最讨厌用设计工具做「表格」类的东西。
为什么会这样?
因为设计工具提供的布局方式就是「绝对定位」
根据以上所示,绝对定位里的概念总结起来就只有一个,叫做「坐标」
3.4 方向、继承以及分配
从上面的结构,我们可以发现几个事情
1
的原点和1.1
的原点是同一个1.2
的宽度等于1
的宽度减去1.1
的宽度1.2.1
和1.2.2
的宽度等于1.2
的宽度
我们要完成的是「布局」而非「绘图」,所以我们可以让这件事情变得更加简单一些。我们分步来说明
3.4.1 划定基础区域
我们首先来制作一块区域,它的名字叫1
,并且宽度是1024
、高度也是1024
,原点位置为(0,0)
,(0,0)
我们把它设为缺省值,所以不表达。
1 | { |
3.4.2 引入分割方向概念
我们现在对其分割,那么这个时候就产生了一个问题,我们是按「水平」方向分割,还是按「垂直」方向分割?因此,我们引入方向(维度)概念。我们简单的定义一下
- column
列
,即在「水平」方向上进行分割 - row
行
, 即在「垂直」方向上进行分割
那么我们对1
进行水平方向上的分割
1 | { |
3.4.3 引入份数概念
那么分割为几份呢?这时候我们又要引入一个份数的概念,那么我们现在就分成2
份。
1 | { |
3.4.4 在另一方向上的继承
这个时候我们意识到,因为是按「水平」方向分割,那么每一块的高度应该是和原来的区域是一样的,相当于每一块都继承了原有区域的高度,但是宽度因为被分割了,所以没法继承。
我们得考虑宽度的分配,现在我们将宽度进行了分割,那么每一块占原来宽度的多少呢?我们假设是均分,每一块就是50%
,那么用一个值去表达
1 | { |
上面的结构可以表述为
有一块
1024
*1024
的区域,拆分成2
列,每一列的宽度是1024
*0.5
同理如果是按「垂直」方向分割的话,则高度需要被分割,而宽度可以继承
3.4.5 引入等份单位
那如果不等分怎么办?我们的一个distribute
不足矣表达,那么如果distribute
是一个数组的话,数组的length
其实就代表了part
的数量,而且可以直接用对象的key
来标识方向,所以可以形式上简化一下
1 | { |
但是以上结构会遇到所有的distribute
之和不是1
,既可能出现大于1
,也可能出现小于1
的情况。这时候我们引入一个「等份」的概念,叫做fr
。1fr
代表空间的1等份
比如说,我们把空间分成3块,第一块占3fr
,第二块占2fr
那么这两块区域的实际宽度是
- 第一区域宽度:
- 第二区域宽度:
这下原来的数据结构可以表达为
1 | { |
3.4.6 绝对数值
并不是所有的情况都是按等份来分配的。比如某块区域,我们想让它占据一个绝对数值的宽度。
1 | { |
这个时候的计算其实是,把200
宽度的区域从原来的里面扣除,在进行等份的计算
两块区域的实际宽度是
- 第一区域宽度:
- 第二区域宽度:
- 第三区域宽度:
那如果全都是绝对数值会怎样
比如有一个区域宽是800
,我们分3
列,每一列宽度是200
1 | { |
这个时候其实就是分开处理了,1
本身是一个「区域」,3
列构成的是另一个「区域」
如果分割的区域之和超出了被分割的区域又该怎么样呢?
其实和上面是一样的,实际上原有的「区域」和实际的「被分割区域」是分开处理的
总结一下就是
当「区域分割规则」都是「绝对数值」的时候,那就直接「划定区域」,而不去做「分割」
前端小伙伴这个时候都笑了,写了这么废话不就是CSS的grid么
我们把最初的数据结构来一次梳理
1 | { |
是不是很简单,但是上面其他的所有部分是不是很复杂?
这就是为什么我们平时用别人做好的东西很简单,但是如果自己实现却无从下手的原因。
3.4.7 回归到坐标系
以上的结构我们再还原到坐标系画线的模式中,看看是怎样的
先画一个最大的「区域」1
1 | lineTop: (0,0)(width,0) //(0,0)(1024,0) |
画1.1
1 | lineTop: (0,0)(width*1/(1+1),0) //(0,0)(512,0) |
画1.2
1 | lineTop: (width*1/(1+1),0)(width,0) //(512,0)(1024,0) |
画1.2.1
1 | lineTop: (width*1/(1+1),0)(width,0) //(512,0)(1024,0) |
画1.2.2
1 | lineTop: (width*1/(1+1),height*1/(1+1))(width,height*1/(1+1)) //(512,512)(1024,512) |
4. 程序实现
我们发现「布局」的根本方式是「绝对坐标」,在之上抽象出了「栅格(grid)」。非常幸运的是,CSS
已经提供了这层抽象。
如果做成vue
组件的话,形式化方面不想采用近似iview
的<row>
</col>
嵌套结构,而是只有一个area
组件,通过一个数据结构来完成area
的递归
4.1 组件属性
基于以上的观察,我们可以构建一个组件叫做area
,那么它应该包含以下属性
- width 区域的宽度
可选
- height 区域的高度
可选
- columns 区域按列切分
- rows 区域按行切分
- name 区域的名称
但是我们之前说过,在另一个方向上存在继承关系,那么应该避免同时设置columns
和rows
,另外考虑到宽度和高度可以从上级继承,我们做一次改进
width 区域的宽度可选
height 区域的高度可选
- grid: 可选
columns
rows
- distribute: 分配规则
- name 区域的名称
4.2 递归
按grid进行递归,如果发现没有grid,就停止递归
5. 用户体验
5.1 兼容性
不是所有的浏览器都支持grid,保险的做法是使用百分比
但是我们就不选择兼容方案了,激进一点
5.2 显性表达区域
用边框表达,还是用颜色表达
- 用边框会占用区域
- 颜色要求相邻的区域颜色不重复
5.3 模板
- 左右
- 上下
- 圣杯