package com.mikepenz.fastadapter.adapters; import android.widget.Filter; import com.mikepenz.fastadapter.AbstractAdapter; import com.mikepenz.fastadapter.IExpandable; import com.mikepenz.fastadapter.IItem; import com.mikepenz.fastadapter.IItemAdapter; import com.mikepenz.fastadapter.ISubItem; import com.mikepenz.fastadapter.utils.IdDistributor; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import static java.util.Arrays.asList; /** * Created by mikepenz on 27.12.15. * A general ItemAdapter implementation based on the AbstractAdapter to speed up development for general items * This adapter has the order of 500 which is the centered order */ public class ItemAdapter<Item extends IItem> extends AbstractAdapter<Item> implements IItemAdapter<Item> { //the items handled and managed by this item private List<Item> mItems = new ArrayList<>(); //defines if the IdDistributor is used to set ID's to all added items private boolean mUseIdDistributor = true; /** * defines if the IdDistributor is used to provide an ID to all added items which do not yet define an id * * @param useIdDistributor false if the IdDistributor shouldn't be used * @return this */ public ItemAdapter withUseIdDistributor(boolean useIdDistributor) { this.mUseIdDistributor = useIdDistributor; return this; } /** * @return if we use the idDistributor with this adapter */ public boolean isUseIdDistributor() { return mUseIdDistributor; } //filters the items private Filter mItemFilter = new ItemFilter(); /** * allows you to define your own Filter implementation instead of the default `ItemFilter` * * @param itemFilter the filter to use * @return this */ public ItemAdapter<Item> withItemFilter(Filter itemFilter) { this.mItemFilter = itemFilter; return this; } /** * @return the filter used to filter items */ public Filter getItemFilter() { return mItemFilter; } //the filter predicate which is used in the ItemFilter private Predicate<Item> mFilterPredicate; /** * define the predicate used to filter the list inside the ItemFilter * * @param filterPredicate the predicate used to filter the list inside the ItemFilter * @return this */ public ItemAdapter<Item> withFilterPredicate(Predicate<Item> filterPredicate) { this.mFilterPredicate = filterPredicate; return this; } /** * filters the items with the constraint using the provided Predicate * * @param constraint the string used to filter the list */ public void filter(CharSequence constraint) { mItemFilter.filter(constraint); } /** * @param listener which will be called after the items were filtered * @return this */ public ItemAdapter<Item> withItemFilterListener(ItemFilterListener listener) { mItemFilterListener = listener; return this; } //the listener which will be called after the items were filtered protected ItemFilterListener mItemFilterListener; /** * interface for the ItemFilterListener */ public interface ItemFilterListener { void itemsFiltered(); } // protected Comparator<Item> mComparator; /** * define a comparator which will be used to sort the list "everytime" it is altered * NOTE this will only sort if you "set" a new list or "add" new items (not if you provide a position for the add function) * * @param comparator used to sort the list * @return this */ public ItemAdapter<Item> withComparator(Comparator<Item> comparator) { return withComparator(comparator, true); } /** * define a comparator which will be used to sort the list "everytime" it is altered * NOTE this will only sort if you "set" a new list or "add" new items (not if you provide a position for the add function) * * @param comparator used to sort the list * @param sortNow specifies if we use the provided comparator to sort now * @return this */ public ItemAdapter<Item> withComparator(Comparator<Item> comparator, boolean sortNow) { this.mComparator = comparator; //we directly sort the list with the defined comparator if (mItems != null && mComparator != null && sortNow) { Collections.sort(mItems, mComparator); getFastAdapter().notifyAdapterDataSetChanged(); } return this; } /** * @return the defined Comparator used for this ItemAdaper */ public Comparator<Item> getComparator() { return mComparator; } /** * @return the order of the items within the FastAdapter */ @Override public int getOrder() { return 500; } /** * @return the count of items within this adapter */ @Override public int getAdapterItemCount() { return mItems.size(); } /** * @return the items within this adapter */ @Override public List<Item> getAdapterItems() { return mItems; } /** * Searches for the given item and calculates its relative position * * @param item the item which is searched for * @return the relative position */ @Override public int getAdapterPosition(Item item) { return getAdapterPosition(item.getIdentifier()); } /** * Searches for the given identifier and calculates its relative position * * @param identifier the identifier of an item which is searched for * @return the relative position */ @Override public int getAdapterPosition(long identifier) { for (int i = 0, size = mItems.size(); i < size; i++) { if (mItems.get(i).getIdentifier() == identifier) { return i; } } return -1; } /** * returns the global position if the relative position within this adapter was given * * @param position the relative position * @return the global position */ public int getGlobalPosition(int position) { return position + getFastAdapter().getPreItemCountByOrder(getOrder()); } /** * @param position the relative position * @return the item inside this adapter */ @Override public Item getAdapterItem(int position) { return mItems.get(position); } /** * sets the subItems of the given collapsible * This method also makes sure identifiers are set if we use the IdDistributor * * @param collapsible the collapsible which gets the subItems set * @param subItems the subItems for this collapsible item * @return the item type of the collapsible */ public <T extends IItem & IExpandable<T, S>, S extends IItem & ISubItem<Item, T>> T setSubItems(T collapsible, List<S> subItems) { if (mUseIdDistributor) { IdDistributor.checkIds(subItems); } return collapsible.withSubItems(subItems); } /** * set a new list of items and apply it to the existing list (clear - add) for this adapter * NOTE may consider using setNewList if the items list is a reference to the list which is used inside the adapter * NOTE this will not sort * * @param items the items to set */ public ItemAdapter<Item> set(List<Item> items) { return set(items, true); } private ItemAdapter<Item> set(List<Item> items, boolean resetFilter) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } //reset the filter if (resetFilter && getItemFilter() instanceof ItemAdapter.ItemFilter && ((ItemFilter) getItemFilter()).getConstraint() != null) { ((ItemFilter) getItemFilter()).performFiltering(null); } //first collapse all items getFastAdapter().collapse(false); //get sizes int newItemsCount = items.size(); int previousItemsCount = mItems.size(); int itemsBeforeThisAdapter = getFastAdapter().getPreItemCountByOrder(getOrder()); //make sure the new items list is not a reference of the already mItems list if (items != mItems) { //remove all previous items if (!mItems.isEmpty()) { mItems.clear(); } //add all new items to the list mItems.addAll(items); } //map the types mapPossibleTypes(items); //if we have a comparator then sort if (mComparator != null) { Collections.sort(mItems, mComparator); } //now properly notify the adapter about the changes if (newItemsCount > previousItemsCount) { if (previousItemsCount > 0) { getFastAdapter().notifyAdapterItemRangeChanged(itemsBeforeThisAdapter, previousItemsCount); } getFastAdapter().notifyAdapterItemRangeInserted(itemsBeforeThisAdapter + previousItemsCount, newItemsCount - previousItemsCount); } else if (newItemsCount > 0 && newItemsCount < previousItemsCount) { getFastAdapter().notifyAdapterItemRangeChanged(itemsBeforeThisAdapter, newItemsCount); getFastAdapter().notifyAdapterItemRangeRemoved(itemsBeforeThisAdapter + newItemsCount, previousItemsCount - newItemsCount); } else if (newItemsCount == 0) { getFastAdapter().notifyAdapterItemRangeRemoved(itemsBeforeThisAdapter, previousItemsCount); } else { getFastAdapter().notifyAdapterDataSetChanged(); } return this; } /** * sets a complete new list of items onto this adapter, using the new list. Calls notifyDataSetChanged * * @param items the new items to set */ public ItemAdapter<Item> setNewList(List<Item> items) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } //reset the filter if (getItemFilter() instanceof ItemAdapter.ItemFilter && ((ItemFilter) getItemFilter()).getConstraint() != null) { ((ItemFilter) getItemFilter()).performFiltering(null); } mItems = new ArrayList<>(items); mapPossibleTypes(mItems); if (mComparator != null) { Collections.sort(mItems, mComparator); } getFastAdapter().notifyAdapterDataSetChanged(); return this; } /** * forces to remap all possible types for the RecyclerView */ public void remapMappedTypes() { clearMappedTypes(); mapPossibleTypes(mItems); } /** * add an array of items to the end of the existing items * * @param items the items to add */ @SafeVarargs public final ItemAdapter<Item> add(Item... items) { return add(asList(items)); } /** * add a list of items to the end of the existing items * * @param items the items to add */ public ItemAdapter<Item> add(List<Item> items) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } int countBefore = mItems.size(); mItems.addAll(items); mapPossibleTypes(items); if (mComparator == null) { getFastAdapter().notifyAdapterItemRangeInserted(getFastAdapter().getPreItemCountByOrder(getOrder()) + countBefore, items.size()); } else { Collections.sort(mItems, mComparator); getFastAdapter().notifyAdapterDataSetChanged(); } return this; } /** * add an array of items at the given position within the existing items * * @param position the global position * @param items the items to add */ @SafeVarargs public final ItemAdapter<Item> add(int position, Item... items) { return add(position, asList(items)); } /** * add a list of items at the given position within the existing items * * @param position the global position * @param items the items to add */ public ItemAdapter<Item> add(int position, List<Item> items) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } if (items != null && items.size() > 0) { mItems.addAll(position - getFastAdapter().getPreItemCountByOrder(getOrder()), items); mapPossibleTypes(items); getFastAdapter().notifyAdapterItemRangeInserted(position, items.size()); } return this; } /** * sets an item at the given position, overwriting the previous item * * @param position the global position * @param item the item to set */ public ItemAdapter<Item> set(int position, Item item) { if (mUseIdDistributor) { IdDistributor.checkId(item); } mItems.set(position - getFastAdapter().getPreItemCount(position), item); mapPossibleType(item); getFastAdapter().notifyAdapterItemChanged(position); return this; } /** * moves an item within the list from a position to a position * * @param fromPosition the position global from which we want to move * @param toPosition the global position to which to move * @return this */ public ItemAdapter<Item> move(int fromPosition, int toPosition) { int preItemCount = getFastAdapter().getPreItemCount(fromPosition); Item item = mItems.get(fromPosition - preItemCount); mItems.remove(fromPosition - preItemCount); mItems.add(toPosition - preItemCount, item); getFastAdapter().notifyAdapterItemMoved(fromPosition, toPosition); return this; } /** * removes an item at the given position within the existing icons * * @param position the global position */ public ItemAdapter<Item> remove(int position) { mItems.remove(position - getFastAdapter().getPreItemCount(position)); getFastAdapter().notifyAdapterItemRemoved(position); return this; } /** * removes a range of items starting with the given position within the existing icons * * @param position the global position * @param itemCount the count of items which were removed */ public ItemAdapter<Item> removeRange(int position, int itemCount) { //global position to relative int length = mItems.size(); int preItemCount = getFastAdapter().getPreItemCount(position); //make sure we do not delete to many items int saveItemCount = Math.min(itemCount, length - position + preItemCount); for (int i = 0; i < saveItemCount; i++) { mItems.remove(position - preItemCount); } getFastAdapter().notifyAdapterItemRangeRemoved(position, saveItemCount); return this; } /** * removes all items of this adapter */ public ItemAdapter<Item> clear() { int count = mItems.size(); mItems.clear(); getFastAdapter().notifyAdapterItemRangeRemoved(getFastAdapter().getPreItemCountByOrder(getOrder()), count); return this; } /** * ItemFilter which extends the Filter api provided by Android * This calls automatically all required methods, just overwrite the filterItems method */ public class ItemFilter extends Filter { private List<Item> mOriginalItems; private CharSequence mConstraint; @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); //return nothing if(mOriginalItems == null && (constraint == null || constraint.length() == 0)) { return results; } //we can not filter on expanded or selected items, because the remembered positions will change if (getFastAdapter().isPositionBasedStateManagement()) { getFastAdapter().deselect(); } getFastAdapter().collapse(false); mConstraint = constraint; if (mOriginalItems == null) { mOriginalItems = new ArrayList<>(mItems); } // We implement here the filter logic if (constraint == null || constraint.length() == 0) { // No filter implemented we return all the list results.values = mOriginalItems; results.count = mOriginalItems.size(); //our filter was cleared we can now forget the old OriginalItems mOriginalItems = null; } else { List<Item> filteredItems = new ArrayList<>(); // We perform filtering operation if (mFilterPredicate != null) { for (Item item : mOriginalItems) { if (!mFilterPredicate.filter(item, constraint)) { filteredItems.add(item); } } } else { filteredItems = mItems; } results.values = filteredItems; results.count = filteredItems.size(); } return results; } public CharSequence getConstraint() { return mConstraint; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { // Now we have to inform the adapter about the new list filtered if (results.values != null) { ItemAdapter.this.set((List<Item>) results.values, false); } if (mItemFilterListener != null) { mItemFilterListener.itemsFiltered(); } } /** * helper method to get all selections from the ItemAdapter's original item list * * @return a Set with the global positions of all selected Items */ public Set<Integer> getSelections() { if (mOriginalItems != null) { Set<Integer> selections = new HashSet<>(); int adapterOffset = getFastAdapter().getPreItemCountByOrder(getOrder()); for (int i = 0, size = mOriginalItems.size(); i < size; i++) { Item item = mOriginalItems.get(i); if (item.isSelected()) { selections.add(i + adapterOffset); } } return selections; } else { return getFastAdapter().getSelections(); } } /** * helper method to get all selections from the ItemAdapter's original item list * * @return a Set with the selected items out of all items in this itemAdapter (not the listed ones) */ public Set<Item> getSelectedItems() { if (mOriginalItems != null) { Set<Item> selections = new HashSet<>(); for (int i = 0, size = mOriginalItems.size(); i < size; i++) { Item item = mOriginalItems.get(i); if (item.isSelected()) { selections.add(item); } } return selections; } else { return getFastAdapter().getSelectedItems(); } } /** * Searches for the given item and calculates its relative position * * @param item the item which is searched for * @return the relative position */ public int getAdapterPosition(Item item) { return getAdapterPosition(item.getIdentifier()); } /** * Searches for the given identifier and calculates its relative position * * @param identifier the identifier of an item which is searched for * @return the relative position */ public int getAdapterPosition(long identifier) { for (int i = 0, size = mOriginalItems.size(); i < size; i++) { if (mOriginalItems.get(i).getIdentifier() == identifier) { return i; } } return -1; } /** * add an array of items to the end of the existing items * * @param items the items to add */ @SafeVarargs public final ItemAdapter<Item> add(Item... items) { return add(asList(items)); } /** * add a list of items to the end of the existing items * will prior check if we are currently filtering * * @param items the items to add */ public ItemAdapter<Item> add(List<Item> items) { if (mOriginalItems != null && items.size() > 0) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } mOriginalItems.addAll(items); publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.add(items); } } /** * add an array of items at the given position within the existing items * * @param position the global position * @param items the items to add */ @SafeVarargs public final ItemAdapter<Item> add(int position, Item... items) { return add(position, asList(items)); } /** * add a list of items at the given position within the existing items * * @param position the global position * @param items the items to add */ public ItemAdapter<Item> add(int position, List<Item> items) { if (mOriginalItems != null && items.size() > 0) { if (mUseIdDistributor) { IdDistributor.checkIds(items); } mOriginalItems.addAll(getAdapterPosition(mItems.get(position)) - getFastAdapter().getPreItemCount(position), items); publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.add(position, items); } } /** * sets an item at the given position, overwriting the previous item * * @param position the global position * @param item the item to set */ public ItemAdapter<Item> set(int position, Item item) { if (mOriginalItems != null) { if (mUseIdDistributor) { IdDistributor.checkId(item); } mOriginalItems.set(getAdapterPosition(mItems.get(position)) - getFastAdapter().getPreItemCount(position), item); publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.set(position, item); } } /** * moves an item within the list from a position to a position * * @param fromPosition the position global from which we want to move * @param toPosition the global position to which to move * @return this */ public ItemAdapter<Item> move(int fromPosition, int toPosition) { if (mOriginalItems != null) { int preItemCount = getFastAdapter().getPreItemCount(fromPosition); int adjustedFrom = getAdapterPosition(mItems.get(fromPosition)); int adjustedTo = getAdapterPosition(mItems.get(toPosition)); Item item = mOriginalItems.get(adjustedFrom - preItemCount); mOriginalItems.remove(adjustedFrom - preItemCount); mOriginalItems.add(adjustedTo - preItemCount, item); performFiltering(mConstraint); return ItemAdapter.this; } else { return ItemAdapter.this.move(fromPosition, toPosition); } } /** * removes an item at the given position within the existing icons * * @param position the global position */ public ItemAdapter<Item> remove(int position) { if (mOriginalItems != null) { mOriginalItems.remove(getAdapterPosition(mItems.get(position))- getFastAdapter().getPreItemCount(position)); publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.remove(position); } } /** * removes a range of items starting with the given position within the existing icons * * @param position the global position * @param itemCount the count of items which were removed */ public ItemAdapter<Item> removeRange(int position, int itemCount) { if (mOriginalItems != null) { //global position to relative int length = mOriginalItems.size(); int preItemCount = getFastAdapter().getPreItemCount(position); //make sure we do not delete to many items int saveItemCount = Math.min(itemCount, length - position + preItemCount); for (int i = 0; i < saveItemCount; i++) { mOriginalItems.remove(position - preItemCount); } publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.removeRange(position, itemCount); } } /** * removes all items of this adapter */ public ItemAdapter<Item> clear() { if (mOriginalItems != null) { mOriginalItems.clear(); publishResults(mConstraint, performFiltering(mConstraint)); return ItemAdapter.this; } else { return ItemAdapter.this.clear(); } } } }