You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android问题:修复RecyclerView与ListAdapter的搜索文本高亮失效问题

问题:SearchView过滤RecyclerView后,文本高亮不生效

Toolbar中的SearchView可以正确过滤CardView组成的RecyclerView列表,但ListAdapter的onBindViewHolder中设置匹配文本绿色高亮的代码无法生效。我尝试通过设置布尔值isSearchMatched标记匹配项,再提交新列表让ListAdapter更新,但高亮效果没出来,请问哪里错了?


MainActivity代码

mSearchView.setOnQueryTextListener() {
    ...
    filter(newText)
}

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);
            cardItem.setSearchMatched(true);
        }
    }  
    if (!searchList.isEmpty()) {  
        adapter.setFilter(searchList, searchText);
    }
}

Card.java(数据模型)

@Entity(tableName = "cards_table")
public class Card implements Parcelable {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "cardId")
    public int id;
    @ColumnInfo(name = "cardTodo")
    private String todo;
    @ColumnInfo(name = "cardMatchedSearchText")
    private boolean isSearchMatched = false;

    ...
@Override
public void writeToParcel(Parcel parcel, int flags) {
    parcel.writeInt(id);
    parcel.writeString(todo);
    parcel.writeByte((byte) (isSearchMatched ? 1 : 0));
}

private void readFromParcel(Parcel in) {
    id = in.readInt();        
    todo = in.readString();
    isSearchMatched = in.readByte() != 0;
}

public int getId() {
    return this.id;
}
public String getTodo() {
    return this.todo;
}
public boolean isSearchMatched() {
    return this.isSearchMatched;
}

public void setId(int id) {
    this.id = id;
}    
public void setTodo(String todo) {
    this.todo = todo;
}
public void setSearchMatched(boolean isSearchMatched) {
    this.isSearchMatched = isSearchMatched;
}

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Card card = (Card) o;
    return Objects.equals(id, card.id) &&
           Objects.equals(todo, card.todo) &&               
           Objects.equals(isSearchMatched, card.isSearchMatched);
}         
     
@Override
public int hashCode() {
    return Objects.hash(id, todo, isSearchMatched);
}    

ListAdapter代码

public class CardRVAdapter extends ListAdapter<Card, CardRVAdapter.ViewHolder> {

    private String searchString = "";
    public Spannable spannable;

    private static final DiffUtil.ItemCallback<Card> DIFF_CALLBACK = new DiffUtil.ItemCallback<Card>() {
    @Override
    public boolean areItemsTheSame(Card oldItem, Card newItem) {
        return oldItem.getId() == newItem.getId();
    }

    @Override
    public boolean areContentsTheSame(Card oldItem, Card newItem) {
        return oldItem.getTodo().equals(newItem.getTodo()) &&
               oldItem.isSearchMatched() == (newItem.isSearchMatched());
    }
};
    
    public void setFilter(List<Card> newSearchList, String adapSearchText) {

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

        TextView cardBlankText2;  // 显示CardView中的文本,与用户搜索输入匹配
    }

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

    void bindData(Card card, final int position) {

        // 将匹配的搜索文本高亮为绿色
        if (searchString != null && !TextUtils.isEmpty(searchString)) {
            
            String todoSearchHighlightFromVH = cardBlankText2.getText().toString().toLowerCase();
            int offsetEnd2 = todoSearchHighlightFromVH.indexOf(searchString.toLowerCase(Locale.US));
            final Spannable spannable2 = new SpannableString(cardBlankText2.getText());
            spannable2.removeSpan(cardBlankText2);
            cardBlankText2.setText(spannable2);
 
            if (card.isSearchMatched()) {
                for (int start2 = 0; start2 < todoSearchHighlightFromVH.length() && offsetEnd2 != -1; start2 = offsetEnd2 + 1) {

                    offsetEnd2 = todoSearchHighlightFromVH.indexOf(searchString.toLowerCase(Locale.US), start2);
                    if (offsetEnd2 == -1) {
                        break;
                    } else {
                        final ForegroundColorSpan foregroundColorSpan2 = new ForegroundColorSpan(Color.GREEN);
                        spannable2.setSpan(foregroundColorSpan2, offsetEnd2, offsetEnd2 + searchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        cardBlankText2.setText(spannable2);
                    }
                }
            }
        }         
    }

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

问题分析与修复

1. 未先设置TextView的原始文本

bindData中直接用cardBlankText2.getText()获取文本,但此时TextView未加载当前Card的todo内容,导致高亮逻辑基于空/旧文本执行。必须先设置原始文本:

void bindData(Card card, final int position) {
    // 先加载当前Card的文本
    cardBlankText2.setText(card.getTodo());
    
    // 高亮逻辑...
}

2. 过滤时未重置未匹配项的状态

filter中只给匹配项设isSearchMatched=true,但未重置不匹配项的状态,旧状态会残留导致逻辑出错:

private void filter(String searchText) {
    ArrayList<Card> searchList = new ArrayList<>();
    // 先重置所有项的匹配状态
    for (Card cardItem : mCards) {
        cardItem.setSearchMatched(false);
    }
    // 再筛选匹配项并标记
    for (Card cardItem : mCards) {        
        if (cardItem.getTodo().toLowerCase().contains(searchText.toLowerCase(Locale.US))) {                    
            searchList.add(cardItem);
            cardItem.setSearchMatched(true);
        }
    }  
    adapter.setFilter(searchList, searchText);
}

3. 高亮逻辑的索引计算与Span处理错误

  • 应该直接用Card的todo文本计算索引,避免TextView文本不一致
  • spannable2.removeSpan(cardBlankText2)是错误操作,应移除旧的高亮Span

修改后的高亮逻辑:

void bindData(Card card, final int position) {
    cardBlankText2.setText(card.getTodo());
    
    if (searchString != null && !TextUtils.isEmpty(searchString) && card.isSearchMatched()) {
        String todoText = card.getTodo();
        String lowerTodo = todoText.toLowerCase(Locale.US);
        String lowerSearch = searchString.toLowerCase(Locale.US);
        
        final Spannable spannable2 = new SpannableString(todoText);
        // 移除之前的所有高亮Span
        ForegroundColorSpan[] oldSpans = spannable2.getSpans(0, spannable2.length(), ForegroundColorSpan.class);
        for (ForegroundColorSpan span : oldSpans) {
            spannable2.removeSpan(span);
        }
        
        int start = 0;
        while (start < lowerTodo.length()) {
            int matchIndex = lowerTodo.indexOf(lowerSearch, start);
            if (matchIndex == -1) break;
            // 设置绿色高亮
            ForegroundColorSpan span = new ForegroundColorSpan(Color.GREEN);
            spannable2.setSpan(span, matchIndex, matchIndex + searchString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            start = matchIndex + searchString.length();
        }
        cardBlankText2.setText(spannable2);
    }
}

4. 补充空搜索的处理逻辑

当前setFilter只在搜索列表非空时提交数据,清空搜索框会导致列表为空,需补充空搜索的处理:

public void setFilter(List<Card> newSearchList, String adapSearchText) {
    this.searchString = adapSearchText.toLowerCase(Locale.US);
    if (TextUtils.isEmpty(adapSearchText)) {
        // 空搜索时提交全量列表
        submitList(new ArrayList<>(mCards)); // 需让Adapter持有原始列表引用,或从外部传入
    } else {
        submitList(new ArrayList<>(newSearchList != null ? newSearchList : Collections.emptyList()));
    }
}

同时在filter中补充空搜索分支:

private void filter(String searchText) {
    if (TextUtils.isEmpty(searchText)) {
        for (Card cardItem : mCards) {
            cardItem.setSearchMatched(false);
        }
        adapter.setFilter(mCards, searchText);
        return;
    }
    // 非空搜索逻辑...
}

内容的提问来源于stack exchange,提问作者AJW

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 15:37:06