一、概述
我们要实现的是模仿微信的搜索效果,通过监听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