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
相关产品推荐
相关产品推荐

