-
Notifications
You must be signed in to change notification settings - Fork 552
5.x | Search Filter
- Introduction
- Configuration
- Special behaviors on action
- Performance result
- Setup the SearchView widget
- 3rd libraries and floating SearchView
To collect items based on the searchText, the item must implement the interface IFilterable
or it will be simply skipped. The searchText will be propagated to the custom implementation of the filter()
method.
Important note: ALWAYS PROVIDE A COPY OF THE ORIGINAL LIST TO
filterItems()
METHOD: due to internal mechanism, items are removed and/or added in order to animate items in the final list.
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 searchText 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!
-
hasSearchText()
: Checks if the current searchText is not empty nor null. -
hasNewSearchText()
: Checks if the searchText is changed with a new text. -
getSearchText()
: The current search text. -
setSearchText(new_text)
: Sets the new search text.
-
filterItems(unfilteredItems)
: The method filters immediately the provided list with the search text previously set withsetSearchText()
. This gives a prompt responsiveness but more work and battery will be consumed. -
filterItems(unfilteredItems, 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 700
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 searchText is reduced in length by the user.
The notification (notifyItemChanged()
) is triggered when items are not added nor deleted. Default value is false
.
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 searchText 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.
- The filter is 100% asynchronous, it uses the internal
AsyncTask
supporting a very high-volume of items. - Sticky Header is disabled during the filter operation and restored when searchText returns empty.
- If searchText is empty or null, the 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.
- Restoring deleted items with Undo feature still works:
- Deleting items before the filter starts, restoring them while the filter is active, it makes the restored items to be displayed if part of the current filtered collection and at the correct position.
- Starting the filter and removing items, then restoring them with no filter active, it makes the restored items to be displayed at the correct position too.
- Screen rotation is also supported but, the search will be performed again(!), unless you program differently the basic initialization.
- 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 searchText="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 searchText="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.
I added the method onPostFilter()
that is invoked just before OnUpdateListener
. It must be overridden by extending the FlexibleAdapter
class.
/**
* This method is called after the execution of Async Filter and before the call
* to the OnUpdateListener#onUpdateEmptyView(int).
*/
protected void onPostFilter() {
// Dedicated for user implementation
}
- 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 searchText="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.hasNewSearchText(newText)) {
Log.d(TAG, "onQueryTextChange newText: " + newText);
mAdapter.setSearchText(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().getDatabaseList(), 200L);
}
// Disable SwipeRefresh if search is active!!
mSwipeRefreshLayout.setEnabled(!mAdapter.hasSearchText());
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!