序言
在最近的項目開發(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é)
紙上得來終覺淺魏宽,絕知此事要躬行腐泻。