三角形兼梯形布局

序言

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

這里寫圖片描述

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


這里寫圖片描述

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

這里寫圖片描述

效果

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

1.自動計算三角形高度

只需要添加view即可恍涂,TriangleLayout會自動計算高度并拼出一個三角形

這里寫圖片描述

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

這里寫圖片描述

3.支持梯形布局

這里寫圖片描述

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

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

這里寫圖片描述

5.支持大小不同的子View

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

這里寫圖片描述

6.支持自動計算Padding

如果設(shè)置了TriangleLayout的高度和寬度尼夺,則TriangleLayout會根據(jù)最寬那個Item的寬度作為Item的
平均值,然后自動計算padding炒瘸。同樣也可以指定padding,然后設(shè)置TriangleLayout為wrap_content則自適應(yīng)寬度淤堵。比如你想讓你的TriangleLayout顯示一行最多5個,Padding自動則可以如下設(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" />

實現(xiàn)

TriangleLayout繼承自ViewGroup所以我會按照:測量顷扩,布局拐邪。來說明。

測量寬高

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

這里寫圖片描述

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

這里寫圖片描述

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

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

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

如圖贵扰,從小到大的示意圖

這里寫圖片描述

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

/**
     * 計算一共有多少行
     */
    private void calculateLineSize() {
        int count = getChildCount();
        mLines.clear();
        if (count == 0) {
            mLineSize = 0;
            return;
        } else {
            //標(biāo)識是否從多到少進(jìn)行計算
            boolean MaxToMin = false;
            if (mWantMaxLineItemSize != AUTO_MAX) {
                MaxToMin = true;
                mRealMaxLineItemSize = mWantMaxLineItemSize;
            }
            int lineNumber = MaxToMin ? mWantMaxLineItemSize : mMinLineNumber;//當(dāng)前行的個數(shù)
            int sum = lineNumber;//所以行的個數(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) {
                //保存實際的最大大小
                mRealMaxLineItemSize = lineNumber;
                //因為draw相關(guān)的函數(shù)是在MaxToMin模式下完成的
                //所以在MinToMax的時候需要將行號倒置
                for (int i = 1; i <= mLineSize; i++) {
                    mLines.get(mLines.size() - i).lineNumber = i;
                }
            }
            //對最后一行的結(jié)束位置進(jìn)行調(diào)整舞丛,因為可能超出邊界
            mLines.get(mLines.size() - 1).end = count - 1;
        }
    }

測量寬高

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

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //計算一共的行數(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) {
            //計算一個item最大可能的寬度
            int itemMaxIdealWidth = 0;
            if (autoWidthPadding) {
                //先不考慮padding户辱,后面計算
                itemMaxIdealWidth = widthSize / mRealMaxLineItemSize;
            } else {
                itemMaxIdealWidth = (widthSize - (mRealMaxLineItemSize + 1) * mItemWidthPadding) / mRealMaxLineItemSize;
            }
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(itemMaxIdealWidth, MeasureSpec.AT_MOST);
        }
        if (heightMode != MeasureSpec.UNSPECIFIED) {
        //計算一個item最大可能的高度度
            int itemMaxIdealHeight = 0;
            if (autoHeightPadding) {
                //先不考慮padding,后面計算
                itemMaxIdealHeight = heightSize / mLineSize;
            } else {
                itemMaxIdealHeight = (heightSize - (mLineSize + 1) * mItemHeightPadding) / mLineSize;
            }
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(itemMaxIdealHeight, MeasureSpec.AT_MOST);

        }
        int realChildMaxWidth = 0;
        int realChildMaxHeight = 0;
        //遍歷子View獲取實際的最大寬高
        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);

    }

布局

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


這里寫圖片描述

代碼如下:


    @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();
                //計算中心點(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();
                //計算中間點(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è)置了的話三角形的最大邊將會固定,因此可以形成一個不完整的三角形也就是一個矩形比如這種布局只需要將rl_max_line_item_size設(shè)置為10韧献,rl_style設(shè)置為rl_style_un_regular_triangle也就是倒三角末患,然后填充指定的數(shù)量即可。

這里寫圖片描述

屬性如下:

    <declare-styleable name="TriangleLayout">
        <!--一行最多item的個數(shù),如果設(shè)置了的話則優(yōu)先滿足最大邊锤窑,否則設(shè)置為auto自動計算成一個三角形-->
        <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時,其可能的值有三種渊啰,但是如果在不知道類型的情況下就去讀取的話探橱,會引起崩潰,于是我開始閱讀TypedArray的源碼绘证,在其中看到了這個隧膏。


這里寫圖片描述

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


這里寫圖片描述

利用這個函數(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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市队询,隨后出現(xiàn)的幾起案子派桩,更是在濱河造成了極大的恐慌,老刑警劉巖蚌斩,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铆惑,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)员魏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門丑蛤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逆趋,你說我怎么就攤上這事盏阶。” “怎么了闻书?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵名斟,是天一觀的道長。 經(jīng)常有香客問我魄眉,道長砰盐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任坑律,我火速辦了婚禮岩梳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晃择。我一直安慰自己冀值,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布宫屠。 她就那樣靜靜地躺著列疗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浪蹂。 梳的紋絲不亂的頭發(fā)上抵栈,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機(jī)與錄音坤次,去河邊找鬼古劲。 笑死,一個胖子當(dāng)著我的面吹牛缰猴,可吹牛的內(nèi)容都是我干的产艾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼滑绒,長吁一口氣:“原來是場噩夢啊……” “哼闷堡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蹬挤,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棘幸,沒想到半個月后焰扳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年吨悍,在試婚紗的時候發(fā)現(xiàn)自己被綠了扫茅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡育瓜,死狀恐怖葫隙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情躏仇,我是刑警寧澤恋脚,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站焰手,受9級特大地震影響糟描,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜书妻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一船响、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧躲履,春花似錦见间、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至域慷,卻和暖如春荒辕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背犹褒。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工抵窒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叠骑。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓李皇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宙枷。 傳聞我的和親對象是個殘疾皇子掉房,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容