Skip to content

5.x | Search Filter

Davide Steduto edited this page Mar 4, 2018 · 28 revisions

In this page


Introduction

To collect items based on the filter, the item must implement the interface IFilterable<FilterClass> or it will be simply skipped. The filter will be propagated to the custom implementation of the filter(FilterClass) method.

Layout Linear

  • Filter object must be of type Serializable in order to maintain save/restore instance state on configuration changes.

  • Items that implement IExpandable interface with a non-empty sub list of children items, will be automatically scanned by the Adapter and sub items picked up if filter has a match.

  • If you don't want to implement the IFilterable interface on the items, then, you can override the method filterObject(item, constraint) to have another filter logic!

⚠️ Warning: The internal list will be copied. Only the references of the items are copied, while the instances remain unique. Due to internal mechanism, items are removed and/or added in order to animate items in the final list.

Configuration

Filter methods
  • hasFilter(): Checks if the current filter is null, in case of String it also checks emptiness "".
  • hasNewFilter(): Checks if the filter has changed with a new one.
  • getFilter(MyFilter.class): The current filter, ex. getFilter(String.class) will return a String.
  • setFilter(myFilter): Sets the new filter, can be of any type but must implement Serializable.
filterItems with current list or with new list?
  • filterItems(): Allows to filter items with the current loaded list.
  • filterItems(unfilteredItemsy): Allows to filter items of the provided list.
filterItems with or without delay?
  • filterItems(unfilteredItems) or filterItems(): The method filters immediately the list with the filter previously set with setFilter(). This gives a prompt responsiveness but more work and battery will be consumed.
  • filterItems(unfilteredItems, delay) or filterItems(delay): The execution of the filter will be delayed of few milliseconds (values of 200-400ms are acceptable), useful to grab more characters from user before starting the filter.

In general items are always animated according to the limit below here.

setAnimateToLimit

Tunes the limit after the which the synchronization animations, occurred during updateDataSet and filter operations, are skipped and notifyDataSetChanged() will be called instead. Default value is 1000 items, number of new items.

setNotifyChangeOfUnfilteredItems

Sometimes it is necessary, while filtering or after the data set has been updated, to rebound the items that remain unfiltered.
If the items have highlighted text, those items must be refreshed in order to change the text back to normal. This happens systematically when filter is reduced in length by the user.
The notification (notifyItemChanged()) is triggered when items are not added nor deleted. Default value is true.

setNotifyMoveOfFilteredItems

This method performs a further step to nicely animate the moved items. The process is very slow on big list of the order of 3000-5000 items and higher, due to the calculation of the correct position for each item to be shifted. Use with caution!
The slowness is more visible when the filter is cleared out after filtering or update data set. Default value is false.

highlightText

From class Utils, sets a spannable text with the accent color (if available) into the provided TextView. Accent color is automatically fetched.

Multi filter

From 5.0.0 the filter can be of any type, extending the possibility to apply a multi filter simultaneously on more fields. IFilterable signature has now a parameter type, so the filter() method can accept the specified custom type.

If filter object is of type String, automatic trim and lowercase is maintained when setting it. Filter object must be of type Serializable in order to maintain save/restore instance state on configuration changes. String it is.

⚠️ Warning: Always specify parameter class type for FlexibleAdapter<common-item-type> to allow to recognize also the filter type! Do it in the activity/fragment as well!

Example with custom type

It's your choice to implement an inclusive (any match) or exclusive (all match) filter, here an exclusive example is shown:

public class ... extends ... implements IFilterable<MyFilter> {
    @Override
    public boolean filter(MyFilter constraint) {
        boolean result = true;
        if (constraint.isElectricCarSet()) {
            result = this.electric == constraint.isElectric());
        }
        if (result && constraint.isPriceSet()) {
            result = this.price <= constraint.getPrice());
        }
        return result;
    }

    @Override **WRONG**
    public void bindViewHolder(FlexibleAdapter adapter, // Wrong! (missing item type)
                               MyViewHolder holder,
                               int position,
                               List payloads) {         // Wrong! (missing list type)
        // Incompatible types: Required 'com.x.y.z.MyFilter' Found 'java.io.Serializable'
        MyFilter filter = adapter.getFilter(MyFilter.class);
    }

    @Override **CORRECT**
    public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, // Must be IFlexible
                               MyViewHolder holder,
                               int position,
                               List<Object> payloads) {            // Must be Object
        // getFilter() can now recognize the type and can return MyFilter type
        MyFilter filter = adapter.getFilter(MyFilter.class);
    }
}

Special behaviors on action

  • The filter is 100% asynchronous, it uses the internal AsyncTask supporting a very high-volume of items.
  • If filter is null, any provided list is the current list.
  • When only sub items are collected, headers/parents are displayed too.
  • Expandable items will collapse/expand only the filtered sub items.
  • You should disable the refresh and the addition of the items.
  • Expected use cases / behaviors in combination with filter and restoring deleted items with Undo feature:
    • Delete items with Undo > Start filter > Commit is triggered > Filter is applied.
    • Start filter > Delete items with Undo > Undo > Items are restored at the filtered positions.
    • Start filter > Delete items with Undo > Change filter again > Commit is triggered > New Filter is applied.
  • Screen rotation is also supported but, the search will be performed again(!), unless you program differently the basic initialization.

onPostFilter()

I added the method onPostFilter() that is invoked just before OnUpdateListener. It must be overridden by extending the FlexibleAdapter class. The method requires to call super() otherwise the emptyView won't get notified!

/**
 * This method is called after the execution of Async Filter and before the call
 * to the OnUpdateListener#onUpdateEmptyView(int).
 */
@CallSuper
protected void onPostFilter() {
    // Call listener to update EmptyView, assuming the filter always made a change
    if (mUpdateListener != null)
        mUpdateListener.onUpdateEmptyView(getMainItemCount());
}

Performance result (when animations are active)

  • A test with 10.000 items with a Samsung S5 running Android 5:
09:53:55.575 27549-27549 D/MainActivity: onQueryTextChange newText: 7
09:53:55.775 27549-29255 D/FilterAsyncTask: doInBackground - started FILTER
09:53:55.775 27549-29255 I/FlexibleAdapter: filterItems with filter="7"
09:53:56.275 27549-29255 V/FlexibleAdapter: Animate changes! oldSize=10000 newSize=3439
09:53:56.535 27549-29255 V/FlexibleAdapter: calculateRemovals total out=6561
09:53:56.535 27549-29255 V/FlexibleAdapter: calculateModifications total mod=3439
09:53:56.555 27549-29255 V/FlexibleAdapter: calculateAdditions total new=0
09:53:56.555 27549-29255 D/FilterAsyncTask: doInBackground - ended FILTER
09:53:56.565 27549-27549 I/FlexibleAdapter: Performing 10000 notifications
09:53:56.915 27549-27549 I/FlexibleAdapter: Animate changes DONE in 1139ms
09:53:56.915 27549-27549 D/MainActivity: onUpdateEmptyView size=3439

As you see it took 1139ms to select 3.440 items when filtering a character in a list of 10.000 items.

  • A more realistic test with 1.000 items:
10:07:28.155 6609-6609 D/MainActivity: onQueryTextChange newText: 7
10:07:28.365 6609-9733 D/FilterAsyncTask: doInBackground - started FILTER
10:07:28.375 6609-9733 I/FlexibleAdapter: filterItems with filter="7"
10:07:28.455 6609-9733 V/FlexibleAdapter: Animate changes! oldSize=1000 newSize=271
10:07:28.485 6609-9733 V/FlexibleAdapter: calculateRemovals total out=729
10:07:28.485 6609-9733 V/FlexibleAdapter: calculateModifications total mod=271
10:07:28.485 6609-9733 V/FlexibleAdapter: calculateAdditions total new=0
10:07:28.485 6609-9733 D/FilterAsyncTask: doInBackground - ended FILTER
10:07:28.485 6609-6609 I/FlexibleAdapter: Performing 1000 notifications
10:07:28.525 6609-6609 I/FlexibleAdapter: Animate changes DONE in 161ms
10:07:28.525 6609-6609 D/MainActivity: onUpdateEmptyView size=271

It took 161ms to select 271 items when filtering a character in a list of 1.000 items.

You can play with the FragmentAsyncFilter to familiarize with the options of the filter.

Under the hood
The queue of notifications is executed in onPostExecute().
Also using the LinkedHashSet instead of List made a big improvement itself. Regarding this aspect, it is strongly recommended to implement hashCode() method coherent with your implementation of equals() method in your IFlexible items.

DiffUtil is slow!

  • A test with DiffUtil engine.
10:16:37.035 6609-6609 D/MainActivity: onQueryTextChange newText: 7
10:16:37.235 6609-9733 D/FilterAsyncTask: doInBackground - started FILTER
10:16:37.235 6609-9733 I/FlexibleAdapter: filterItems with filter="7"
10:16:37.335 6609-9733 V/FlexibleAdapter: Animate changes with DiffUtils! oldSize=1000 newSize=271
10:16:38.365 6609-9733 D/FilterAsyncTask: doInBackground - ended FILTER
10:16:38.365 6609-6609 I/FlexibleAdapter: Dispatching notifications
10:16:38.375 6609-6609 I/FlexibleAdapter: Animate changes DONE in 1134ms
10:16:38.375 6609-6609 D/MainActivity: onUpdateEmptyView size=271

It took 1134ms in its best try. You can use it from rc1 release (just for comparing purpose), but it is already deprecated and it will be removed in the final release.

Setup the SearchView widget (code snipped from the demoApp)

To setup the Android widget SearchView the following files must be properly configured:

manifests/AndroidManifest.xml

...
<activity android:name=".MainActivity">
	<meta-data
		android:name="android.app.searchable"
		android:resource="@xml/searchable"/>
	<intent-filter>
		<action android:name="android.intent.action.SEARCH"/>
	</intent-filter>
</activity>
...

xml/searchable.xml

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
	android:label="@string/app_name"
	android:searchMode="showSearchIconAsBadge"
	android:hint="@string/action_search"/>

menu/menu_filter.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

	<!-- Search, should appear as action button -->
	<item android:id="@+id/action_search"
	      android:title="@string/action_search"
	      android:icon="@drawable/ic_action_search"
	      app:showAsAction="collapseActionView|always"
	      android:animateLayoutChanges="true"
	      app:actionViewClass="android.support.v7.widget.SearchView"/>

</menu>

MainActivity.java

public class MainActivity extends AppCompatActivity
                          implements SearchView.OnQueryTextListener {
...
}
@Override
public boolean onQueryTextChange(String newText) {
	if (mAdapter.hasNewFilter(newText)) {
		Log.d(TAG, "onQueryTextChange newFilter: " + newText);
		mAdapter.setFilter(newText);
		// Fill and filter mItems with your custom list and automatically
		// animate the changes. Watch out! The original list must be a copy.
		mAdapter.filterItems(DatabaseService.getInstance().getListToFilter(), 200L);
	}
	// Disable SwipeRefresh if search is active!!
	mSwipeRefreshLayout.setEnabled(!mAdapter.hasFilter());
	return true;
}

@Override
public boolean onQueryTextSubmit(String query) {
	Log.v(TAG, "onQueryTextSubmit called!");
	return onQueryTextChange(query);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
	super.onCreateOptionsMenu(menu, inflater);
	inflater.inflate(R.menu.menu_filter, menu);
	initSearchView(menu);
}

private void initSearchView(final Menu menu) {
	// Associate searchable configuration with the SearchView
	SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
	MenuItem searchItem = menu.findItem(R.id.action_search);
	if (searchItem != null) {
		MenuItemCompat.setOnActionExpandListener(
			searchItem, new MenuItemCompat.OnActionExpandListener() {
				@Override
				public boolean onMenuItemActionExpand(MenuItem item) {
					MenuItem listTypeItem = menu.findItem(R.id.action_list_type);
					if (listTypeItem != null)
						listTypeItem.setVisible(false);
					hideFab();
					return true;
				}

				@Override
				public boolean onMenuItemActionCollapse(MenuItem item) {
					MenuItem listTypeItem = menu.findItem(R.id.action_list_type);
					if (listTypeItem != null)
						listTypeItem.setVisible(true);
					showFab();
					return true;
				}
			});
		mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);
		mSearchView.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
		mSearchView.setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN);
		mSearchView.setQueryHint(getString(R.string.action_search));
		mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
		mSearchView.setOnQueryTextListener(this);
	}
}

3rd libraries and floating SearchView

I've found several interesting libraries that implement the floating SearchView (below listed with my personal evaluation), they use a custom adapter and a layout to display the result with suggestions too. Still didn't try them out with FlexibleAdapter, but in theory no conflict with this library.

arimorty/floatingsearchview (9/10)
A search view that implements a floating search bar also known as persistent search.

renaudcerrato/FloatingSearchView (7/10)
Yet another floating search view implementation, also known as persistent search.

lapism/SearchView (7/10)
Persistent SearchView Library in Material Design.

crysehillmes/PersistentSearchView (6/10)
A library that implements Google Play like PersistentSearch view.

edsilfer/custom-searchable (6/10)
This repository contains a library that aims to provide a custom searchable interface for android applications.

sahildave/Search-View-Layout (5/10)
Search View Layout like Lollipop Dialer.

Clone this wiki locally