Android自定義標籤列表控制元件LabelsView解析

DonKingLiang發表於2017-05-04

無論是在移動端的App,還是在前端的網頁,我們經常會看到下面這種標籤的列表效果:

標籤列表
標籤從左到右擺放,一行顯示不下時自動換行。這樣的效果用Android源生的控制元件很不好實現,所以往往需要我們自己去自定義控制元件。我在開發中就遇到過幾次要實現這樣的標籤列表效果,所以就自己寫了個控制元件,放到我的GitHub,方便以後使用。有興趣的同學也歡迎訪問我的GitHub、檢視原始碼實現和使用該控制元件。下面我將為大家介紹該控制元件的具體實現和使用。 要實現這樣一個標籤列表其實並不難,列表中的item可以直接用TextView來實現,我們只需要關心列表控制元件的大小和標籤的擺放就可以了。也就是說我們需要做的只要兩件事:測量佈局(onMeasure)和擺放標籤(onLayout)。這是自定義ViewGroup的基本步驟,相信對自定義View有所瞭解的同學都不會陌生。下面我們就來看看具體的程式碼實現。 控制元件的測量:

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int count = getChildCount();
        int maxWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();

        int contentHeight = 0; //記錄內容的高度
        int lineWidth = 0; //記錄行的寬度
        int maxLineWidth = 0; //記錄最寬的行寬
        int maxItemHeight = 0; //記錄一行中item高度最大的高度
        boolean begin = true; //是否是行的開頭

		//迴圈測量item並計算控制元件的內容寬高
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            
           if(!begin) {
                lineWidth += mWordMargin;
            }else {
                begin = false;
            }
			//當前行顯示不下item時換行。
            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;
    }
複製程式碼

標籤的擺放:

	@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();
        //迴圈擺放item
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);

			//當前行顯示不下item時換行。
            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的實現程式碼基本是一樣的,不同的只是一個是測量寬高,一個是擺放位置而已。實現起來非常的簡單。 以上是LabelsView的核心程式碼,LabelsView除了實現了item的測量和擺放以外,還提供了一系列的方法讓使用者可以方便設定標籤的樣式(包括標籤被選中的樣式)和標籤點選、選中的監聽等。下面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"     //標籤的背景
       app:labelTextColor="@drawable/label_text_color" //標籤的字型顏色 可以是一個顏色值
       app:labelTextSize="14sp"      //標籤的字型大小
       app:labelTextPaddingBottom="5dp"   //標籤的上下左右邊距
       app:labelTextPaddingLeft="10dp"
       app:labelTextPaddingRight="10dp"
       app:labelTextPaddingTop="5dp"
       app:lineMargin="10dp"   //行與行的距離
       app:wordMargin="10dp"   //標籤與標籤的距離
       app:selectType="SINGLE"  //標籤的選擇型別 有單選(可反選)、單選(不可反選)、多選、不可選四種型別
       app:maxSelect="5" />  //標籤的最大選擇數量,只有多選的時候才有用,0為不限數量
複製程式碼

這裡有兩個地方需要說明一下:

1)標籤的正常樣式和選中樣式是通過drawable來實現的。比如下面兩個drawable。

<!-- 標籤的背景 label_bg -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 標籤選中時的背景 -->
    <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>
    <!-- 標籤的正常背景 -->
    <item>
        <shape>
            <stroke android:width="2dp" android:color="#656565" />
            <corners android:radius="8dp" />
            <solid android:color="@android:color/white" />
        </shape>
    </item>
</selector>
複製程式碼
<!-- 標籤的文字顏色 label_text_color -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 標籤選中時的文字顏色 -->
    <item android:color="#fb435b" android:state_selected="true" />
    <!-- 標籤的正常文字顏色 -->
    <item android:color="#2d2b2b" />
</selector>
複製程式碼

TextView的textColor屬性除了可以設定一個顏色值以外,也可以通過資源來設定的,這一點很多同學都不知道。

2)標籤的選擇型別有四種:

NONE :標籤不可選中,也不響應選中事件監聽,這是預設值。

SINGLE:單選(可反選)。這種模式下,可以一個也不選。

SINGLE_IRREVOCABLY:單選(不可反選)。這種模式下,有且只有一個是選中的。預設是第一個。

MULTI:多選,可以通過設定maxSelect限定選擇的最大數量,0為不限數量。maxSelect只有在多選的時候才有效。多選模式下可以設定一些標籤為必選項。必選項的標籤預設選中,且不能取消。

3、設定標籤:

labelsView = (LabelsView) findViewById(labels);
ArrayList<String> label = new ArrayList<>();
label.add("Android");
label.add("IOS");
label.add("前端");
label.add("後臺");
label.add("微信開發");
label.add("遊戲開發");
labelsView.setLabels(label); //直接設定一個字串陣列就可以了。

//LabelsView可以設定任何型別的資料,而不僅僅是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("後臺",4));
testList.add(new TestBean("微信開發",5));
testList.add(new TestBean("遊戲開發",6));
labelsView.setLabels(testList, new LabelsView.LabelTextProvider<TestBean>() {
    @Override
    public CharSequence getLabelText(TextView label, int position, TestBean data) {
    	//根據data和position返回label需要顯示的資料。
        return data.getName();
    }
});
複製程式碼

4、設定事件監聽:(如果需要的話)

//標籤的點選監聽
labelsView.setOnLabelClickListener(new LabelsView.OnLabelClickListener() {
    @Override
    public void onLabelClick(TextView label, Object data, int position) {
         //label是被點選的標籤,data是標籤所對應的資料,position是標籤的位置。
    }
});
//標籤的選中監聽
labelsView.setOnLabelSelectChangeListener(new LabelsView.OnLabelSelectChangeListener() {
    @Override
    public void onLabelSelectChange(TextView label, Object data, boolean isSelect, int position) {
        //label是被選中的標籤,data是標籤所對應的資料,isSelect是是否選中,position是標籤的位置。
    }
});
複製程式碼

5、常用方法

//設定選中標籤。
//positions是個可變型別,表示被選中的標籤的位置。
//比喻labelsView.setSelects(1,2,5);選中第1,3,5個標籤。如果是單選的話,只有第一個引數有效。
public void setSelects(int... positions);
public void setSelects(List<Integer> positions)//獲取選中的標籤(返回的是所有選中的標籤的位置)。返回的是一個Integer的陣列,表示被選中的標籤的下標。如果沒有選中,陣列的size等於0。
public ArrayList<Integer> getSelectLabels();
//獲取選中的label(返回的是所有選中的標籤的資料)。如果沒有選中,陣列的size等於0。T表示標籤的資料型別。
public <T> List<T> getSelectLabelDatas();

//取消所有選中的標籤。
public void clearAllSelect();

//設定標籤的選擇型別,有NONE、SINGLE、SINGLE_IRREVOCABLY和MULTI四種型別。
public void setSelectType(SelectType selectType);

//設定最大的選擇數量,只有selectType等於MULTI是有效。
public void setMaxSelect(int maxSelect);

//設定必選項,只有在多項模式下,這個方法才有效
public void setCompulsorys(int... positions)
public void setCompulsorys(List<Integer> positions)

//清空必選項,只有在多項模式下,這個方法才有效
public void clearCompulsorys()

//設定標籤背景
public void setLabelBackgroundResource(int resId);

//設定標籤的文字顏色
public void setLabelTextColor(int color);
public void setLabelTextColor(ColorStateList color);

//設定標籤的文字大小(單位是px)
public void setLabelTextSize(float size);

//設定標籤內邊距
public void setLabelTextPadding(int left, int top, int right, int bottom);

//設定行間隔
public void setLineMargin(int margin);

//設定標籤的間隔
public void setWordMargin(int margin);
複製程式碼

所有的set方法都有對應的get方法,這裡就不說了。

效果圖:

效果圖.gif

最後給出該控制元件在GitHub中的地址,歡迎大家訪問和使用。 github.com/donkinglian…

文章已同步到我的簡書

相關文章