Saturday, November 28, 2020

Android RecyclerView adding Search Filter

 Today, in this article we are going to learn how to add search filter functionality to RecyclerView. Adding search is very simple task, we’ll use Toolbar’s search widget to input the search query. To demonstrate I am taking an example of contacts list and search for a contact by name or phone number.

Android RecyclerView adding Search Filter

RecyclerView Search Filter – getFilter()

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 contact by name or phone number from a list of contacts.

@Override

    public Filter getFilter() {

        return new Filter() {

            @Override

            protected FilterResults performFiltering(CharSequence charSequence) {

                String charString = charSequence.toString();

                if (charString.isEmpty()) {

                    contactListFiltered = contactList;

                } else {

                    List<Contact> filteredList = new ArrayList<>();

                    for (Contact row : contactList) {

 

                        // name match condition. this might differ depending on your requirement

                        // here we are looking for name or phone number match

                        if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {

                            filteredList.add(row);

                        }

                    }

 

                    contactListFiltered = filteredList;

                }

 

                FilterResults filterResults = new FilterResults();

                filterResults.values = contactListFiltered;

                return filterResults;

            }

 

            @Override

            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {

                contactListFiltered = (ArrayList<Contact>) filterResults.values;

 

                // refresh the list with filtered data

                notifyDataSetChanged();

            }

        };

    }

Example JSON

For this example I am going to use the json from below url. This json contains list of contacts and each contact will have name, phone number and profile image.

https://api.androidhive.info/json/contacts.json

[{

        "name": "Tom Hardy",

        "image": "https://api.androidhive.info/json/images/tom_hardy.jpg",

        "phone": "(541) 754-3010"

    },

    {

        "name": "Johnny Depp",

        "image": "https://api.androidhive.info/json/images/johnny.jpg",

        "phone": "(452) 839-1210"

    }

]

reating New Project

Now we’ll start with a new project in Android Studio and see how to get the desired search output.

1. Create a new project in Android Studio from File ⇒ New Project and select Basic Activity from templates.

2. Open build.gradle under app folder and add RecyclerView, Glide and Volley dependencies.

build.gradle

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // ...

 

    // recycler view

    implementation 'com.android.support:recyclerview-v7:26.1.0'

 

    // glide image library

    implementation 'com.github.bumptech.glide:glide:4.3.1'

 

    // volley http library

    implementation 'com.android.volley:volley:1.0.0'

    implementation 'com.google.code.gson:gson:2.6.2'

 

}

Add the below resources to respective strings.xml, dimens.xml, colors.xml files.

strings.xml

<resources>

    <string name="app_name">RecyclerView Search</string>

    <string name="action_settings">Settings</string>

    <string name="toolbar_title">Contacts</string>

    <string name="action_search">Search</string>

    <string name="search_hint">Type name…</string>

</resources>

dimens.xml

<resources>

    <dimen name="fab_margin">16dp</dimen>

    <dimen name="activity_margin">16dp</dimen>

    <dimen name="thumbnail">40dp</dimen>

    <dimen name="row_padding">10dp</dimen>

    <dimen name="contact_name">15dp</dimen>

    <dimen name="contact_number">12dp</dimen>

</resources>

colors.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <color name="colorPrimary">#111</color>

    <color name="colorPrimaryDark">#FFF</color>

    <color name="colorAccent">#ea3732</color>

    <color name="contact_name">#333333</color>

    <color name="contact_number">#8c8c8c</color>

</resources>

Create a class named MyApplication.java and extend the class from Application. This is a singleton class in which volley is initiated.

public class MyApplication extends Application {

    public static final String TAG = MyApplication.class

            .getSimpleName();

 

    private RequestQueue mRequestQueue;

 

    private static MyApplication mInstance;

 

    @Override

    public void onCreate() {

        super.onCreate();

        mInstance = this;

    }

 

    public static synchronized MyApplication getInstance() {

        return mInstance;

    }

 

    public RequestQueue getRequestQueue() {

        if (mRequestQueue == null) {

            mRequestQueue = Volley.newRequestQueue(getApplicationContext());

        }

 

        return mRequestQueue;

    }

 

    public <T> void addToRequestQueue(Request<T> req, String tag) {

        // set the default tag if tag is empty

        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);

        getRequestQueue().add(req);

    }

 

    public <T> void addToRequestQueue(Request<T> req) {

        req.setTag(TAG);

        getRequestQueue().add(req);

    }

 

    public void cancelPendingRequests(Object tag) {

        if (mRequestQueue != null) {

            mRequestQueue.cancelAll(tag);

        }

    }

}

Open AndroidManifest.xml and add MyApplication to <application> tag. Also add the INTERNET permission as we are going to make http calls.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="info.androidhive.recyclerviewsearch">

 

    <uses-permission android:name="android.permission.INTERNET" />

 

    <application

        android:name=".MyApplication"

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:roundIcon="@mipmap/ic_launcher_round"

        android:supportsRtl="true"

        android:theme="@style/AppTheme">

        <activity

            android:name=".MainActivity"

            android:label="@string/app_name"

            android:theme="@style/AppTheme.NoActionBar">

 

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

 

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>

 

</manifest>

As we need to parse the json, we need a POJO class to serialize the json. Create a class named Contact.java and add name, image and phone number.

public class Contact {

    String name;

    String image;

    String phone;

 

    public Contact() {

    }

 

    public String getName() {

        return name;

    }

 

    public String getImage() {

        return image;

    }

 

    public String getPhone() {

        return phone;

    }

}

Create a class named MyDividerItemDecoration.java. This step is completely optional but to add some margin to RecyclerView divider. This is a custom divider class to add left margin to divider line.

public class MyDividerItemDecoration  extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{

            android.R.attr.listDivider

    };

 

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

 

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

 

    private Drawable mDivider;

    private int mOrientation;

    private Context context;

    private int margin;

 

    public MyDividerItemDecoration(Context context, int orientation, int margin) {

        this.context = context;

        this.margin = margin;

        final TypedArray a = context.obtainStyledAttributes(ATTRS);

        mDivider = a.getDrawable(0);

        a.recycle();

        setOrientation(orientation);

    }

 

    public void setOrientation(int orientation) {

        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {

            throw new IllegalArgumentException("invalid orientation");

        }

        mOrientation = orientation;

    }

 

    @Override

    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {

        if (mOrientation == VERTICAL_LIST) {

            drawVertical(c, parent);

        } else {

            drawHorizontal(c, parent);

        }

    }

 

    public void drawVertical(Canvas c, RecyclerView parent) {

        final int left = parent.getPaddingLeft();

        final int right = parent.getWidth() - parent.getPaddingRight();

 

        final int childCount = parent.getChildCount();

        for (int i = 0; i < childCount; i++) {

            final View child = parent.getChildAt(i);

            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

                    .getLayoutParams();

            final int top = child.getBottom() + params.bottomMargin;

            final int bottom = top + mDivider.getIntrinsicHeight();

            mDivider.setBounds(left + dpToPx(margin), top, right, bottom);

            mDivider.draw(c);

        }

    }

 

    public void drawHorizontal(Canvas c, RecyclerView parent) {

        final int top = parent.getPaddingTop();

        final int bottom = parent.getHeight() - parent.getPaddingBottom();

 

        final int childCount = parent.getChildCount();

        for (int i = 0; i < childCount; i++) {

            final View child = parent.getChildAt(i);

            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

                    .getLayoutParams();

            final int left = child.getRight() + params.rightMargin;

            final int right = left + mDivider.getIntrinsicHeight();

            mDivider.setBounds(left, top + dpToPx(margin), right, bottom - dpToPx(margin));

            mDivider.draw(c);

        }

    }

 

    @Override

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

        if (mOrientation == VERTICAL_LIST) {

            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

        } else {

            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

        }

    }

 

    private int dpToPx(int dp) {

        Resources r = context.getResources();

        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));

    }

}

Writing the Adapter class with Filter

Now as the resources ready, let’s start writing the adapter class. You need to particularly focus on this class as it is main component in this article.

9. Create a layout named user_row_item.xml and add the below layout. This layout renders the single contact item in the list. This layout contains two TextViews to render name, phone number and an ImageView to display the profile image.

user_row_item.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:background="?attr/selectableItemBackground"

    android:clickable="true"

    android:paddingBottom="@dimen/row_padding"

    android:paddingLeft="@dimen/activity_margin"

    android:paddingRight="@dimen/activity_margin"

    android:paddingTop="@dimen/row_padding">

 

    <ImageView

        android:id="@+id/thumbnail"

        android:layout_width="@dimen/thumbnail"

        android:layout_height="@dimen/thumbnail"

        android:layout_centerVertical="true"

        android:layout_marginRight="@dimen/row_padding" />

 

    <TextView

        android:id="@+id/name"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_toRightOf="@id/thumbnail"

        android:fontFamily="sans-serif-medium"

        android:textColor="@color/contact_name"

        android:textSize="@dimen/contact_name" />

 

    <TextView

        android:id="@+id/phone"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_below="@id/name"

        android:layout_toRightOf="@id/thumbnail"

        android:textColor="@color/contact_number"

        android:textSize="@dimen/contact_number" />

 

</RelativeLayout>

10. Create class named ContactsAdapter.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 contact by name or mobile number is performed using query string.

You will have to adjust the search condition depending on your app requirement.

ContactsAdapterListener interface provides onContactSelected() callback method whenever a contact is selected from the list.

public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.MyViewHolder>

        implements Filterable {

    private Context context;

    private List<Contact> contactList;

    private List<Contact> contactListFiltered;

    private ContactsAdapterListener listener;

 

    public class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView name, phone;

        public ImageView thumbnail;

 

        public MyViewHolder(View view) {

            super(view);

            name = view.findViewById(R.id.name);

            phone = view.findViewById(R.id.phone);

            thumbnail = view.findViewById(R.id.thumbnail);

 

            view.setOnClickListener(new View.OnClickListener() {

                @Override

                public void onClick(View view) {

                    // send selected contact in callback

                    listener.onContactSelected(contactListFiltered.get(getAdapterPosition()));

                }

            });

        }

    }

 

 

    public ContactsAdapter(Context context, List<Contact> contactList, ContactsAdapterListener listener) {

        this.context = context;

        this.listener = listener;

        this.contactList = contactList;

        this.contactListFiltered = contactList;

    }

 

    @Override

    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View itemView = LayoutInflater.from(parent.getContext())

                .inflate(R.layout.user_row_item, parent, false);

 

        return new MyViewHolder(itemView);

    }

 

    @Override

    public void onBindViewHolder(MyViewHolder holder, final int position) {

        final Contact contact = contactListFiltered.get(position);

        holder.name.setText(contact.getName());

        holder.phone.setText(contact.getPhone());

 

        Glide.with(context)

                .load(contact.getImage())

                .apply(RequestOptions.circleCropTransform())

                .into(holder.thumbnail);

    }

 

    @Override

    public int getItemCount() {

        return contactListFiltered.size();

    }

 

    @Override

    public Filter getFilter() {

        return new Filter() {

            @Override

            protected FilterResults performFiltering(CharSequence charSequence) {

                String charString = charSequence.toString();

                if (charString.isEmpty()) {

                    contactListFiltered = contactList;

                } else {

                    List<Contact> filteredList = new ArrayList<>();

                    for (Contact row : contactList) {

 

                        // name match condition. this might differ depending on your requirement

                        // here we are looking for name or phone number match

                        if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {

                            filteredList.add(row);

                        }

                    }

 

                    contactListFiltered = filteredList;

                }

 

                FilterResults filterResults = new FilterResults();

                filterResults.values = contactListFiltered;

                return filterResults;

            }

 

            @Override

            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {

                contactListFiltered = (ArrayList<Contact>) filterResults.values;

                notifyDataSetChanged();

            }

        };

    }

 

    public interface ContactsAdapterListener {

        void onContactSelected(Contact contact);

    }

}

Adding Search Widget and Filtering List

Now everything is ready. All we have to do is, enable SearchView in Toolbar, render the RecyclerView by parsing the json and pass the search query to adapter.

11. Open / create menu_main.xml located under res ⇒ menus and add the SearchView widget and make it always visible.

menu_main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    tools:context="info.androidhive.recyclerviewsearch.MainActivity">

    <item

        android:id="@+id/action_search"

        android:icon="@drawable/ic_search_black_24dp"

        android:orderInCategory="100"

        android:title="@string/action_search"

        app:showAsAction="always"

        app:actionViewClass="android.support.v7.widget.SearchView" />

</menu>

Under res ⇒ xml folder, create an xml file named searchable.xml (If xml folder doesn’t exists, create a new one)

<?xml version="1.0" encoding="utf-8"?>

<searchable xmlns:android="http://schemas.android.com/apk/res/android"

    android:hint="@string/search_hint"

    android:label="@string/app_name" />

Open AndroidManifest.xml and configure the search as shown below.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="info.androidhive.recyclerviewsearch">

 

    <uses-permission android:name="android.permission.INTERNET" />

 

    <application ...>

        <activity

            android:name=".MainActivity"

            android:label="@string/app_name"

            android:theme="@style/AppTheme.NoActionBar">

 

            <meta-data

                android:name="android.app.searchable"

                android:resource="@xml/searchable" />

 

            <intent-filter>

                <action android:name="android.intent.action.SEARCH" />

            </intent-filter>

 

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

 

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>

 

</manifest>

Open the layout files of main activity activity_main.xml and content_main.xml and add RecyclerView element.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context="info.androidhive.recyclerviewsearch.MainActivity">

 

    <android.support.design.widget.AppBarLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:theme="@style/AppTheme.AppBarOverlay">

 

        <android.support.v7.widget.Toolbar

            android:id="@+id/toolbar"

            android:layout_width="match_parent"

            android:layout_height="?attr/actionBarSize"

            android:background="@android:color/white"

            app:popupTheme="@style/AppTheme.PopupOverlay" />

 

    </android.support.design.widget.AppBarLayout>

 

    <include layout="@layout/content_main" />

 

</android.support.design.widget.CoordinatorLayout>

content_main.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    app:layout_behavior="@string/appbar_scrolling_view_behavior"

    tools:context="info.androidhive.recyclerviewsearch.MainActivity"

    tools:showIn="@layout/activity_main">

 

    <android.support.v7.widget.RecyclerView

        android:id="@+id/recycler_view"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:scrollbars="vertical" />

 

</RelativeLayout>

 Finally open the MainActivity.java and add the code as shown below.

 public class MainActivity extends AppCompatActivity implements ContactsAdapter.ContactsAdapterListener {

    private static final String TAG = MainActivity.class.getSimpleName();

    private RecyclerView recyclerView;

    private List<Contact> contactList;

    private ContactsAdapter mAdapter;

    private SearchView searchView;

 

    // url to fetch contacts json

    private static final String URL = "https://api.androidhive.info/json/contacts.json";

 

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        Toolbar toolbar = findViewById(R.id.toolbar);

        setSupportActionBar(toolbar);

 

        // toolbar fancy stuff

        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        getSupportActionBar().setTitle(R.string.toolbar_title);

 

        recyclerView = findViewById(R.id.recycler_view);

        contactList = new ArrayList<>();

        mAdapter = new ContactsAdapter(this, contactList, this);

 

        // white background notification bar

        whiteNotificationBar(recyclerView);

 

        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());

        recyclerView.setLayoutManager(mLayoutManager);

        recyclerView.setItemAnimator(new DefaultItemAnimator());

        recyclerView.addItemDecoration(new MyDividerItemDecoration(this, DividerItemDecoration.VERTICAL, 36));

        recyclerView.setAdapter(mAdapter);

 

        fetchContacts();

    }

 

    /**

     * fetches json by making http calls

     */

    private void fetchContacts() {

        JsonArrayRequest request = new JsonArrayRequest(URL,

                new Response.Listener<JSONArray>() {

                    @Override

                    public void onResponse(JSONArray response) {

                        if (response == null) {

                            Toast.makeText(getApplicationContext(), "Couldn't fetch the contacts! Pleas try again.", Toast.LENGTH_LONG).show();

                            return;

                        }

 

                        List<Contact> items = new Gson().fromJson(response.toString(), new TypeToken<List<Contact>>() {

                        }.getType());

 

                        // adding contacts to contacts list

                        contactList.clear();

                        contactList.addAll(items);

 

                        // refreshing recycler view

                        mAdapter.notifyDataSetChanged();

                    }

                }, new Response.ErrorListener() {

            @Override

            public void onErrorResponse(VolleyError error) {

                // error in getting json

                Log.e(TAG, "Error: " + error.getMessage());

                Toast.makeText(getApplicationContext(), "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();

            }

        });

 

        MyApplication.getInstance().addToRequestQueue(request);

    }

 

    @Override

    public boolean onCreateOptionsMenu(Menu menu) {

        getMenuInflater().inflate(R.menu.menu_main, menu);

 

        // Associate searchable configuration with the SearchView

        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);

 

        // listening to search query text change

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {

            @Override

            public boolean onQueryTextSubmit(String query) {

                // filter recycler view when query submitted

                mAdapter.getFilter().filter(query);

                return false;

            }

 

            @Override

            public boolean onQueryTextChange(String query) {

                // filter recycler view when text is changed

                mAdapter.getFilter().filter(query);

                return false;

            }

        });

        return true;

    }

 

    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();

 

        //noinspection SimplifiableIfStatement

        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();

    }

 

    private void whiteNotificationBar(View view) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

            int flags = view.getSystemUiVisibility();

            flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;

            view.setSystemUiVisibility(flags);

            getWindow().setStatusBarColor(Color.WHITE);

        }

    }

 

    @Override

    public void onContactSelected(Contact contact) {

        Toast.makeText(getApplicationContext(), "Selected: " + contact.getName() + ", " + contact.getPhone(), Toast.LENGTH_LONG).show();

    }

}

No comments:

Post a Comment