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