-
Notifications
You must be signed in to change notification settings - Fork 554
5.x | Search Filter
- Introduction
- Configuration
- Multi filter
- Special behaviors on action
- Performance result
- Setup the SearchView widget
- 3rd libraries and floating SearchView
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.
-
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 methodfilterObject(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.
-
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 aString
. -
setFilter(myFilter)
: Sets the new filter, can be of any type but must implementSerializable
.
-
filterItems()
: Allows to filter items with the current loaded list. -
filterItems(unfilteredItemsy)
: Allows to filter items of the provided list.
-
filterItems(unfilteredItems)
orfilterItems()
: The method filters immediately the list with the filter previously set withsetFilter()
. This gives a prompt responsiveness but more work and battery will be consumed. -
filterItems(unfilteredItems, delay)
orfilterItems(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.
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.
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
.
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
.
From class Utils
, sets a spannable text with the accent color (if available) into the provided TextView. Accent color is automatically fetched.
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.
FlexibleAdapter<common-item-type>
to allow to recognize also the filter type! Do it in the activity/fragment as well!
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);
}
}
- 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.
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());
}
- 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 inonPostExecute()
.
Also using theLinkedHashSet
instead ofList
made a big improvement itself. Regarding this aspect, it is strongly recommended to implementhashCode()
method coherent with your implementation ofequals()
method in yourIFlexible
items.
- 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.
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);
}
}
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.
- Update Data Set
- Selection modes
- Headers and Sections
- Scrollable Headers and Footers
- Expandable items
- Drag&Drop and Swipe
- EndlessScroll / On Load More
- Search Filter
- FastScroller
- Adapter Animations
- Third party Layout Managers
- Payload
- Smooth Layout Managers
- Flexible Item Decoration
- Utils
- ActionModeHelper
- AnimatorHelper
- EmptyViewHelper
- UndoHelper
* = Under revision!