Android自定義View系列:標籤LabelView實戰篇
前言部分
本文主要介紹如何自定義一個常見的labels標籤,功能上主要支援,單選、多選、點選三種模式。因為這個使用率很高,並且這個是比較典型學習自定義ViewGroup的例子,所以特意動手實踐,加深對Android的認識。這個專案主要是為了自己學習使用,所以並不是很完善,先上一個效果圖,瞭解一下:
內容部分
-
ViewGroup的定義主要還是分佈在兩個部分,一個是測量,另一個是佈局。layout子view是作為容器最基本的工作。
-
測量的部分主要還是遵循view的三種測量模式來不同處理。介紹測量規則的部落格很多,這裡不多解釋。
{@link android.
-
佈局部分主要是需要我們特殊處理的地方,因為我們的每個labels的大小是根據內容決定的,所以我們要自己根據view的尺寸進行擺放。
程式碼實現
首先介紹onMeasure方法中的實現,根絕子view的尺寸來決定容器view的測量情況。
private void measureMyChild(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//寬度
maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int count = getChildCount();
//總高度
int contentHeight = 0;
//記錄最寬的行寬
int maxLineWidth = 0;
// 每行寬度
int startLayoutWidth = 0;
//一行中子控制元件最高的高度,用於決定下一行高度應該在目前基礎上累加多少
int maxChildHeight = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LogUtil.i("onLayout--getPaddingRight:" +
child.getPaddingRight() +
"getPaddingLeft:" + child.getPaddingLeft());
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//測量的寬高
int childMeasureWidth = child.getMeasuredWidth();
int childMeasureHeight = child.getMeasuredHeight();
LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:");
if (startLayoutWidth + childMeasureWidth < maxWidth) {
//如果一行沒有排滿,繼續往右排列
startLayoutWidth += childMeasureWidth + margiHorizontal;
} else {
// 初始化為0
maxChildHeight = 0;
startLayoutWidth = 0;
}
if (childMeasureHeight > maxChildHeight) {
maxChildHeight = childMeasureHeight;
}
//獲取總的高度
contentHeight += maxChildHeight + margiVertical;
//獲取最長的行總的寬度
maxLineWidth = Math.max(maxLineWidth, startLayoutWidth);
}
//如果沒有子元素,就設定寬高都為0(簡化處理)
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else
//寬和高都是AT_MOST,則設定寬度最寬的字元素的寬度的和;高度設定為最高的元素的高度;
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(maxLineWidth, contentHeight);
}
//如果寬度是wrap_content,則寬度為最寬的一行的寬度
else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(maxLineWidth, heightSize);
}
//如果高度是wrap_content,則高度為最高的字元素的高度
else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, contentHeight);
}
}
寬度的測量過程是,最大的寬度直接是取的容器view的尺寸。然後我們計算每一行的子view尺寸,通過求和來和容器view的最大寬度進行比較(公式:startLayoutWidth + childMeasureWidth < maxWidth)累加的寬度加上下一個字view的寬度和最大寬度比較來決定是否進行換行。
高度的測量過程是,先把所有的子view的高度進行相加求和,在根據容器view的mode來進行尺寸選擇。這裡內容都是常規的測量原則。主要區別還說在於寬高的取值。
寬高的取值我們的原則是,高度我們取值為最高的字元素;寬度我們取值為字元素相加,最寬的一行字元素。
測量部分的內容就是這些,主要還說需要我們對測量規則進行了解。然後根據我們自己的需求來進行選擇。
下面介紹佈局子view的部分程式碼
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
LogUtil.i("onLayout--width:" + maxWidth);
final int count = getChildCount();
int childMeasureWidth = 0;
int childMeasureHeight = 0;
// 開始的X位置
int startLayoutWidth = getPaddingLeft();
// 開始的Y位置
int startLayoutHeight = getPaddingTop();
//一行中子控制元件最高的高度,用於決定下一行高度應該在目前基礎上累加多少
int maxChildHeight = 0;
for (int i = 0; i < count; i++) {
int position = i;
TextView child = (TextView) getChildAt(i);
//注意此處不能使用getWidth和getHeight,這兩個方法必須在onLayout執行完,才能正確獲取寬高
childMeasureWidth = child.getMeasuredWidth() + child.getPaddingLeft() + child.getPaddingRight();
childMeasureHeight = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();
LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:" + startLayoutWidth);
if (startLayoutWidth + childMeasureWidth < maxWidth - getPaddingRight()) {
//如果一行沒有排滿,繼續往右排列
left = startLayoutWidth;
right = left + childMeasureWidth;
top = startLayoutHeight;
bottom = top + childMeasureHeight;
} else {
//排滿後換行
startLayoutWidth = getPaddingLeft();
startLayoutHeight += maxChildHeight + margiVertical;
maxChildHeight = 0;
left = startLayoutWidth;
right = left + childMeasureWidth;
top = startLayoutHeight;
bottom = top + childMeasureHeight;
}
//寬度累加
startLayoutWidth += childMeasureWidth + margiHorizontal;
if (childMeasureHeight > maxChildHeight) {
maxChildHeight = childMeasureHeight;
}
//確定子控制元件的位置,四個引數分別代表(左上右下)點的座標值
child.layout(left, top, right, bottom);
initListener(child, position);
}
}
上面內容主要分為兩部分,一部分是寬度的佈局,一部分是高度的佈局。
寬度佈局:主要是根據子view的寬度和容器view的寬度的比較,來決定什麼時候進行換行。這裡需要注意的一些邊界值,如我們經常使用的margin和padding值。
高度佈局:主要是根據最高的子view的高度決定每一行的高度,這樣可以讓我們每一行都保持一樣的高度。
完成上面兩個大的步驟,基本上這個view也就完成的差不多了。
額外注意的點:
因為標籤是通過建立textview設定屬性新增到容器中,所以這裡設定文字顏色變化的方法和在xml中有些區別:
//設定每一個標籤
private void drawTextView() {
for (String text : textList) {
TextView label = new TextView(context);
label.setPadding(30, 30, 0, 0);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 40);
label.setBackgroundResource(R.drawable.selector_text_bg);
label.setText(text);
label.setTextColor(createColorStateList("#ffffffff", "#ff44e6ff"));
addView(label);
}
}
//工具方法
private static ColorStateList createColorStateList(String selected, String normal) {
int[] colors = new int[]{Color.parseColor(selected), Color.parseColor(normal)};
int[][] states = new int[2][];
states[0] = new int[]{android.R.attr.state_selected};
states[1] = new int[]{};
ColorStateList colorList = new ColorStateList(states, colors);
return colorList;
}
主要是介紹ColorStateList的使用,通過對映關係來進行顏色變化。
以上的步驟也可以通過填充xml中的textview來實現,這種實現方式你可以更輕鬆的設定你的textview。以前定義過的一個螞蟻森林能量球效果,就說這種方式實現。螞蟻森林效果
結束語
讀萬卷書,行萬里路。雖然這些東西早就有人實現了,我們也許也使用過,但是親自實踐的必要性還是在的。
你的鼓勵是我前進的動力。