Android實現(xiàn)三角形兼梯形布局
在最近的項目開發(fā)中遇到了這種UI:

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

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

效果
先看效果,如果覺得效果好,你可以繼續(xù)看怎么實現(xiàn),否則就沒必要浪費時間了,不是嗎。
1.自動計算三角形高度
只需要添加view即可,TriangleLayout會自動計算高度并拼出一個三角形

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

3.支持梯形布局

4.支持三角形的形狀改變
step表示相鄰兩行item個數(shù)的差值,如果step越小則三角形會越陡。

5.支持大小不同的子View
其中心點在一個三角形上。

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.TriangleLayoutandroid: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 {//標識是否從多到少進行計算boolean MaxToMin = false;if (mWantMaxLineItemSize != AUTO_MAX) {MaxToMin = true;mRealMaxLineItemSize = mWantMaxLineItemSize;}int lineNumber = MaxToMin ? mWantMaxLineItemSize : mMinLineNumber;//當前行的個數(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é)束位置進行調(diào)整,因為可能超出邊界mLines.get(mLines.size() - 1).end = count - 1;}}
測量寬高
其核心思想是根據(jù)父控件傳遞的測量模式和尺寸,確定子布局的測量尺寸,然后遍歷子View獲取最大的寬度和高度,作為平均值,根據(jù)我們的寬高公式得出TriangleLayout的寬高,需要注意的是如果padding為AutoPadding,則需要先計算出子View的寬度,再用總的寬度減去需要的寬度得到padding。
@Overrideprotected 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)的計算公式如下:

代碼如下:
@Overrideprotected 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;//負責布局的孩子在child中的索引,前后閉區(qū)間[begin,end]int begin = -1, end = -1;public void layoutChildTopToDown(int l, int t, int r, int b) {//當前行的left偏移量int mLeft = l + mItemWidthPadding + (lineNumber - 1) * (mItemWidth + mItemWidthPadding) * mStep / 2;//當前行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ù)中心點確定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ù)中間點確定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;}}
源碼地址:
https://github.com/zhuguohui/TrigangleLayoutDemo
到這里就結(jié)束啦.
