
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