Android根據標籤長度自動換行

我們在APP中經常看到這樣的效果:



這是美團的熱門搜索界面,裏面羅列出了長度不等的標籤,應用會根據標籤的長度自動換行,比如第一行有3個標籤,而第二行只有2個標籤,這篇文章就來講下如何實現這種效果,首先來看效果圖,爲了更好的展示不同長度和寬度時的顯示效果,這裏加了一個紅色背景,實際使用時自己去掉即可

1. 當寬度爲MATCH_PARENT,高度爲WRAP_CONTENT時



2. 當寬度爲WRAP_CONTENT,高度爲WRAP_CONTENT時



3. 當寬度爲指定值時,這裏爲400dp,高度爲WRAP_CONTENT時



4. 當寬度和高度都爲指定值時,這裏寬度爲500dp,高度爲200dp



其它情況不一一列出,不管寬高度如何設置,MATCH_PARENT,WRAP_CONTENT或者指定值(當然指定值不能小於實際需要的長度),均可正常工作,下面來說下原理:


這裏毫無疑問,用到了自定義控件,這裏每個標籤都是一個View,所以這個自定義控件是一個ViewGroup,用來管理這裏所有的子View。自定義ViewGroup最重要的就是onMeasure和onLayout方法,前者用來測量自定義控件本身的大小,後者用來確定自定義控件中的每個子控件的位置,來看自定義ViewGroup的實現:

1. 繼承ViewGroup,聲明構造函數

public class TagLayout extends ViewGroup {
    private List<int[]> children;

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        children = new ArrayList<int[]>();
    }

這裏聲明瞭一個變量children,這個變量用來存儲每個child的位置,int數組中int[0]爲child的left座標,int[1]爲child的top座標,int[2]爲child的right座標,int[3]爲child的bottom座標,之所以設置這樣一個變量存儲每個child的位置,是因爲在onMeasure中計算自定義控件的大小時,就需要根據所有子控件佔據的空間來確定,這時已經算出了子控件的位置,而在onLayout中,再次計算子控件的大小就重複了,所以設置這樣一個變量,在onMeasure中賦值,而在onLayout中使用


2. 實現onMeasure方法

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

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        final int count = getChildCount(); // tag的數量
        int left = 0; // 當前的左邊距離
        int top = 0; // 當前的上邊距離
        int totalHeight = 0; // WRAP_CONTENT時控件總高度
        int totalWidth = 0; // WRAP_CONTENT時控件總寬度

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();

            if (i == 0) { // 第一行的高度
                totalHeight = params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            if (left + params.leftMargin + child.getMeasuredWidth() + params.rightMargin > getMeasuredWidth()) { // 換行
                left = 0;
                top += params.topMargin + child.getMeasuredHeight() + params.bottomMargin; // 每個TextView的高度都一樣,隨便取一個都行
                totalHeight += params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            children.add(new int[]{left + params.leftMargin, top + params.topMargin, left + params.leftMargin + child.getMeasuredWidth(), top + params.topMargin + child.getMeasuredHeight()});

            left += params.leftMargin + child.getMeasuredWidth() + params.rightMargin;

            if (left > totalWidth) { // 當寬度爲WRAP_CONTENT時,取寬度最大的一行
                totalWidth = left;
            }
        }

        int height = 0;
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        } else {
            height = totalHeight;
        }

        int width = 0;
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        } else {
            width = totalWidth;
        }

        setMeasuredDimension(width, height);
    }

這個方法有點長,但原理比較簡單,首先調用measureChildren(widthMeasureSpec, heightMeasureSpec);方法,這個方法的作用在於測量每個子控件的大小和模式,因爲下面我們需要獲取每個子控件的寬高和margin等參數的值,所以必須首先調用該方法。

接下來聲明瞭一些用到的變量left, top, totalHeight, totalWidth等,變量的作用註釋已經說明了。

下面進入核心部分,遍歷自定義控件的所有子控件,在這裏,大家首先需要明白,對自定義控件的長和寬的值,有兩種大的情況,一種是MATCH_PARENT或指定值,一種是WRAP_CONTENT,其中前者的值是確定的,我們可以直接通過MeasureSpec來獲取,而後者的值是不定的,我們需要自己計算,上面的totalHeight和totalWidth變量,都是針對的後一種情況。

當i == 0,也就是第一個子控件時,我們首先計算totalHeight的值,因爲每個子控件的高度都一樣,所以就取第一個即可,它的值包括3個部分,自身的高度和上下的邊距。

接下來,我們計算當前的left加上我們現在準備添加的子控件的寬度後,是否大於自定義控件的寬度,如果大於,那說明要換行了,這個時候要將left重新賦值爲0,因爲換行後它的左邊距離爲0了,並且將top和totalHeight都加上一個子控件的高度,也就是一行的高度。而如果當前的left加上準備添加的子控件的寬度小於自定義控件的寬度,則說明在這行新加一個子控件是沒有問題的。

接着,將子控件的位置存儲在children中,這裏就是每個子控件在自定義控件中的left,top,right,bottom,根據當前已有的left,top以及子控件本身的寬高和margin,很容易計算出來,存儲在children中後,就可以在onLayout的時候直接用了。

完後,將left的值加上當前子控件佔據的控件,也就是放上新子控件後,left的新位置,同時還要記得比較left和totalWidth的值,當自定義控件的寬度爲WRAP_CONTENT時,totalWidth的值爲最寬一行的寬度。

下面,當前自定義控件的模式,也就是最開始說的兩種大的情況,當模式爲EXACTLY時,說明寬高是精確的,直接通過MeasureSpec.getSize取出即可,而否則,就是寬高不固定的情況,這時就要用我們上面定義的totalWidth和totalHeight的值了。

最後,調用setMeasuredDimension方法來確定自定義控件的寬高。

3. 實現onLayout方法

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            int[] position = children.get(i);
            child.layout(position[0], position[1], position[2], position[3]);
        }
    }

在onMeasure方法中,我們已經得到了每個子控件的left, top, right,bottom,所以這裏就很簡單了,直接調用layout方法確定每個子控件的位置即可。


完整的TagLayout.java方法如下:

public class TagLayout extends ViewGroup {
    private List<int[]> children;

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        children = new ArrayList<int[]>();
    }

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

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        final int count = getChildCount(); // tag的數量
        int left = 0; // 當前的左邊距離
        int top = 0; // 當前的上邊距離
        int totalHeight = 0; // WRAP_CONTENT時控件總高度
        int totalWidth = 0; // WRAP_CONTENT時控件總寬度

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();

            if (i == 0) { // 第一行的高度
                totalHeight = params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            if (left + params.leftMargin + child.getMeasuredWidth() + params.rightMargin > getMeasuredWidth()) { // 換行
                left = 0;
                top += params.topMargin + child.getMeasuredHeight() + params.bottomMargin; // 每個TextView的高度都一樣,隨便取一個都行
                totalHeight += params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            children.add(new int[]{left + params.leftMargin, top + params.topMargin, left + params.leftMargin + child.getMeasuredWidth(), top + params.topMargin + child.getMeasuredHeight()});

            left += params.leftMargin + child.getMeasuredWidth() + params.rightMargin;

            if (left > totalWidth) { // 當寬度爲WRAP_CONTENT時,取寬度最大的一行
                totalWidth = left;
            }
        }

        int height = 0;
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        } else {
            height = totalHeight;
        }

        int width = 0;
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        } else {
            width = totalWidth;
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            int[] position = children.get(i);
            child.layout(position[0], position[1], position[2], position[3]);
        }
    }
}

其它的工作就比較簡單了,界面佈局activity_main.xml如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/root"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
        >

    <com.my.flowlayout.TagLayout
            android:id="@+id/tags"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#F00">
    </com.my.flowlayout.TagLayout>
</LinearLayout>

注意這裏com.my.flowlayout要改成你自己的包名,android:background="#F00"是我爲了清楚看到不同寬高的背景,實際使用中請去掉

主類MainActivity.java如下:

public class MainActivity extends Activity {
    TagLayout mFlowLayout;
    String[] tags = new String[] {"別人家孩子作業做到轉鍾", "別人家孩子週末都在家學習", "成天就知道玩遊戲", "別人上清華了", "比你優秀的人還比你勤奮", "我怎麼教出你這麼個不爭氣的敗家子", "因爲你是小明?"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mFlowLayout = (TagLayout) findViewById(R.id.tags);
        for (int i = 0; i < tags.length; i++) {
            TextView tv = new TextView(this);
            tv.setText(tags[i]);
            tv.setTextColor(Color.BLACK);

            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
            params.setMargins(10, 10, 10, 10);
            tv.setLayoutParams(params);

            tv.setBackgroundResource(R.drawable.text_background);
            mFlowLayout.addView(tv);
        }
    }
}

這裏使用了我們自定義的ViewGroup類TagLayout,完後動態添加TextView,也就是標籤,params.setMargins(10, 10, 10, 10);是每個標籤的margin的值,可以自行根據實際需要修改,這裏還用到了一個背景text_background.xml,如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="#CCC"/>

    <corners android:radius="5dp"/>

    <padding
            android:bottom="5dp"
            android:left="10dp"
            android:right="10dp"
            android:top="5dp"/>
</shape>

這就是每個標籤的背景,沒什麼好說的,至此就完成了所有的工作。


如果不想複製粘貼,也可以直接下載項目源碼下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章