1. 程式人生 > >FlowLayout流式佈局實現搜尋歷史或熱門標籤

FlowLayout流式佈局實現搜尋歷史或熱門標籤

  最近專案中有這麼一個需求:實現搜尋歷史記錄的展示,預設只展示最近搜尋的10條記錄,並且最近搜尋的首先展示,其餘按搜尋時的先後順序依次展示;筆者想到(FlowLayout+SharedPreferences+List+TextView)來實現;
  看一下實現的效果圖:
這裡寫圖片描述
  筆者想到用FlowLayout流式佈局來展示搜尋歷史(自己實現或者使用開源庫),為了實現最近搜尋的最先展示,且不展示重複的搜尋歷史,筆者想到使用List集合來儲存搜尋歷史,並在儲存的時候進行去重,使用SharedPreferences來儲存搜尋歷史(SharedPreferences預設不能存取List集合,需要編寫工具類來儲存List集合),展示搜尋歷史時倒序遍歷List集合展示就OK啦!
  本文程式碼傳送門:
  

https://github.com/henryneu/TestHistorySearch
  構思完畢,開始動手擼程式碼!!!
  1、Android並沒有提供FlowLayout流式佈局,但是很多情況下,流式佈局的使用會非常合適,就比如關鍵字搜尋歷史、熱門標籤等等;流式佈局即我們新增到FlowLayout中的控制元件會根據ViewGroup的寬,自動的往右新增,如果當前所在行的剩餘空間放不下,則自動新增到下一行;
  那麼,既然Android沒有提供,我們可以自己實現一個,當然現在開源庫上有實現好的FlowLayout,不過自己動手實現一個,我想收貨肯定會更大的;實現FlowLayout類主要實現onMeasure、onLayout和generateLayoutParams方法,下面將分別介紹:
  1.1、onMeasure:測量所有子View的寬高值,然後根據所有子View的寬高值,計算自己的寬高值(如果子View的佈局不是設定的wrap_content,直接使用父ViewGroup傳入的計算值即可);

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 獲取父容器 ViewGroup 的 Padding
        int mPaddingLeft = getPaddingLeft();
        int mPaddingRight = getPaddingRight();
        int mPaddingTop = getPaddingTop();
        int mPaddingBottom = getPaddingBottom();

        // 獲得父容器 ViewGroup 為子 View 設定的測量模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int lineUsed = mPaddingLeft + mPaddingRight; int lineY = mPaddingTop; // 記錄每一行的高度值 int lineHeight = 0; // 迴圈遍歷所有的子 View for (int i = 0; i < this.getChildCount(); i++) { View child = this.getChildAt(i); if (child.getVisibility() == GONE) { continue; } // 記錄寬高值 int spaceWidth = 0; int spaceHeight = 0; // 獲取父容器 ViewGroup 為子 View 設定的 佈局 LayoutParams childLp = child.getLayoutParams(); if (childLp instanceof MarginLayoutParams) { // 測量每一個子 View 的實際寬高值 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, lineY); MarginLayoutParams mlp = (MarginLayoutParams) childLp; spaceWidth = mlp.leftMargin + mlp.rightMargin; spaceHeight = mlp.topMargin + mlp.bottomMargin; } else { // 測量每一個子 View 的寬高值 measureChild(child, widthMeasureSpec, heightMeasureSpec); } int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); // 得到子 View 所佔據的實際寬高值 spaceWidth += childWidth; spaceHeight += childHeight; if (lineUsed + spaceWidth > widthSize) { // 達到寬度的限制,移動到下一行 lineY += lineHeight + lineSpacing; lineUsed = mPaddingLeft + mPaddingRight; lineHeight = 0; } if (spaceHeight > lineHeight) { lineHeight = spaceHeight; } lineUsed += spaceWidth; } setMeasuredDimension( widthSize, heightMode == MeasureSpec.EXACTLY ? heightSize : lineY + lineHeight + mPaddingBottom ); }

  1.2、onLayout:對所有子View進行佈局,即設定子View在ViewGroup中的位置;

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mPaddingLeft = getPaddingLeft();
        int mPaddingRight = getPaddingRight();
        int mPaddingTop = getPaddingTop();

        int lineX = mPaddingLeft;
        int lineY = mPaddingTop;
        int lineWidth = r - l;
        usefulWidth = lineWidth - mPaddingLeft - mPaddingRight;
        int lineUsed = mPaddingLeft + mPaddingRight;
        int lineHeight = 0;
        int lineNum = 0;

        lineNumList.clear();
        for (int i = 0; i < this.getChildCount(); i++) {
            View child = this.getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            int spaceWidth = 0;
            int spaceHeight = 0;
            int left = 0;
            int top = 0;
            int right = 0;
            int bottom = 0;
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            LayoutParams childLp = child.getLayoutParams();
            if (childLp instanceof MarginLayoutParams) {
                MarginLayoutParams mlp = (MarginLayoutParams) childLp;
                spaceWidth = mlp.leftMargin + mlp.rightMargin;
                spaceHeight = mlp.topMargin + mlp.bottomMargin;
                left = lineX + mlp.leftMargin;
                top = lineY + mlp.topMargin;
                right = lineX + mlp.leftMargin + childWidth;
                bottom = lineY + mlp.topMargin + childHeight;
            } else {
                left = lineX;
                top = lineY;
                right = lineX + childWidth;
                bottom = lineY + childHeight;
            }
            spaceWidth += childWidth;
            spaceHeight += childHeight;

            if (lineUsed + spaceWidth > lineWidth) {
                // 達到寬度的限制,移動到下一行
                lineNumList.add(lineNum);
                lineY += lineHeight + lineSpacing;
                lineUsed = mPaddingLeft + mPaddingRight;
                lineX = mPaddingLeft;
                lineHeight = 0;
                lineNum = 0;
                if (childLp instanceof MarginLayoutParams) {
                    MarginLayoutParams mlp = (MarginLayoutParams) childLp;
                    left = lineX + mlp.leftMargin;
                    top = lineY + mlp.topMargin;
                    right = lineX + mlp.leftMargin + childWidth;
                    bottom = lineY + mlp.topMargin + childHeight;
                } else {
                    left = lineX;
                    top = lineY;
                    right = lineX + childWidth;
                    bottom = lineY + childHeight;
                }
            }
            // 子 View 設定計算後在佈局中的位置
            child.layout(left, top, right, bottom);
            lineNum ++;
            if (spaceHeight > lineHeight) {
                lineHeight = spaceHeight;
            }
            lineUsed += spaceWidth;
            lineX += spaceWidth;
        }
        // 新增最後一行的 Num
        lineNumList.add(lineNum);
    }

  1.3、generateLayoutParams:與當前ViewGroup所對應的LayoutParams,FlowLayout這裡我們只需要支援margin,因此使用系統的MarginLayoutParams;

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(super.generateDefaultLayoutParams());
    }

  2、筆者想使用SharedPreferences來儲存List集合,但是SharedPreferences能存取基本的如String、Int等資料,也能存取Set集合,偏偏不能存取List集合,哭一會兒去;讀者可能會想那就用Set集合實現唄!Set集合儲存搜尋歷史,我們還不用手動去重,這是因為Set自己就能避免重複,但是Set是無序的集合,實現我們的需求也比較麻煩,所以筆者就自己寫了一個工具類使用SharedPreferences來存取以及移除List集合;

public class StorageListSPUtils {

    private static SharedPreferences mSharedPreferences;

    public StorageListSPUtils(Context context, String preferenceName) {
        mSharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
    }

    /**
     * 儲存 List
     * @param tag
     * @param datalist
     */
    public <T> void saveDataList(String tag, List<T> datalist) {
        if (null == datalist || datalist.size() <= 0)
            return;

        Gson gson = new Gson();
        // 轉換成 Json 資料,再儲存
        String strJson = gson.toJson(datalist);
        SharedPreferences.Editor mEditor = mSharedPreferences.edit();
        mEditor.clear();
        mEditor.putString(tag, strJson);
        mEditor.apply();
    }

    /**
     * 獲取 List
     * @param tag
     * @return
     */
    public <T> List<T> loadDataList(String tag) {
        List<T> dataList = new ArrayList<>();
        // 獲取儲存的 Json 資料
        String strJson = mSharedPreferences.getString(tag, null);
        if (null == strJson) {
            return dataList;
        }

        Gson gson = new Gson();
        dataList = gson.fromJson(strJson, new TypeToken<List<T>>() {}.getType());
        return dataList;
    }

    /**
     * 移除 List
     * @param tag
     */
    public <T> void removeDateList(String tag) {
        SharedPreferences.Editor mEditor = mSharedPreferences.edit();
        mEditor.remove(tag);
        mEditor.apply();
    }
}

  3、工具準備完了,可以開始動手擼程式碼測試效果啦!
  3.1、TextView的佈局檔案
  search_history_tv.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="10dp"
    android:layout_marginBottom="10dp"
    android:gravity="center"
    android:background="@drawable/search_history_bg_selector"
    android:textSize="16sp"
    android:textColor="@drawable/search_history_color_selector"
    android:text="@string/app_search_history_tv"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:paddingTop="6dp"
    android:paddingBottom="6dp">
</TextView>

  3.2、TextView邊框的樣式

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
    <solid android:color="#FFFFFFFF" />
    <stroke android:width="1dp" android:color="#FF5079F0" />
    <corners android:radius="6dp" />
</shape>

  3.3、
  Activity中的主要程式碼如下:

    /**
     * 初始化搜尋歷史佈局
     */
    private void initView() {
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        // 獲取 SharedPreferences 中已儲存的 搜尋歷史
        mSearchHistoryLists = mStorageListSPUtils.loadDataList(TAG_SEARCH_HISTORY);
        if (mSearchHistoryLists.size() != 0) {
            mSearchListLayout.setVisibility(View.VISIBLE);
            for (int i = mSearchHistoryLists.size() - 1; i >= 0; i--) {
                TextView textView = (TextView) layoutInflater.inflate(R.layout.search_history_tv, mSearchHistoryFl, false);
                final String historyStr = mSearchHistoryLists.get(i);
                textView.setText(historyStr);
                // 設定搜尋歷史的回顯
                textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mSearchHeaderTv.setText(historyStr);
                        mSearchHeaderTv.setSelection(historyStr.length());
                    }
                });
                // FlowLayout 中新增 搜尋歷史
                mSearchHistoryFl.addView(textView);
            }
        }
    }

    /**
     * 存取 SharedPreferences 中儲存的搜尋歷史並做相應的處理
     */
    private void processAction() {
        // 獲取 EditText 輸入內容
        String searchInput = mSearchHeaderTv.getText().toString().trim();
        if (TextUtils.isEmpty(searchInput)) {
            Toast.makeText(this, getResources().getString(R.string.app_search_input_empty), Toast.LENGTH_SHORT).show();
        } else {
            // 先獲取之前已經儲存的搜尋歷史
            List<String> previousLists = mStorageListSPUtils.loadDataList(TAG_SEARCH_HISTORY);
            if (previousLists.size() != 0) {
                // 如果之前有搜尋歷史,則新增
                mSearchHistoryLists.clear();
                mSearchHistoryLists.addAll(previousLists);
            }
            // 去除重複,如果搜尋歷史中已經存在則remove,然後新增到後面
            if (!mSearchHistoryLists.contains(searchInput)) {
                // 如果搜尋歷史超過設定的默認個數,去掉最先新增的,並把新的新增到最後
                // 這裡只展示10個搜尋歷史,根據需要修改為你自己想要的數值
                if (mSearchHistoryLists.size() >= DEFAULT_SEARCH_HISTORY_COUNT) {
                    mSearchHistoryLists.remove(0);
                    mSearchHistoryLists.add(mSearchHistoryLists.size(), searchInput);
                } else {
                    mSearchHistoryLists.add(searchInput);
                }
            } else {
                // 如果搜尋歷史已存在,找到其所在的下標值
                int inputIndex = -1;
                for (int i = 0; i< mSearchHistoryLists.size(); i++) {
                    if (searchInput.equals(mSearchHistoryLists.get(i))) {
                        inputIndex = i;
                    }
                }
                // 如果搜尋歷史已存在,先從 List 集合中移除再新增到集合的最後
                mSearchHistoryLists.remove(inputIndex);
                mSearchHistoryLists.add(mSearchHistoryLists.size(), searchInput);
            }
            // 儲存新的搜尋歷史到 SharedPreferences
            mStorageListSPUtils.saveDataList(TAG_SEARCH_HISTORY, mSearchHistoryLists);
            Toast.makeText(this, getResources().getString(R.string.app_search_input) + searchInput, Toast.LENGTH_SHORT).show();
        }
    }

  主佈局檔案、以及文字和邊框的點選之後的顏色選擇器都不在一一展示了,筆者把程式碼都會上傳到程式碼庫的,效果已展示在文章的開篇之處,歡迎批評指正以及修改不足之處哈!
  本文程式碼傳送門:
  https://github.com/henryneu/TestHistorySearch