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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值