Android: Исправление выделения текста ForegroundColorSpan при поиске с помощью RecyclerView и ListAdapter

Вопрос или проблема

У меня есть SearchView внутри Toolbar, который располагается над списком CardView в RecyclerView. SearchView корректно работает для фильтрации списка на основе текстовых вводов. Однако код для выделения искомого текста цветом Color.GREEN не работает должным образом.

Первый CardView в вертикальном списке RecyclerView никогда не имеет выделенного искомого текста. А остальные CardView с искомым текстом ниже него также часто не выделяются. Кроме того, при прокрутке вниз и обратно вверх по списку некоторые найденные искомые тексты начинают выделяться, хотя этот текст не был выделен, когда список RecyclerView был первоначально возвращен.

Возможно, проблема связана с использованием ListAdapter в качестве адаптера, так как представления обновляются в фоновом потоке, и набор данных еще не готов? Есть идеи, как правильно выделить искомый текст, используя ListAdapter в вертикальном прокручиваемом RecyclerView?

MainActivity
....  
public boolean onCreateOptionsMenu(Menu menu) {

    mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {

            filter(newText); 
            return false;
        }
    }); 
} 

private void filter(String searchText) {
        
    ArrayList<Card> searchList = new ArrayList<>();

    for (Card cardItem : mCards) {
    
        if (cardItem.getTodo().toLowerCase().contains(searchText.toLowerCase(Locale.US))) {                    
            searchList.add(cardItem);
        }
    }  
    if (!searchList.isEmpty()) {  
        adapter.setFilter(searchList, searchText);
    }
}

ListAdapter

public class CardRVAdapter extends ListAdapter<Card, CardRVAdapter.ViewHolder> {
....
    private String searchString = "";
    public Spannable spannable;
    
    public void setFilter(List<Card> newSearchList, String adapSearchText) {

        if (newSearchList != null && !newSearchList.isEmpty()) {
            this.searchString = adapSearchText.toLowerCase(Locale.US);
            ArrayList<Card> tempList = new ArrayList<>(newSearchList); 
            submitList(tempList);
        }
    }
  
    public class ViewHolder extends RecyclerView.ViewHolder {

        TextView cardBlankText2;  // отображает текст в CardView и сопоставляется с введенным искомым текстом.
        ForegroundColorSpan fcs = new ForegroundColorSpan(Color.GREEN);

        public ForegroundColorSpan getFCS() {
            return fcs;
        }
    }

    public ViewHolder(@NonNull final View itemView) {
        super(itemView);
         
        cardBlankText2 = itemView.findViewById(R.id.cardBlankText2);
    }

    void bindData(Card card, final int position) {

        spannable = Spannable.Factory.getInstance().newSpannable(cardBlankText2.getText().toString());

        // Получить любые предыдущие спаны и удалить их
        ForegroundColorSpan[] foregroundSpans = spannable.getSpans(0,spannable.length(), ForegroundColorSpan.class);

        // Подсветить совпадения из искомых символов зеленым цветом.
        if (searchString != null && !TextUtils.isEmpty(searchString)) {

            for (ForegroundColorSpan span : foregroundSpans) {
                spannable.removeSpan(span);
            } 
           
            int index = spannable.toString().toLowerCase(Locale.US).indexOf(searchString);    
            while (index != -1) {
                spannable.setSpan(getFCS(), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                index = spannable.toString().indexOf(searchString,index + searchString.length());
            }
            cardBlankText2.setText(spannable, TextView.BufferType.SPANNABLE);
        }    
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        
        final Card card = getCardAt(position);
        if (card != null) {        
            holder.bindData(card, position);
        }
    }

    public Card getCardAt(int position) {
        return getItem(position);
    }    

}   

Ответ или решение

Для решения проблемы с подсветкой текста в RecyclerView, когда используется ListAdapter, необходимо внести несколько изменений в ваш код. Основные проблемы заключаются в обновлении Spannable текста и в том, как ListAdapter управляет повторной привязкой элементов, когда список обновляется. Вот исправленный и улучшенный вариант вашего кода:

Внесенные исправления

  1. Использование Spannable: Лучше создавать новый объект Spannable в методе bindData, чтобы не использовать один и тот же объект для всех элементов. Это гарантирует, что каждый элемент получит уникальную подсветку.

  2. Удаление предыдущих спанов: Нужно удалить все спаны, а не только те, которые были установлены в последнем обновлении списка.

  3. Оптимизация кода: Сделаем код более читабельным и надежным.

Обновленный код

public class CardRVAdapter extends ListAdapter<Card, CardRVAdapter.ViewHolder> {
    private String searchString = "";

    public void setFilter(List<Card> newSearchList, String adapSearchText) {
        this.searchString = adapSearchText.toLowerCase(Locale.US);
        ArrayList<Card> tempList = new ArrayList<>(newSearchList);
        submitList(tempList);
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView cardBlankText2;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            cardBlankText2 = itemView.findViewById(R.id.cardBlankText2);
        }

        void bindData(Card card) {
            String cardText = card.getTodo();
            Spannable spannable = new SpannableString(cardText);

            if (!TextUtils.isEmpty(searchString)) {
                int index = cardText.toLowerCase(Locale.US).indexOf(searchString);
                while (index != -1) {
                    spannable.setSpan(new ForegroundColorSpan(Color.GREEN), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    index = cardText.toLowerCase(Locale.US).indexOf(searchString, index + searchString.length());
                }
            }
            cardBlankText2.setText(spannable, TextView.BufferType.SPANNABLE);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Card card = getItem(position);
        holder.bindData(card);
    }
}

Пояснения к изменениям

  1. Создание нового Spannable: Теперь для каждого элемента списка создается новый объект Spannable, что избавляет от проблем с повторным использованием Spannable между элементами.

  2. Удаление спанов: Удаление всех старых спанов больше не требуется, так как каждый элемент получает свою новую строку Spannable.

  3. Улучшение читаемости: Код стал более понятным, и каждая часть кода имеет ясное назначение.

Заключение

С данным исправленным кодом ваша подсветка текста должна корректно работать для всех элементов в RecyclerView. Каждый элемент будет показывать выделение искомого текста при прокрутке, и не будет возникать проблем с неправильным отображением подсветки. Попробуйте этот подход и убедитесь, что он решает вашу основную проблему.

Оцените материал
Добавить комментарий

Капча загружается...