How to filter RecyclerView via SearchView in Toolbar

How to filter RecyclerView via SearchView in Toolbar

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

android_recycleview_filter.png

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 field
  • onQueryTextSubmit is triggered when the search is pressed

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.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

android_recycleview_filter_view.png

Tips

You can set focus on SearchView using following snippet.

searchView.setFocusable(true);
searchView.setIconified(false);
searchView.requestFocusFromTouch();

Useful libraries