無論是在移動(dòng)端的App,還是在前端的網(wǎng)頁纪吮,我們經(jīng)常會(huì)看到下面這種標(biāo)簽的列表效果:
標(biāo)簽從左到右擺放,一行顯示不下時(shí)自動(dòng)換行要糊。這樣的效果用Android源生的控件很不好實(shí)現(xiàn),所以往往需要我們自己去自定義控件局劲。我在開發(fā)中就遇到過幾次要實(shí)現(xiàn)這樣的標(biāo)簽列表效果,所以就自己寫了個(gè)控件奶赠,放到我的GitHub鱼填,方便以后使用。有興趣的同學(xué)也歡迎訪問我的GitHub苹丸、查看源碼實(shí)現(xiàn)和使用該控件苇经。下面我將為大家介紹該控件的具體實(shí)現(xiàn)和使用。
要實(shí)現(xiàn)這樣一個(gè)標(biāo)簽列表其實(shí)并不難扇单,列表中的item可以直接用TextView來實(shí)現(xiàn)蜘澜,我們只需要關(guān)心列表控件的大小和標(biāo)簽的擺放就可以了施流。也就是說我們需要做的只要兩件事:測量布局(onMeasure)和擺放標(biāo)簽(onLayout)瞪醋。這是自定義ViewGroup的基本步驟扮碧,相信對自定義View有所了解的同學(xué)都不會(huì)陌生杏糙。下面我們就來看看具體的代碼實(shí)現(xiàn)蚓土。
控件的測量:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int contentHeight = 0; //記錄內(nèi)容的高度
int lineWidth = 0; //記錄行的寬度
int maxLineWidth = 0; //記錄最寬的行寬
int maxItemHeight = 0; //記錄一行中item高度最大的高度
boolean begin = true; //是否是行的開頭
//循環(huán)測量item并計(jì)算控件的內(nèi)容寬高
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
if(!begin) {
lineWidth += mWordMargin;
}else {
begin = false;
}
//當(dāng)前行顯示不下item時(shí)換行蜀漆。
if (maxWidth <= lineWidth + view.getMeasuredWidth()) {
contentHeight += mLineMargin;
contentHeight += maxItemHeight;
maxItemHeight = 0;
maxLineWidth = Math.max(maxLineWidth, lineWidth);
lineWidth = 0;
begin = true;
}
maxItemHeight = Math.max(maxItemHeight, view.getMeasuredHeight());
lineWidth += view.getMeasuredWidth();
}
contentHeight += maxItemHeight;
maxLineWidth = Math.max(maxLineWidth, lineWidth);
//測量控件的最終寬高
setMeasuredDimension(measureWidth(widthMeasureSpec,maxLineWidth),
measureHeight(heightMeasureSpec, contentHeight));
}
//測量控件的寬
private int measureWidth(int measureSpec, int contentWidth) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = contentWidth + getPaddingLeft() + getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
//這一句是為了支持minWidth屬性。
result = Math.max(result, getSuggestedMinimumWidth());
return result;
}
//測量控件的高
private int measureHeight(int measureSpec, int contentHeight) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = contentHeight + getPaddingTop() + getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
//這一句是為了支持minHeight屬性绷耍。
result = Math.max(result, getSuggestedMinimumHeight());
return result;
}
標(biāo)簽的擺放:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int x = getPaddingLeft();
int y = getPaddingTop();
int contentWidth = right - left;
int maxItemHeight = 0;
int count = getChildCount();
//循環(huán)擺放item
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
//當(dāng)前行顯示不下item時(shí)換行鲜侥。
if (contentWidth < x + view.getMeasuredWidth() + getPaddingRight()) {
x = getPaddingLeft();
y += mLineMargin;
y += maxItemHeight;
maxItemHeight = 0;
}
view.layout(x, y, x + view.getMeasuredWidth(), y + view.getMeasuredHeight());
x += view.getMeasuredWidth();
x += mWordMargin;
maxItemHeight = Math.max(maxItemHeight, view.getMeasuredHeight());
}
}
onMeasure和onLayout的實(shí)現(xiàn)代碼基本是一樣的描函,不同的只是一個(gè)是測量寬高,一個(gè)是擺放位置而已舀寓。實(shí)現(xiàn)起來非常的簡單互墓。
以上是LabelsView的核心代碼,LabelsView除了實(shí)現(xiàn)了item的測量和擺放以外篡撵,還提供了一系列的方法讓使用者可以方便設(shè)置標(biāo)簽的樣式(包括標(biāo)簽被選中的樣式)和標(biāo)簽點(diǎn)擊、選中的監(jiān)聽等骂租。下面LabelsView的使用介紹斑司。
1但汞、引入依賴
在Project的build.gradle在添加以下代碼
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
在Module的build.gradle在添加以下代碼
dependencies {
compile 'com.github.donkingliang:LabelsView:1.4.1'
}
2、編寫布局:
<com.donkingliang.labels.LabelsView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/labels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:labelBackground="@drawable/label_bg" //標(biāo)簽的背景
app:labelTextColor="@drawable/label_text_color" //標(biāo)簽的字體顏色 可以是一個(gè)顏色值
app:labelTextSize="14sp" //標(biāo)簽的字體大小
app:labelTextPaddingBottom="5dp" //標(biāo)簽的上下左右邊距
app:labelTextPaddingLeft="10dp"
app:labelTextPaddingRight="10dp"
app:labelTextPaddingTop="5dp"
app:lineMargin="10dp" //行與行的距離
app:wordMargin="10dp" //標(biāo)簽與標(biāo)簽的距離
app:selectType="SINGLE" //標(biāo)簽的選擇類型 有單選(可反選)僵缺、單選(不可反選)磕潮、多選、不可選四種類型
app:maxSelect="5" /> //標(biāo)簽的最大選擇數(shù)量自脯,只有多選的時(shí)候才有用,0為不限數(shù)量
這里有兩個(gè)地方需要說明一下:
1)標(biāo)簽的正常樣式和選中樣式是通過drawable來實(shí)現(xiàn)的锻狗。比如下面兩個(gè)drawable焕参。
<!-- 標(biāo)簽的背景 label_bg -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 標(biāo)簽選中時(shí)的背景 -->
<item android:state_selected="true">
<shape>
<stroke android:width="2dp" android:color="#fb435b" />
<corners android:radius="8dp" />
<solid android:color="@android:color/white" />
</shape>
</item>
<!-- 標(biāo)簽的正常背景 -->
<item>
<shape>
<stroke android:width="2dp" android:color="#656565" />
<corners android:radius="8dp" />
<solid android:color="@android:color/white" />
</shape>
</item>
</selector>
<!-- 標(biāo)簽的文字顏色 label_text_color -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 標(biāo)簽選中時(shí)的文字顏色 -->
<item android:color="#fb435b" android:state_selected="true" />
<!-- 標(biāo)簽的正常文字顏色 -->
<item android:color="#2d2b2b" />
</selector>
TextView的textColor屬性除了可以設(shè)置一個(gè)顏色值以外叠纷,也可以通過資源來設(shè)置的,這一點(diǎn)很多同學(xué)都不知道涩嚣。
2)標(biāo)簽的選擇類型有四種:
NONE :標(biāo)簽不可選中缓艳,也不響應(yīng)選中事件監(jiān)聽,這是默認(rèn)值阶淘。
SINGLE:單選(可反選)。這種模式下坤塞,可以一個(gè)也不選澈蚌。
SINGLE_IRREVOCABLY:單選(不可反選)。這種模式下浮禾,有且只有一個(gè)是選中的份汗。默認(rèn)是第一個(gè)。
MULTI:多選匆帚,可以通過設(shè)置maxSelect限定選擇的最大數(shù)量旁钧,0為不限數(shù)量互拾。maxSelect只有在多選的時(shí)候才有效嚎幸。多選模式下可以設(shè)置一些標(biāo)簽為必選項(xiàng)。必選項(xiàng)的標(biāo)簽?zāi)J(rèn)選中或衡,且不能取消车遂。
3、設(shè)置標(biāo)簽:
labelsView = (LabelsView) findViewById(labels);
ArrayList<String> label = new ArrayList<>();
label.add("Android");
label.add("IOS");
label.add("前端");
label.add("后臺(tái)");
label.add("微信開發(fā)");
label.add("游戲開發(fā)");
labelsView.setLabels(label); //直接設(shè)置一個(gè)字符串?dāng)?shù)組就可以了坡疼。
//LabelsView可以設(shè)置任何類型的數(shù)據(jù)衣陶,而不僅僅是String。
ArrayList<TestBean> testList = new ArrayList<>();
testList.add(new TestBean("Android",1));
testList.add(new TestBean("IOS",2));
testList.add(new TestBean("前端",3));
testList.add(new TestBean("后臺(tái)",4));
testList.add(new TestBean("微信開發(fā)",5));
testList.add(new TestBean("游戲開發(fā)",6));
labelsView.setLabels(testList, new LabelsView.LabelTextProvider<TestBean>() {
@Override
public CharSequence getLabelText(TextView label, int position, TestBean data) {
//根據(jù)data和position返回label需要顯示的數(shù)據(jù)教沾。
return data.getName();
}
});
4译断、設(shè)置事件監(jiān)聽:(如果需要的話)
//標(biāo)簽的點(diǎn)擊監(jiān)聽
labelsView.setOnLabelClickListener(new LabelsView.OnLabelClickListener() {
@Override
public void onLabelClick(TextView label, Object data, int position) {
//label是被點(diǎn)擊的標(biāo)簽,data是標(biāo)簽所對應(yīng)的數(shù)據(jù)堪唐,position是標(biāo)簽的位置翎蹈。
}
});
//標(biāo)簽的選中監(jiān)聽
labelsView.setOnLabelSelectChangeListener(new LabelsView.OnLabelSelectChangeListener() {
@Override
public void onLabelSelectChange(TextView label, Object data, boolean isSelect, int position) {
//label是被選中的標(biāo)簽,data是標(biāo)簽所對應(yīng)的數(shù)據(jù)合陵,isSelect是是否選中逞力,position是標(biāo)簽的位置。
}
});
5、常用方法
//設(shè)置選中標(biāo)簽执隧。
//positions是個(gè)可變類型户侥,表示被選中的標(biāo)簽的位置峦嗤。
//比喻labelsView.setSelects(1,2,5);選中第1,3,5個(gè)標(biāo)簽烁设。如果是單選的話,只有第一個(gè)參數(shù)有效装黑。
public void setSelects(int... positions);
public void setSelects(List<Integer> positions);
//獲取選中的標(biāo)簽(返回的是所有選中的標(biāo)簽的位置)糠睡。返回的是一個(gè)Integer的數(shù)組疚颊,表示被選中的標(biāo)簽的下標(biāo)。如果沒有選中均抽,數(shù)組的size等于0其掂。
public ArrayList<Integer> getSelectLabels();
//獲取選中的label(返回的是所有選中的標(biāo)簽的數(shù)據(jù))。如果沒有選中喘漏,數(shù)組的size等于0华烟。T表示標(biāo)簽的數(shù)據(jù)類型。
public <T> List<T> getSelectLabelDatas();
//取消所有選中的標(biāo)簽负饲。
public void clearAllSelect();
//設(shè)置標(biāo)簽的選擇類型喂链,有NONE、SINGLE洞坑、SINGLE_IRREVOCABLY和MULTI四種類型蝇率。
public void setSelectType(SelectType selectType);
//設(shè)置最大的選擇數(shù)量刽沾,只有selectType等于MULTI是有效排拷。
public void setMaxSelect(int maxSelect);
//設(shè)置必選項(xiàng),只有在多項(xiàng)模式下布蔗,這個(gè)方法才有效
public void setCompulsorys(int... positions)
public void setCompulsorys(List<Integer> positions)
//清空必選項(xiàng)浪腐,只有在多項(xiàng)模式下,這個(gè)方法才有效
public void clearCompulsorys()
//設(shè)置標(biāo)簽背景
public void setLabelBackgroundResource(int resId);
//設(shè)置標(biāo)簽的文字顏色
public void setLabelTextColor(int color);
public void setLabelTextColor(ColorStateList color);
//設(shè)置標(biāo)簽的文字大新饽小(單位是px)
public void setLabelTextSize(float size);
//設(shè)置標(biāo)簽內(nèi)邊距
public void setLabelTextPadding(int left, int top, int right, int bottom);
//設(shè)置行間隔
public void setLineMargin(int margin);
//設(shè)置標(biāo)簽的間隔
public void setWordMargin(int margin);
所有的set方法都有對應(yīng)的get方法傍睹,這里就不說了。
效果圖:
最后給出該控件在GitHub中的地址吮炕,歡迎大家訪問和使用访得。
https://github.com/donkingliang/LabelsView