androidandroid-recyclerviewspannable

android: how to fix SpannableStrings from search in RecyclerView?


I have a SearchView set up on the top Toolbar in MainActivity. A user clicks on the search icon and then enters search character(s) from a soft keyboard. A ViewModel observer returns matching data from a Room database and then runs a setFilter() method for the CardsAdapter to set up a RecyclerView list that shows CardViews that matched the search String. I set up Spannable code in the Adapter's onBindViewHolder() to highlight the characters that match the search String, for each returned CardView.

The search code is returning the correct CardViews. However, the Spannable code is not working as expected. What am I missing here?

Here are the outcomes I am trying to fix:

  1. if a small "t" is entered in the SearchView,

    a) then only the first "t" is highlighted (in green color). All remaining small "t"s for that CardView EditText line are not highlighted. I would like all small "t"s highlighted.

    b) none of the CardViews that have a uppercase "T" are highlighted in the green color. I would like all Uppercase "T"s to be highlighted as well.

Here is a sample CardView showing the two issues: only the first small "t" is highlighted in green and the Uppercase "T" is not highlighted at all.

enter image description here

  1. if an Uppercase "T" is entered in the SearchView,

    a) then only the first "T" is highlighted (in green color). All remaining uppercase "T"s are not highlighted. I would like all "T"s to be highlighted.

    b) none of the CardViews that have a small "t" are highlighted in the green color. I would like all of the small "t"s to be highlighted as well.

In onBindViewHolder(), if I append the "String todoHighlight = card.getTodo()" with ".toLowerCase()" then:

  1. if a small "t" is entered in the SearchView,

    a) then only the first "T" or "t" is highlighted (in green color). All remaining "T"s or "t"s are not highlighted. I would like them all highlighted.

    b) The capital "T"s are highlighted in the green color as I would like.

  2. if an uppercase "T" is entered in the SearchView,

    a) none of the CardViews that have a small "t" or an uppercase "T" are highlighted in the green color. I would like them all highlighted.

    MainActivity

    ...

     mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
             EditText mSearchEditText = mSearchView.findViewById(androidx.appcompat.R.id.search_src_text);
             mSearchEditText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
             // use the link to EditText of the SearchView so that a
             // TextWatcher can be used.
             mSearchEditText.addTextChangedListener(new TextWatcher() {
    
         @Override
         public void afterTextChanged(Editable s) {
    
             if (s.toString().length() > 0) {
    
                 String queryText = "%" + s.toString() + "%";
    
                 mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchQuickcards -> {
    
                     searchList = searchQuickcards;
                     if (!mSearchView.isIconified() && searchList.size() > 0) {
                         cardsAdapter.setFilter(searchList, s.toString());
                     }
                 });    
    

    CardsAdapter

    ...

     public void setFilter(List<card> searchCards, String searchString) {
         mListItems = new ArrayList<>();
         mListItems.clear();
         mListItems.addAll(searchCards);
         this.searchString = searchString;
         notifyDataSetChanged();
     }
    
     public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
    
         ...
         // also tried "String todoHighlight = card.getTodo().toLowerCase();"
         String todoHighlight = card.getTodo();
    
         if (searchString != null && !searchString.isEmpty() && todoHighlight.contains(searchString)) {
    
             int todoStartPos = todoHighlight.indexOf(searchString);
             int todoEndPos = todoStartPos + searchString.length();
             Spannable spanString2 = new SpannableString(itemHolder.cardBlankText2.getText());
    
             if (spanString2 != null) {
                 spanString2.removeSpan(itemHolder.getForegroundColorSpan());
                 itemHolder.cardBlankTextNumstotal.setText(spanString2);
                 spanString2.setSpan(new ForegroundColorSpan(Color.GREEN), todoStartPos, todoEndPos,
                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 itemHolder.cardBlankText2.setText(spanString2);
             }
         }
    

revised code that is working some of the time and crashing other times:

in onBindViewHolder():

String todoSearchHighlight = mListItems.get(position).getTodo();
Spannable spannable = new SpannableString(todoSearchHighlight);
if (searchString != null && !TextUtils.isEmpty(searchString)) {
    highLightText(spannable, todoSearchHighlight, 0);
    itemHolder.cardBlankText2.setText(spannable);
}
else { // searchString == null
    if (spannable != null) {
    spannable.removeSpan(itemHolder.getForegroundColorSpan());
    itemHolder.cardBlankText2.setText(todoSearchHighlight);
    }
}

private void highLightText(Spannable spannable, String string, int start) {

    int startPos = start + string.substring(start).toLowerCase(Locale.US).indexOf(searchString.toLowerCase(Locale.US));
    int endPos = startPos + searchString.length();
    if (string.substring(endPos).toLowerCase(Locale.US).contains(searchString.toLowerCase(Locale.US))){
        highLightText(spannable, string, endPos);
    }
    ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.GREEN);
    spannable.setSpan(foregroundColorSpan, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
     

logcat error after implementing answer below:

java.lang.IndexOutOfBoundsException: setSpan (-1 ... 0) starts before 0 at android.text.SpannableStringInternal.checkRange(SpannableStringInternal.java:442) at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:163) at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:152) at android.text.SpannableString.setSpan(SpannableString.java:46) at com.todo.quickcards.adapter.CardsAdapter.highLightText(CardsAdapter.java:665) at com.todo.quickcards.adapter.CardsAdapter.onBindViewHolder(CardsAdapter.java:643)


Solution

  • Below code is for Highlight multiple text on question: Highlight filtered text in recyclerView

    @Override
    public void onBindViewHolder(final MyViewHolder holder, int position) {
        if (!filterPattern.equals("")) {
            String tmpCname = data.get(position);
    
            //int startPos = tmpCname.toLowerCase(Locale.US).indexOf(filterPattern.toLowerCase(Locale.US));
            //int endPos = startPos + filterPattern.length();
            //Spannable spannable = new SpannableString(tmpCname);
            //ColorStateList blueColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.BLUE});
            //TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
            //spannable.setSpan(highlightSpan, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    
            Spannable spannable = new SpannableString(tmpCname);
            highLightText(spannable, tmpCname, 0);
    
            holder.mTitle.setText(spannable);
        } else {
            holder.mTitle.setText(data.get(position));
        }
    }
    
    private void highLightText(Spannable spannable, String string, int start) {
        int startPos = start + string.substring(start).toLowerCase(Locale.US).indexOf(filterPattern.toLowerCase(Locale.US));
        if (startPos > -1) {
            int endPos = startPos + filterPattern.length();
            if (string.substring(endPos).toLowerCase(Locale.US).contains(filterPattern.toLowerCase(Locale.US))){
                highLightText(spannable, string, endPos);
            }
            ColorStateList blueColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.BLUE});
            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
            spannable.setSpan(highlightSpan, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }