Flex简介
Flex是一种网页布局方案,其名字来源于“弹性盒子”(flexible box),能够实现如垂直居中、水平居中、对齐等效果,相比于原来的盒状布局更加灵活,本文将模拟flex的部分属性,基于pixijs模拟在canvas上的flex布局,用于一些UI的设计。需要了解felx的移步阮一峰的教程。
开始
flex-direction
flex-direction属性实例。分别可设置为:
- row(从左到右)
- column(从上到下)
- row-reverse(从右到左)
- column-reverse(从下到上)
在实现flex-direction之前,需要先做好对换行的设置,比如上面图中的元素从左到右,如果不换行就会超出容器。换行属性很简单,就三种:nowrap、wrap、wrap-reverse。
对direction的配置如下:
/**
* 排布方向调整 第一个调整
* info 包含wrap类型
* item传递flex-drection属性
*/
flex_direction(item, info){
const attr_axis = {
"row": 'x',
"row-reverse": 'x',
"column": 'y',
"column-reverse": 'y',
}
const attr_cross = {
"row": 'y',
"row-reverse": 'y',
"column": 'x',
"column-reverse": 'x',
}
const acc_axis = {
"row": 'width',
"row-reverse": 'width',
"column": 'height',
"column-reverse": 'height',
}
const acc_cross = {
"row": 'height',
"row-reverse": 'height',
"column": 'width',
"column-reverse": 'width',
}
// 生长方向
const direction_grow = {
"row": 1,
"row-reverse": -1,
"column": 1,
"column-reverse": -1,
}
// 逆转都是改主轴
const direction_start = {
"row": {//从左到右
axis: 0,
cross: 0,
},
"row-reverse": {//从右到左
axis: this.width,
cross: 0,
},
"column": {//从上到下
axis: 0,
cross: 0,
},
"column-reverse": {//从下到上
axis: this.height,
cross: 0,
},
}
const anchor = {
"row": {
x: 0,
y: 0,
},
"row-reverse": {
x: 1,
y: 0,
},
"column": {
x: 0,
y: 0,
},
"column-reverse": {
x: 0,
y: 1,
},
}
// 返回信息:
let ret = {
grow:{ // 0 增长方向 1/-1/0
axis: direction_grow[item],
cross: info.wrap == 'wrap-inverse' ? -1:(info.wrap == 'nowrap' ? 0 : 1),
},
start_pos: { // 1 主轴、交叉轴起始点的坐标,此时经供参考,之后要转化成array形式——记录每一行的起始点
axis: direction_start[item].axis,
cross: direction_start[item].cross,
},
start: {
axis: [],
cross: [],
},
anchor: anchor[item], // 2. 项目的锚点是否变化(方向相反则锚点0-1
step: { // 3. 额外步长(宽度平均 measure后填充array {}
axis: [],
cross: [],
},
attr: { // 4. 定位属性
axis: attr_axis[item],
cross: attr_cross[item],
},
attr_acc:{ // 5. 积累的属性
axis: acc_axis[item],
cross: acc_cross[item],
},
measure:{
arr:[],//每一行的长宽 0, 1 ,2 ..
width:0, // 最大宽度
height:0, // 最大高度
},
wrap: info.wrap,
};
return ret;
}
justify-content
进行排布方向的处理后,接下来是对主轴对齐属性justify-content的处理
- flex-start 顶对齐:这里的主轴方向选的是column所以是靠顶部,如果是row的话就是左对齐
- flex-end 底对齐
- center 居中
- space-between 间隔:上下边是没有间隔的
- space-around 等距:上下边各有1/2的间隔
由于要测间隔,因此在进行justify-content属性的处理前要先对容器内的元素进行测量:
let info = {wrap: wrap};
// 0 计算方位
info = this.flex_direction(direction, info);
// 1 测算行列
let axis_sum = [0];
let index = 0;
let arr = [[]];
let measure = {width:0, height:0,0:{height:0, width:0}};// 最大宽高 以及各行的累积宽、最大高
let max_cross_acc = 0;
for(let c of this.children){
if(c===this.skin)continue; // 皮肤不参与
// 计算最大高
max_cross_acc = max_cross_acc > c[info.attr_acc.cross] ? max_cross_acc: c[info.attr_acc.cross];
if(wrap!='nowrap' && measure[index][info.attr_acc.axis] + c[info.attr_acc.axis] > this[info.attr_acc.axis]){
measure[info.attr_acc.cross] += max_cross_acc;
measure[index][info.attr_acc.cross] = max_cross_acc;
max_cross_acc = c[info.attr_acc.cross];
index += 1;
arr.push([]);
measure[index] = {
height: 0,
width: 0,
}
axis_sum.push(0);
}
// 累积宽
measure[index][info.attr_acc.axis] += c[info.attr_acc.axis];
// 统计最大宽
measure[info.attr_acc.axis] = measure[index][info.attr_acc.axis] > measure[info.attr_acc.axis] ? measure[index][info.attr_acc.axis] : measure[info.attr_acc.axis];
arr[index].push(c);
}
if(measure[index][info.attr_acc.axis]){
max_cross_acc = (max_cross_acc > measure[index][info.attr_acc.cross] ? max_cross_acc:measure[index][info.attr_acc.cross]);
measure[info.attr_acc.cross] += max_cross_acc;
measure[index][info.attr_acc.cross] = max_cross_acc;
}
info.arr = arr;
info.measure = measure;
然后用测量的信息来计算主轴的调整信息:
/**
* 主轴对齐调整
* info需包含:
* 1. arr信息 此属性仅限一行使用
* 2. measure 整体长宽测量信息,measure: [{width, height}]
* @param {*} item
*/
justify_content(item, info){
// if(!info.arr || info.arr.length > 1)return info;
let ast = info.start_pos.axis;
let cst = info.start_pos.cross;
switch(item) {
// 左对齐
case 'flex-start':
for(let i in info.arr){
info.start.axis[i] = ast;
}
break;
// 右对齐
case 'flex-end':
for(let i in info.arr){
let blank = this[info.attr_acc.axis] - info.measure[i][info.attr_acc.axis];
info.start.axis[i] = ast === 0 ? blank: ast - blank;
}
break;
// 居中
case 'center':
for(let i in info.arr){
let hfblank = (this[info.attr_acc.axis] - info.measure[i][info.attr_acc.axis]) >> 1;
info.start.axis[i] = ast === 0 ? hfblank: ast - hfblank;
}
break;
// 空间隔
case 'space-between':
for(let i in info.arr){
let itcount = info.arr[i].length;
let blank = this[info.attr_acc.axis] - info.measure[i][info.attr_acc.axis];
if(itcount - 1>0){
info.step.axis[i] = ~~(blank/(itcount-1));
}else {
info.start.axis[i] = blank>>1;
}
}
break;
// 空周边
case 'space-around':
for(let i in info.arr){
let itcount = info.arr[i].length;
let blank = this[info.attr_acc.axis] - info.measure[i][info.attr_acc.axis];
info.step.axis[i] = ~~(blank / itcount);
info.start.axis[i] = info.step.axis[i]>>1;
}
break;
}
return info;
}
align-items
接下来是交叉轴调整,即同一主轴上的各个元素相对于这条主轴的排布。这里有一部分没有实现。
为了看出调整效果,把三把钥匙的容器宽度降低了:
- flex-start:所有元素顶贴着主轴
- center:居中
- flex-end:所有元素底贴着主轴
- streatch(未实现)
- baseline(未实现)
如果使用sprite对象的话,很容易通过anchor调整——三个属性分别对应交叉轴方向的anchor为0、0.5、1. 但对于容器嵌套的情况,container是没有anchor成员的,需要自己实现。
streatch本来是flex的默认属性,但它需要拉伸元素,这在UI里似乎没必要,就没有做。baseline需要对齐子元素文字,比较复杂,也没有做。
/**
* 交叉轴对齐调整——对主轴的所有元素的信息进行调整
* TODO:
* 1. 顶对齐
* 2. 居中对齐
* 3. 底对齐
* 4. 填充
* 5. 文字对齐
*
*
* info需包含:
* 1. arr信息
* 2. measure长宽测量信息,measure: [{width, height}]
* @param {*} item
*/
align_items(item, info){
let cross_attr = info.attr.cross;
switch(item) {
case 'flex-start':// 顶对齐 每一个元素的anchor调整为0 cross出发点为0
info.anchor[cross_attr] = 0;
info.start_pos.cross = 0;
for(let i in info.arr){
info.start.cross[i] = 0;
}
break;
case 'flex-end':// 底对齐 每一个元素anchor为1 cross出发点为最大值——交叉最高高度
info.anchor[cross_attr] = 1;
// info.start_pos.cross = info.measure[info.attr_acc.cross];
for(let i in info.arr){
let blank = info.measure[i][info.attr_acc.cross];// 交叉的高度
info.start.cross[i] = blank;
}
break;
case 'stretch':// 需要填充宽度 暂时不考虑 因为拉伸sprite会很丑
case 'baseline':// ?? 如何实现?——其实就是每一个元素的第一个子元素对齐 需要专门建立信息储备
case 'center': // 居中对齐
info.anchor[cross_attr] = 0.5;
for(let i in info.arr){
let blank = info.measure[i][info.attr_acc.cross]>>1;// todo:交叉的高度
info.start.cross[i] = blank;
}
break;
}
return info;
}
align-content
在教程中说,多行交叉对齐在只有一根主线的时候不起作用,因为直接默认stretch了,但我这里没有实现stretch,所以默认为居中。
在row模式下演示(此时有多轴,效果比较明显):
分别对应:
- flex-start 靠顶
- center 居中
- flex-end 靠底
- space-between 间隔
- space-around 等距
/**
* 多交叉对齐调整——即所有轴线相对于容器的排布
* info需包含:
* 1. arr信息
* 2. measure每一行的长宽测量信息,measure: [{width, height}]
* @param {*} item
*/
align_content(item, info){
let blank = this[info.attr_acc.cross] - info.measure[info.attr_acc.cross];
const itcount = info.arr.length;// 主轴数目
switch(item) {
case 'flex-start':// 默认如此 无需调整
break;
case 'flex-end':// 最后对齐 需要调整出发点
info.start_pos.cross = info.start_pos.cross == 0 ? blank : info.start_pos.cross-blank;
break;
case 'center':
blank >>= 1;
info.start_pos.cross = info.start_pos.cross == 0 ? blank : info.start_pos.cross-blank;
break;
case 'space-between':
if(itcount - 1>0){
info.step.cross = ~~(blank/(itcount-1));
}else {
blank >>= 1;
info.start_pos.cross = info.start_pos.cross == 0 ? blank : info.start_pos.cross-blank;
}
break;
case 'space-around':
info.step.cross = ~~(blank / itcount);
info.start_pos.cross = info.step.cross>>1;
break;
}
return info;
}
在经过各个属性处理后,最后统一应用到container:
// 5. 依据最终信息实际调整项目
// 生长方向:
let gc = info.grow.cross, ga = info.grow.axis;
let cross_start = info.start_pos.cross;
for(let i in arr){
let axis_start = (info.start.axis[i] || 0);
for(let j in arr[i]){
const c = arr[i][j];
let abias = 0, cbias = (info.start.cross[i]||0);
if(c.anchor){
c.anchor[info.attr.axis] = info.anchor[info.attr.axis];
c.anchor[info.attr.cross] = info.anchor[info.attr.cross];
}else{
abias += -~~(info.anchor[info.attr.axis] * c[info.attr_acc.axis]);
cbias += -~~(info.anchor[info.attr.cross] * c[info.attr_acc.cross]);
}
if(info.attr.axis=='x'){
c.setTransform(axis_start + abias, cross_start + cbias);
}else {
c.setTransform(cross_start + cbias, axis_start + abias);
}
axis_start += ~~((info.step.axis[i]||0) + c[info.attr_acc.axis])*ga;
}
cross_start += ~~((info.step.cross||0) + info.measure[i][info.attr_acc.cross])*gc;
}
小结
这次是做UI的时候经提醒得知有这样一个布局思路,学习后发现这种布局不仅是用于网页,也可用于游戏中的UI布局,是一种范式,所以复刻了过来。也许原生实现效率会高很多,但亲手实现一遍会获益良多,这其中还有不少的坑点和漏洞,比如stretch和baseline,需要之后具体UI设计实现的时候进一步挖掘。