三角形兼梯形布局

序言

在最近的項(xiàng)目開發(fā)中遇到了這種UI。

這里寫圖片描述

傳統(tǒng)的辦法就是通過兩個(gè)線性布局進(jìn)行計(jì)算,但是第二行每個(gè)item的寬度是根據(jù)第一行計(jì)算出來的,而第一行每個(gè)Item的寬度又得根據(jù)屏幕寬度來計(jì)算。且第二行還有一個(gè)偏移量需要計(jì)算。如果有多行這種梯形布局。比如鍵盤。又該怎么處理呢。


這里寫圖片描述

于是我想能不能有一種梯形布局來實(shí)現(xiàn)這種遞減的效果。實(shí)現(xiàn)自動(dòng)布局,我們只需要將View放置在其中就可以了。但是應(yīng)該叫什么名字,最后發(fā)現(xiàn)其實(shí)這種布局最終的效果就是一個(gè)三角形。只是這個(gè)三角形不完整。于是我給我的Layout起名為——TriangleLayout

這里寫圖片描述

效果

先看效果,如果覺得效果好,你可以繼續(xù)看怎么實(shí)現(xiàn),否則就沒必要浪費(fèi)時(shí)間了,不是嗎。

1.自動(dòng)計(jì)算三角形高度

只需要添加view即可,TriangleLayout會(huì)自動(dòng)計(jì)算高度并拼出一個(gè)三角形

這里寫圖片描述

2.支持正三角和倒三角轉(zhuǎn)換

這里寫圖片描述

3.支持梯形布局

這里寫圖片描述

4.支持三角形的形狀改變

step表示相鄰兩行item個(gè)數(shù)的差值,如果step越小則三角形會(huì)越陡。

這里寫圖片描述

5.支持大小不同的子View

其中心點(diǎn)在一個(gè)三角形上。

這里寫圖片描述

6.支持自動(dòng)計(jì)算Padding

如果設(shè)置了TriangleLayout的高度和寬度,則TriangleLayout會(huì)根據(jù)最寬那個(gè)Item的寬度作為Item的
平均值,然后自動(dòng)計(jì)算padding。同樣也可以指定padding,然后設(shè)置TriangleLayout為wrap_content則自適應(yīng)寬度。比如你想讓你的TriangleLayout顯示一行最多5個(gè),Padding自動(dòng)則可以如下設(shè)置:

    <com.trs.cqjb.gov.view.TriangleLayout
        android:id="@+id/triangleLayout"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        app:rl_item_height_padding="auto_padding"
        app:rl_item_width_padding="auto_padding"
        app:rl_max_line_item_size="5"
        app:rl_step="1"
        app:rl_style="rl_style_un_regular_triangle" />

實(shí)現(xiàn)

TriangleLayout繼承自ViewGroup所以我會(huì)按照:測(cè)量,布局。來說明。

測(cè)量寬高

我們可以發(fā)現(xiàn)TriangleLayout的寬度和最大行item的個(gè)數(shù)與item水平方向之間的Padding有關(guān)。

這里寫圖片描述

而TriangleLayout的高度和行數(shù)與item豎直方向的Padding有關(guān)。如圖:

這里寫圖片描述

因此要測(cè)量TriangleLayout的寬高,則必須先知道三角形的高度和最后一層Item的數(shù)量。

求三角形的高度和最后一層Item的數(shù)量。

一共有兩種計(jì)算方法,從少到多與從多到少,其核心思想是從最初行開始計(jì)算,加上或減去Step形成新的一行。累加新行的個(gè)數(shù),如果總數(shù)還是小于實(shí)際的總數(shù)則繼續(xù)形成新行。

如圖,從小到大的示意圖

這里寫圖片描述

實(shí)際代碼,就是一個(gè)While循環(huán):
需要注意的是如果指定了最大行的數(shù)量,則會(huì)從大大小開始計(jì)算三角形的高度,這也是梯形布局的原理,即一個(gè)不完整的三角形而已。

/**
     * 計(jì)算一共有多少行
     */
    private void calculateLineSize() {
        int count = getChildCount();
        mLines.clear();
        if (count == 0) {
            mLineSize = 0;
            return;
        } else {
            //標(biāo)識(shí)是否從多到少進(jìn)行計(jì)算
            boolean MaxToMin = false;
            if (mWantMaxLineItemSize != AUTO_MAX) {
                MaxToMin = true;
                mRealMaxLineItemSize = mWantMaxLineItemSize;
            }
            int lineNumber = MaxToMin ? mWantMaxLineItemSize : mMinLineNumber;//當(dāng)前行的個(gè)數(shù)
            int sum = lineNumber;//所以行的個(gè)數(shù)
            int lineSize = 1;
            LineInfo firstLine = new LineInfo();
            firstLine.lineNumber = 1;
            firstLine.begin = 0;
            firstLine.end = lineNumber - 1;
            mLines.add(firstLine);
            while (sum < count) {
                LineInfo lineInfo = new LineInfo();
                if (MaxToMin) {
                    lineNumber -= mStep;
                } else {
                    lineNumber += mStep;
                }
                lineInfo.begin = sum;
                sum += lineNumber;
                lineInfo.end = sum - 1;
                lineSize++;
                lineInfo.lineNumber = lineSize;
                mLines.add(lineInfo);
            }
            mLineSize = lineSize;
            if (!MaxToMin) {
                //保存實(shí)際的最大大小
                mRealMaxLineItemSize = lineNumber;
                //因?yàn)閐raw相關(guān)的函數(shù)是在MaxToMin模式下完成的
                //所以在MinToMax的時(shí)候需要將行號(hào)倒置
                for (int i = 1; i <= mLineSize; i++) {
                    mLines.get(mLines.size() - i).lineNumber = i;
                }
            }
            //對(duì)最后一行的結(jié)束位置進(jìn)行調(diào)整,因?yàn)榭赡艹鲞吔?            mLines.get(mLines.size() - 1).end = count - 1;
        }
    }

測(cè)量寬高

其核心思想是根據(jù)父控件傳遞的測(cè)量模式和尺寸,確定子布局的測(cè)量尺寸,然后遍歷子View獲取最大的寬度和高度,作為平均值,根據(jù)我們的寬高公式得出TriangleLayout的寬高,需要注意的是如果padding為AutoPadding,則需要先計(jì)算出子View的寬度,再用總的寬度減去需要的寬度得到padding。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //計(jì)算一共的行數(shù)
        calculateLineSize();
        if (getChildCount() == 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        int childWidthMeasureSpec = widthMeasureSpec;
        int childHeightMeasureSpec = heightMeasureSpec;
        if (widthMode != MeasureSpec.UNSPECIFIED) {
            //計(jì)算一個(gè)item最大可能的寬度
            int itemMaxIdealWidth = 0;
            if (autoWidthPadding) {
                //先不考慮padding,后面計(jì)算
                itemMaxIdealWidth = widthSize / mRealMaxLineItemSize;
            } else {
                itemMaxIdealWidth = (widthSize - (mRealMaxLineItemSize + 1) * mItemWidthPadding) / mRealMaxLineItemSize;
            }
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(itemMaxIdealWidth, MeasureSpec.AT_MOST);
        }
        if (heightMode != MeasureSpec.UNSPECIFIED) {
        //計(jì)算一個(gè)item最大可能的高度度
            int itemMaxIdealHeight = 0;
            if (autoHeightPadding) {
                //先不考慮padding,后面計(jì)算
                itemMaxIdealHeight = heightSize / mLineSize;
            } else {
                itemMaxIdealHeight = (heightSize - (mLineSize + 1) * mItemHeightPadding) / mLineSize;
            }
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(itemMaxIdealHeight, MeasureSpec.AT_MOST);

        }
        int realChildMaxWidth = 0;
        int realChildMaxHeight = 0;
        //遍歷子View獲取實(shí)際的最大寬高
        for (int i = 0; i < getChildCount(); i++) {
            getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
            int childWidth = getChildAt(i).getMeasuredWidth();
            int childHeight = getChildAt(i).getMeasuredHeight();
            if (childWidth > realChildMaxWidth) {
                realChildMaxWidth = childWidth;
            }
            if (childHeight > realChildMaxHeight) {
                realChildMaxHeight = childHeight;
            }
        }
        mItemWidth = realChildMaxWidth;
        mItemHeight = realChildMaxHeight;
        if (autoWidthPadding) {
            //確定最終的padding;
            mItemWidthPadding = (widthSize - mRealMaxLineItemSize * mItemWidth) / (mRealMaxLineItemSize + 1);
        }

        if (autoHeightPadding) {
            mItemHeightPadding = (heightSize - mLineSize * mItemHeight) / (mLineSize + 1);
        }

        //根據(jù)最大值設(shè)置Layout的寬高
        int mWidth = mRealMaxLineItemSize * mItemWidth + (mRealMaxLineItemSize + 1) * mItemWidthPadding;
        int mHeight = mLineSize * (mItemHeight + mItemHeightPadding) + mItemHeightPadding;

        setMeasuredDimension(mWidth, mHeight);

    }

布局

在計(jì)算寬高的時(shí)候使用了一個(gè)內(nèi)部類保存每一行的信息,在布局的時(shí)候只需要遍歷這個(gè)類的集合就可以了。其相關(guān)的計(jì)算公式如下:


這里寫圖片描述

代碼如下:


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (isRegularTriangle) {
            layoutDownToTop(l, t, r, b);
        } else {
            layoutTopToDown(l, t, r, b);
        }
    }

    /**
     * 自上而下的布局
     *
     * @param l
     * @param t
     * @param r
     * @param b
     */
    private void layoutTopToDown(int l, int t, int r, int b) {

        for (LineInfo info : mLines) {
            info.layoutChildTopToDown(l, t, r, b);
        }
    }

    private void layoutDownToTop(int l, int t, int r, int b) {
        for (LineInfo info : mLines) {
            info.layoutChildDownToTop(l, t, r, b);
        }
    }

   /**
     * 保存每一行的信息
     */
    private class LineInfo {
        //所在行數(shù) 從1開始
        int lineNumber;
        //負(fù)責(zé)布局的孩子在child中的索引,前后閉區(qū)間[begin,end]
        int begin = -1, end = -1;

        public void layoutChildTopToDown(int l, int t, int r, int b) {
            //當(dāng)前行的left偏移量
            int mLeft = l + mItemWidthPadding + (lineNumber - 1) * (mItemWidth + mItemWidthPadding) * mStep / 2;
            //當(dāng)前行top的偏移量
            int mTop = t + (lineNumber - 1) * (mItemHeightPadding + mItemHeight) + mItemHeightPadding;

            if (begin < 0 || end < 0) {
                return;
            }
            int index = 0;
            for (int i = begin; i <= end; i++) {
                View view = getChildAt(i);
                int height = view.getMeasuredHeight();
                int width = view.getMeasuredWidth();
                //計(jì)算中心點(diǎn)根據(jù)中心點(diǎn)確定left;
                int middleWidth = mLeft + index * (mItemWidthPadding + mItemWidth) + mItemWidth / 2;
                int middleHeight = mTop + mItemHeight / 2;
                int cLeft = middleWidth - width / 2;
                int cTop = middleHeight - height / 2;
                int cRight = cLeft + width;
                int cDown = cTop + height;
                view.layout(cLeft, cTop, cRight, cDown);
                index++;

            }
        }

        public void layoutChildDownToTop(int l, int t, int r, int b) {
            int mLeft = l + mItemWidthPadding + (lineNumber - 1) * ((mItemWidth + mItemWidthPadding) * mStep / 2);
            int mTop = t + (mLineSize - lineNumber) * (mItemHeightPadding + mItemHeight) + mItemHeightPadding;

            if (begin < 0 || end < 0) {
                return;
            }
            int index = 0;
            for (int i = begin; i <= end; i++) {
                View view = getChildAt(i);
                int height = view.getMeasuredHeight();
                int width = view.getMeasuredWidth();
                //計(jì)算中間點(diǎn)根據(jù)中間點(diǎn)確定left;
                int middleWidth = mLeft + index * (mItemWidthPadding + mItemWidth) + mItemWidth / 2;
                int middleHeight = mTop + mItemHeight / 2;
                int cLeft = middleWidth - width / 2;
                int cTop = middleHeight - height / 2;
                int cRight = cLeft + width;
                int cDown = cTop + height;
                view.layout(cLeft, cTop, cRight, cDown);
                index++;

            }
        }
    }

自定義屬性

最重要的是rl_max_line_item_size,如果設(shè)置了的話三角形的最大邊將會(huì)固定,因此可以形成一個(gè)不完整的三角形也就是一個(gè)矩形比如這種布局只需要將rl_max_line_item_size設(shè)置為10,rl_style設(shè)置為rl_style_un_regular_triangle也就是倒三角,然后填充指定的數(shù)量即可。

這里寫圖片描述

屬性如下:

    <declare-styleable name="TriangleLayout">
        <!--一行最多item的個(gè)數(shù),如果設(shè)置了的話則優(yōu)先滿足最大邊,否則設(shè)置為auto自動(dòng)計(jì)算成一個(gè)三角形-->
        <attr name="rl_max_line_item_size" format="integer|enum">
            <enum name="auto" value="-1" />
        </attr>
        <!--每一行相差的數(shù)量-->
        <attr name="rl_step" format="integer" />
        <!--item水平方向的padding-->
        <attr name="rl_item_width_padding" format="dimension|enum|reference">
            <enum name="auto_padding" value="-1" />
        </attr>
        <!--item豎直方向的padding-->
        <attr name="rl_item_height_padding" format="dimension|enum|reference">
            <enum name="auto_padding" value="-1" />
        </attr>
        <!--顯示樣式 正三角或-->
        <attr name="rl_style" format="enum">
            <enum name="rl_style_regular_triangle" value="0" />
            <enum name="rl_style_un_regular_triangle" value="1" />
        </attr>
    </declare-styleable>

讀取多種類型的屬性值,例如聲明rl_item_width_padding時(shí),其可能的值有三種,但是如果在不知道類型的情況下就去讀取的話,會(huì)引起崩潰,于是我開始閱讀TypedArray的源碼,在其中看到了這個(gè)。


這里寫圖片描述

不過這是API21才添加的,為了系統(tǒng)的兼容性,我又找到了這個(gè)。


這里寫圖片描述

利用這個(gè)函數(shù),實(shí)現(xiàn)了讀取多種類型屬性的功能

   TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TriangleLayout);
        TypedValue widthPaddingValue = array.peekValue(R.styleable.TriangleLayout_rl_item_width_padding);
        if (widthPaddingValue != null) {
            if (widthPaddingValue.type == TypedValue.TYPE_DIMENSION) {
                mItemWidthPadding = array.getDimensionPixelSize(R.styleable.TriangleLayout_rl_item_width_padding, 0);
                if (mItemWidthPadding < 0) {
                    throw new IllegalArgumentException("ItemWidthPadding must  be a positive number");
                }
                autoWidthPadding = false;
            } else {
                autoWidthPadding = true;
                mItemWidthPadding = 0;
            }
        }

源碼

歡迎star哈
TrigangleLayoutDemo

總結(jié)

紙上得來終覺淺,絕知此事要躬行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容