基于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设计实现的时候进一步挖掘。

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