序言
在最近的項目開發中遇到了這種UI。
傳統的辦法就是通過兩個線性布局進行計算,但是第二行每個item的寬度是根據第一行計算出來的,而第一行每個Item的寬度又得根據屏幕寬度來計算。且第二行還有一個偏移量需要計算。如果有多行這種梯形布局。比如鍵盤。又該怎么處理呢。
于是我想能不能有一種梯形布局來實現這種遞減的效果。實現自動布局,我們只需要將View放置在其中就可以了。但是應該叫什么名字,最后發現其實這種布局最終的效果就是一個三角形。只是這個三角形不完整。于是我給我的Layout起名為——TriangleLayout
效果
先看效果,如果覺得效果好,你可以繼續看怎么實現,否則就沒必要浪費時間了,不是嗎。
1.自動計算三角形高度
只需要添加view即可,TriangleLayout會自動計算高度并拼出一個三角形
2.支持正三角和倒三角轉換
3.支持梯形布局
4.支持三角形的形狀改變
step表示相鄰兩行item個數的差值,如果step越小則三角形會越陡。
5.支持大小不同的子View
其中心點在一個三角形上。
6.支持自動計算Padding
如果設置了TriangleLayout的高度和寬度,則TriangleLayout會根據最寬那個Item的寬度作為Item的
平均值,然后自動計算padding。同樣也可以指定padding,然后設置TriangleLayout為wrap_content則自適應寬度。比如你想讓你的TriangleLayout顯示一行最多5個,Padding自動則可以如下設置:
<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" />
實現
TriangleLayout繼承自ViewGroup所以我會按照:測量,布局。來說明。
測量寬高
我們可以發現TriangleLayout的寬度和最大行item的個數與item水平方向之間的Padding有關。
而TriangleLayout的高度和行數與item豎直方向的Padding有關。如圖:
因此要測量TriangleLayout的寬高,則必須先知道三角形的高度和最后一層Item的數量。
求三角形的高度和最后一層Item的數量。
一共有兩種計算方法,從少到多與從多到少,其核心思想是從最初行開始計算,加上或減去Step形成新的一行。累加新行的個數,如果總數還是小于實際的總數則繼續形成新行。
如圖,從小到大的示意圖
實際代碼,就是一個While循環:
需要注意的是如果指定了最大行的數量,則會從大大小開始計算三角形的高度,這也是梯形布局的原理,即一個不完整的三角形而已。
/**
* 計算一共有多少行
*/
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;//當前行的個數
int sum = lineNumber;//所以行的個數
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相關的函數是在MaxToMin模式下完成的
//所以在MinToMax的時候需要將行號倒置
for (int i = 1; i <= mLineSize; i++) {
mLines.get(mLines.size() - i).lineNumber = i;
}
}
//對最后一行的結束位置進行調整,因為可能超出邊界
mLines.get(mLines.size() - 1).end = count - 1;
}
}
測量寬高
其核心思想是根據父控件傳遞的測量模式和尺寸,確定子布局的測量尺寸,然后遍歷子View獲取最大的寬度和高度,作為平均值,根據我們的寬高公式得出TriangleLayout的寬高,需要注意的是如果padding為AutoPadding,則需要先計算出子View的寬度,再用總的寬度減去需要的寬度得到padding。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//計算一共的行數
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);
}
//根據最大值設置Layout的寬高
int mWidth = mRealMaxLineItemSize * mItemWidth + (mRealMaxLineItemSize + 1) * mItemWidthPadding;
int mHeight = mLineSize * (mItemHeight + mItemHeightPadding) + mItemHeightPadding;
setMeasuredDimension(mWidth, mHeight);
}
布局
在計算寬高的時候使用了一個內部類保存每一行的信息,在布局的時候只需要遍歷這個類的集合就可以了。其相關的計算公式如下:
代碼如下:
@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 {
//所在行數 從1開始
int lineNumber;
//負責布局的孩子在child中的索引,前后閉區間[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();
//計算中心點根據中心點確定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();
//計算中間點根據中間點確定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,如果設置了的話三角形的最大邊將會固定,因此可以形成一個不完整的三角形也就是一個矩形比如這種布局只需要將rl_max_line_item_size設置為10,rl_style設置為rl_style_un_regular_triangle也就是倒三角,然后填充指定的數量即可。
屬性如下:
<declare-styleable name="TriangleLayout">
<!--一行最多item的個數,如果設置了的話則優先滿足最大邊,否則設置為auto自動計算成一個三角形-->
<attr name="rl_max_line_item_size" format="integer|enum">
<enum name="auto" value="-1" />
</attr>
<!--每一行相差的數量-->
<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才添加的,為了系統的兼容性,我又找到了這個。
利用這個函數,實現了讀取多種類型屬性的功能
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
總結
紙上得來終覺淺,絕知此事要躬行。