4

How to highlight search text result in RecyclerView. I found some posts regarding Spannable TextView, but not sure where to implement in my case. Appreciate you can look and assist.

MainActivity or Chapter1

 public class Chapter1 extends AppCompatActivity implements SearchView.OnQueryTextListener {
        MyRecAdapter myRecAdapter;
        RecyclerView recyclerView;
        List<Post> list;        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(layout.chapter1_main);
            recyclerView = (RecyclerView) findViewById(R.id.myrec);
            createdata();
            myRecAdapter = new MyRecAdapter(list, Chapter1.this);
            recyclerView.setLayoutManager(new LinearLayoutManager(Chapter1.this));
            recyclerView.setAdapter(myRecAdapter);
        }        
        void createdata() {             
            list = new ArrayList<>();               
            String topic_1_1 = getResources().getString(string.topic_1_1);
            String text_1_1 = getString(string.text_1_1);
            String topic_1_2 = getResources().getString(string.topic_1_2);
            String topic_1_3 = getResources().getString(string.topic_1_3);
            String text_1_3 = getString(string.text_1_3);         
            list.add(new Post(topic_1_1, text_1_1));
            list.add(new Post(topic_1_2, ""));
            list.add(new Post(topic_1_3, text_1_3));               

        }       

        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            getMenuInflater().inflate(R.menu.menu_search, menu);
            getMenuInflater().inflate(R.menu.main, menu);        
            MenuItem item = menu.findItem(R.id.menu_search);
            SearchView searchView = (SearchView) MenuItemCompat.getActionView(item);        
            searchView.setOnQueryTextListener(this);        
            return super.onCreateOptionsMenu(menu);
        }     


        @Override
        public boolean onQueryTextChange(String newText) {        

            final List<Post> filteredModelList = filter(list, newText);        
            if (filteredModelList.size() > 0) {        
                myRecAdapter.setFilter(filteredModelList);
                return true;
            } else {
                Toast.makeText(Chapter1.this, "Not Found", Toast.LENGTH_SHORT).show();
                return false;
            }
        }        
        private List<Post> filter(List<Post> models, String query ) {
            query = query.toLowerCase();        
            final List<Post> filteredModelList = new ArrayList<>();
            for (Post model : models) {        
                final String text = model.getPostTitle().toLowerCase();
                final String text_sub = model.getPostSubTitle().toLowerCase();        
                if (text.contains(query)) {
                    filteredModelList.add(model);

                }
                else {
                    if (text_sub.contains(query)) {
                        filteredModelList.add(model);
                    }        
                }     
            }
            createdata();
            myRecAdapter = new MyRecAdapter(filteredModelList, Chapter1.this);
            recyclerView.setLayoutManager(new LinearLayoutManager(Chapter1.this));
            recyclerView.setAdapter(myRecAdapter);
            myRecAdapter.notifyDataSetChanged();
            return filteredModelList;
        }
    }

Adapter

public class MyRecAdapter extends RecyclerView.Adapter<MyRecAdapter.VH> {
        public List<Post> parkingList;        
        public Context context;
        ArrayList<Post> mCountryModel;        
        public MyRecAdapter(List<Post> parkingList, Context context) {
            this.parkingList = parkingList;
            this.context = context;
        }        
        @Override
        public MyRecAdapter.VH onCreateViewHolder(ViewGroup parent, int viewType) {
            return new MyRecAdapter.VH(LayoutInflater.from(parent.getContext()).inflate(R.layout.mycardview, parent, false));
        }        
        @Override
        public void onBindViewHolder(MyRecAdapter.VH holder, int position) {       
                holder.t1.setText(Html.fromHtml(parkingList.get(position).getPostTitle()));                    holder.t2.setText(Html.fromHtml(parkingList.get(position).getPostSubTitle()));        
        }        
        @Override
        public int getItemCount() {
            return parkingList.size();
        }        
        public class VH extends RecyclerView.ViewHolder {
            TextView t1, t2;        
            public VH(View view) {
                super(view);        
                t1 = (TextView) view.findViewById(R.id.list_title);
                t2 = (TextView) view.findViewById(R.id.list_desc);        
            }        
        }          
        public void setFilter(List<Post> countryModels) {
            mCountryModel = new ArrayList<>();
            mCountryModel.addAll(countryModels);        
            notifyDataSetChanged();        
        }

    }
ivan
  • 43
  • 1
  • 3
  • which field do you want to highlight title or desc? – H Raval Oct 26 '16 at 08:07
  • @HRaval I would like to high light Title and Desc – ivan Oct 26 '16 at 08:10
  • as far as understand your question...you have filered list based on query text and now you are displaying only filtered result right? then why do you want to highlight? – H Raval Oct 26 '16 at 08:24
  • @HRaval yes, correct, filter based on query text, but in many cases there are more then 500 worlds in TextView(mostly in Desc), and list is very wide, even after filtering. Highlightening will improve design view very much... – ivan Oct 26 '16 at 11:10

5 Answers5

8

As per accepted answer indexOf() is not working in case of string value is "Mumbai" same occurrences inside string. So here I have used "Pattern" and "Matcher" class to make it work. Also, I have added ".toLowerCase()" to make string with same case and query work on string as ignore case. If you no need of ignore case of query string, You can remove .toLowerCase() from this code snippet

  SpannableStringBuilder sb = new SpannableStringBuilder(desc);
        Pattern word = Pattern.compile(query.toLowerCase());
        Matcher match = word.matcher(desc.toLowerCase());

        while (match.find()) {
            ForegroundColorSpan fcs = new ForegroundColorSpan(
                    ContextCompat.getColor(context, R.color.colorPrimary)); //specify color here
                sb.setSpan(fcs, match.start(), match.end(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
        }
        viewHolders.text_view_title.setText(sb);
Anant Shah
  • 3,744
  • 1
  • 35
  • 48
7

change your adapter to

public class MyRecAdapter extends RecyclerView.Adapter<MyRecAdapter.VH> {
        public List<Post> parkingList;        
        public Context context;
        ArrayList<Post> mCountryModel;  
        String searchText;
        
        public MyRecAdapter(List<Post> parkingList, Context context) {
            this.parkingList = parkingList;
            this.context = context;
        }        
        @Override
        public MyRecAdapter.VH onCreateViewHolder(ViewGroup parent, int viewType) {
            return new MyRecAdapter.VH(LayoutInflater.from(parent.getContext()).inflate(R.layout.mycardview, parent, false));
        }        
        @Override
        public void onBindViewHolder(MyRecAdapter.VH holder, int position) {       
                String title = parkingList.get(position).getPostTitle();
                String desc = parkingList.get(position).getPostSubTitle();
                
                holder.t1.setText(Html.fromHtml(title));                    
                if(searchText.length()>0){
                    //color your text here
                    int index = desc.indexOf(searchText);
                    while(index>0){
                       SpannableStringBuilder sb = new SpannableStringBuilder(desc);
                       ForegroundColorSpan fcs = new ForegroundColorSpan(Color.rgb(158, 158, 158)); //specify color here
                       sb.setSpan(fcs, index, index+searchText.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); 
                        index = desc.indexOf(searchText,index+1);
                       
                    }
                    holder.t2.setText(sb);  
                    
                }else{
                holder.t2.setText(Html.fromHtml(desc));  
                }
                      
        }        
        @Override
        public int getItemCount() {
            return parkingList.size();
        }        
        public class VH extends RecyclerView.ViewHolder {
            TextView t1, t2;        
            public VH(View view) {
                super(view);        
                t1 = (TextView) view.findViewById(R.id.list_title);
                t2 = (TextView) view.findViewById(R.id.list_desc);        
            }        
        }          
        public void setFilter(List<Post> countryModels,String searchText) {
            mCountryModel = new ArrayList<>();
            mCountryModel.addAll(countryModels);        
            this.searchText = searchText;
            notifyDataSetChanged();        
        }

    }

and set onQueryTextChange to

 @Override
        public boolean onQueryTextChange(String newText) {        

            final List<Post> filteredModelList = filter(list, newText);        
            if (filteredModelList.size() > 0) {        
                myRecAdapter.setFilter(filteredModelList,newText);
                return true;
            } else {
                Toast.makeText(Chapter1.this, "Not Found", Toast.LENGTH_SHORT).show();
                return false;
            }
        }     
H Raval
  • 1,903
  • 17
  • 39
  • @H Raval why do you use "searchText.length()" in setSpan()? And why use "index+1" in index = desc.indexOf...? – AJW Feb 10 '17 at 18:52
  • length is used to color word from index to length and inxex+1 for next matching – H Raval Feb 13 '17 at 06:40
3

Use inside on Bind view holder in Adapter class

String title1 = Itemlist.get(position).getN_stall_name().toLowerCase(Locale.getDefault());
            holder.title.setText(Itemlist.get(position).getN_stall_name());

if (title1.contains(searchText)) {
                int startPos = title1.indexOf(searchText);
                int endPos = startPos + searchText.length();
                Spannable spanString = Spannable.Factory.getInstance().newSpannable(holder.title.getText());
                spanString.setSpan(new ForegroundColorSpan(Color.RED), startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                holder.title.setText(spanString);
            }

And now create public method in Adapter class

 public void updateList(List<GetShop> list, String searchText) {
            Itemlist = new ArrayList<>();
            Itemlist.addAll(list);
            this.searchText = searchText;
            notifyDataSetChanged();
        }

Search view listener use inside Activity where you use search view

SearchView searchview=findviewbyid(R.id.searchview);
    searchview.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
                @Override
                public boolean onQueryTextSubmit(String query) {
                    return false;
                }
                @Override
                public boolean onQueryTextChange(String newText) {
                    newText = newText.toLowerCase();
                    ArrayList<GetShop> newList = new ArrayList<>();
                    for (GetShop userInfo : items) {
                        String type = userInfo.getN_stall_name().toLowerCase();
                        if (type.contains(newText)) {
                            newList.add(userInfo);
                        }
                    }
                    disp_adapter.updateList(newList, newText);
                    return true;
                }
            });
Prabh deep
  • 1,024
  • 11
  • 15
0
        while(index>**-1**){
                   SpannableStringBuilder sb = new SpannableStringBuilder(desc);
                   ForegroundColorSpan fcs = new ForegroundColorSpan(Color.rgb(158, 158, 158)); //specify color here
                   sb.setSpan(fcs, index, **index** + searchText.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); 
                    index = desc.indexOf(searchText,index+1);

                }

A little add-on to the previous answer , you should check index >-1 instead, else the first text wont be highlighted. For setSpan , the int end should be your index + searchText.length().

Joshua
  • 61
  • 1
  • 9
  • what does the "index + searchText.length()" do with "int end"? – AJW Feb 10 '17 at 17:55
  • 1
    int end is at which index(position) the highlighted text stop at. you need to get the start index of the text you want to highlight and add in the length of the text. – Joshua Feb 14 '17 at 03:51
  • Ah, now I understand. Cheers! – AJW Feb 15 '17 at 04:32
0
/**
 * Kotlin Code
 * highlight the backgroundText
 * searchText -> pass the hightlight that appear in background
 * name -> actual string name
 * textView -> append the data to widget.
 */
private fun highlightText(
    searchText: String,
    name: String,
    textView: TextView
) {
    if (searchText.isNotEmpty() && name.contains(searchText)) {
        val sb = SpannableStringBuilder(name)
        var index: Int = name.lowercase().indexOf(searchText.lowercase())
        while (index >= 0 && index < name.length) {
            val fcs = BackgroundColorSpan(Color.rgb(0, 200, 200)) //specify color here
            sb.setSpan(
                fcs,
                index,
                index + searchText.length,
                Spannable.SPAN_INCLUSIVE_INCLUSIVE
            )
            index = name.indexOf(searchText, index + 1)
        }
        textView.text = sb
    } else {
        textView.text = name
    }
}
  • Please don't post only code as an answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Tyler2P Sep 18 '21 at 18:30