Introduction
In this article we are going to learn how to add search filter functionality to RecyclerView.
Android allows us to use the search functionality in our app by displaying the SearchView
widget either in the ToolBar
/ActionBar
or inserting it in a layout. Android SearchView
widget is available from Android 3.0 onwards.
Android provides Filterable
class to filter the data by a filter (condition). Usually the getFilter()
method has to be overridden in the adapter class in which the filter condition is provided to search through a list. Below is an example of getFilter()
method to search a movie by name from a list of movies.
@Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String query = charSequence.toString(); List<String> filtered = new ArrayList<>(); if (query.isEmpty()) { filtered = items; } else { for (String movie : items) { if (movie.toLowerCase().contains(query.toLowerCase())) { filtered.add(movie); } } } FilterResults results = new FilterResults(); results.count = filtered.size(); results.values = filtered; return results; } @Override protected void publishResults(CharSequence charSequence, FilterResults results) { itemsFiltered = (ArrayList<String>) results.values; notifyDataSetChanged(); } }; } public interface MoviesAdapterListener { void onSelected(String item); }
Go to GradleScripts > build.gradle (Module:app) and add below libraries to this file under dependencies block. My dependencies block is
dependencies { ... compile 'com.android.support:appcompat-v7:26.1.0' compile 'com.android.support:recyclerview-v7:26.1.0' }
Let’s start writing the adapter class. You need to particularly focus on this class as it is main component in this article.
Create a layout named movie_row_item.xml and add the below layout. This layout renders the single movie item in the list. This layout contains a TextViews
to render name.
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tvName" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" android:clickable="true" android:padding="5dp"/>
Create class named MoviesAdapter.java and implement the class from Filterable
which asks you to override the getFilter()
method.
In getFilter()
method, the search string is passed to performFiltering()
method. The search for a movie by name is performed using query string.
MoviesAdapterListener
interface provides onSelected()
callback method whenever a movie is selected from the list.
public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.ViewHolder> implements Filterable { private Context context; private List<String> items; private List<String> itemsFiltered; private MoviesAdapterListener listener; public class ViewHolder extends RecyclerView.ViewHolder { public TextView name; public ViewHolder(View view) { super(view); name = view.findViewById(R.id.tvName); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { listener.onSelected(itemsFiltered.get(getAdapterPosition())); } }); } } public MoviesAdapter(Context context, List<String> items, MoviesAdapterListener listener) { this.context = context; this.listener = listener; this.items = items; this.itemsFiltered = items; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.movie_row_item, parent, false); return new ViewHolder(itemView); } @Override public void onBindViewHolder(ViewHolder holder, final int position) { final String name = itemsFiltered.get(position); holder.name.setText(name); } @Override public int getItemCount() { return itemsFiltered.size(); } @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String query = charSequence.toString(); List<String> filtered = new ArrayList<>(); if (query.isEmpty()) { filtered = items; } else { for (String movie : items) { if (movie.toLowerCase().contains(query.toLowerCase())) { filtered.add(movie); } } } FilterResults results = new FilterResults(); results.count = filtered.size(); results.values = filtered; return results; } @Override protected void publishResults(CharSequence charSequence, FilterResults results) { itemsFiltered = (ArrayList<String>) results.values; notifyDataSetChanged(); } }; } public interface MoviesAdapterListener { void onSelected(String item); } }
Adding SearchView to Toolbar
Now everything is ready. All we have to do is, enable SearchView
in Toolbar, render the RecyclerView
by parsing the list and pass the search query to adapter.
Open/create menu_main.xml located under res > menus and add the SearchView
widget and make it always visible.
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_search" android:icon="@android:drawable/ic_menu_search" android:orderInCategory="100" android:title="Search" app:showAsAction="always" app:actionViewClass="android.support.v7.widget.SearchView" /> </menu>
Open the layout files of main activity activity_main.xml and add RecyclerView
element.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:theme="@style/AppTheme.Toolbar" android:contentInsetLeft="0dp" android:contentInsetStart="0dp" android:contentInsetRight="0dp" android:contentInsetEnd="0dp" app:contentInsetLeft="0dp" app:contentInsetStart="0dp" app:contentInsetRight="0dp" app:contentInsetEnd="0dp" /> <android.support.v7.widget.RecyclerView android:id="@+id/rvItems" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> </LinearLayout>
Finally open the MainActivity.java and add the code as shown below.
In onCreateOptionsMenu()
the menu is inflated and SearchView
is displayed.
searchView.setOnQueryTextListener()
listens to character changes while user typing in search filed. The entered query then passed to adapter class using adapter.getFilter().filter(query)
, then the RecyclerView
is refreshed with filtered data.
onContactSelected()
will be called when a contact is selected from the list.
public class MainActivity extends AppCompatActivity implements MoviesAdapter.MoviesAdapterListener { private static final String TAG = MainActivity.class.getSimpleName(); private RecyclerView rvItems; private List<String> movies; private MoviesAdapter adapter; private SearchView searchView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setTitle("Movies"); rvItems = findViewById(R.id.rvItems); movies = new ArrayList<>(); movies.add("The Shawshank Redemption"); movies.add("The Godfather"); movies.add("The Godfather: Part II"); movies.add("The Dark Knight"); adapter = new MoviesAdapter(this, movies, this); RecyclerView.LayoutManager lm = new LinearLayoutManager(getApplicationContext()); rvItems.setLayoutManager(lm); rvItems.setItemAnimator(new DefaultItemAnimator()); rvItems.setAdapter(adapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setMaxWidth(Integer.MAX_VALUE); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { adapter.getFilter().filter(query); return false; } @Override public boolean onQueryTextChange(String query) { adapter.getFilter().filter(query); return false; } }); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_search) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { // close search view on back button pressed if (!searchView.isIconified()) { searchView.setIconified(true); return; } super.onBackPressed(); } @Override public void onSelected(String item) { Toast.makeText(this, "Selected: " + item, Toast.LENGTH_LONG).show(); } }
Let's add some styles to values/styles.xml file
<resources> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <style name="AppTheme.Toolbar" parent="Widget.AppCompat.Toolbar"> <item name="android:background">@android:color/holo_green_dark</item> <item name="android:gravity">center_vertical</item> <item name="android:minHeight">?attr/actionBarSize</item> <item name="android:textColorPrimary">@android:color/white</item> <item name="android:textColorSecondary">@android:color/white</item> <item name="android:editTextColor">@android:color/holo_red_dark</item> <item name="searchViewStyle">@style/AppTheme.Toolbar.SearchView</item> </style> <style name="AppTheme.Toolbar.SearchView" parent="Widget.AppCompat.SearchView"> <item name="queryBackground">@android:color/holo_green_light</item> <item name="searchIcon">@android:drawable/ic_menu_search</item> </style> </resources>
You can read more about attributes for SearchView
here.
Result
Adding SearchView to layout as standalone View
There are many forms for searching in Android such as voice search, suggestions etc. In this part we’ll use SearchView.OnQueryTextListener
and Filterable
interfaces.
The Filterable
interface filters the queried text over a RecyclerView
and displays the resulted RecyclerView
rows.
OnQueryTextListener
interface can detect two events.
onQueryTextChange
is called when the user types each character in the text fieldonQueryTextSubmit
is triggered when the search is pressedOpen the layout files of main activity activity_main.xml and add RecyclerView
element.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.SearchView android:id="@+id/svMovies" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true"/> <android.support.v7.widget.RecyclerView android:id="@+id/rvItems" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> </LinearLayout>
The MainActivity.java
is given below.
public class MainActivity extends AppCompatActivity implements MoviesAdapter.MoviesAdapterListener { private static final String TAG = MainActivity.class.getSimpleName(); private RecyclerView rvItems; private List<String> movies; private MoviesAdapter adapter; private SearchView searchView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); SearchView svMovies = findViewById(R.id.svMovies); rvItems = findViewById(R.id.rvItems); movies = new ArrayList<>(); movies.add("The Shawshank Redemption"); movies.add("The Godfather"); movies.add("The Godfather: Part II"); movies.add("The Dark Knight"); adapter = new MoviesAdapter(this, movies, this); RecyclerView.LayoutManager lm = new LinearLayoutManager(getApplicationContext()); rvItems.setLayoutManager(lm); rvItems.setItemAnimator(new DefaultItemAnimator()); rvItems.setAdapter(adapter); svMovies.setActivated(true); svMovies.setQueryHint("Type your keyword here"); svMovies.onActionViewExpanded(); svMovies.setIconified(false); svMovies.clearFocus(); svMovies.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { adapter.getFilter().filter(newText); return false; } }); } @Override public void onBackPressed() { // close search view on back button pressed if (!searchView.isIconified()) { searchView.setIconified(true); return; } super.onBackPressed(); } @Override public void onSelected(String item) { Toast.makeText(this, "Selected: " + item, Toast.LENGTH_LONG).show(); } }
Result
Tips
You can set focus on SearchView
using following snippet.
searchView.setFocusable(true); searchView.setIconified(false); searchView.requestFocusFromTouch();
Useful libraries