0

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)

AJW
  • 1,578
  • 3
  • 36
  • 77

1 Answers1

2

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);
    }
}
i_A_mok
  • 2,744
  • 2
  • 11
  • 15
  • Well done, works beautifully! Answer accepted and upvoted, cheers. – AJW May 11 '21 at 02:37
  • I'm curious about one more thing: I tried to move the creation of the Spannable into the ViewHolder and then get the reference to it from within onBindViewHolder. That way the Spannable doesn't have to get re-created everytime the views are updated in onBindViewHolder. But the project crashes with a "java.lang.IndexOutOfBoundsException: setSpan (9 ... 10) ends beyond length 4" logcat. I also tried using SpannableString in the ViewHolder. Any ideas on how to put the Spannable in the ViewHolder? – AJW May 11 '21 at 02:47
  • I don't get your idea. I tried `spannable = new SpannableString(tmpCname);` inside onBindViewHolder while spannable is declared somewhere else (inside ViewHolder or global in the Adapter), but has no error. Pls update question with your tested code. – i_A_mok May 11 '21 at 04:37
  • Project is crashing. I am getting an IndexOutofBoundsException when the setSpan () is called. See above. It only happens sometimes, other times the letter being searched is perfectly highlighted. I put logcat out put above. Any thouhgts? – AJW May 11 '21 at 22:37
  • I think filtered list is a list that has **only** items with the search word. But from your **setFilter()** and original **onBindViewHolder()**, your filtered list is a list that has **all** items and highlights those items that contains the search word. Therefore my original answer crash on items that does not contain the search word (start = 0 and substring() = -1, so startPos = -1 that crash) . Make a simple check can avoid the crash. – i_A_mok May 12 '21 at 03:30