项目场景:
最近项目上有个新功能需要实现一个本地数据的搜索历史记录,在这记录一下,主要使用到的功能,以及使用的一些技术和代码。
例如:项目场景:首先第一次进入搜索页面需要展示(如果有搜索历史展示搜索历史(限制最多三行,超出的自行删除)没有的话展示空页面)。有一个清除的按钮。数据是每次搜索的时候,存在本地即可。需要注意的是,如果这个有多人使用的情况下。需要添加唯一标识来区分每个人的数据。
功能实现
首先就是在实现这个功能前需要先继承一个三方库,这边是从网上下载了一个大神的demo:
第一步就是先导入依赖implementation 'com.hyman:flowlayout-lib:1.1.2'
会下载一个流式布局的三方库。
第一步
public class RecordsDao {
RecordSQLiteOpenHelper recordHelper;
SQLiteDatabase recordsDb;
public RecordsDao(Context context) {
recordHelper = new RecordSQLiteOpenHelper(context);
}
//添加搜索记录
public void addRecords(String record ,String code) {
if (!isHasRecord(record)) {
recordsDb = recordHelper.getReadableDatabase();
ContentValues values = new ContentValues();
values.put("name", record);
values.put("code",code);
//添加
recordsDb.insert("records", null, values);
//关闭
recordsDb.close();
}
}
//判断是否含有该搜索记录
public boolean isHasRecord(String record) {
boolean isHasRecord = false;
recordsDb = recordHelper.getReadableDatabase();
Cursor cursor = recordsDb.query("records", null, null, null, null, null, null);
while (cursor.moveToNext()) {
if (record.equals(cursor.getString(cursor.getColumnIndexOrThrow("name")))) {
isHasRecord = true;
}
}
//关闭数据库
recordsDb.close();
cursor.close();
return isHasRecord;
}
//获取全部搜索记录
public List<String> getRecordsList() {
List<String> recordsList = new ArrayList<>();
recordsDb = recordHelper.getReadableDatabase();
Cursor cursor = recordsDb.query("records", null, null, null, null, null, null);
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
recordsList.add(name);
}
//关闭数据库
recordsDb.close();
cursor.close();
return recordsList;
}
//模糊查询
public List<Map<String, Object>> querySimlarRecord(String record, String code) {
StringBuffer queryStr = new StringBuffer("select * from records ");
if (code != null) {
queryStr.append(" where code='" + code + "'");
}
if (!StringUtils.isEmpty(record) && queryStr.toString().contains("where")) {
queryStr.append(" and name like '%" + record + "%' order by name ");
}else if (!StringUtils.isEmpty(record) && !queryStr.toString().contains("where")) {
queryStr.append(" where name like '%" + record + "%' order by name ");
}
List<Map<String, Object>> similarRecords = new ArrayList<>();
Cursor cursor = recordHelper.getReadableDatabase().rawQuery(queryStr.toString(), null);
while (cursor.moveToNext()) {
Map<String, Object> map = new HashMap<>(2);
String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
Integer id = cursor.getInt(cursor.getColumnIndexOrThrow("_id"));
map.put("name", name);
map.put("id", id);
similarRecords.add(map);
}
cursor.close();
return similarRecords;
}
//清空搜索记录
public void deleteAllRecords() {
recordsDb = recordHelper.getWritableDatabase();
recordsDb.execSQL("delete from records");
recordsDb.close();
}
//删除单个搜索记录
public void deleteRecords(Integer id) {
recordsDb = recordHelper.getWritableDatabase();
if (!Objects.isNull(id)) {
recordsDb.execSQL("delete from records WHERE _id =" + id);
}
recordsDb.close();
}
}
public class RecordSQLiteOpenHelper extends SQLiteOpenHelper {
private final static String DB_NAME = "temp.db";
private final static int DB_VERSION = 1;
public RecordSQLiteOpenHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String sqlStr = "CREATE TABLE IF NOT EXISTS records (_id INTEGER PRIMARY KEY AUTOINCREMENT, name,code TEXT);";
db.execSQL(sqlStr);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
布局:
initview里面初始化的方法:
code是唯一标识,区分用户的。
第二步
//颠倒list顺序,用户输入的信息会从上依次往下显示
private void reversedList() {
searchRecordsList.clear();
String size = PreferencesUtil.getString(this, PreferencesEntivity.SEARCHHISTORYSIZE, "");//页面上展示总的item个数
String Allsize = PreferencesUtil.getString(this, PreferencesEntivity.SEARCHHISTORYALLSIZE, "");//显示的行数
if (!TextUtils.isEmpty(size) && !TextUtils.isEmpty(Allsize)) {
int sizes = (Integer.parseInt(size));
int Allsizes = (Integer.parseInt(Allsize));
if (!TextUtils.isEmpty(size) && !TextUtils.isEmpty(Allsize) && tempList.size() > sizes && Allsizes + 1 > 3) {//最大行数为3,加1为了删除多余的数据
int i1 = (tempList.size()) - sizes;
for (int i = 0; i < i1; i++) {
Integer id = (Integer) tempList.get(i).get("id");
recordsDao.deleteRecords(id);
PreferencesUtil.putString(this, PreferencesEntivity.SEARCHHISTORYSIZE, 0 + "");
}
for (int i = tempList.size() - 1; i >= 0; i--) {//删除完多余的数据,倒序将数据插入到新的集合里面
String name = (String) tempList.get(i).get("name");
Integer id = (Integer) tempList.get(i).get("id");
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("id", id);
searchRecordsList.add(map);
}
} else {
for (int i = tempList.size() - 1; i >= 0; i--) {
String name = (String) tempList.get(i).get("name");
Integer id = (Integer) tempList.get(i).get("id");
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("id", id);
searchRecordsList.add(map);
}
}
} else {
for (int i = tempList.size() - 1; i >= 0; i--) {
String name = (String) tempList.get(i).get("name");
Integer id = (Integer) tempList.get(i).get("id");
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("id", id);
searchRecordsList.add(map);
}
}
}
实现倒序,自动删除限制三行以外的数据。
//当没有匹配的搜索数据的时候不显示历史记录栏
private void checkRecordsSize() {
if (tempList.size() == 0) {
Co_history.setVisibility(View.GONE);//搜索的历史数据
ns_list.setVisibility(View.GONE);
} else {
ns_list.setVisibility(View.GONE);
Co_history.setVisibility(View.VISIBLE);
setAdapter(rv_history);
}
}
//设置适配器
private void setAdapter(MaxLineFlowLayout mFlowLayout) {
//限制最大行数
mFlowLayout.setMaxLine(3, recordsDao);
//设置数据
mFlowLayout.setAdapter(new TagAdapter<Map<String, Object>>(searchRecordsList) {
@Override
public View getView(FlowLayout parent, int position, Map<String, Object> stringObjectMap) {
//加入你的布局
TextView tv = (TextView) View.inflate(SearchAllActivity.this, R.layout.search_records, null);
String s = (String) stringObjectMap.get("name");
tv.setText(s);
return tv;
}
});
//点击历史记录,直接搜索
mFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
@Override
public boolean onTagClick(View view, int position, FlowLayout parent) {
String Click_History = (String) searchRecordsList.get(position).get("name");
Log.i("搜索历史", "onTagClick: " + Click_History);
et_search.setText(Click_History);
return false;
}
});
}
因为这个三方库里面少一个限制最大行数的功能。我重新继承了一下,修改了一下他的绘制方法。
/**
* 流式布局 https://github.com/hongyangAndroid/FlowLayout
* <p>
* Change by mChenys on 2019/1/8.
* 修改后,支持行数限制,优化了在onLayout的时候的二次循环初始化参数,支持获取当前可见的item个数
*/
public class MaxLineFlowLayout extends TagFlowLayout implements TagAdapter.OnDataChangedListener {
private static final String TAG = "FlowLayout";
protected static final int LEFT = -1;
protected static final int CENTER = 0;
protected static final int RIGHT = 1;
protected List<List<View>> mAllViews = new ArrayList<List<View>>();//记录所有行
protected List<Integer> mLineHeight = new ArrayList<Integer>();//记录所有行高
protected List<Integer> mLineWidth = new ArrayList<Integer>();//记录所有行宽
protected List<View> lineViews = new ArrayList<>();//临时记录每行的view
protected int mGravity;
private int maxLine = -1;//最大行数
private RecordsDao recordsDao;
private boolean isExceedingMaxLimit; //预设的子View是否超出了最大行数限制
public void setMaxLine(int maxLine, RecordsDao recordsDao) {
this.maxLine = maxLine;
this.recordsDao = recordsDao;
}
public MaxLineFlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout);
mGravity = ta.getInt(R.styleable.TagFlowLayout_tag_gravity, LEFT);
int layoutDirection = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault());
if (layoutDirection == LayoutDirection.RTL) {
if (mGravity == LEFT) {
mGravity = RIGHT;
} else {
mGravity = LEFT;
}
}
ta.recycle();
}
public MaxLineFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MaxLineFlowLayout(Context context) {
this(context, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mAllViews.clear();//记录所有行的view
mLineHeight.clear();//记录每一行的高度
mLineWidth.clear();//记录每一行的宽度
lineViews.clear();//记录每一行的view
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
// wrap_content 最终宽高
int width = 0;
int height = 0;
//当前已用行宽高
int lineWidth = 0;
int lineHeight = 0;
int cCount = getChildCount();
// LogUtils.e("cys", "cCount:" + cCount);
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
//测量子view
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//子View宽高
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
if (maxLine > 0 && mAllViews.size() + 1 >= maxLine) { //+1是因为后面还有最后一行
isExceedingMaxLimit = true;
break;//超过最大行数跳出循环
}
//需要换行
if (i == 0 && width == 0 && lineWidth == 0 && height == 0 && lineHeight == 0) {
//如果第一个子View就满足换行条件,那么width和height就是子View的宽高
width = lineWidth = childWidth;
height = lineHeight = childHeight;
lineViews.add(child);
} else {
width = Math.max(width, lineWidth);//记录最大行宽
height += lineHeight;//累加包裹内容所需的高度
}
//换行前,保存当前行数据
mLineHeight.add(lineHeight);
mLineWidth.add(lineWidth);
mAllViews.add(lineViews);
//换行,新行数据初始化
lineWidth = 0;//重新赋值行宽
lineHeight = 0;//重新赋值行高
lineViews = new ArrayList<View>();//创建新行
if (i == 0 && width > 0 && height > 0) {
//如果第一个子View就满足换行条件并且数据已经保存,则不需要下面重复添加了
continue;
}
}
//新行或者当前行继续添加子View
lineWidth += childWidth;//累加行宽
lineHeight = Math.max(lineHeight, childHeight);//取当前行最大高度作为行高
lineViews.add(child);
}
//添加最后一行数据
width = Math.max(lineWidth, width);//包裹内容所需的最大宽度
height += lineHeight;//累加高度
mLineHeight.add(lineHeight);
mLineWidth.add(lineWidth);
mAllViews.add(lineViews);
setMeasuredDimension(
//父控件宽高确定则用确定的,否则用测量后的
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
);
int totalLine = getTotalLine();
int totalByLine = getTotalByLine(totalLine);
PreferencesUtil.putString(getContext(), PreferencesEntivity.SEARCHHISTORYSIZE, totalByLine + "");
PreferencesUtil.putString(getContext(), PreferencesEntivity.SEARCHHISTORYALLSIZE, totalLine + "");
Log.i(TAG, "changeAdapter: " + totalLine + totalByLine);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// LogUtils.e("cys","onLayout");
//总宽
int width = getWidth();
//当前已用行宽高
int lineHeight = 0;
//下面是对每一行的View进行布局
int left = getPaddingLeft();
int top = getPaddingTop();
int lineNum = mAllViews.size();
for (int i = 0; i < lineNum; i++) {
//获取当前行和行高
lineViews = mAllViews.get(i);
lineHeight = mLineHeight.get(i);
// set gravity
int currentLineWidth = this.mLineWidth.get(i);
switch (this.mGravity) {
case LEFT:
left = getPaddingLeft();
break;
case CENTER:
left = (width - currentLineWidth) / 2 + getPaddingLeft();
break;
case RIGHT:
// 适配了rtl,需要补偿一个padding值 ,从右边向左开始布局
left = width - (currentLineWidth + getPaddingLeft()) - getPaddingRight();
// 适配了rtl,需要把lineViews里面的数组倒序排,从右边开始存放view
Collections.reverse(lineViews);
break;
}
//开始布局
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
//更新下一个view添加到当前行的left
left += child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
}
//更新下一个view添加到下一行的top
top += lineHeight;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
/**
* 获取指定行数内的item个数
*
* @return 每行的个数之和
* @lineNum 总行数
*/
public int getTotalByLine(int lineNum) {
int count = 0;
if (lineNum <= mAllViews.size()) {
for (int i = 0; i < lineNum; i++) {
List<View> line = mAllViews.get(i);
count += line.size();
}
} else {
for (int i = 0; i < mAllViews.size(); i++) {
List<View> line = mAllViews.get(i);
count += line.size();
}
}
return count;
}
/**
* 返回总行数
*
* @return
*/
public int getTotalLine() {
return mAllViews.size();
}
/**
* 设置的数据是否超过了最大限制
*
* @return
*/
public boolean isExceedingMaxLimit() {
return isExceedingMaxLimit;
}
private TagAdapter mTagAdapter;
private int mSelectedMax = -1;//-1为不限制数量
private Set<Integer> mSelectedView = new HashSet<Integer>();
private TagFlowLayout.OnSelectListener mOnSelectListener;
private TagFlowLayout.OnTagClickListener mOnTagClickListener;
public interface OnSelectListener {
void onSelected(Set<Integer> selectPosSet);
}
public interface OnTagClickListener {
boolean onTagClick(View view, int position, FlowLayout parent);
}
public void setOnSelectListener(TagFlowLayout.OnSelectListener onSelectListener) {
mOnSelectListener = onSelectListener;
}
public void setOnTagClickListener(TagFlowLayout.OnTagClickListener onTagClickListener) {
mOnTagClickListener = onTagClickListener;
}
public void setAdapter(TagAdapter adapter) {
mTagAdapter = adapter;
mTagAdapter.setOnDataChangedListener(this);
mSelectedView.clear();
changeAdapter();
}
@SuppressWarnings("ResourceType")
private void changeAdapter() {
removeAllViews();
TagAdapter adapter = mTagAdapter;
TagView tagViewContainer = null;
HashSet preCheckedList = mTagAdapter.getPreCheckedList();
for (int i = 0; i < mTagAdapter.getCount(); i++) {
Log.i("删除搜索历史", "changeAdapter: " + mTagAdapter.getCount());
View tagView = adapter.getView(this, i, adapter.getItem(i));
tagViewContainer = new TagView(getContext());
tagView.setDuplicateParentStateEnabled(true);
if (tagView.getLayoutParams() != null) {
tagViewContainer.setLayoutParams(tagView.getLayoutParams());
} else {
MarginLayoutParams lp = new MarginLayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
lp.setMargins(dip2px(getContext(), 5),
dip2px(getContext(), 5),
dip2px(getContext(), 5),
dip2px(getContext(), 5));
tagViewContainer.setLayoutParams(lp);
}
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
tagView.setLayoutParams(lp);
tagViewContainer.addView(tagView);
addView(tagViewContainer);
if (preCheckedList.contains(i)) {
setChildChecked(i, tagViewContainer);
}
if (mTagAdapter.setSelected(i, adapter.getItem(i))) {
setChildChecked(i, tagViewContainer);
}
tagView.setClickable(false);
final TagView finalTagViewContainer = tagViewContainer;
final int position = i;
tagViewContainer.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
doSelect(finalTagViewContainer, position);
if (mOnTagClickListener != null) {
mOnTagClickListener.onTagClick(finalTagViewContainer, position,
MaxLineFlowLayout.this);
}
}
});
}
mSelectedView.addAll(preCheckedList);
}
public void setMaxSelectCount(int count) {
if (mSelectedView.size() > count) {
Log.w(TAG, "you has already select more than " + count + " views , so it will be clear .");
mSelectedView.clear();
}
mSelectedMax = count;
}
public Set<Integer> getSelectedList() {
return new HashSet<Integer>(mSelectedView);
}
private void setChildChecked(int position, TagView view) {
view.setChecked(true);
mTagAdapter.onSelected(position, view.getTagView());
}
private void setChildUnChecked(int position, TagView view) {
view.setChecked(false);
mTagAdapter.unSelected(position, view.getTagView());
}
private void doSelect(TagView child, int position) {
if (!child.isChecked()) {
//处理max_select=1的情况
if (mSelectedMax == 1 && mSelectedView.size() == 1) {
Iterator<Integer> iterator = mSelectedView.iterator();
Integer preIndex = iterator.next();
TagView pre = (TagView) getChildAt(preIndex);
setChildUnChecked(preIndex, pre);
setChildChecked(position, child);
mSelectedView.remove(preIndex);
mSelectedView.add(position);
} else {
if (mSelectedMax > 0 && mSelectedView.size() >= mSelectedMax) {
return;
}
setChildChecked(position, child);
mSelectedView.add(position);
}
} else {
setChildUnChecked(position, child);
mSelectedView.remove(position);
}
if (mOnSelectListener != null) {
mOnSelectListener.onSelected(new HashSet<Integer>(mSelectedView));
}
}
private static final String KEY_CHOOSE_POS = "key_choose_pos";
private static final String KEY_DEFAULT = "key_default";
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_DEFAULT, super.onSaveInstanceState());
String selectPos = "";
if (mSelectedView.size() > 0) {
for (int key : mSelectedView) {
selectPos += key + "|";
}
selectPos = selectPos.substring(0, selectPos.length() - 1);
}
bundle.putString(KEY_CHOOSE_POS, selectPos);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
String mSelectPos = bundle.getString(KEY_CHOOSE_POS);
if (!TextUtils.isEmpty(mSelectPos)) {
String[] split = mSelectPos.split("\\|");
for (String pos : split) {
int index = Integer.parseInt(pos);
mSelectedView.add(index);
TagView tagView = (TagView) getChildAt(index);
if (tagView != null) {
setChildChecked(index, tagView);
}
}
}
super.onRestoreInstanceState(bundle.getParcelable(KEY_DEFAULT));
return;
}
super.onRestoreInstanceState(state);
}
@Override
public void onChanged() {
mSelectedView.clear();
changeAdapter();
}
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
使用到sp保存了两个字段用来删除数据用的。
最后附上效果图