最近項目的需求:需要一個View可以清晰的展現(xiàn)出員工的名字,并且可以進(jìn)行添加或刪除員工,本來打算Github找一個,但最近看了 Android開發(fā)藝術(shù)探索 看完了自定View的章節(jié)一直沒有動手寫一個,終于現(xiàn)在寫完了..
先看demo效果圖:

Android自帶的LineLayout只支持水平或者垂直布局不支持自動換行這點相信都知道,只要解決LineLayout不會自動換行和確定每個子View應(yīng)該顯示的位置,基本就算解決問題了.
有了解過自定義View都知道主要有兩個方法:
- onMeasure() --- 測量 (測量View的寬高)
- onLayout() --- 布局 (把View顯示在想要的位置)
看一眼上面的效果圖,簡單的說onMeasure就是計算出整個ViewGrop()在屏幕中所占的大小.onLayout是控制每個藍(lán)色的標(biāo)簽在整個View的位置.
onMeasure寬高應(yīng)該是多少,該怎么樣測量?
邏輯是這樣的: 以上面的gif圖為例,標(biāo)簽的第一行從"陳奕迅"到"權(quán)志龍"一共能塞下5個標(biāo)簽,并且仔細(xì)看會發(fā)現(xiàn)"權(quán)志龍"的標(biāo)簽的右邊還有剩下了一小部分空間,但是多出來空間不足以塞下一個標(biāo)簽"周星馳"所以只能加多一行了,到了第二行的最后一個標(biāo)簽"趙又廷"又發(fā)現(xiàn)剩下的空間塞不下"吳孟達(dá)"了只能再加多一行...一直到最后一行的一共需要4行才能剛好夠塞下所有標(biāo)簽.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//rows表示的是行數(shù),默認(rèn)為1
int rows = 1;
// lineWidth當(dāng)前行累計的總寬度
int lineWidth = 0;
//eachHeight表示的是當(dāng)前行的高度(注意是當(dāng)前行)
int eachHeight ;
//有N個子View就for循環(huán)執(zhí)行N次
for (int i = 0; i < childCount; i++) {
//獲取第N個子View
final View childView = getChildAt(i);
//獲取第N個子View的布局參數(shù)
LinearLayout.LayoutParams childViewLayoutParams= (LayoutParams) childView.getLayoutParams();
//獲取子View的布局參數(shù) 距離左右邊緣的大小
int left=childViewLayoutParams.leftMargin;
int right=childViewLayoutParams.rightMargin;
/*這里比較重要,只要for循環(huán)不是執(zhí)行到最后一次,就用當(dāng)前子View的(高度或?qū)挾?和下一個子View
的(高度或?qū)挾?進(jìn)行比較,把較大的值用(childViewMaxHeight或childViewmaxWidth)記錄下來.*/
if(i!=childCount-1){
childViewMaxHeight=getChildAt(i).getMeasuredHeight()<getChildAt(i+1).getMeasuredHeight()?getChildAt(i+1).getMeasuredHeight():childViewMaxHeight;
childViewmaxWidth=getChildAt(i).getMeasuredWidth()<getChildAt(i+1).getMeasuredWidth()?getChildAt(i+1).getMeasuredWidth():childViewmaxWidth;
}
//如果當(dāng)前行的總寬度+當(dāng)前子View的寬度+子View距離左右邊緣的大小>當(dāng)前屏幕的寬度
if(lineWidth+getChildAt(i).getMeasuredWidth() + left+right>=widthSpecSize){
//說明需要加一行,所以rows++
rows++;
lineWidth=left;
}
//當(dāng)前行的總寬度+=當(dāng)前子View的寬度+距離左右間距大小
lineWidth += (getChildAt(i).getMeasuredWidth() + left+right);
//每行的高度 = 當(dāng)前行的最高高度
eachHeight = childViewMaxHeight +top_bottom_margin ;
if (childCount == 0) {
//如果一個子View都沒有寬度和高度設(shè)置為0即可
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecpMode == MeasureSpec.AT_MOST) {
//如果寬高的屬性都是wrap_content,寬度是行的總寬度,高度是標(biāo)簽的 高度*行數(shù)
setMeasuredDimension(lineWidth, rows * eachHeight + top_bottom_margin);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
//如果高度和寬度的屬性都是march_parent,
setMeasuredDimension(lineWidth, heightSpecpSize);
} else if (heightSpecpMode == MeasureSpec.AT_MOST) {
//如果高度的屬性是wrap_content的用下面的測量方式
//寬度是屏幕的大小,高度是標(biāo)簽的(高度*行數(shù))
setMeasuredDimension(widthSpecSize, rows * eachHeight + top_bottom_margin);
}
}
需要注意的是下面這條判斷語句:
if(lineWidth+getChildAt(i).getMeasuredWidth() + left+right>=widthSpecSize){
rows++;
lineWidth=left;
}
因為lineWidth是當(dāng)前行的第一個標(biāo)簽到最后一個能完整顯示的標(biāo)簽所累加的寬度總和,而widthSpecSize是當(dāng)前手機屏幕寬度,這里的需求是每個標(biāo)簽都需要完整的顯示不能超出屏幕的寬度,否者會顯示不全,所以只要 [前者加上當(dāng)前標(biāo)簽的寬度總和>手機屏幕寬度] 說明當(dāng)前行已經(jīng)沒有足夠的位置塞下當(dāng)前標(biāo)簽了,所以需要加多一行 rows++.
onLayout里面要如何為每個子View顯示在合適的位置?
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//當(dāng)前子View的left
int childLeft = 0;
// childCount子View的個數(shù)
final int childCount = getChildCount();
//獲取第一個子View
View childView = getChildAt(0);
//獲取第一個子View的布局屬性
LinearLayout.LayoutParams childViewLayoutParams = (LayoutParams) childView.getLayoutParams();
//獲取第一個子View左右距離邊緣的大小
int left = childViewLayoutParams.leftMargin;
int right = childViewLayoutParams.rightMargin;
//把子View距離左邊緣的值賦值給childLeft(為什么這么做下面會說)
childLeft = left;
//初始化當(dāng)前當(dāng)前子View的高度
int currentHeight = 0;
//判斷當(dāng)前是否是第一行
boolean firstLine = true;
//略....
for (int i = 0; i < childCount; i++) {
childView = getChildAt(i);
childViewLayoutParams = (LayoutParams) childView.getLayoutParams();
left = childViewLayoutParams.leftMargin;
right = childViewLayoutParams.rightMargin;
//如果當(dāng)前子View可見
if (childView.getVisibility() != View.GONE) {
//獲取當(dāng)前子View的寬度
final int childWidth = childView.getMeasuredWidth();
//如果childLeft+子View的寬度 > 手機屏幕的寬度
if (childLeft + childWidth + left + right > getMeasuredWidth()) {
//childLeft從新開始計算
childLeft = left;
//currentHeight += 當(dāng)前行高度最高的子View的高度(childViewMaxHeight在onMeasure申明)
currentHeight += childViewMaxHeight + top_bottom_margin;
firstLine = false;
}
if (firstLine) {
childView.layout(childLeft, top_bottom_margin, childLeft + childWidth, childViewMaxHeight + top_bottom_margin);
} else {
childView.layout(childLeft, currentHeight + top_bottom_margin, childLeft + childWidth, childViewMaxHeight + currentHeight + top_bottom_margin);
}
//childLeft進(jìn)行累加
childLeft += childWidth + left + right;
}
}
}
onLayout邏輯相比onMeasure比較復(fù)雜,但理解了原理之后其實很簡單.
先理清幾個比較重要的成員變量的含義:
- childLeft
有過自定義屬性經(jīng)驗的人都應(yīng)該知道確定一個View在屏幕上顯示的位置主要由4個參數(shù)決定:Left,Top,Right,Bottom這里的childLeft就是每個子View的Left,請注意這里的childLeft的值是動態(tài)改變的.
可以看到onLayout函數(shù)中for循環(huán)語句的最后一段代碼是:
childLeft += childWidth + left + right;
意思是每遍歷一個子View就 讓 childLeft +=當(dāng)前子View 的寬度
以下面的圖為例:

假設(shè)陳奕迅這個子View的寬度是100,如果陳奕迅不想被周星馳擋住那么周星馳的Left至少要大于100
if (childLeft + childWidth + left + right > getMeasuredWidth()) {
childLeft = left;
currentHeight += childViewMaxHeight + top_bottom_margin;
firstLine = false;
}
上面這段代碼的意思可以這樣理解:
假設(shè)for循環(huán)執(zhí)行到了第二行的周星馳的時候進(jìn)行了判斷語句的時候, childLeft的值就是權(quán)志龍這個標(biāo)簽的 Left,如果childLeft + 當(dāng)前標(biāo)簽的寬度(這里指周星馳這個標(biāo)簽) > 手機屏幕的寬度,說明周星馳顯示不下了,只能換行了,所以currentHeight+= childViewMaxHeight(childViewMaxHeight在onMeasure申明過)這樣就不會出現(xiàn)第二行的周星馳把第一行的陳奕迅擋住的情況
if (firstLine) {
childView.layout(childLeft, top_bottom_margin, childLeft + childWidth, childViewMaxHeight + top_bottom_margin);
} else {
childView.layout(childLeft, currentHeight + top_bottom_margin, childLeft + childWidth, childViewMaxHeight + currentHeight + top_bottom_margin);
}
最后要注意的就是上面的代碼:
如果是第一行Top就是top_bottom_margin(這里是讓每個標(biāo)簽的上下之間有一個間距)
否者如果第二行Top就是取當(dāng)前標(biāo)簽所在的那一行中子View高度最高的值+top_bottom_margin
基本onLayout需要注意的代碼就是這些.
剩下的是添加標(biāo)簽,刪除標(biāo)簽,切換刪除標(biāo)簽的模式代碼,不寫解析了.沒看懂的可以在下面留言.
private boolean isMutil = false;
private List<View> AllViews;
//添加標(biāo)簽的主要的代碼:
public MyLinearLayout addTag(String... tag) {
for (int i = 0; i < tag.length; i++) {
if (AllViews == null)
AllViews = new ArrayList<>();
TextView tagView=NewTagView(tag[i]);
addView(tagView);
if (!AllViews.contains(tagView))
AllViews.add(tagView);
MultiRemove(isMutil);
}
return this;
}
private TextView NewTagView(String text) {
TextView tagView = new TextView(getContext());
tagView.setTextSize(20);
tagView.setPadding(30, 20, 30, 20);
tagView.setTextColor(Color.WHITE);
tagView.setBackgroundResource(normal_color);
tagView.setText(text);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp.setMargins(20, 10, 0, 0);
tagView.setLayoutParams(lp);
return tagView;
}
private List<View> selectView;
//執(zhí)行移除多個標(biāo)簽的代碼:
public void removeMultiTagViews() {
for (View view : selectView) {
removeView(view);
AllViews.remove(view);
}
}
//切換移除標(biāo)簽的模式(多選后點擊刪除按鈕刪除 或 長按某個標(biāo)簽直接刪除):
public void ChangeRemoveModel() {
isMutil = isMutil == false ? true : false;
MultiRemove(isMutil);
}
private void MultiRemove(boolean isMulti) {
for (View view : AllViews) {
final TextView tagView = (TextView) view;
if (isMulti) {
tagView.setOnClickListener(null);
tagView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
removeView(v);
AllViews.remove(v);
return false;
}
});
} else {
if (selectView == null)
selectView = new ArrayList<>();
tagView.setOnLongClickListener(null);
tagView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (selectView.contains(v)) {
tagView.setBackgroundResource(normal_color);
selectView.remove(v);
} else {
TextView tagView1 = (TextView) v;
tagView1.setBackgroundResource(select_color);
selectView.add(v);
}
}
});
}
}
}
調(diào)用代碼:
Button add;
Button remove;
Button switch_mode;
EditText tag_content;
MyLinearLayout tag_layout; //本文的自定義View
......
public void onClick(View v) {
switch (v.getId()){
case R.id.add:
if(tag_content.getText().toString().isEmpty()) {
Toast.makeText(MainActivity.this, "請輸入tag內(nèi)容再點擊新增TAG", Toast.LENGTH_SHORT).show();
return;
}
tag_layout.addTag(tag_content.getText().toString());
tag_content.setText("");
break;
case R.id.remove:
tag_layout.removeMultiTagViews();
break;
case R.id.switch_mode:
tag_layout.ChangeRemoveModel();
break;
}
}
如果文章有錯誤的地方希望能指出,避免誤導(dǎo)他人.