× {{alert.msg}} Never ask again
Get notified about new tutorials RECEIVE NEW TUTORIALS

How to filter a RecyclerView with a SearchView

Xaver Kapeller
May 29, 2015
<h2>Introduction</h2> <p>Since it is not really clear form your question with what exactly you are having trouble I wrote up this quick walkthrough about how to implement this feature, if you still have questions feel free to ask.</p> <p>I have a working example of everything I am talking about here in this <a href="https://github.com/Wrdlbrnft/Searchable-RecyclerView-Demo"><strong>GitHub Repository</strong></a>.</p> <p>The result should looks something like this:</p> <p><img src="http://i.stack.imgur.com/htz0Y.gif" alt="enter image description here"></p> <hr> <h2>1) Setting up the <code>SearchView</code></h2> <p>In your menu xml just add an item and set the <code>actionViewClass</code> to ``. Since you are using the support library you have to use the namespace of the support library to set the <code>actionViewClass</code> attribute. Your menu xml should look something like this:</p> <pre><code>&lt;menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"&gt; &lt;item android:id="@+id/action_search" android:title="@string/action_search" app:actionViewClass="android.support.v7.widget.SearchView" app:showAsAction="always"/&gt; &lt;/menu&gt; </code></pre> <p>In your <code>Fragment</code> or <code>Activity</code> you have to inflate this menu xml like usual, then you can look for the <code>MenuItem</code> which contains the <code>SearchView</code> and implement the <code>OnQueryTextListener</code> which we are going to use to listen for changes to the text entered into the <code>SearchView</code>:</p> <pre><code>@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_main, menu); final MenuItem item = menu.findItem(R.id.action_search); final SearchView searchView = (SearchView) MenuItemCompat.getActionView(item); searchView.setOnQueryTextListener(this); } @Override public boolean onQueryTextChange(String query) { // Here is where we are going to implement our filter logic return false; } @Override public boolean onQueryTextSubmit(String query) { return false; } </code></pre> <p>And now the <code>SearchView</code> is ready to be used. We will implement the filter logic later on in <code>onQueryTextChange()</code> once we are finished implementing the <code>Adapter</code>.</p> <hr> <h2>2) Setting up the <code>Adapter</code></h2> <p>First and foremost this is the model class I am going to use for this example: </p> <pre><code>public class ExampleModel { private final String mText; public ExampleModel(String text) { mText = text; } public String getText() { return mText; } } </code></pre> <p>It's just your basic model which will display a text in the <code>RecyclerView</code>. This is the layout I am going to use to display the text: </p> <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true" android:background="?attr/selectableItemBackground"&gt; &lt;TextView android:id="@+id/tvText" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp"/&gt; &lt;/FrameLayout&gt; </code></pre> <p>And the <code>ViewHolder</code> I am using looks like this:</p> <pre><code>public class ExampleViewHolder extends RecyclerView.ViewHolder { private final TextView tvText; public ExampleViewHolder(View itemView) { super(itemView); tvText = (TextView) itemView.findViewById(R.id.tvText); } public void bind(ExampleModel model) { tvText.setText(model.getText()); } } </code></pre> <p>Again nothing special. It just sets the text from the <code>ExampleModel</code> to the <code>TextView</code> in our layout.<br> Now we can finally come to the really interesting part: Writing the Adapter. I am going to skip over the basic implementation of the <code>Adapter</code> and am instead going to concentrate on the parts which are relevant for the <code>SearchView</code>. This is the basic implementation of the <code>Adapter</code> I started out with:</p> <pre><code>public class ExampleAdapter extends RecyclerView.Adapter&lt;ExampleViewHolder&gt; { private final LayoutInflater mInflater; private final List&lt;ExampleModel&gt; mModels; public ExampleAdapter(Context context, List&lt;ExampleModel&gt; models) { mInflater = LayoutInflater.from(context); mModels = new ArrayList&lt;&gt;(models); } @Override public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View itemView = mInflater.inflate(R.layout.item_example, parent, false); return new ExampleViewHolder(itemView); } @Override public void onBindViewHolder(ExampleViewHolder holder, int position) { final ExampleModel model = mModels.get(position); holder.bind(model); } @Override public int getItemCount() { return mModels.size(); } public void setModels(List&lt;ExampleModel&gt; models) { mModels = new ArrayList&lt;&gt;(models); } } </code></pre> <p>Basically this is all you need to make this work. You can filter items in <code>onQueryTextChange()</code> and use the <code>setModels()</code> method to change the models in the <code>Adapter</code> and then call <code>notifyDatasetChanged()</code> on the <code>Adapter</code> to update the <code>RecyclerView</code>. Or at least that would have been the way to do this with a <code>ListView</code>. But the <code>RecyclerView</code> brings a whole new element to the table which we can take advantage off: out-of-the-box support for item animations!</p> <hr> <h2>3) Getting the <code>Adapter</code> ready for item animations</h2> <p>The first thing we have to do is define three helper methods which enable us to add, remove or move items around in the <code>Adapter</code>. Those methods will automatically call the required <code>notify...</code> method to trigger the item animation that goes along with it. </p> <pre><code>public ExampleModel removeItem(int position) { final ExampleModel model = mModels.remove(position); notifyItemRemoved(position); return model; } public void addItem(int position, ExampleModel model) { mModels.add(position, model); notifyItemInserted(position); } public void moveItem(int fromPosition, int toPosition) { final ExampleModel model = mModels.remove(fromPosition); mModels.add(toPosition, model); notifyItemMoved(fromPosition, toPosition); } </code></pre> <p>Again nothing special. We just modify the internal list of objects in the <code>Adapter</code> by either removing, adding or moving objects and once we are done call a <code>notify...</code> method.</p> <p>Now we are going to implement a method which will animate between the <code>List</code> of objects currently displayed in the <code>Adapter</code> to the filtered <code>List</code> we are going to supply to the method. </p> <pre><code>public void animateTo(List&lt;ExampleModel&gt; models) { applyAndAnimateRemovals(models); applyAndAnimateAdditions(models); applyAndAnimateMovedItems(models); } </code></pre> <p>The three methods contained in <code>animateTo()</code> do all the work here, but the order is important! The most difficult part about animating multiple items like this is keeping track of indexes. What I mean by that is that if for example you add an item then all items below the item you added are moved down. Equally if you remove an item all items below it are moved up. This is a big problem because all <code>notify...()</code> methods which trigger the item animations require the index of an item. If you are not careful and add or remove items and then try to call the <code>notify...</code> methods you are going to end up with weird glitchy animations or even an <code>ArrayIndexOutOfBoundsException</code>.</p> <p>We do two things to greatly simplify this problem for us:</p> <ol> <li>We have our 3 three helper methods from above which we can use to add, remove or move items and they automatically call the correct <code>notify...</code> method. So we don't have to worry about the animations anymore. We can purely concentrate on modifying the internal <code>List</code> of the <code>Adapter</code>.</li> <li>We define a specific order of operations when modifying the internal <code>List</code> of the <code>Adapter</code>. </li> </ol> <p>As I mentioned above we supply the filtered <code>List</code> to the <code>Adapter</code> by passing it into the <code>animateTo()</code> method. The first step is to remove all items which do not exist in the filtered <code>List</code> anymore. The next step is to add all items which did not exist in the original <code>List</code> but do in the filtered <code>List</code>. The final step is to move all items which exist in both <code>Lists</code>. This is exactly what the three methods in <code>animateTo()</code> do.</p> <p>Now we are going to look at the implementation of those three methods.</p> <p>Lets start in order with <code>applyAndAnimateRemovals()</code>:</p> <pre><code>private void applyAndAnimateRemovals(List&lt;ExampleModel&gt; newModels) { for (int i = mModels.size() - 1; i &gt;= 0; i--) { final ExampleModel model = mModels.get(i); if (!newModels.contains(model)) { removeItem(i); } } } </code></pre> <p>As you can see this method iterates through the internal <code>List</code> of the <code>Adapter</code> backwards and checks if each item is contained in the new filtered <code>List</code>. If it is not it calls <code>removeItem()</code>. The reason we iterate backwards is to avoid having to keep track of an offset. If you remove an item all items below it move up. If you iterate through to the <code>List</code> from the bottom up then only items which you have already iterated over are moved.</p> <p>Now lets look at <code>applyAndAnimateAdditions()</code>:</p> <pre><code>private void applyAndAnimateAdditions(List&lt;ExampleModel&gt; newModels) { for (int i = 0, count = newModels.size(); i &lt; count; i++) { final ExampleModel model = newModels.get(i); if (!mModels.contains(model)) { addItem(i, model); } } } </code></pre> <p>It basically does the same thing as <code>applyAndAnimateRemovals()</code> but instead of iterating through the internal <code>List</code> of the <code>Adapter</code> it iterates through the filtered <code>List</code> and checks if the item exists in the internal <code>List</code>. If it does not it calls <code>addItem()</code>.</p> <p>And finally lets look at <code>applyAndAnimateMovedItems()</code>:</p> <pre><code>private void applyAndAnimateMovedItems(List&lt;ExampleModel&gt; newModels) { for (int toPosition = newModels.size() - 1; toPosition &gt;= 0; toPosition--) { final ExampleModel model = newModels.get(toPosition); final int fromPosition = mModels.indexOf(model); if (fromPosition &gt;= 0 &amp;&amp; fromPosition != toPosition) { moveItem(fromPosition, toPosition); } } } </code></pre> <p>This method implements a more complicated logic than the previous two. It is essentially a combination of <code>applyAndAnimateRemovals()</code> and <code>applyAndAnimateAdditions()</code> but with a twist. You have to realize that at this point <code>applyAndAnimateRemovals()</code> and <code>applyAndAnimateAdditions()</code> have already been called. So we have removed all the items that need to be removed and we added all new items which need to be added. So the internal <code>List</code> of the <code>Adapter</code> and the filtered <code>List</code> contain the exactly same items, but they may be in a different order. What <code>applyAndAnimateMovedItems()</code> now does is it iterates through the filtered <code>List</code> backwards and looks up the index of each item in the internal <code>List</code>. If it detects a difference in the index it calls <code>moveItem()</code> to bring the internal <code>List</code> of the <code>Adapter</code> in line with the filtered <code>List</code>.</p> <p>And with that our <code>Adapter</code> is complete. It can now display items and automatically triggers appropriate animations as we filter the <code>List</code> of objects. The only thing missing now is to connect the <code>SearchView</code> to the <code>RecyclerView</code>!</p> <p>One thing I should still mention is that while this is a very simple approach to animating the items it is by far not the most efficient one. But for most use cases it should be more than enough and once you understand the gist of it you can implement it very quickly.</p> <hr> <h2>4) Implementing the filter logic</h2> <p>One thing in your question which caught my eye is that you maintain the list of items you want to display directly in the <code>Adapter</code>. While the <code>Adapter</code> of course has to have a <code>List</code> of items internally you should not completely maintain the <code>List</code> in there. The <code>Adapter</code> - as the name implies - should just turn <code>Objects</code> into <code>Views</code>. You give it a <code>List</code> of <code>Objects</code> and the <code>RecyclerView</code> gets a <code>View</code> representing each <code>Object</code>. What the <code>Adapter</code> is definitely not responsible for is creating those <code>Objects</code>.</p> <p>To implement the filter logic we first have to define a <code>List</code> of all possible <code>Objects</code>. As I mentioned above we will do that not inside the <code>Adapter</code>, but outside in the <code>Fragment</code> or <code>Activity</code> which contains the <code>RecyclerView</code>. The basic implementation (in my case for a <code>Fragment</code>) looks like this:</p> <pre><code>private RecyclerView mRecyclerView; private ExampleAdapter mAdapter; private List&lt;ExampleModel&gt; mModels; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_main, container, false); mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView); return view; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setHasOptionsMenu(true); mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); mModels = new ArrayList&lt;&gt;(); for (String movie : MOVIES) { mModels.add(new ExampleModel(movie)); } mAdapter = new ExampleAdapter(getActivity(), mModels); mRecyclerView.setAdapter(mAdapter); } </code></pre> <p>Again nothing special. We setup the <code>RecyclerView</code> and instantiate the <code>Adapter</code>. We create a <code>List</code> of items and store that in a field. This list will be our reference. It contains all possible items which we will filter with the <code>SearchView</code>. In the above example <code>MOVIES</code> is a <code>String[]</code> I defined which contains my test data.</p> <p>Now we can go back to <code>onQueryTextChange()</code> which we defined earlier and implement a simple filter logic based on the text inside <code>ExampleModel</code>.</p> <pre><code>@Override public boolean onQueryTextChange(String query) { query = query.toLowerCase(); final List&lt;ExampleModel&gt; filteredModelList = new ArrayList&lt;&gt;(); for (ExampleModel model : mModels) { final String text = model.getText().toLowerCase(); if (text.contains(query)) { filteredModelList.add(model); } } mAdapter.animateTo(filteredModelList); mRecyclerView.scrollToPosition(0); return true; } </code></pre> <p>One thing to note here is that we call <code>toLowerCase()</code> both on the query string and on the text in <code>ExampleModel</code>. This is done so we can compare both texts regardless of case. If the text in <code>ExampleModel</code> contains the query string we add it to the filtered <code>List</code>. After iterating through all the objects in our reference <code>List</code> and adding all items which match the query string to the filtered <code>List</code> we just have to call <code>animateTo()</code> on the <code>Adapter</code> and pass in the filtered <code>List</code>. We also have to call <code>scrollToPosition(0)</code> on the <code>RecyclerView</code> to ensure that the user can always see all items when searching for something. Otherwise the <code>RecyclerView</code> might stay in a scrolled down position while filtering and hide a few items.</p> <p>And that is everything you have to do to get this to work! I realize that this is a very detailed description which probably makes this whole thing seem more complicated than it really is. I suggest you look at the working example in the <a href="https://github.com/Wrdlbrnft/Searchable-RecyclerView-Demo"><strong>GitHub Repository</strong></a> I linked to above if you are having trouble understanding a specific aspect of this whole thing.</p> <p>This tip was originally posted on <a href="http://stackoverflow.com/questions/30398247/How%20to%20filter%20a%20RecyclerView%20with%20a%20SearchView/30429439">Stack Overflow</a>.</p>
comments powered by Disqus