EditText结合TextWatcher搜索列表时输入卡顿,求解决方案
Hey there, let's tackle that input lag issue you're seeing. Even with a small list (40-60 items), your current setup can cause stutters—here's why and how to fix it:
Why the Lag Happens
Your onTextChanged callback runs on the main thread, which handles all UI updates. Every time the user types a character, you:
- Iterate through the entire list synchronously
- Create a new ArrayList
- Call
setNewDatawhich triggers a full RecyclerView refresh
Even with 60 items, this repeated main-thread work adds up and blocks UI rendering, causing the lag you're seeing.
Solution 1: Use RecyclerView's Built-in Filterable Interface
The Filterable interface is designed exactly for this scenario—it runs the filtering logic on a background thread automatically, so your main thread stays free for UI work. Here's how to implement it:
Step 1: Update your Adapter to implement Filterable
public class ProductListAdapter extends RecyclerView.Adapter<ProductListAdapter.ViewHolder> implements Filterable { private List<ProductInformation> originalList; private List<ProductInformation> filteredList; // Constructor: Initialize originalList and filteredList public ProductListAdapter(List<ProductInformation> productList) { this.originalList = new ArrayList<>(productList); this.filteredList = new ArrayList<>(productList); } // ... Your existing ViewHolder and onBindViewHolder code ... @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); List<ProductInformation> filtered = new ArrayList<>(); // Filter logic runs on background thread if (constraint == null || constraint.length() == 0) { filtered.addAll(originalList); } else { String query = constraint.toString().toLowerCase().trim(); for (ProductInformation info : originalList) { if (info.getName().toLowerCase().contains(query)) { filtered.add(info); } } } results.values = filtered; results.count = filtered.size(); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { filteredList.clear(); filteredList.addAll((List<ProductInformation>) results.values); notifyDataSetChanged(); // Runs on main thread } }; } // Optional: Add a method to update the original list if needed public void updateOriginalList(List<ProductInformation> newList) { this.originalList = new ArrayList<>(newList); getFilter().filter(""); // Reset filter } }
Step 2: Update your TextWatcher to use the filter
searchBox.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { // Just trigger the filter—no heavy work here! productListAdapter.getFilter().filter(charSequence); } @Override public void afterTextChanged(Editable editable) {} });
Solution 2: Add Debouncing to Reduce Unnecessary Searches
Even with background filtering, triggering a search on every single character (like when a user types fast) can be overkill. Add a small delay (300ms is a good starting point) to wait until the user stops typing before running the search. Here's how to do it with a Handler:
private Handler searchHandler = new Handler(Looper.getMainLooper()); private Runnable searchRunnable; searchBox.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { // Cancel any pending search runnable if (searchRunnable != null) { searchHandler.removeCallbacks(searchRunnable); } // Create a new runnable to perform the search after delay final String query = charSequence.toString(); searchRunnable = () -> { // Use the filter from Solution 1, or your optimized search logic here productListAdapter.getFilter().filter(query); }; // Post the runnable with 300ms delay searchHandler.postDelayed(searchRunnable, 300); } @Override public void afterTextChanged(Editable editable) {} });
Solution 3: Optimize the Search Logic Further
- Pre-process names: Convert all product names to lowercase once (when you load the list) instead of every time you check
contains. This saves repeated string processing. - Use DiffUtil for smarter UI updates: Instead of
notifyDataSetChanged()(which refreshes all items), useDiffUtilto calculate the difference between the old and new list, then only update the changed items. This reduces UI rendering overhead.
Quick Win: Avoid Creating a New ArrayList Every Time
If you stick with your original approach, at least reuse a single ArrayList instead of creating a new one on every text change. This reduces garbage collection overhead:
// Declare this as a class variable private ArrayList<ProductInformation> tempList = new ArrayList<>(); @Override public void onTextChanged(final CharSequence charSequence, int i, int i1, int i2) { tempList.clear(); String query = charSequence.toString().toLowerCase(); for (ProductInformation info : currentList) { if (info.getName().toLowerCase().contains(query)) { tempList.add(info); } } productListAdapter.setNewData(tempList); }
Any of these solutions should eliminate the lag you're seeing—start with Solution 1 (Filterable) since it's the most idiomatic Android approach, then add debouncing if you still want to reduce unnecessary filter runs.
内容的提问来源于stack exchange,提问作者Mehdi bahmanpour

