基於pixijs的仿Flex佈局實現

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章