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設計實現的時候進一步挖掘。