最近项目中有这么一个需求:实现搜索历史记录的展示,默认只展示最近搜索的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