1. 程式人生 > >Android FlowLayout實現熱門標籤功能

Android FlowLayout實現熱門標籤功能

FlowLayout實現熱門標籤的功能想必大家都見過,有的為搜尋的歷史記錄,有的則是一些推薦等等。總之熱門標籤在很多應用裡面都有使用,先看一下實現的效果圖
這裡寫圖片描述
下面的一張是擷取的淘寶搜尋的效果
這裡寫圖片描述
那麼我們如何實現上面的效果呢?我實現的效果是充滿屏寬狀態的,而淘寶的則是沒有充滿屏寬的。如何實現充滿屏寬其實也不是很難。
下面我們就來探討一下如何實現:
首頁我們需要自定義一個控制元件也就是我們說的FlowLayout流式佈局。實現這樣的佈局我們要注意以下幾點:
1.新增子控制元件的時候是否超出邊界,如果超出邊界需要換行。
這裡寫圖片描述
2.就是要考慮我們間距和螢幕的邊界值,如果我們最後一個標籤的寬度+前面的標籤的寬度+前面標籤之間的間距=螢幕的寬度,此時是沒有最後一個標籤與螢幕右邊的邊距的也就是下面的情況
這裡寫圖片描述


這種情況怎麼辦呢?就是需要最後一個標籤移到下一行顯示,剩餘的空間有剩餘的幾個標籤平分。
3.如果單獨標籤的長度過長已經超出螢幕,那麼這個標籤的寬度就要壓縮到跟螢幕的寬度相同。
這裡寫圖片描述
為了防止上面的三種情況的發生我們需要在onMeasure(int widthMeasureSpec, int heightMeasureSpec)這個方法裡面獲取螢幕的寬度和高度

int availableWidth  = MeasureSpec.getSize(widthMeasureSpec)
                - getPaddingRight() - getPaddingLeft();
        int
availableHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

以上程式碼獲取的是可用空間的寬和高,因為我們一般設定標籤的時候會設定一些邊距,關於上面的程式碼的不懂的可以點選檢視自定義控制元件

裡面有詳解。
獲取到螢幕的可用的寬度以後,我們就需要根據我們自己標籤的寬度和可用的寬度進行比較。所以我們還需要獲取子控制元件的寬度和高度,實現程式碼如下

final View child = getChildAt(i);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(availableWidth ,
                widthMode== MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST: widthMode);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(availableHeight,
                heightMode== MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST: heightMode);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

第一種情況的發生,也就是新增子控制元件的時候超出邊界的判斷

int childWidth = child.getMeasuredWidth();
            mHaveUsedWidth += childWidth;
            if (mHaveUsedWidth <= availableWidth ) {
                mTagLine.addView(child);// 新增子控制元件
                mHaveUsedWidth += mHSpac;// 加上間隔
                if (mHaveUsedWidth >= availableWidth ) {
                    addLine();
                }
            }

上面的程式碼也包含了第二種情況mHaveUsedWidth >= availableWidth 就是說明如果加上間距等於可用空間,第三種情況的發生,也就是一個子控制元件的寬度很長已經超出了螢幕的寬度

if (mTagLine.getViewCount() == 0) {
    mTagLine.addView(child);
    addLine();
}else {
    addLine();
    mTagLine.addView(child);
    mHaveUsedWidth += childWidth + mHSpac;
    }

判斷mTagLine.getViewCount() == 0說明上一行已經被子控制元件佔滿,而下個子控制元件剛好是新的一行的第一個子控制元件,我們已經知道下一個子控制元件的長度已經超出了螢幕的寬度,所以下一個子控制元件肯定是要在新增加的那一行裡面,新增加的那一行的子控制元件的數量當然也就是0了。else裡面程式碼的意思就是此行已經有子控制元件了,因為這個子控制元件的長度大於螢幕的寬度,在任何一行上面只要有子控制元件,不論子控制元件的長度多小,都需要換行。
mTagLine.addView(child);的作用就是強制這個子控制元件在這一行。
addLine()的作用就是新增加一行,實現程式碼如下:

private void addLine() {
        mTagLines.add(mTagLine);
        mTagLine = new TagLine();
        mHaveUsedWidth = 0;
    }

TagLine是一個類代表的是一行,具體程式碼如下:

private class TagLine {
        int mAllChildWidth = 0;// 該行中所有的子控制元件加起來的寬度
        int mChildHeight = 0;// 子控制元件的高度
        List<View> viewList= new ArrayList<View>();
        public void addView(View view) {// 新增子控制元件
            viewList.add(view);
            mAllChildWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            mChildHeight = childHeight;// 行的高度當然是有子控制元件的高度決定了
        }
        public int getViewCount() {
            return viewList.size();
        }

        public void layoutView(int left, int top) {
            int childCount = getViewCount();
            //除去左右邊距後可以使用的寬度
            int validWidth= getMeasuredWidth() - getPaddingLeft()
                    - getPaddingRight();
            // 除了子控制元件以及子控制元件之間的間距後剩餘的空間
            int remainWidth = validWidth- mAllChildWidth - mHSpac
                    * (childCount - 1);
            if (remainWidth >= 0) {
                int divideSpac = (int) (remainWidth / childCount + 0.5);
                for (int i = 0; i < childCount; i++) {
                    final View view = viewList.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    // 把剩餘的空間平均分配到每個子控制元件上面
                    childWidth = childWidth + divideSpac;
                    view.getLayoutParams().width = childWidth;
                    // 由於平均分配剩餘空間導致子控制元件的長度發生了變化,需要重新測量
                    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                                childWidth, MeasureSpec.EXACTLY);
                        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                                childHeight, MeasureSpec.EXACTLY);
                        view.measure(widthMeasureSpec, heightMeasureSpec);
                    // 設定子控制元件的位置
                    view.layout(left, top, left + childWidth, top
                             + childHeight);
                    left += childWidth + mHSpac; // 獲取到的left值是下一個子控制元件的左邊所在的位置
                }
            } else {
                if (childCount == 1) {//這一種就是一行只有一個子控制元件的情況
                    View view = viewList.get(0);
                    view.layout(left, top, left + view.getMeasuredWidth(), top
                            + view.getMeasuredHeight());
                } 
            }
        }
    }

當然了,有時候標籤平分後感覺控制元件不是那麼的美觀,不想平分剩下的空間怎麼辦?也就是下面的情況
這裡寫圖片描述
要想實現上面的效果很簡單,就是去掉兩行程式碼即可。這兩行程式碼就是關於平分剩餘空間的

int divideSpac = (int) (remainWidth / childCount + 0.5);
childWidth = childWidth + divideSpac;

divideSpac 的值就是剩餘的空間平分到每個控制元件的值,childWidth 這個值就是自身的寬度加上平分的空間,其實就是我們所看到的平分後的控制元件的寬度。
下面就是全部的程式碼了
MainActivity 類

package com.lyxrobert.flowlayout;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.StateListDrawable;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {
    private FlowLayout flowLayout;
    private ClearEditText et_clear;
    private String[] data =  new String[]{"全部","這是","測試標籤",
        "這是測試標籤","FlowLayout","衣服","鞋子",
        "春","夏","深秋","寒冬",
        "測一下看看效果如何","心情還不錯哦","這是測試標籤","這是測試標籤",
        "這是測試標籤","受益匪淺啊","123456789","電話號碼"};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();

        }

    private void initView() {
        flowLayout = (FlowLayout) findViewById(R.id.fl);
        et_clear = (ClearEditText) findViewById(R.id.et_clear);
    }

    private void initData() {
        int padding = dip2px(5);
        flowLayout.setPadding(padding, padding, padding, padding);// 設定內邊距
        for (int i = 0; i < data.length; i++) {
            final String tag = data[i];
            TextView tv = new TextView(this);
            tv.setText(tag);
            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
            tv.setPadding(padding, padding, padding, padding);
            tv.setGravity(Gravity.CENTER);
            int color = 0xffcecece;// 按下後偏白的背景色
            StateListDrawable selector;
            if (i==0){
                tv.setTextColor(Color.WHITE);
                tv.setEnabled(false);
              selector = DrawableUtils.getSelector(false,Color.parseColor("#2c90d7"), color, dip2px(30));
            }else {
                selector = DrawableUtils.getSelector(true,Color.WHITE, color, dip2px(30));
            }
            tv.setBackgroundDrawable(selector);
            flowLayout.addView(tv);
            tv.setOnClickListener(new View.OnClickListener() {

                @Override
                public void onClick(View v) {
                    et_clear.setText(tag);
                    et_clear.setSelection(tag.trim().length());
                }
            });
    }

}

    public int dip2px(float dip) {
        float density = this.getResources().getDisplayMetrics().density;
        return (int) (dip * density + 0.5f);
    }
}

FlowLayout類

package com.lyxrobert.flowlayout;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class FlowLayout extends ViewGroup {
    /** 橫向間隔 */
    private int mHSpac = 0;
    /** 縱向間隔 */
    private int mVSpac = 0;
    /** 當前行已用的寬度*/
    private int mHaveUsedWidth = 0;
    /** 每一行的集合 */
    private final List<TagLine> mTagLines = new ArrayList<TagLine>();
    private TagLine mTagLine = null;
    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setHorizontalSpacing(dip2px(5));
        setVerticalSpacing(dip2px(5));
    }
    public int dip2px(float dip) {
        float density = this.getResources().getDisplayMetrics().density;
        return (int) (dip * density + 0.5f);
    }
    public void setHorizontalSpacing(int spacing) {
        if (mHSpac != spacing) {
            mHSpac = spacing;
            requestLayout();
        }
    }

    public void setVerticalSpacing(int spacing) {
        if (mVSpac != spacing) {
            mVSpac = spacing;
            requestLayout();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int availableWidth  = MeasureSpec.getSize(widthMeasureSpec)
                - getPaddingRight() - getPaddingLeft();
        int availableHeight = MeasureSpec.getSize(heightMeasureSpec)
                - getPaddingTop() - getPaddingBottom();
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        resetLine();// 將行的狀態重置為最原始的狀態,因為新的一行的資料跟以往的無關
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(availableWidth ,
                    widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST
                            : widthMode);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    availableHeight,
                    heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST
                            : heightMode);
            // 測量子控制元件
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            if (mTagLine == null) {
                mTagLine = new TagLine();
            }
            int childWidth = child.getMeasuredWidth();
            mHaveUsedWidth += childWidth;// 增加使用的寬度
            if (mHaveUsedWidth <= availableWidth ) {// 已經使用的寬度小於可用寬度,說明還有剩餘空間,該子控制元件新增到這一行。
                mTagLine.addView(child);// 新增子控制元件
                mHaveUsedWidth += mHSpac;// 加上間距
                if (mHaveUsedWidth >= availableWidth ) {// 加上間距後已經使用的寬度大於等於可用寬度,說明這一行已滿或者已經超出需要換行
                    addLine();
                }
            } else {
                //說明上一行已經被子控制元件佔滿,而下個子控制元件剛好是新的一行的第一個子控制元件
                if (mTagLine.getViewCount() == 0) {
                    mTagLine.addView(child);
                    addLine();
                } else {
                    //因為這個子控制元件的長度大於螢幕的寬度,在任何一行上面只要有子控制元件,不論子控制元件的長度多小,都需藥換行
                    addLine();
                    mTagLine.addView(child);
                    mHaveUsedWidth += childWidth + mHSpac;
                }
            }
        }

        if (mTagLine != null && mTagLine.getViewCount() > 0
                && !mTagLines.contains(mTagLine)) {
            //此段程式碼的作用是為了防止因最後一行程式碼的子控制元件未佔滿空間,但是畢竟也是一行,所以也要新增到行的集合裡面
            mTagLines.add(mTagLine);
        }

        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
        int totalHeight = 0;
        final int size = mTagLines.size();
        for (int i = 0; i < size; i++) {// 加上所有行的高度
            totalHeight += mTagLines.get(i).mChildHeight;
        }
        totalHeight += mVSpac * (size - 1);// 加上所有間距的高度
        totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding
        setMeasuredDimension(totalWidth,
                resolveSize(totalHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int left = getPaddingLeft();// 獲取最初的左上點
            int top = getPaddingTop();
            final int linesCount = mTagLines.size();
            for (int i = 0; i < linesCount; i++) {
                final TagLine oneLine = mTagLines.get(i);
                oneLine.layoutView(left, top);// 設定每一行所在的位置
                top += oneLine.mChildHeight + mVSpac;// 這個top的值其實就是下一個的上頂點值
            }
        }

    /** 將行的狀態重置為最原始的狀態*/
    private void resetLine() {
        mTagLines.clear();
        mTagLine = new TagLine();
        mHaveUsedWidth = 0;
    }

    /** 新增加一行 */
    private void addLine() {
        mTagLines.add(mTagLine);
            mTagLine = new TagLine();
            mHaveUsedWidth = 0;
    }
    /**
     * 代表著一行,封裝了一行所佔高度,該行子View的集合,以及所有View的寬度總和
     */
    private class TagLine {
        int mAllChildWidth = 0;// 該行中所有的子控制元件加起來的寬度
        int mChildHeight = 0;// 子控制元件的高度
        List<View> viewList= new ArrayList<View>();
        public void addView(View view) {// 新增子控制元件
            viewList.add(view);
            mAllChildWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            mChildHeight = childHeight;// 行的高度當然是有子控制元件的高度決定了
        }
        public int getViewCount() {
            return viewList.size();
        }

        public void layoutView(int left, int top) {
            int childCount = getViewCount();
            //除去左右邊距後可以使用的寬度
            int validWidth= getMeasuredWidth() - getPaddingLeft()
                    - getPaddingRight();
            // 除了子控制元件以及子控制元件之間的間距後剩餘的空間
            int remainWidth = validWidth- mAllChildWidth - mHSpac
                    * (childCount - 1);
            if (remainWidth >= 0) {
                int divideSpac = (int) (remainWidth / childCount + 0.5);
                for (int i = 0; i < childCount; i++) {
                    final View view = viewList.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    // 把剩餘的空間平均分配到每個子控制元件上面
                    childWidth = childWidth + divideSpac;
                    view.getLayoutParams().width = childWidth;
                    // 由於平均分配剩餘空間導致子控制元件的長度發生了變化,需要重新測量
                    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            childWidth, MeasureSpec.EXACTLY);
                    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            childHeight, MeasureSpec.EXACTLY);
                    view.measure(widthMeasureSpec, heightMeasureSpec);
                    // 設定子控制元件的位置
                    view.layout(left, top, left + childWidth, top
                            + childHeight);
                    left += childWidth + mHSpac; // 獲取到的left值是下一個子控制元件的左邊所在的位置
                }
            } else {
                if (childCount == 1) {//這一種就是一行只有一個子控制元件的情況
                    View view = viewList.get(0);
                    view.layout(left, top, left + view.getMeasuredWidth(), top
                            + view.getMeasuredHeight());
                }
            }
        }
    }

}

佈局檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <com.lyxrobert.flowlayout.ClearEditText
        android:id="@+id/et_clear"
        android:layout_margin="10dp"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:textSize="18dp"
        android:textColor="@android:color/white"
        android:background="@drawable/search_bg"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <com.lyxrobert.flowlayout.FlowLayout
        android:id="@+id/fl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    >
    </com.lyxrobert.flowlayout.FlowLayout>
    </ScrollView>
</LinearLayout>

如有疑問歡迎留言

掃一掃關於個人公眾號

這裡寫圖片描述