一、概述
??我們要實現的是模仿微信的搜索效果,通過監聽Edittext中文字的變化動態匹配Recyclerview列表中文字,刷新列表,并將關鍵字變色顯示。
??首先上圖,展示我們將要實現的效果(關鍵字是有顏色變化的,列表也有刷新。我們的gif圖表現的不是很明顯)。
??然后是部分匹配——>即例如我們數據“第一天第一天”,只有第一個“一”變色。我本意是要寫上邊那個全部變色的效果的,偶然發現了只能匹配部分的問題,所以拿出來問題與解決方法與大家分享下。
二、實現
??所有代碼已上傳,并且有詳細的注釋,鏈接地址在文末。大家稍后可以下載。
- 首先上item布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="match_parent"
android:id="@+id/ll_item"
android:layout_marginTop="3dp"
android:background="@drawable/shape_search"
android:layout_marginRight="8dp"
android:layout_marginLeft="8dp"
android:layout_height="60dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/imgv_simple"
android:layout_marginRight="5dp"
android:layout_marginLeft="10dp"
android:layout_gravity="center"
fresco:backgroundImage="@mipmap/imgv_girl"
fresco:placeholderImage="@mipmap/imgv_girl"
fresco:roundBottomLeft="false"
fresco:roundBottomRight="true"
fresco:roundTopLeft="true"
fresco:roundTopRight="false"
fresco:roundedCornerRadius="50dp"
android:layout_width="45dp"
android:layout_height="45dp" />
<TextView
android:textColor="#7f44ff"
android:gravity="center"
android:text="123"
android:id="@+id/tv_text"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"
android:focusable="true"
android:singleLine="true"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="45dp" />
</LinearLayout>
??利用Fresco的圓角效果實現我們item中圖片的葉子形(姑且叫它葉子形吧)樣式,并且給整個LinearLayout布局加一個5dp
圓角并且帶黑色邊框的background
,如此便形成了我們效果圖中每個item的效果。
- 列表頁布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#f5f2f2"
tools:context="com.example.txs.myapplication.MainActivityWhole">
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#6CC4B8"
android:gravity="center"
android:text="搜索匹配關鍵字(全部變色)"
android:textColor="#fff" />
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/shape_search"
android:orientation="horizontal">
<ImageView
android:layout_marginLeft="3dp"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="center"
android:scaleType="centerInside"
android:src="@mipmap/imgv_search" />
<EditText
android:id="@+id/edt_search"
android:layout_width="0dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:layout_weight="1"
android:background="@null"
android:imeOptions="actionSearch"
android:lines="1"
android:singleLine="true" />
<ImageView
android:layout_marginRight="3dp"
android:id="@+id/imgv_delete"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="center"
android:scaleType="centerInside"
android:src="@mipmap/imgv_delete"
android:visibility="gone" />
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rc_search"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
??整個頁面很簡單,由上到下是title,搜索框,Recyclerview。我們的重點不在這里,所以界面搭建的不是很復雜,能看就好~接下來才是我們的重點。
- 適配器①—>部分匹配的適配器:
/**
* @author txs
* @date 2018/01/16
*/
public class RcAdapterPartChange extends RecyclerView.Adapter<RcAdapterPartChange.MyViewHolder> {
private Context context;
/**
* adapter傳遞過來的數據集合
*/
private List<String> list = new ArrayList<>();
/**
* 變色數據的其實位置 position
*/
private int beginChangePos;
/**
* 需要改變顏色的text
*/
private String text;
/**
* text改變的顏色
*/
private ForegroundColorSpan span;
/**
* 在MainActivity中設置text和span
*/
public void setText(String text, ForegroundColorSpan span) {
this.text = text;
this.span = span;
}
public RcAdapterPartChange(Context context, List<String> list) {
this.context = context;
this.list = list;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item_search, parent, false));
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
/**如果沒有進行搜索操作或者搜索之后點擊了刪除按鈕 我們會在MainActivity中把text置空并傳遞過來*/
if (text != null) {
//獲取匹配文字的 position
beginChangePos = list.get(position).indexOf(text);
// 文字的builder 用來做變色操作
SpannableStringBuilder builder = new SpannableStringBuilder(list.get(position));
//如果沒有匹配到關鍵字的話 list.get(position).indexOf(text)會返回-1
if (beginChangePos != -1) {
//設置呈現的文字
builder.setSpan(span, beginChangePos, beginChangePos + text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.mTvText.setText(builder);
}
} else {
holder.mTvText.setText(list.get(position));
}
//點擊監聽
holder.mLlItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemClickListener.onClick(view, position);
}
});
}
@Override
public int getItemCount() {
return list.size();
}
public interface onItemClickListener {
void onClick(View view, int pos);
}
/**
* Recyclerview的點擊監聽接口
*/
private onItemClickListener onItemClickListener;
public void setOnItemClickListener(RcAdapterPartChange.onItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
class MyViewHolder extends RecyclerView.ViewHolder {
private LinearLayout mLlItem;
private SimpleDraweeView mImgvSimple;
private TextView mTvText;
public MyViewHolder(View itemView) {
super(itemView);
mLlItem = (LinearLayout) itemView.findViewById(R.id.ll_item);
mImgvSimple = (SimpleDraweeView) itemView.findViewById(R.id.imgv_simple);
mTvText = (TextView) itemView.findViewById(R.id.tv_text);
}
}
}
??先說說這個適配器的瑕疵,由于.indexOf()
的坑,使用這個適配器產生的最終效果如我們的第二張圖,只能匹配每個item中第一條關鍵字,即比如我們的數據“第一天一天”,它只能使第一個"一"字變色(當然整個列表的刷新和匹配效果是沒問題的,它只影響了關鍵字的變色效果。僅此而已!!)。而且不論我們后續還有多少個“一”,它依舊只能變色第一個“一”字。有的人可能碰巧會需要這個效果,所以我放上來代碼和解決思路供大家參考。
首先我們適配器在創建時傳過來一個list
集合,集合里面可以包含你從網絡或者數據庫或者其他方式獲取到的數據(已經經過篩選,比如我們搜索“一”字,傳過來的集合是那些包含“一”字的數據)。然后提供一個set
方法void setText(String text, ForegroundColorSpan span)
,在刷新適配器之前用setText()
將我們的關鍵字以及關鍵字要變成的顏色傳過來,像這樣:
//設置要變色的關鍵字
adapter.setText(text, redSpan);
//刷新適配器
refreshUI();
然后適配器就會重新執行到onBindViewHolder
方法,刷新界面,就可以看到我們的篩選和變色效果了。下面我們來分析這段代碼:
/**如果沒有進行搜索操作或者搜索之后點擊了刪除按鈕 我們會在MainActivity中把text置空并傳遞過來*/
if (text != null) {
//獲取匹配文字的 position
beginChangePos = list.get(position).indexOf(text);
// 文字的builder 用來做變色操作
SpannableStringBuilder builder = new SpannableStringBuilder(list.get(position));
//如果沒有匹配到關鍵字的話 list.get(position).indexOf(text)會返回-1
if (beginChangePos != -1) {
//設置呈現的文字
builder.setSpan(span, beginChangePos, beginChangePos + text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.mTvText.setText(builder);
}
} else {
holder.mTvText.setText(list.get(position));
}
??開始我們有一個對text
判空的操作,在后面的Activity
代碼中你可以看到,我在刷新適配器之前,先判斷edittext中是否輸入了關鍵字,如果有關鍵字則會通過setText(text,span)
把關鍵字傳遞過來,如果沒有關鍵字則會置空setText(null,null)
。如果有關鍵字的話,我們用indexOf()
找到它的起始位置(position),當然如果沒有匹配到關鍵字的話list.get(position).indexOf(text)
會返回-1,然后我們會通過SpannableStringBuilder
對關鍵字進行變色操作。下面我們再來驗證一些indexOf()
的問題,上代碼:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mBtnIndexof;
private Button mBtnMatcher;
private String mString;
private String mKeyword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnIndexof = (Button) findViewById(R.id.btn_indexof);
mBtnMatcher = (Button) findViewById(R.id.btn_matcher);
mString = "第一天第一夜第一個時辰";
mKeyword = "一";
setListener();
}
private void setListener() {
mBtnIndexof.setOnClickListener(this);
mBtnMatcher.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_indexof:
int pos = mString.indexOf(mKeyword);
Log.e("test", "indexof== "+pos);
break;
case R.id.btn_matcher:
//條件 keyword
Pattern pattern = Pattern.compile(mKeyword);
//匹配
Matcher matcher = pattern.matcher(mString);
while (matcher.find()) {
int start = matcher.start();
Log.e("test", "macher== "+start);
int end = matcher.end();
}
break;
default:
break;
}
}
}
結果:
01-16 22:23:06.831 1581-1581/com.example.testinndexof E/test: indexof== 1
01-16 22:23:10.139 1581-1581/com.example.testinndexof E/test: macher== 1
01-16 22:23:10.139 1581-1581/com.example.testinndexof E/test: macher== 4
01-16 22:23:10.139 1581-1581/com.example.testinndexof E/test: macher== 7
??通過控制臺輸出的結果我們可以看到,indexOf()
只匹配到了第一個“一”的位置,之后沒有繼續匹配。這也是indexOf()
的原理所致。所以用此方法只能匹配到首個對應字符的問題已經找到了,接下來應該怎么做讓它完全匹配呢?上述代碼已經給出了解決方法,用Matcher
,matcher.find()
。
- 適配器②—>全部匹配的適配器:
/**
* @author txs
* @date 2018/01/16
*/
public class RcAdapterWholeChange extends RecyclerView.Adapter<RcAdapterWholeChange.MyViewHolder> {
private Context context;
/**
* adapter傳遞過來的數據集合
*/
private List<String> list = new ArrayList<>();
/**
* 需要改變顏色的text
*/
private String text;
/**
* 在MainActivity中設置text
*/
public void setText(String text) {
this.text = text;
}
public RcAdapterWholeChange(Context context, List<String> list) {
this.context = context;
this.list = list;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item_search, parent, false));
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
/**如果沒有進行搜索操作或者搜索之后點擊了刪除按鈕 我們會在MainActivity中把text置空并傳遞過來*/
if (text != null) {
//設置span
SpannableString string = matcherSearchText(Color.rgb(255, 0, 0), list.get(position), text);
holder.mTvText.setText(string);
} else {
holder.mTvText.setText(list.get(position));
}
//點擊監聽
holder.mLlItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemClickListener.onClick(view, position);
}
});
}
@Override
public int getItemCount() {
return list.size();
}
/**
* Recyclerview的點擊監聽接口
*/
public interface onItemClickListener {
void onClick(View view, int pos);
}
private onItemClickListener onItemClickListener;
public void setOnItemClickListener(RcAdapterWholeChange.onItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
class MyViewHolder extends RecyclerView.ViewHolder {
private LinearLayout mLlItem;
private SimpleDraweeView mImgvSimple;
private TextView mTvText;
public MyViewHolder(View itemView) {
super(itemView);
mLlItem = (LinearLayout) itemView.findViewById(R.id.ll_item);
mImgvSimple = (SimpleDraweeView) itemView.findViewById(R.id.imgv_simple);
mTvText = (TextView) itemView.findViewById(R.id.tv_text);
}
}
/**
* 正則匹配 返回值是一個SpannableString 即經過變色處理的數據
*/
private SpannableString matcherSearchText(int color, String text, String keyword) {
SpannableString spannableString = new SpannableString(text);
//條件 keyword
Pattern pattern = Pattern.compile(keyword);
//匹配
Matcher matcher = pattern.matcher(spannableString);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
//ForegroundColorSpan 需要new 不然也只能是部分變色
spannableString.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//返回變色處理的結果
return spannableString;
}
}
??改動不大,重點是一個matcherSearchText
方法,返回值是SpannableString
,也就是經過我們經過變色處理的文字。主要使用matcher.find()
方法找到所以有匹配的關鍵字,它的效果已經在上邊的代碼中展示過了(請看上邊的控制臺輸出結果)。
- Activity中的代碼:
public class MainActivityWhole extends AppCompatActivity {
/**
* 搜索框
*/
private EditText mEdtSearch;
/**
* 刪除按鈕
*/
private ImageView mImgvDelete;
/**
* recyclerview
*/
private RecyclerView mRcSearch;
/**
* 全部匹配的適配器
*/
private RcAdapterWholeChange adapter;
/**
* 所有數據 可以是聯網獲取 如果有需要可以將其儲存在數據庫中 我們用簡單的String做演示
*/
private List<String> wholeList;
/**
* 此list用來保存符合我們規則的數據
*/
private List<String> list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_whole);
initView();
initData();
refreshUI();
setListener();
}
/**
* 設置監聽
*/
private void setListener() {
//edittext的監聽
mEdtSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
//每次edittext內容改變時執行 控制刪除按鈕的顯示隱藏
@Override
public void afterTextChanged(Editable editable) {
if (editable.length() == 0) {
mImgvDelete.setVisibility(View.GONE);
} else {
mImgvDelete.setVisibility(View.VISIBLE);
}
//匹配文字 變色
doChangeColor(editable.toString().trim());
}
});
//recyclerview的點擊監聽
adapter.setOnItemClickListener(new RcAdapterWholeChange.onItemClickListener() {
@Override
public void onClick(View view, int pos) {
Toast.makeText(MainActivityWhole.this, "妹子 pos== " + pos, Toast.LENGTH_SHORT).show();
}
});
//刪除按鈕的監聽
mImgvDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mEdtSearch.setText("");
}
});
}
/**
* 字體匹配方法
*/
private void doChangeColor(String text) {
//clear是必須的 不然只要改變edittext數據,list會一直add數據進來
list.clear();
//不需要匹配 把所有數據都傳進來 不需要變色
if (text.equals("")) {
list.addAll(wholeList);
//防止匹配過文字之后點擊刪除按鈕 字體仍然變色的問題
adapter.setText(null);
refreshUI();
} else {
//如果edittext里面有數據 則根據edittext里面的數據進行匹配 用contains判斷是否包含該條數據 包含的話則加入到list中
for (String i : wholeList) {
if (i.contains(text)) {
list.add(i);
}
}
//設置要變色的關鍵字
adapter.setText(text);
refreshUI();
}
}
private void initData() {
//假數據 實際開發中請從網絡或者數據庫獲取
wholeList = new ArrayList<>();
list = new ArrayList<>();
wholeList.add("第一天一天");
wholeList.add("第二天一天");
wholeList.add("第三天一天");
wholeList.add("第四天一天");
wholeList.add("第五天五天");
wholeList.add("第六天一天");
wholeList.add("第七天七天");
wholeList.add("第一天八天");
wholeList.add("第一天九天");
wholeList.add("第一天十天");
wholeList.add("第一天十一天");
//初次進入程序時 展示全部數據
list.addAll(wholeList);
}
/**
* 刷新UI
*/
private void refreshUI() {
if (adapter == null) {
adapter = new RcAdapterWholeChange(this, list);
mRcSearch.setAdapter(adapter);
} else {
adapter.notifyDataSetChanged();
}
}
private void initView() {
mEdtSearch = (EditText) findViewById(R.id.edt_search);
mImgvDelete = (ImageView) findViewById(R.id.imgv_delete);
mRcSearch = (RecyclerView) findViewById(R.id.rc_search);
//Recyclerview的配置
mRcSearch.setLayoutManager(new LinearLayoutManager(this));
}
}
??這里我們的思路是首先定義兩個集合(wholeList
和list
),wholeList
用來保存我們獲取的全部數據,list
用來保存我們經過篩選后的數據。在為進行搜索操作是默認展示所有數據,所以會有list.addAll(wholeList)
。之后通過對Edittext的變化監聽afterTextChanged
,在里面執行刪除按鈕的顯示隱藏以及匹配文字并變色的doChangeColor()
方法。
//每次edittext內容改變時執行 控制刪除按鈕的顯示隱藏
@Override
public void afterTextChanged(Editable editable) {
if (editable.length() == 0) {
mImgvDelete.setVisibility(View.GONE);
} else {
mImgvDelete.setVisibility(View.VISIBLE);
}
//匹配文字 變色
doChangeColor(editable.toString().trim());
}
??接下來我們要講的是doChangeColor()
這個方法,首先看代碼:
/**
* 字體匹配方法
*/
private void doChangeColor(String text) {
//clear是必須的 不然只要改變edittext數據,list會一直add數據進來
list.clear();
//不需要匹配 把所有數據都傳進來 不需要變色
if (text.equals("")) {
list.addAll(wholeList);
//防止匹配過文字之后點擊刪除按鈕 字體仍然變色的問題
adapter.setText(null);
refreshUI();
} else {
//如果edittext里面有數據 則根據edittext里面的數據進行匹配 用contains判斷是否包含該條數據 包含的話則加入到list中
for (String i : wholeList) {
if (i.contains(text)) {
list.add(i);
}
}
//設置要變色的關鍵字
adapter.setText(text);
refreshUI();
}
}
??在執行doChangeColor()
之初,我們要清空一下list
,不然如果你第一次搜索了“一”,第二次搜索了“二”,那么最終的展示效果會是包含了“一”和“二”數據的并集~,接下來我們會判斷Edittext里面是否有關鍵字(搜索條件),如果沒有關鍵字,即進行展示全部數據并且不變色的操作
list.addAll(wholeList);
//防止匹配過文字之后點擊刪除按鈕 字體仍然變色的問題
adapter.setText(null);
如果有關鍵字,則對wholeList
進行遍歷,匹配。把符合條件(i.contains(text)
)的數據加入到list
集合中并進行展示。
三、后記
??整個項目并不難,而且代碼中都有詳細的注釋。但是例如SpannableString
的玩法以及Pattern
、Matcher
的使用沒有展開來講。最近我在考慮寫一些合集來把一些基礎知識總結一下放上來,這樣以后在寫文章的時候可以這樣寫:
重點是一個
matcherSearchText
方法,返回值是SpannableString
,SpannableString怎么用?請見我的文章《Android 之SpannableString用法詳解》。
??有寫的不好的地方,歡迎大家指教。
github項目地址:https://github.com/tangxuesong6/editchange