Android: как исправить SpannableStrings из поиска в RecyclerView?

У меня есть SearchView, настроенный на верхней панели инструментов в MainActivity. Пользователь щелкает значок поиска, а затем вводит символы поиска с экранной клавиатуры. Наблюдатель ViewModel возвращает совпадающие данные из базы данных Room, а затем запускает метод setFilter() для CardsAdapter, чтобы настроить список RecyclerView, который показывает CardViews, соответствующие строке поиска. Я настроил код Spannable в адаптере onBindViewHolder(), чтобы выделить символы, соответствующие строке поиска, для каждого возвращаемого CardView.

Код поиска возвращает правильный CardViews. Однако код Spannable не работает должным образом. Что мне здесь не хватает?

Вот результаты, которые я пытаюсь исправить:

  1. если в SearchView введена маленькая буква t,

    а) тогда выделяется только первая t (зеленым цветом). Все остальные маленькие ts для этой строки CardView EditText не выделяются. Я хотел бы, чтобы все маленькие ts были выделены.

    б) ни один из элементов CardView с заглавной буквой T не выделен зеленым цветом. Я хотел бы, чтобы все буквы T в верхнем регистре также были выделены.

Вот пример CardView, показывающий две проблемы: только первая маленькая буква t выделена зеленым цветом, а буква T в верхнем регистре вообще не выделена.

введите здесь описание изображения

  1. если в SearchView введена заглавная буква T,

    а) тогда выделена только первая Т (зеленым цветом). Все остальные прописные буквы T не выделены. Я хотел бы, чтобы все T были выделены.

    б) ни один из CardViews с маленькой буквой t не выделен зеленым цветом. Я хотел бы, чтобы все маленькие ts также были выделены.

Если в onBindViewHolder() добавить строку todoHighlight = card.getTodo() с .toLowerCase(), то:

  1. если в SearchView введена маленькая буква t,

    а) тогда выделяется только первая буква T или t (зеленым цветом). Все остальные Ts или ts не выделены. Я бы хотел, чтобы они все были выделены.

    б) Заглавные буквы выделены зеленым цветом, как я и хотел.

  2. если в SearchView введена заглавная буква T,

    а) ни один из элементов CardView, в которых есть маленькая буква t или заглавная буква T, не выделен зеленым цветом. Я бы хотел, чтобы они все были выделены.

    Основная деятельность

    ...

     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());
                     }
                 });    
    

    КартыАдаптер

    ...

     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);
             }
         }
    

переработанный код, который работает некоторое время и дает сбой в других случаях:

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 после реализации ответа ниже:

java.lang.IndexOutOfBoundsException: setSpan (-1 ... 0) начинается до 0 в android.text.SpannableStringInternal.checkRange(SpannableStringInternal.java:442) в android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:163) в android .text.SpannableStringInternal.setSpan(SpannableStringInternal.java:152) в android.text.SpannableString.setSpan(SpannableString.java:46) в com.todo.quickcards.adapter.CardsAdapter.highLightText(CardsAdapter.java:665) в com. todo.quickcards.adapter.CardsAdapter.onBindViewHolder(CardsAdapter.java:643)


person AJW    schedule 25.04.2021    source источник


Ответы (1)


Ниже приведен код для выделения нескольких текстов в вопросе: 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);
    }
}
person i_A_mok    schedule 10.05.2021
comment
Молодец, красиво работает! Ответ принят и одобрен, ура. - person AJW; 11.05.2021
comment
Мне любопытно еще одно: я попытался переместить создание Spannable в ViewHolder, а затем получить ссылку на него изнутри onBindViewHolder. Таким образом, Spannable не нужно заново создавать каждый раз, когда представления обновляются в onBindViewHolder. Но проект падает с java.lang.IndexOutOfBoundsException: setSpan (9... 10) заканчивается за пределами длины 4 logcat. Я также пытался использовать SpannableString в ViewHolder. Любые идеи о том, как поместить Spannable в ViewHolder? - person AJW; 11.05.2021
comment
Я не понимаю твоей идеи. Я попробовал spannable = new SpannableString(tmpCname); внутри onBindViewHolder, в то время как spannable объявлен где-то еще (внутри ViewHolder или глобально в адаптере), но без ошибок. Пожалуйста, обновите вопрос с вашим проверенным кодом. - person i_A_mok; 11.05.2021
comment
Сбой проекта. Я получаю исключение IndexOutofBoundsException при вызове setSpan(). См. выше. Это происходит только иногда, в других случаях искомая буква отлично выделяется. Я поставил logcat выше. Любые мысли? - person AJW; 12.05.2021
comment
Я думаю, что отфильтрованный список — это список, содержащий только элементы с искомым словом. Но из вашего setFilter() и исходного onBindViewHolder() ваш отфильтрованный список представляет собой список, который содержит все элементы и выделяет те элементы, которые содержат поисковое слово. Поэтому мой первоначальный ответ дает сбой на элементах, которые не содержат искомого слова (start = 0 и substring() = -1, поэтому startPos = -1 приводит к сбою). Сделайте простую проверку, чтобы избежать сбоя. - person i_A_mok; 12.05.2021