关于grid布局的一些思考

0. 基础概念

0.1. 什么场景

当一个设计者在进行排版初期时,想要对布局进行整体划分

0.2. 什么问题

只能局部设计,无法从宏观角度设计并逐步深入细节。

0.3. 解决方案

通过对布局进行简单的「数据结构」描述,比如「一棵树」描述,生成可视化的布局结果

0.4. 概念阐述

在容器上进行布局轨道的划分,当内容填充进去时会按照轨道排列

具体解释: Basic concepts of grid layout

1. 数据结构

从具象的角度来出发,一般我们的划分一个区域的过程是这样的:

  1. 先画一个「矩形」
  2. 将这个「矩形」进行分割,从而形成新的矩形区域

做一次最简单的划分:

  1. 先画一个「矩形」1
  2. 将这个「矩形」分为2个,形成了1.11.2
  3. 再将1.2分为2个,形成了1.2.11.2.2

那么可以表达为以下结构

1
2
3
4
5
1
|- 1.1
|- 1.2
|- 1.2.1
|- 1.2.2

使用对象和数组进行表达

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
name: '1',
children: [
{
name: '1.1'
},
{
name: '1.2',
children: [
{
name: '1.2.1'
},
{
name: '1.2.2'
}
]
}
]
}

2. 嵌套关系

采用类似html的方式来进行表达

1
2
3
4
5
6
7
<area name="1">
<area name="1.1"></area>
<area name="1.2">
<area name="1.2.1"></area>
<area name="1.2.2"></area>
</area>
</area>

3. 结构演化

仅凭最基础的数据结构,是无法进行视觉表达的,因为不知道这块区域有多大,也不知道它在哪儿。在视觉上,我们认为的「区域」是有「面积」概念。在这里我们通过「宽度」和「高度」对「区域」进行描述

假设我们做这样一个布局

area

3.1 宽高结构

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
27
28
29
{
name: '1',
width: 1024,
height: 1024,
children: [
{
name: '1.1',
width: 512,
height: 1024
},
{
name: '1.2',
width: 512,
height: 1024,
children: [
{
name: '1.2.1',
width: 512,
height: 512
},
{
name: '1.2.2',
width: 512,
height: 512
}
]
}
]
}

按照以上结构,虽然对「面积」有了描述,但是对于「位置」并没有阐释。那我们再引入一个包含xy两个维度的坐标系。

我们用的「坐标」来描述一个「矩形」,我们需要四个

前提是矩形的相邻边都是互相垂直的

在一个平面直角坐标系中,一个的描述为

用4个点来表达一个矩形

我们可以认为矩形是4条线段组成的图形

  • 第一条线段:

  • 第二条线段:

  • 第三条线段:

  • 第四条线段:

3.2 坐标结构

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
{
name: '1',
coords: [
{
x: 0,
y: 0
},
{
x: 1024,
y: 0
},
{
x: 0,
y: -1024
},
{
x: 1024,
y: -1024
}
],
children: [
{
name: '1.1',
coords: [
{
x: 0,
y: 0
},
{
x: 512,
y: 0
},
{
x: 0,
y: -1024
},
{
x: 512,
y: -1024
}
]
},
{
name: '1.2',
coords: [
{
x: 512,
y: 0
},
{
x: 1024,
y: 0
},
{
x: 512,
y: -1024
},
{
x: 1024,
y: -1024
}
]
children: [
{
name: '1.2.1',
coords: [
{
x: 512,
y: 0
},
{
x: 1024,
y: 0
},
{
x: 512,
y: -512
},
{
x: 1024,
y: -512
}
]
},
{
name: '1.2.2',
coords: [
{
x: 512,
y: -512
},
{
x: 1024,
y: -512
},
{
x: 512,
y: -1024
},
{
x: 1024,
y: -1024
}
]
}
]
}
]
}

全部用点来表达显得非常复杂,考虑在web前端的场景中,我们来试图简化一下

既然是矩形,其表达可以如下

在浏览器的区域里,我们可以认定原点是在左上角。

  • 原点右侧x+
  • 原点左侧x-
  • 原点上方y-
  • 原点下方y+

原点坐标是(0,0),并且位于左上方

那么我们可以只通过4个参数值来表达一个区域:

  • x 左上原点x坐标

  • y 左上原点y坐标

  • width 矩形宽度,实际是右下点的x坐标

  • height 矩形高度,实际是右下点的y坐标

3.3 宽高和坐标

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
27
28
29
30
31
32
33
34
35
36
37
38
39
{
name: '1',
x: 0,
y: 0,
width: 1024,
height: 1024
children: [
{
name: '1.1',
x: 0,
y: 0,
width: 512,
height: 1024
},
{
name: '1.2',
x: 512,
y: 0,
width: 512,
height: 1024
children: [
{
name: '1.2.1',
x: 512,
y: 0,
width: 512,
height: 512
},
{
name: '1.2.2',
x: 512,
y: 512,
width: 512,
height: 512
}
]
}
]
}

到这一步,我们发现这其实就是「绝对定位」,那么其缺点也显而易见了。就是每一块区域都是独立的,相互之间的关系需要你严格设定。

大多数的设计师在布局的时候都是先画一个区域,然后再画一个区域,逐个进行调整。然后带来的问题就是,设计师最讨厌用设计工具做「表格」类的东西。

为什么会这样?

因为设计工具提供的布局方式就是「绝对定位」

根据以上所示,绝对定位里的概念总结起来就只有一个,叫做「坐标」

3.4 方向、继承以及分配

从上面的结构,我们可以发现几个事情

  • 1的原点和1.1的原点是同一个
  • 1.2的宽度等于1的宽度减去1.1的宽度
  • 1.2.11.2.2的宽度等于1.2的宽度

我们要完成的是「布局」而非「绘图」,所以我们可以让这件事情变得更加简单一些。我们分步来说明

3.4.1 划定基础区域

我们首先来制作一块区域,它的名字叫1,并且宽度是1024、高度也是1024,原点位置为(0,0)(0,0)我们把它设为缺省值,所以不表达。

1
2
3
4
5
{
name: 1,
width: 1024,
height: 1024
}

3.4.2 引入分割方向概念

我们现在对其分割,那么这个时候就产生了一个问题,我们是按「水平」方向分割,还是按「垂直」方向分割?因此,我们引入方向(维度)概念。我们简单的定义一下

  • column ,即在「水平」方向上进行分割
  • row , 即在「垂直」方向上进行分割

那么我们对1进行水平方向上的分割

1
2
3
4
5
6
{
name: '1',
width: 1024,
height: 1024,
split: 'column'
}

3.4.3 引入份数概念

那么分割为几份呢?这时候我们又要引入一个份数的概念,那么我们现在就分成2份。

1
2
3
4
5
6
7
{
name: '1',
width: 1024,
height: 1024,
split: 'column',
part: 2
}

3.4.4 在另一方向上的继承

这个时候我们意识到,因为是按「水平」方向分割,那么每一块的高度应该是和原来的区域是一样的,相当于每一块都继承了原有区域的高度,但是宽度因为被分割了,所以没法继承。

我们得考虑宽度的分配,现在我们将宽度进行了分割,那么每一块占原来宽度的多少呢?我们假设是均分,每一块就是50%,那么用一个值去表达

1
2
3
4
5
6
7
8
{
name: '1',
width: 1024,
height: 1024,
split: 'column',
part: 2,
distribute: 0.5
}

上面的结构可以表述为

有一块1024 * 1024 的区域,拆分成2列,每一列的宽度是1024 * 0.5

2column

同理如果是按「垂直」方向分割的话,则高度需要被分割,而宽度可以继承

3.4.5 引入等份单位

那如果不等分怎么办?我们的一个distribute不足矣表达,那么如果distribute是一个数组的话,数组的length其实就代表了part的数量,而且可以直接用对象的key来标识方向,所以可以形式上简化一下

1
2
3
4
5
6
{
name: '1',
width: 1024,
height: 1024,
columns: [0.4, 0.6]
}

但是以上结构会遇到所有的distribute之和不是1,既可能出现大于1,也可能出现小于1的情况。这时候我们引入一个「等份」的概念,叫做fr1fr代表空间的1等份

比如说,我们把空间分成3块,第一块占3fr,第二块占2fr

那么这两块区域的实际宽度是

  • 第一区域宽度:

  • 第二区域宽度:

这下原来的数据结构可以表达为

1
2
3
4
5
6
{
name: '1',
width: 1024,
height: 1024,
columns: [1fr, 1fr]
}

3.4.6 绝对数值

并不是所有的情况都是按等份来分配的。比如某块区域,我们想让它占据一个绝对数值的宽度。

1
2
3
4
5
6
{
name: '1',
width: 1024,
height: 1024,
columns: [200, 1fr, 2fr]
}

这个时候的计算其实是,把200宽度的区域从原来的里面扣除,在进行等份的计算

两块区域的实际宽度是

  • 第一区域宽度:

  • 第二区域宽度:

  • 第三区域宽度:

那如果全都是绝对数值会怎样

比如有一个区域宽是800,我们分3列,每一列宽度是200

1
2
3
4
5
6
{
name: '1',
width: 800,
height: 800,
columns: [200, 200, 200],
}

这个时候其实就是分开处理了,1本身是一个「区域」,3列构成的是另一个「区域」

如果分割的区域之和超出了被分割的区域又该怎么样呢?

其实和上面是一样的,实际上原有的「区域」和实际的「被分割区域」是分开处理的

总结一下就是

当「区域分割规则」都是「绝对数值」的时候,那就直接「划定区域」,而不去做「分割」

前端小伙伴这个时候都笑了,写了这么废话不就是CSS的grid么

我们把最初的数据结构来一次梳理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
name: '1',
width: 1024,
height: 1024,
columns: [1fr, 1fr],
children: [
{
name: '1.1'
},
{
name: '1.2',
rows: [1fr, 1fr]
children: [
{
name: '1.2.1'
},
{
name: '1.2.2'
}
]
}
]
}

是不是很简单,但是上面其他的所有部分是不是很复杂?

这就是为什么我们平时用别人做好的东西很简单,但是如果自己实现却无从下手的原因。

3.4.7 回归到坐标系

以上的结构我们再还原到坐标系画线的模式中,看看是怎样的

先画一个最大的「区域」1

1
2
3
4
lineTop: (0,0)(width,0) //(0,0)(1024,0)
lineRight: (width,0)(width,height) //(1024,0)(1024,1024)
lineBottom: (width,height)(0,height) //(1024,1024)(0,1024)
lineLeft: (0,height)(0,0) //(0,1024)(0,0)

1.1

1
2
3
4
lineTop: (0,0)(width*1/(1+1),0) //(0,0)(512,0)
lineRight: (width*1/(1+1),0)(width*1/(1+1),height) //(512,0)(512,1024)
lineBottom: (width*1/(1+1),height)(0,height) //(512,1024)(0,1024)
lineLeft: (0,height)(0,0) //(0,1024)(0,0)

1.2

1
2
3
4
lineTop: (width*1/(1+1),0)(width,0) //(512,0)(1024,0)
lineRight: (width,0)(width,height) //(1024,0)(1024,1024)
lineBottom: (width,height)(width*1/(1+1),height) //(1024,1024)(512,1024)
lineLeft: (width*1/(1+1),height)(width*1/(1+1),0) //(512,1024)(512,0)

1.2.1

1
2
3
4
lineTop: (width*1/(1+1),0)(width,0) //(512,0)(1024,0)
lineRight: (width,0)(width,height*1/(1+1)) //(1024,0)(1024,512)
lineBottom: (width,height*1/(1+1))(width*1/(1+1),height*1/(1+1)) //(1024,512)(512,512)
lineLeft: (width*1/(1+1),height*1/(1+1))(width*1/(1+1),0) //(512,512)(512,0)

1.2.2

1
2
3
4
lineTop: (width*1/(1+1),height*1/(1+1))(width,height*1/(1+1)) //(512,512)(1024,512)
lineRight: (width,height*1/(1+1))(width,height) //(1024,512)(1024,1024)
lineBottom: (width,height)(width*1/(1+1),height) //(1024,1024)(512,1024)
lineLeft: (width*1/(1+1),height)(width*1/(1+1),0) //(512,1024)(512,0)

4. 程序实现

我们发现「布局」的根本方式是「绝对坐标」,在之上抽象出了「栅格(grid)」。非常幸运的是,CSS已经提供了这层抽象。

如果做成vue组件的话,形式化方面不想采用近似iview<row> </col>嵌套结构,而是只有一个area组件,通过一个数据结构来完成area的递归

4.1 组件属性

基于以上的观察,我们可以构建一个组件叫做area,那么它应该包含以下属性

  • width 区域的宽度可选
  • height 区域的高度 可选
  • columns 区域按列切分
  • rows 区域按行切分
  • name 区域的名称

但是我们之前说过,在另一个方向上存在继承关系,那么应该避免同时设置columnsrows,另外考虑到宽度和高度可以从上级继承,我们做一次改进

  • width 区域的宽度可选
  • height 区域的高度 可选
  • grid: 可选columns rows
  • distribute: 分配规则
  • name 区域的名称

4.2 递归

按grid进行递归,如果发现没有grid,就停止递归

5. 用户体验

5.1 兼容性

不是所有的浏览器都支持grid,保险的做法是使用百分比

但是我们就不选择兼容方案了,激进一点

5.2 显性表达区域

用边框表达,还是用颜色表达

  • 用边框会占用区域
  • 颜色要求相邻的区域颜色不重复

5.3 模板

  • 左右
  • 上下
  • 圣杯