Maintain Drag & Dropped items position when restarting - android

I have a RecyclerView - Grid, with drag & drop, using this code I've manage to achieve that, And made a lot of changes, only one problem, i can't save the dragged items position on restarting ( the app not the phone ).
What i thought about is adding " int position " to my item.java constructor, but what i can't do is getting the changed position .
I'm using the same drag & drop codes provided in the link.
ItemTouchHelper.Callback _ithCallback = new ItemTouchHelper.Callback() {
//and in your imlpementaion of
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// get the viewHolder's and target's positions in your adapter data, swap them
Collections.swap(AllItems, viewHolder.getAdapterPosition(), target.getAdapterPosition());
// and notify the adapter that its dataset has changed
rcAdapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//TODO
}
//defines the enabled move directions in each state (idle, swiping, dragging).
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.START | ItemTouchHelper.END);
}
};
Here's the code in onCreate :
ItemTouchHelper ith = new ItemTouchHelper(_ithCallback);
ith.attachToRecyclerView(RcView);
Getting Duplicated items after position changing, Code :
#Override
public void onStop()
{
super.onStop();
SharedPreferencesTools.setOrderedItems(this, AllItems);
}
getAllItemList :
private List<Item> getAllItemList(){
AllItems = SharedPreferencesTools.getOrderedItems(this);
//Add item .. etc
//return items
}

Just keep your modified collection AllItems in SharedPreferences and load it on the app start & store it back, once you get out of the app.
To do this, you need to serialize your collection to json by Gson and store as a String. And then deserialize it afterwards:
public final class SharedPreferencesTools {
private static final String USER_SETTINGS_PREFERENCES_NAME = "UserSettings";
private static final String ALL_ITEMS_LIST = "AllItemsList";
private static Gson gson = new GsonBuilder().create();
public static List<Item> getOrderedItems(Context context) {
String stringValue = getUserSettings(context).getString(ALL_ITEMS_LIST, "");
Type collectionType = new TypeToken<List<Item>>() {
}.getType();
List<Item> result = gson.fromJson(stringValue, collectionType);
return (result == null) ? new ArrayList<Item>() : result;
}
public static void setOrderedItems(Context context, List<Item> items) {
String stringValue = gson.toJson(items);
SharedPreferences sharedPreferences = getUserSettings(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(ALL_ITEMS_LIST, stringValue);
editor.apply();
}
static SharedPreferences getUserSettings(Context context) {
return context.getSharedPreferences(USER_SETTINGS_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
}
The usage of these two methods:
SharedPreferencesTools.setOrderedItems(getActivity(), AllItems);
...
List<Item> AllItems = SharedPreferencesTools.getOrderedItems(getActivity());

Related

Android how to sort RecyclerView List when using AsyncListDiffer?

I have a RecyclerView that shows a list of CardViews. I recently switched the project from using RecyclerView Adapter to using an AsyncListDiffer Adapter to take advantage of adapter updates on a background thread. I have converted over all previous CRUD and filter methods for the list but cannot get the sort method working.
I have different types or categories of CardViews and I would like to sort by the types/categories. I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list, as compared to the existing list that I wanted to sort. And then I use AsynListDiffer's submitList().
The list is not sorting. What am I missing here?
MainActivity:
private static List<Card> mCards = null;
...
mCardViewModel = new ViewModelProvider(this).get(CardViewModel.class);
mCardViewModel.getAllCards().observe(this,(cards -> {
mCards = cards;
cardsAdapter.submitList(mCards);
}));
mRecyclerView.setAdapter(cardsAdapter);
A click on a "Sort" TextView runs the following code:
ArrayList<Card> sortItems = new ArrayList<>();
for (Card card : mCards) {
sortItems.add(card.clone());
}
Collections.sort(sortItems, new Comparator<Card>() {
#Override
public int compare(Card cardFirst, Card cardSecond) {
return cardFirst.getType().compareTo(cardSecond.getType());
}
});
cardsAdapter.submitList(sortItems);
// mRecyclerView.setAdapter(cardsAdapter); // Adding this did not help
AsyncListDifferAdapter:
public AsyncListDifferAdapter(Context context) {
this.mListItems = new AsyncListDiffer<>(this, DIFF_CALLBACK);
this.mContext = context;
this.mInflater = LayoutInflater.from(mContext);
}
public void submitList(List<Quickcard> list) {
if (list != null) {
mListItems.submitList(list);
}
}
public static final DiffUtil.ItemCallback<Card> DIFF_CALLBACK
= new DiffUtil.ItemCallback<Card>() {
#Override
public boolean areItemsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
return oldItem.equals(newItem);
}
#Nullable
#Override
public Object getChangePayload(#NonNull Card oldItem, #NonNull Card newItem) {
return super.getChangePayload(oldItem, newItem);
}
};
Model:
#Entity(tableName = "cards")
public class Card implements Parcelable, Cloneable {
// Parcelable code not shown for brevity
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "cardId")
public int id;
#ColumnInfo(name = "cardType")
private String type;
#Ignore
public Card(int id, String type) {
this.id = id;
this.type = type;
}
public int getId() {
return this.id;
}
public String getType() {
return this.type;
}
#Override
public boolean equals(Object obj) {
if (obj == this)
return true;
else if (obj instanceof Card) {
Card card = (Card) obj;
return id == card.getId() &&
type.equals(card.getType());
} else {
return false;
}
}
#NonNull
#Override
public Card clone() {
Card clone;
try {
clone = (Card) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return clone;
}
Instead of using notifyDataSetChanged() we can use notifyItemMoved(). That solution gives us a nice animation of sorting. I put the sort order within the adapter. We need a displayOrderList that will contain the currently displayed elements because mDiffer.getCurrentList() doesn't change the order of elements after notifyItemMoved(). We first moved the element that is first sorted to the first place, the second sorted element to second place,... So inside the adapter put the following:
public void sortByType()
{
List<Card> sortedList = new ArrayList<>(mDiffer.getCurrentList());
sortedList.sort(Comparator.comparing(Card::getType));
List<Card> displayOrderList = new ArrayList<>(mDiffer.getCurrentList());
for (int i = 0; i < sortedList.size(); ++i)
{
int toPos = sortedList.indexOf(displayOrderList.get(i));
notifyItemMoved(i, toPos);
listMoveTo(displayOrderList, i, toPos);
}
}
private void listMoveTo(List<Card> list, int fromPos, int toPos)
{
Card fromValue = list.get(fromPos);
int delta = fromPos < toPos ? 1 : -1;
for (int i = fromPos; i != toPos; i += delta) {
list.set(i, list.get(i + delta));
}
list.set(toPos, fromValue);
}
and then call from activity cardsAdapter.sortByType();
I think issue is in below method
public void submitList(List<Quickcard> list) {
if (list != null) {
mListItems.submitList(list);
}
}
because here first you need to clear old arraylist "mListItems" using
mListItems.clear();
//then add new data
if (list != null) {
mListItems.addAll(list);
}
//now notify adapter
notifyDataSetChanged();
Or Also you can direct notify adapter after sorting.
First set adapter and pass your main list in adapter's constructor
Collections.sort(sortItems, new Comparator<Card>() {
#Override
public int compare(Card cardFirst, Card cardSecond) {
return cardFirst.getType().compareTo(cardSecond.getType());
}
});
//now direct notify adpter
your_adapter_object.notifyDataSetChanged();
I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list
DiffUtil will detect changes by your implementation of DiffUtil.ItemCallback<Card>, so when you clone a card you just create a new instance of it with the same ID, therefor DiffUtil sees it as the same item because you are checking if the items are the same based on their ID:
#Override
public boolean areItemsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldItem.getId() == newItem.getId();
}
but because the cloned object's reference is different from the original item, DiffUitl will detect the items content change, because of:
#Override
public boolean areContentsTheSame(#NonNull Card oldItem, #NonNull Card newItem) {
return oldItem.equals(newItem);
}
so DiffUtil will call adapter.notifyItemChanged(updateIndex) and will update the viewHolder in that index, but it won't change the order of items in the recyclerView.
There seems to be another problem in your sort code. You say
I would like to sort by the types/categories
but you are sorting your list alphabetically every time you click the textView. This code will give you same result every time you run it and won't change the order of recyclerView assuming the original list is sorted in the first hand. If the original list isn't sorted, clicking the textView will change the order just once.

When Deleting View with Swipe in RecyclerView doesn't deletin in SharedPreference

I am trying delete View with Swipe in RecylerView, I saved in SharedPreferences to selected View with favorite button. After I saved to selected View in SharedPreferences, I am trying remove Favorite Activity with Swipe to left ,I was done this but When I return Favorite Activity, I saw old items doesn't updated SharedPreferences When I swipe to left.
How can I do this?
public class SharedPreference {
public static final String PREFS_NAME = "NKDROID_APP";
public static final String FAVORITES = "Favorite";
public SharedPreference() {
super();
}
public void storeFavorites(Context context, List<OrderModel> favorites) {
SharedPreferences settings;
SharedPreferences.Editor editor;
settings = context.getSharedPreferences(PREFS_NAME,
Context.MODE_PRIVATE);
editor = settings.edit();
Gson gson = new Gson();
String jsonFavorites = gson.toJson(favorites);
editor.putString(FAVORITES, jsonFavorites);
editor.commit();
}
public ArrayList<OrderModel> loadFavorites(Context context) {
SharedPreferences settings;
List<OrderModel> favorites;
settings = context.getSharedPreferences(PREFS_NAME,Context.MODE_PRIVATE);
if (settings.contains(FAVORITES)) {
String jsonFavorites = settings.getString(FAVORITES, null);
Gson gson = new Gson();
OrderModel[] favoriteItems = gson.fromJson(jsonFavorites,OrderModel[].class);
favorites = Arrays.asList(favoriteItems);
favorites = new ArrayList<OrderModel>(favorites);
} else
return null;
return (ArrayList<OrderModel>) favorites;
}
public void addFavorite(Context context, OrderModel beanSampleList) {
List<OrderModel> favorites = loadFavorites(context);
if (favorites == null)
favorites = new ArrayList<OrderModel>();
favorites.add(beanSampleList);
storeFavorites(context, favorites);
}
public void removeFavorite(Context context, OrderModel beanSampleList) {
ArrayList<OrderModel> favorites = loadFavorites(context);
if (favorites != null) {
favorites.remove(beanSampleList);
storeFavorites(context, favorites);
}
}
/*
public void saveHighScoreList(String scoreString) {
editor.putString(FAVORITES, scoreString);
editor.commit();
}
public String getHighScoreList() {
return settings.getString(FAVORITES, "");
}
*/
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position) {
if (viewHolder instanceof OrderAdapter.OrderViewHolder) {
// get the removed item name to display it in snack bar
String name = order_models.get(viewHolder.getAdapterPosition()).getOrder_name();
final OrderModel deletedItem = order_models.get(viewHolder.getAdapterPosition());
final int deletedIndex = viewHolder.getAdapterPosition();
order_adapter.removeItem(viewHolder.getAdapterPosition());
//remove from shared preferences
sharedPreference.removeFavorite(Orders.this, deletedItem);
order_models.remove(deletedItem);
order_adapter.notifyDataSetChanged();
Toast.makeText(Orders.this, "Success Remove",Toast.LENGTH_SHORT).show();
// showing snack bar with Undo option
Snackbar snackbar = Snackbar
.make(constraint, name + " removed from cart!", Snackbar.LENGTH_LONG);
snackbar.setAction("UNDO", new View.OnClickListener() {
#Override
public void onClick(View view) {
// undo is selected, restore the deleted item
order_adapter.restoreItem(deletedItem, deletedIndex);
}
});
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
}
}
For one, you probably shouldn't be loading the list of favorites from preferences every single time you want to query or modify the list. Instead, query it once when the Activity this RecyclerView belongs to is created (you could do that from the Adapter itself or from the Activity), and store that to a global variable. ie:
class SomeActivity extends Activity {
private ArrayList<OrderModel> favorites = new ArrayList<>();
private SharedPreference prefsHelper = new SharedPreference();
#Override
public void onCreate(Bundle savedInstanceState) {
//....
favorites.addAll(prefsHelper.loadFavorites(this));
}
}
Then, when you want to change something, modify that ArrayList and then save it directly:
public void addFavorite(OrderModel model) {
favorites.add(model);
prefsHelper.storeFavorites(this, favorites);
}
You'll probably need to modify this to fit your code, but it's an example of what to do.
What you currently have won't work, because every time you go to modify the list, you're recreating it from a String representation. That means that the list of favorites you have loaded contains completely different instances of the models, even if they contain the same values.
When you pass an OrderModel to your removeFavorite() method, it's not going to remove anything, because nothing is equal; by reloading the list, you have completely fresh instances.
If you really want to keep your current code structure, switch to indexes instead of passing the object. Or, override equals() in OrderModel and have it manually compare the values, so even different instances can be matched.

How to view RecyclerView items sorted by ID?

In my app i have something like a menu that load data stored in SharedPreferences to a Constructor and build from it 2 RecyclerView's.
Now i want to make that if i press an button from the bottom recyclerView like "CICCIO" that has in it constructor item like ID set by 1 i would that in the other recyclerView where there are a lot of colored buttons to be visualized just button's that have also ID set by 1 in their constructor but i'm not very in to android and i can't get how can i make a "sort method" like that.
I have yet the onClick method set on bottom RecyclerView and i have yet method's getID from the bottom recyclerView constructor and getID from the top RecyclerView.
ACTIVITY SCREENSHOT HERE
And here is the code from my activity:
// Builder del BOTTOM recyclerView
public void buildRecyclerViewMenu(){
RecyclerView mRecyclerView = findViewById(R.id.recyclerViewMenu);
mRecyclerView.setHasFixedSize(true);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL, false);
RecyclerViewMenu recyclerViewMenu = new RecyclerViewMenu(menuConstructors);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(recyclerViewMenu);
recyclerViewMenu.setOnItemClickListener(new RecyclerViewMenu.OnItemClickListener() {
#Override
public void onItemClick(int position) {
}
});
}
// Builder del TOP recyclerView
public void buildRecyclerView(){
mRecyclerViewBOT = findViewById(R.id.recyclerView);
mRecyclerViewBOT.setHasFixedSize(true);
RecyclerView.LayoutManager mLayoutManager = new GridLayoutManager(this, 4);
Adapter mAdapter = new Adapter(items);
mRecyclerViewBOT.setLayoutManager(mLayoutManager);
mRecyclerViewBOT.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new Adapter.OnItemClickListener() {
#SuppressLint("SetTextI18n")
#Override
public void onItemClick(final int position) {
}
});
}
While here are the method's where i load data to Bottom and top RecyclerView
private void loadDataMenu(){
new ArrayList<MenuConstructor>();
SharedPreferences sharedPreferences = getSharedPreferences("MENU_SAVE", MODE_PRIVATE);
Gson gson = new Gson();
String json = sharedPreferences.getString("menu list",null);
Type type = new TypeToken<ArrayList<MenuConstructor>>() {}.getType();
menuConstructors = gson.fromJson(json, type );
if(menuConstructors == null){
menuConstructors = new ArrayList<>();
Toast.makeText(cassa.this,"NESSUN TASTO DA VISUALIZZARE" ,Toast.LENGTH_SHORT).show();
}
}
private void loadDataTasti(){
new ArrayList<Item>();
SharedPreferences sharedPreferences = getSharedPreferences("TASTI_SAVE", MODE_PRIVATE);
Gson gson = new Gson();
String json = sharedPreferences.getString("tasti list",null);
Type type = new TypeToken<ArrayList<Item>>() {}.getType();
items = gson.fromJson(json, type );
if(items == null){
items = new ArrayList<>();
Toast.makeText(cassa.this,"NESSUN TASTO DA VISUALIZZARE" ,Toast.LENGTH_SHORT).show();
}
}
While here is my Item.java that is the model class of TOP recycler View:
public class Item {
private int menu;
private String tipo;
private String codice;
private String deskT;
private String deskS;
private String sfondo;
private String font;
private String qta;
private double pre;
Item(int id, String typo, String codice_t, String desk_T,String desk_S,String sfondo_c,String font_c,String quant,double prezzo){
menu = id;
tipo = typo;
codice = codice_t;
deskT = desk_T;
deskS = desk_S;
sfondo = sfondo_c;
font = font_c;
qta = quant;
pre = prezzo;
}
public int getMenu(){return menu;}
public String getTipo(){return tipo;}
public String getCodice(){return codice;}
public String getDeskT(){return deskT;}
public String getDeskS(){return deskS;}
public String getSfondo(){return sfondo;}
public String getFont(){return font;}
public String getQuant(){return qta;}
public double getPrice(){return pre;}
}
And here is the model of MenuConstructor (bottom recyclerView)
public class MenuConstructor {
int id;
private String deskT;
private String sfondo;
private String font;
MenuConstructor(int idID,String Desk,String Sfondo,String Font){
id = idID;
deskT = Desk;
sfondo = Sfondo;
font = Font;
}
public int getBtnID(){
return id;
}
public String getDesk(){
return deskT;
}
public String getSfondoColor(){
return sfondo;
}
public String getFontColor(){
return font;
}
}
so i want to compare the id from MenuConstructor with menu (id) from the Item.java and show just item with a certain ID.
You can using Collections.sort()
private List<Item> getSortedItems(List<Item> items) {
Collections.sort(items, new Comparator<Item>() {
#Override
public int compare(Item item, Item nextItem) {
return ComparisonChain.start()
.compare(item.getMenu(), nextItem.getMenu())
.result();
}
});
return items;
}
this method will return the item list sort by menu
hope this helps
Solved by creating a new ArrayList and comparing the item.getMenu() with menuConstructors.getBtnID() in a for cycle which scrolls for the same lenght of the ArrayList.
recyclerViewMenu.setOnItemClickListener(new RecyclerViewMenu.OnItemClickListener() {
#Override
public void onItemClick(int position) {
Toast.makeText(cassa.this,String.valueOf(menuConstructors.get(position).getBtnID()),Toast.LENGTH_SHORT).show();
ArrayList<Item> filteredList = new ArrayList<>();
for(Item item : items) {
if(item.getMenu() == menuConstructors.get(position).getBtnID() ) {
filteredList.add(item);
}
}
mAdapter.filterList(filteredList);
}
});

How to refresh recyclerview using MVP Structure in android?

I want simple example for MVP structure in android to refresh recyclerview item not the whole list of recyclerview.
It will refresh only items in recyclerview of android.
This is a problem I've thought about quite a lot. There are two possible ways of doing it:
Pass the new List of data to the Adapter and it does the work of working out what's changed and updating the correct items.
Keep a record of the current items in your model then when the new list is calculated send ListChangeItems to the Adapter.
I'll outline both in more detail below. In both cases you need to calculate the differences between what is currently showing and the new data. I have a helper class ListDiffHelper<T> which does this comparison:
public class ListDiffHelper<T> {
private List<T> oldList;
private List<T> newList;
private List<Integer> inserted = new ArrayList<>();
private List<Integer> removed = new ArrayList<>();
public List<Integer> getInserted() {return inserted;}
public List<Integer> getRemoved() {return removed;}
public ListDiffHelper(List<T> oldList, List<T> newList) {
this.oldList = oldList;
this.newList = newList;
checkForNull();
findInserted();
findRemoved();
}
private void checkForNull() {
if (oldList == null) oldList = Collections.emptyList();
if (newList == null) newList = Collections.emptyList();
}
private void findInserted() {
Set<T> newSet = new HashSet<>(newList);
newSet.removeAll(new HashSet<>(oldList));
for (T item : newSet) {
inserted.add(newList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return lhs - rhs;
}
});
}
private void findRemoved() {
Set<T> oldSet = new HashSet<>(oldList);
oldSet.removeAll(new HashSet<>(newList));
for (T item : oldSet) {
removed.add(oldList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return rhs - lhs;
}
});
}
}
For this to work properly you need to ensure that the equals() method of the Data class compares things in a suitable way.
Adapter Lead
In this case your Presenter calls getData() on the model (or subscribes to it if you're using Rx) and receives List<Data>. It then passes this List to the view through a setData(data) method which in turn give the list to the Adapter. The method in the Adapter would look something like:
private void setData(List<Data> data) {
if (this.data == null || this.data.isEmpty() || data.isEmpty()) {
this.data = data;
adapter.notifyDataSetChanged();
return;
}
ListDiffHelper<Data> diff = new ListDiffHelper<>(this.data, data);
this.data = data;
for (Integer index : diff.getRemoved()) {
notifyItemRemoved(index);
}
for (Integer index : diff.getInserted()) {
notifyItemInserted(index);
}
}
It is important to remove items first before adding new ones otherwise the order will not be maintained correctly.
Model Lead
The alternative approach is to keep the Adapter much dumber and do the calculation of what has changed in your model layer. You then need a wrapper class to send the individual changes to your View/Adapter. Something like:
public class ListChangeItem {
private static final int INSERTED = 0;
private static final int REMOVED = 1;
private int type;
private int position;
private Data data;
public ListChangeItem(int type, int position, Data data) {
this.type = type;
this.position = position;
this.data = data;
}
public int getType() {return type;}
public int getPosition() {return position;}
public Data getData() {return data;}
}
You would then pass a List of these to your Adapter via the view interface. Again it would be important to have the removals actioned before the inserts to ensure the data is in the correct order.

How to filter a RecyclerView with a SearchView

I am trying to implement the SearchView from the support library. I want the user to be to use the SearchView to filter a List of movies in a RecyclerView.
I have followed a few tutorials so far and I have added the SearchView to the ActionBar, but I am not really sure where to go from here. I have seen a few examples but none of them show results as you start typing.
This is my MainActivity:
public class MainActivity extends ActionBarActivity {
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new CardAdapter() {
#Override
public Filter getFilter() {
return null;
}
};
mRecyclerView.setAdapter(mAdapter);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
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_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
And this is my Adapter:
public abstract class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> implements Filterable {
List<Movie> mItems;
public CardAdapter() {
super();
mItems = new ArrayList<Movie>();
Movie movie = new Movie();
movie.setName("Spiderman");
movie.setRating("92");
mItems.add(movie);
movie = new Movie();
movie.setName("Doom 3");
movie.setRating("91");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers");
movie.setRating("88");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 2");
movie.setRating("87");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 3");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Noah");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 2");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 3");
movie.setRating("86");
mItems.add(movie);
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_card_item, viewGroup, false);
return new ViewHolder(v);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
Movie movie = mItems.get(i);
viewHolder.tvMovie.setText(movie.getName());
viewHolder.tvMovieRating.setText(movie.getRating());
}
#Override
public int getItemCount() {
return mItems.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
public TextView tvMovie;
public TextView tvMovieRating;
public ViewHolder(View itemView) {
super(itemView);
tvMovie = (TextView)itemView.findViewById(R.id.movieName);
tvMovieRating = (TextView)itemView.findViewById(R.id.movieRating);
}
}
}
Introduction
Since it is not really clear from your question what exactly you are having trouble with, I wrote up this quick walkthrough about how to implement this feature; if you still have questions feel free to ask.
I have a working example of everything I am talking about here in this GitHub Repository.
In any case the result should looks something like this:
If you first want to play around with the demo app you can install it from the Play Store:
Anyway lets get started.
Setting up the SearchView
In the folder res/menu create a new file called main_menu.xml. In it add an item and set the actionViewClass to android.support.v7.widget.SearchView. Since you are using the support library you have to use the namespace of the support library to set the actionViewClass attribute. Your xml file should look something like this:
<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:title="#string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"/>
</menu>
In your Fragment or Activity you have to inflate this menu xml like usual, then you can look for the MenuItem which contains the SearchView and implement the OnQueryTextListener which we are going to use to listen for changes to the text entered into the SearchView:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
final MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
return true;
}
#Override
public boolean onQueryTextChange(String query) {
// Here is where we are going to implement the filter logic
return false;
}
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
And now the SearchView is ready to be used. We will implement the filter logic later on in onQueryTextChange() once we are finished implementing the Adapter.
Setting up the Adapter
First and foremost this is the model class I am going to use for this example:
public class ExampleModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
}
It's just your basic model which will display a text in the RecyclerView. This is the layout I am going to use to display the text:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.github.wrdlbrnft.searchablerecyclerviewdemo.ui.models.ExampleModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="#{model.text}"/>
</FrameLayout>
</layout>
As you can see I use Data Binding. If you have never worked with data binding before don't be discouraged! It's very simple and powerful, however I can't explain how it works in the scope of this answer.
This is the ViewHolder for the ExampleModel class:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public void bind(ExampleModel item) {
mBinding.setModel(item);
}
}
Again nothing special. It just uses data binding to bind the model class to this layout as we have defined in the layout xml above.
Now we can finally come to the really interesting part: Writing the Adapter. I am going to skip over the basic implementation of the Adapter and am instead going to concentrate on the parts which are relevant for this answer.
But first there is one thing we have to talk about: The SortedList class.
SortedList
The SortedList is a completely amazing tool which is part of the RecyclerView library. It takes care of notifying the Adapter about changes to the data set and does so it a very efficient way. The only thing it requires you to do is specify an order of the elements. You need to do that by implementing a compare() method which compares two elements in the SortedList just like a Comparator. But instead of sorting a List it is used to sort the items in the RecyclerView!
The SortedList interacts with the Adapter through a Callback class which you have to implement:
private final SortedList.Callback<ExampleModel> mCallback = new SortedList.Callback<ExampleModel>() {
#Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
#Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
#Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
#Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
#Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
#Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
#Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
}
In the methods at the top of the callback like onMoved, onInserted, etc. you have to call the equivalent notify method of your Adapter. The three methods at the bottom compare, areContentsTheSame and areItemsTheSame you have to implement according to what kind of objects you want to display and in what order these objects should appear on the screen.
Let's go through these methods one by one:
#Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
This is the compare() method I talked about earlier. In this example I am just passing the call to a Comparator which compares the two models. If you want the items to appear in alphabetical order on the screen. This comparator might look like this:
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
#Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
Now let's take a look at the next method:
#Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
The purpose of this method is to determine if the content of a model has changed. The SortedList uses this to determine if a change event needs to be invoked - in other words if the RecyclerView should crossfade the old and new version. If you model classes have a correct equals() and hashCode() implementation you can usually just implement it like above. If we add an equals() and hashCode() implementation to the ExampleModel class it should look something like this:
public class ExampleModel implements SortedListAdapter.ViewModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExampleModel model = (ExampleModel) o;
if (mId != model.mId) return false;
return mText != null ? mText.equals(model.mText) : model.mText == null;
}
#Override
public int hashCode() {
int result = (int) (mId ^ (mId >>> 32));
result = 31 * result + (mText != null ? mText.hashCode() : 0);
return result;
}
}
Quick side note: Most IDE's like Android Studio, IntelliJ and Eclipse have functionality to generate equals() and hashCode() implementations for you at the press of a button! So you don't have to implement them yourself. Look up on the internet how it works in your IDE!
Now let's take a look at the last method:
#Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
The SortedList uses this method to check if two items refer to the same thing. In simplest terms (without explaining how the SortedList works) this is used to determine if an object is already contained in the List and if either an add, move or change animation needs to be played. If your models have an id you would usually compare just the id in this method. If they don't you need to figure out some other way to check this, but however you end up implementing this depends on your specific app. Usually it is the simplest option to give all models an id - that could for example be the primary key field if you are querying the data from a database.
With the SortedList.Callback correctly implemented we can create an instance of the SortedList:
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
As the first parameter in the constructor of the SortedList you need to pass the class of your models. The other parameter is just the SortedList.Callback we defined above.
Now let's get down to business: If we implement the Adapter with a SortedList it should look something like this:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
#Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
#Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
#Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
#Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
#Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
#Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
#Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
});
private final LayoutInflater mInflater;
private final Comparator<ExampleModel> mComparator;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
#Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
#Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
#Override
public int getItemCount() {
return mSortedList.size();
}
}
The Comparator used to sort the item is passed in through the constructor so we can use the same Adapter even if the items are supposed to be displayed in a different order.
Now we are almost done! But we first need a way to add or remove items to the Adapter. For this purpose we can add methods to the Adapter which allow us to add and remove items to the SortedList:
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
We don't need to call any notify methods here because the SortedList already does this for through the SortedList.Callback! Aside from that the implementation of these methods is pretty straight forward with one exception: the remove method which removes a List of models. Since the SortedList has only one remove method which can remove a single object we need to loop over the list and remove the models one by one. Calling beginBatchedUpdates() at the beginning batches all the changes we are going to make to the SortedList together and improves performance. When we call endBatchedUpdates() the RecyclerView is notified about all the changes at once.
Additionally what you have to understand is that if you add an object to the SortedList and it is already in the SortedList it won't be added again. Instead the SortedList uses the areContentsTheSame() method to figure out if the object has changed - and if it has the item in the RecyclerView will be updated.
Anyway, what I usually prefer is one method which allows me to replace all items in the RecyclerView at once. Remove everything which is not in the List and add all items which are missing from the SortedList:
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
This method again batches all updates together to increase performance. The first loop is in reverse since removing an item at the start would mess up the indexes of all items that come up after it and this can lead in some instances to problems like data inconsistencies. After that we just add the List to the SortedList using addAll() to add all items which are not already in the SortedList and - just like I described above - update all items that are already in the SortedList but have changed.
And with that the Adapter is complete. The whole thing should look something like this:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
#Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
#Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
#Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
#Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
#Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
#Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
#Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1 == item2;
}
});
private final Comparator<ExampleModel> mComparator;
private final LayoutInflater mInflater;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
#Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(mInflater, parent, false);
return new ExampleViewHolder(binding);
}
#Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
#Override
public int getItemCount() {
return mSortedList.size();
}
}
The only thing missing now is to implement the filtering!
Implementing the filter logic
To implement the filter logic we first have to define a List of all possible models. For this example I create a List of ExampleModel instances from an array of movies:
private static final String[] MOVIES = new String[]{
...
};
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
#Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
private ExampleAdapter mAdapter;
private List<ExampleModel> mModels;
private RecyclerView mRecyclerView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mAdapter = new ExampleAdapter(this, ALPHABETICAL_COMPARATOR);
mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
mBinding.recyclerView.setAdapter(mAdapter);
mModels = new ArrayList<>();
for (String movie : MOVIES) {
mModels.add(new ExampleModel(movie));
}
mAdapter.add(mModels);
}
Nothing special going on here, we just instantiate the Adapter and set it to the RecyclerView. After that we create a List of models from the movie names in the MOVIES array. Then we add all the models to the SortedList.
Now we can go back to onQueryTextChange() which we defined earlier and start implementing the filter logic:
#Override
public boolean onQueryTextChange(String query) {
final List<ExampleModel> filteredModelList = filter(mModels, query);
mAdapter.replaceAll(filteredModelList);
mBinding.recyclerView.scrollToPosition(0);
return true;
}
This is again pretty straight forward. We call the method filter() and pass in the List of ExampleModels as well as the query string. We then call replaceAll() on the Adapter and pass in the filtered List returned by filter(). We also have to call scrollToPosition(0) on the RecyclerView to ensure that the user can always see all items when searching for something. Otherwise the RecyclerView might stay in a scrolled down position while filtering and subsequently hide a few items. Scrolling to the top ensures a better user experience while searching.
The only thing left to do now is to implement filter() itself:
private static List<ExampleModel> filter(List<ExampleModel> models, String query) {
final String lowerCaseQuery = query.toLowerCase();
final List<ExampleModel> filteredModelList = new ArrayList<>();
for (ExampleModel model : models) {
final String text = model.getText().toLowerCase();
if (text.contains(lowerCaseQuery)) {
filteredModelList.add(model);
}
}
return filteredModelList;
}
The first thing we do here is call toLowerCase() on the query string. We don't want our search function to be case sensitive and by calling toLowerCase() on all strings we compare we can ensure that we return the same results regardless of case. It then just iterates through all the models in the List we passed into it and checks if the query string is contained in the text of the model. If it is then the model is added to the filtered List.
And that's it! The above code will run on API level 7 and above and starting with API level 11 you get item animations for free!
I realize that this is a very detailed description which probably makes this whole thing seem more complicated than it really is, but there is a way we can generalize this whole problem and make implementing an Adapter based on a SortedList much simpler.
Generalizing the problem and simplifying the Adapter
In this section I am not going to go into much detail - partly because I am running up against the character limit for answers on Stack Overflow but also because most of it already explained above - but to summarize the changes: We can implemented a base Adapter class which already takes care of dealing with the SortedList as well as binding models to ViewHolder instances and provides a convenient way to implement an Adapter based on a SortedList. For that we have to do two things:
We need to create a ViewModel interface which all model classes have to implement
We need to create a ViewHolder subclass which defines a bind() method the Adapter can use to bind models automatically.
This allows us to just focus on the content which is supposed to be displayed in the RecyclerView by just implementing the models and there corresponding ViewHolder implementations. Using this base class we don't have to worry about the intricate details of the Adapter and its SortedList.
SortedListAdapter
Because of the character limit for answers on StackOverflow I can't go through each step of implementing this base class or even add the full source code here, but you can find the full source code of this base class - I called it SortedListAdapter - in this GitHub Gist.
To make your life simple I have published a library on jCenter which contains the SortedListAdapter! If you want to use it then all you need to do is add this dependency to your app's build.gradle file:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
You can find more information about this library on the library homepage.
Using the SortedListAdapter
To use the SortedListAdapter we have to make two changes:
Change the ViewHolder so that it extends SortedListAdapter.ViewHolder. The type parameter should be the model which should be bound to this ViewHolder - in this case ExampleModel. You have to bind data to your models in performBind() instead of bind().
public class ExampleViewHolder extends SortedListAdapter.ViewHolder<ExampleModel> {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
#Override
protected void performBind(ExampleModel item) {
mBinding.setModel(item);
}
}
Make sure that all your models implement the ViewModel interface:
public class ExampleModel implements SortedListAdapter.ViewModel {
...
}
After that we just have to update the ExampleAdapter to extend SortedListAdapter and remove everything we don't need anymore. The type parameter should be the type of model you are working with - in this case ExampleModel. But if you are working with different types of models then set the type parameter to ViewModel.
public class ExampleAdapter extends SortedListAdapter<ExampleModel> {
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
super(context, ExampleModel.class, comparator);
}
#Override
protected ViewHolder<? extends ExampleModel> onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
#Override
protected boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
#Override
protected boolean areItemContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
}
After that we are done! However one last thing to mention: The SortedListAdapter does not have the same add(), remove() or replaceAll() methods our original ExampleAdapter had. It uses a separate Editor object to modify the items in the list which can be accessed through the edit() method. So if you want to remove or add items you have to call edit() then add and remove the items on this Editor instance and once you are done, call commit() on it to apply the changes to the SortedList:
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
All changes you make this way are batched together to increase performance. The replaceAll() method we implemented in the chapters above is also present on this Editor object:
mAdapter.edit()
.replaceAll(mModels)
.commit();
If you forget to call commit() then none of your changes will be applied!
All you need to do is to add filter method in RecyclerView.Adapter:
public void filter(String text) {
items.clear();
if(text.isEmpty()){
items.addAll(itemsCopy);
} else{
text = text.toLowerCase();
for(PhoneBookItem item: itemsCopy){
if(item.name.toLowerCase().contains(text) || item.phone.toLowerCase().contains(text)){
items.add(item);
}
}
}
notifyDataSetChanged();
}
itemsCopy is initialized in adapter's constructor like itemsCopy.addAll(items).
If you do so, just call filter from OnQueryTextListener:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
adapter.filter(query);
return true;
}
#Override
public boolean onQueryTextChange(String newText) {
adapter.filter(newText);
return true;
}
});
It's an example from filtering my phonebook by name and phone number.
Following #Shruthi Kamoji in a cleaner way, we can just use a filterable, its meant for that:
public abstract class GenericRecycleAdapter<E> extends RecyclerView.Adapter implements Filterable
{
protected List<E> list;
protected List<E> originalList;
protected Context context;
public GenericRecycleAdapter(Context context,
List<E> list)
{
this.originalList = list;
this.list = list;
this.context = context;
}
...
#Override
public Filter getFilter() {
return new Filter() {
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
list = (List<E>) results.values;
notifyDataSetChanged();
}
#Override
protected FilterResults performFiltering(CharSequence constraint) {
List<E> filteredResults = null;
if (constraint.length() == 0) {
filteredResults = originalList;
} else {
filteredResults = getFilteredResults(constraint.toString().toLowerCase());
}
FilterResults results = new FilterResults();
results.values = filteredResults;
return results;
}
};
}
protected List<E> getFilteredResults(String constraint) {
List<E> results = new ArrayList<>();
for (E item : originalList) {
if (item.getName().toLowerCase().contains(constraint)) {
results.add(item);
}
}
return results;
}
}
The E here is a Generic Type, you can extend it using your class:
public class customerAdapter extends GenericRecycleAdapter<CustomerModel>
Or just change the E to the type you want (<CustomerModel> for example)
Then from searchView (the widget you can put on menu.xml):
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String text) {
return false;
}
#Override
public boolean onQueryTextChange(String text) {
yourAdapter.getFilter().filter(text);
return true;
}
});
In Adapter:
public void setFilter(List<Channel> newList){
mChannels = new ArrayList<>();
mChannels.addAll(newList);
notifyDataSetChanged();
}
In Activity:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
newText = newText.toLowerCase();
ArrayList<Channel> newList = new ArrayList<>();
for (Channel channel: channels){
String channelName = channel.getmChannelName().toLowerCase();
if (channelName.contains(newText)){
newList.add(channel);
}
}
mAdapter.setFilter(newList);
return true;
}
});
simply create two list in adapter one orignal and one temp and implements Filterable.
#Override
public Filter getFilter() {
return new Filter() {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
final FilterResults oReturn = new FilterResults();
final ArrayList<T> results = new ArrayList<>();
if (origList == null)
origList = new ArrayList<>(itemList);
if (constraint != null && constraint.length() > 0) {
if (origList != null && origList.size() > 0) {
for (final T cd : origList) {
if (cd.getAttributeToSearch().toLowerCase()
.contains(constraint.toString().toLowerCase()))
results.add(cd);
}
}
oReturn.values = results;
oReturn.count = results.size();//newly Aded by ZA
} else {
oReturn.values = origList;
oReturn.count = origList.size();//newly added by ZA
}
return oReturn;
}
#SuppressWarnings("unchecked")
#Override
protected void publishResults(final CharSequence constraint,
FilterResults results) {
itemList = new ArrayList<>((ArrayList<T>) results.values);
// FIXME: 8/16/2017 implement Comparable with sort below
///Collections.sort(itemList);
notifyDataSetChanged();
}
};
}
where
public GenericBaseAdapter(Context mContext, List<T> itemList) {
this.mContext = mContext;
this.itemList = itemList;
this.origList = itemList;
}
With Android Architecture Components through the use of LiveData this can be easily implemented with any type of Adapter. You simply have to do the following steps:
1. Setup your data to return from the Room Database as LiveData as in the example below:
#Dao
public interface CustomDAO{
#Query("SELECT * FROM words_table WHERE column LIKE :searchquery")
public LiveData<List<Word>> searchFor(String searchquery);
}
2. Create a ViewModel object to update your data live through a method that will connect your DAO and your UI
public class CustomViewModel extends AndroidViewModel {
private final AppDatabase mAppDatabase;
public WordListViewModel(#NonNull Application application) {
super(application);
this.mAppDatabase = AppDatabase.getInstance(application.getApplicationContext());
}
public LiveData<List<Word>> searchQuery(String query) {
return mAppDatabase.mWordDAO().searchFor(query);
}
}
3. Call your data from the ViewModel on the fly by passing in the query through onQueryTextListener as below:
Inside onCreateOptionsMenu set your listener as follows
searchView.setOnQueryTextListener(onQueryTextListener);
Setup your query listener somewhere in your SearchActivity class as follows
private android.support.v7.widget.SearchView.OnQueryTextListener onQueryTextListener =
new android.support.v7.widget.SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
getResults(query);
return true;
}
#Override
public boolean onQueryTextChange(String newText) {
getResults(newText);
return true;
}
private void getResults(String newText) {
String queryText = "%" + newText + "%";
mCustomViewModel.searchQuery(queryText).observe(
SearchResultsActivity.this, new Observer<List<Word>>() {
#Override
public void onChanged(#Nullable List<Word> words) {
if (words == null) return;
searchAdapter.submitList(words);
}
});
}
};
Note: Steps (1.) and (2.) are standard AAC ViewModel and DAO implementation, the only real "magic" going on here is in the OnQueryTextListener which will update the results of your list dynamically as the query text changes.
If you need more clarification on the matter please don't hesitate to ask.
I hope this helped :).
I don't know why everyone is using 2 copies of the same list to solve this. This uses too much RAM...
Why not just hide the elements that are not found, and simply store their index in a Set to be able to restore them later? That's much less RAM especially if your objects are quite large.
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.SampleViewHolders>{
private List<MyObject> myObjectsList; //holds the items of type MyObject
private Set<Integer> foundObjects; //holds the indices of the found items
public MyRecyclerViewAdapter(Context context, List<MyObject> myObjectsList)
{
this.myObjectsList = myObjectsList;
this.foundObjects = new HashSet<>();
//first, add all indices to the indices set
for(int i = 0; i < this.myObjectsList.size(); i++)
{
this.foundObjects.add(i);
}
}
#NonNull
#Override
public SampleViewHolders onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View layoutView = LayoutInflater.from(parent.getContext()).inflate(
R.layout.my_layout_for_staggered_grid, null);
MyRecyclerViewAdapter.SampleViewHolders rcv = new MyRecyclerViewAdapter.SampleViewHolders(layoutView);
return rcv;
}
#Override
public void onBindViewHolder(#NonNull SampleViewHolders holder, int position)
{
//look for object in O(1) in the indices set
if(!foundObjects.contains(position))
{
//object not found => hide it.
holder.hideLayout();
return;
}
else
{
//object found => show it.
holder.showLayout();
}
//holder.imgImageView.setImageResource(...)
//holder.nameTextView.setText(...)
}
#Override
public int getItemCount() {
return myObjectsList.size();
}
public void findObject(String text)
{
//look for "text" in the objects list
for(int i = 0; i < myObjectsList.size(); i++)
{
//if it's empty text, we want all objects, so just add it to the set.
if(text.length() == 0)
{
foundObjects.add(i);
}
else
{
//otherwise check if it meets your search criteria and add it or remove it accordingly
if (myObjectsList.get(i).getName().toLowerCase().contains(text.toLowerCase()))
{
foundObjects.add(i);
}
else
{
foundObjects.remove(i);
}
}
}
notifyDataSetChanged();
}
public class SampleViewHolders extends RecyclerView.ViewHolder implements View.OnClickListener
{
public ImageView imgImageView;
public TextView nameTextView;
private final CardView layout;
private final CardView.LayoutParams hiddenLayoutParams;
private final CardView.LayoutParams shownLayoutParams;
public SampleViewHolders(View itemView)
{
super(itemView);
itemView.setOnClickListener(this);
imgImageView = (ImageView) itemView.findViewById(R.id.some_image_view);
nameTextView = (TextView) itemView.findViewById(R.id.display_name_textview);
layout = itemView.findViewById(R.id.card_view); //card_view is the id of my androidx.cardview.widget.CardView in my xml layout
//prepare hidden layout params with height = 0, and visible layout params for later - see hideLayout() and showLayout()
hiddenLayoutParams = new CardView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
hiddenLayoutParams.height = 0;
shownLayoutParams = new CardView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
#Override
public void onClick(View view)
{
//implement...
}
private void hideLayout() {
//hide the layout
layout.setLayoutParams(hiddenLayoutParams);
}
private void showLayout() {
//show the layout
layout.setLayoutParams(shownLayoutParams);
}
}
}
And I simply have an EditText as my search box:
cardsSearchTextView.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void afterTextChanged(Editable editable) {
myViewAdapter.findObject(editable.toString().toLowerCase());
}
});
Result:
I have solved the same problem using the link with some modifications in it. Search filter on RecyclerView with Cards. Is it even possible? (hope this helps).
Here is my adapter class
public class ContactListRecyclerAdapter extends RecyclerView.Adapter<ContactListRecyclerAdapter.ContactViewHolder> implements Filterable {
Context mContext;
ArrayList<Contact> customerList;
ArrayList<Contact> parentCustomerList;
public ContactListRecyclerAdapter(Context context,ArrayList<Contact> customerList)
{
this.mContext=context;
this.customerList=customerList;
if(customerList!=null)
parentCustomerList=new ArrayList<>(customerList);
}
// other overrided methods
#Override
public Filter getFilter() {
return new FilterCustomerSearch(this,parentCustomerList);
}
}
//Filter class
import android.widget.Filter;
import java.util.ArrayList;
public class FilterCustomerSearch extends Filter
{
private final ContactListRecyclerAdapter mAdapter;
ArrayList<Contact> contactList;
ArrayList<Contact> filteredList;
public FilterCustomerSearch(ContactListRecyclerAdapter mAdapter,ArrayList<Contact> contactList) {
this.mAdapter = mAdapter;
this.contactList=contactList;
filteredList=new ArrayList<>();
}
#Override
protected FilterResults performFiltering(CharSequence constraint) {
filteredList.clear();
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
filteredList.addAll(contactList);
} else {
final String filterPattern = constraint.toString().toLowerCase().trim();
for (final Contact contact : contactList) {
if (contact.customerName.contains(constraint)) {
filteredList.add(contact);
}
else if (contact.emailId.contains(constraint))
{
filteredList.add(contact);
}
else if(contact.phoneNumber.contains(constraint))
filteredList.add(contact);
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mAdapter.customerList.clear();
mAdapter.customerList.addAll((ArrayList<Contact>) results.values);
mAdapter.notifyDataSetChanged();
}
}
//Activity class
public class HomeCrossFadeActivity extends AppCompatActivity implements View.OnClickListener,OnFragmentInteractionListener,OnTaskCompletedListner
{
Fragment fragment;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_homecrossfadeslidingpane2);CardView mCard;
setContentView(R.layout.your_main_xml);}
//other overrided methods
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
MenuInflater inflater = getMenuInflater();
// Inflate menu to add items to action bar if it is present.
inflater.inflate(R.menu.menu_customer_view_and_search, menu);
// Associate searchable configuration with the SearchView
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView =
(SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setQueryHint("Search Customer");
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
if(fragment instanceof CustomerDetailsViewWithModifyAndSearch)
((CustomerDetailsViewWithModifyAndSearch)fragment).adapter.getFilter().filter(newText);
return false;
}
});
return true;
}
}
In OnQueryTextChangeListener() method use your adapter. I have casted it to fragment as my adpter is in fragment. You can use the adapter directly if its in your activity class.
This is my take on expanding #klimat answer to not losing filtering animation.
public void filter(String query){
int completeListIndex = 0;
int filteredListIndex = 0;
while (completeListIndex < completeList.size()){
Movie item = completeList.get(completeListIndex);
if(item.getName().toLowerCase().contains(query)){
if(filteredListIndex < filteredList.size()) {
Movie filter = filteredList.get(filteredListIndex);
if (!item.getName().equals(filter.getName())) {
filteredList.add(filteredListIndex, item);
notifyItemInserted(filteredListIndex);
}
}else{
filteredList.add(filteredListIndex, item);
notifyItemInserted(filteredListIndex);
}
filteredListIndex++;
}
else if(filteredListIndex < filteredList.size()){
Movie filter = filteredList.get(filteredListIndex);
if (item.getName().equals(filter.getName())) {
filteredList.remove(filteredListIndex);
notifyItemRemoved(filteredListIndex);
}
}
completeListIndex++;
}
}
Basically what it does is looking through a complete list and adding/removing items to a filtered list one by one.
I recommend modify the solution of #Xaver Kapeller with 2 things below to avoid a problem after you cleared the searched text (the filter didn't work anymore) due to the list back of adapter has smaller size than filter list and the IndexOutOfBoundsException happened. So the code need to modify as below
public void addItem(int position, ExampleModel model) {
if(position >= mModel.size()) {
mModel.add(model);
notifyItemInserted(mModel.size()-1);
} else {
mModels.add(position, model);
notifyItemInserted(position);
}
}
And modify also in moveItem functionality
public void moveItem(int fromPosition, int toPosition) {
final ExampleModel model = mModels.remove(fromPosition);
if(toPosition >= mModels.size()) {
mModels.add(model);
notifyItemMoved(fromPosition, mModels.size()-1);
} else {
mModels.add(toPosition, model);
notifyItemMoved(fromPosition, toPosition);
}
}
Hope that It could help you!
Add an interface in your adapter.
public interface SelectedUser{
void selectedUser(UserModel userModel);
}
implement the interface in your mainactivity and override the method.
#Override
public void selectedUser(UserModel userModel) {
startActivity(new Intent(MainActivity.this, SelectedUserActivity.class).putExtra("data",userModel));
}
Full tutorial and source code:
Recyclerview with searchview and onclicklistener
If you want to search on button click then this works fine.
filterIcon.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
String strCHR = homeSearchEdit.getText().toString();
if (homeSearchEdit.getText().toString().length() > 0) {
ArrayList<ServiceModel> listNew = new ArrayList<>();
for (int l = 0; l < arrayList.size(); l++) {
String serviceName = arrayList.get(l).getServiceName().toLowerCase();
if (serviceName.contains(strCHR.toLowerCase())) {
listNew.add(arrayList.get(l));
}
}
recyclerView.setVisibility(View.VISIBLE);
adapter = new ServiceAdapter(HomeActivity.this, listNew);
recyclerView.setAdapter(adapter);
} else {
recyclerView.setVisibility(View.VISIBLE);
adapter = new ServiceAdapter(HomeActivity.this, arrayList);
recyclerView.setAdapter(adapter);
}
}
});
where ,filterIcon is button and homeSearchEdit is editText(where we apply for search).
Android has provided DiffUtil.Callback() and DiffUtil.ItemCallback<T> and they help us filter our recycler view nicely
DiffUtil is a utility class that calculates the difference between two
lists and outputs a list of update operations that converts the first
list into the second one.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
DiffUtil.Callback() is used with RecyclerView.Adapter
and
DiffUtil.ItemCallback is used with ListAdapter
Filter using RecyclerView
Create your RecyclerView Like you normally would overriding the
onCreateViewHolder
onBindViewHolder
getItemCount
and extending the RecyclerView.ViewHolder Class
Just Like you have done(This is the Kotlin version of snippets from your code)
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder? {
val v: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.recycler_view_card_item, viewGroup, false)
return ViewHolder(v)
}
fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val movie: Movie = mItems.get(i)
viewHolder.tvMovie.setText(movie.getName())
viewHolder.tvMovieRating.setText(movie.getRating())
}
override fun getItemCount(): Int {
return mItems.size()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvMovie: TextView
var tvMovieRating: TextView
init {
tvMovie = itemView.findViewById<View>(R.id.movieName) as TextView
tvMovieRating = itemView.findViewById<View>(R.id.movieRating) as TextView
}
}
Now Create another class which will implement the DiffUtil.Callback()
This class will help convert the recyclerviews currentlist to the filtered list
class MoviesDiffUtilCallback(private val oldList: List<Movies>, private val newList: List<Movies>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition].aUniqueId == newList[newItemPosition]. aUniqueId
//aUniqueId-> a field that is unique to each item in your listItems
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition] == newList[newItemPosition]
}
In your Activity or Fragment Class Setup your adapter and your filter
private fun setupAdapter() {
//mItems is the list you will pass to the adapter
adapter = CardAdapter(mItems)
recyclerView.adapter = adapter
}
fun filter(searchText : String){
val newFilter = mItems.filter {
it.name.lowercase().contains(text.lowercase()) //filterlogic
}
//Calculate the list of update operations that can covert one list into the other one
val diffResult = DiffUtil.calculateDiff(PostsDiffUtilCallback(mItems,newFilter))
mItems.clear()
mItems.addAll(newFilter)
//dispatch all updates to the RecyclerView
diffResult.dispatchUpdatesTo(adapter)
}
Filter using ListAdapter
We will be using the filterable interface to help us filter (still figuring out why I shouldn't just use a filter function to get filteredLists and submitList(filteredLists) Directly)
Create your ListAdapter class
class CardAdapter (
private val mItems : List<Movies>) : ListAdapter<Movies, CardAdapter.BillsPackageViewHolder>(MoviesDiffCallback()),
Filterable {
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder? {
val v: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.recycler_view_card_item, viewGroup, false)
return ViewHolder(v)
}
fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val movie: Movie = mItems.get(i)
viewHolder.tvMovie.setText(movie.getName())
viewHolder.tvMovieRating.setText(movie.getRating())
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
return FilterResults().apply {
values = if (constraint.isNullOrEmpty())
mItems
else
onFilter(mItems, constraint.toString())
}
}
#Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
submitList(results?.values as? List<Movies>)
}
}
}
fun onFilter(list: List<Movies>, constraint: String) : List<Movies>{
val filteredList = list.filter {
it.name.lowercase().contains(constraint.lowercase())
}
return filteredList
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvMovie: TextView
var tvMovieRating: TextView
init {
tvMovie = itemView.findViewById<View>(R.id.movieName) as TextView
tvMovieRating = itemView.findViewById<View>(R.id.movieRating) as TextView
}
}
}
Now Create another class which will implement the DiffUtil.ItemCallback
class MoviesDiffCallback : DiffUtil.ItemCallback<Movies>() {
override fun areItemsTheSame(oldItem: Movies, newItem: Movies): Boolean {
return oldItem.someUniqueid == newItem.someUniqueid
}
override fun areContentsTheSame(oldItem: Movies, newItem: Movies): Boolean {
return oldItem == newItem
}
}
and in your MainActivity or Fragment Setup your adapter and your filter
private fun setupAdapter() {
adapter = CardAdapter(mItems)
recyclerView.adapter = adapter
}
fun filter(searchString : String){
adapter.filter.filter(searchString)
}

Categories

Resources