Android仿微信搜索,Recyclerview+关键字动态匹配筛选变色效果(Edittext+Recyclerview)

一、概述

  我们要实现的是模仿微信的搜索效果,通过监听Edittext中文字的变化动态匹配Recyclerview列表中文字,刷新列表,并将关键字变色显示。
  首先上图,展示我们将要实现的效果(关键字是有颜色变化的,列表也有刷新。我们的gif图表现的不是很明显)。
1. 关键字全部变色效果
  然后是部分匹配——>即例如我们数据“第一天第一天”,只有第一个“一”变色。我本意是要写上边那个全部变色的效果的,偶然发现了只能匹配部分的问题,所以拿出来问题与解决方法与大家分享下。
2. 部分匹配效果

二、实现

  所有代码已上传,并且有详细的注释,链接地址在文末。大家稍后可以下载。
1. 首先上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的效果。
2. 列表页布局文件:

<?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。我们的重点不在这里,所以界面搭建的不是很复杂,能看就好~接下来才是我们的重点。
3. 适配器①—>部分匹配的适配器:

/**
 * @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()的原理所致。所以用此方法只能匹配到首个对应字符的问题已经找到了,接下来应该怎么做让它完全匹配呢?上述代码已经给出了解决方法,用Matchermatcher.find()
4. 适配器②—>全部匹配的适配器:

/**
 * @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()方法找到所以有匹配的关键字,它的效果已经在上边的代码中展示过了(请看上边的控制台输出结果)。
5. 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));
    }
}

  这里我们的思路是首先定义两个集合(wholeListlist),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的玩法以及PatternMatcher的使用没有展开来讲。最近我在考虑写一些合集来把一些基础知识总结一下放上来,这样以后在写文章的时候可以这样写:

重点是一个matcherSearchText方法,返回值是SpannableStringSpannableString怎么用?请见我的文章《Android 之SpannableString用法详解》。

  有写的不好的地方,欢迎大家指教。
github项目地址:https://github.com/tangxuesong6/editchange

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值