diff --git a/library-kotlin-android-extensions/src/main/java/com/xwray/groupie/groupiex/GroupAdapterExt.kt b/library-kotlin-android-extensions/src/main/java/com/xwray/groupie/groupiex/GroupAdapterExt.kt index 70ba54f..4379cea 100644 --- a/library-kotlin-android-extensions/src/main/java/com/xwray/groupie/groupiex/GroupAdapterExt.kt +++ b/library-kotlin-android-extensions/src/main/java/com/xwray/groupie/groupiex/GroupAdapterExt.kt @@ -6,6 +6,6 @@ import com.xwray.groupie.GroupieViewHolder // TODO(zhuinden): move this into its own artifact later: `groupiex` (or rather, `groupie-ktx`) operator fun GroupAdapter.plusAssign(element: Group) = this.add(element) -operator fun GroupAdapter.plusAssign(groups: Collection) = this.addAll(groups) +operator fun GroupAdapter.plusAssign(groups: List) = this.addAll(groups) operator fun GroupAdapter.minusAssign(element: Group) = this.remove(element) -operator fun GroupAdapter.minusAssign(groups: Collection) = this.removeAll(groups) \ No newline at end of file +operator fun GroupAdapter.minusAssign(groups: List) = this.removeAll(groups) \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index c1c1dba..f277a9d 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -17,8 +17,8 @@ android { defaultConfig { minSdkVersion rootProject.minimumSdkVersion targetSdkVersion rootProject.sdkVersion - versionCode 1 - versionName "1.0" + versionCode 3 + versionName "2.9.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -40,6 +40,4 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.1.0" testImplementation "junit:junit:$junit_version" testImplementation "org.mockito:mockito-core:$mockito_version" -} - - +} \ No newline at end of file diff --git a/library/src/main/java/com/xwray/groupie/AsyncDiffUtil.java b/library/src/main/java/com/xwray/groupie/AsyncDiffUtil.java index b332ae9..2f6e1fe 100644 --- a/library/src/main/java/com/xwray/groupie/AsyncDiffUtil.java +++ b/library/src/main/java/com/xwray/groupie/AsyncDiffUtil.java @@ -6,49 +6,39 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListUpdateCallback; -import java.util.Collection; +import java.util.List; /** * A wrapper around {@link DiffUtil} that calculates diff in a background thread */ class AsyncDiffUtil { + interface Callback extends ListUpdateCallback { /** * Called on the main thread before DiffUtil dispatches the result */ @MainThread - void onDispatchAsyncResult(@NonNull Collection newGroups); + void onDispatchAsyncResult(List mergedGroups); } - private final Callback asyncDiffUtilCallback; + final Callback asyncDiffUtilCallback; private int maxScheduledGeneration; - private Collection groups; AsyncDiffUtil(@NonNull Callback callback) { this.asyncDiffUtilCallback = callback; } - @NonNull - Callback getAsyncDiffUtilCallback() { - return asyncDiffUtilCallback; - } - - @NonNull - Collection getGroups() { - return groups; - } - int getMaxScheduledGeneration() { return maxScheduledGeneration; } - void calculateDiff(@NonNull Collection newGroups, - @NonNull DiffUtil.Callback diffUtilCallback, - @Nullable OnAsyncUpdateListener onAsyncUpdateListener, + void calculateDiff(@NonNull List oldGroups, + @NonNull List newGroups, + @Nullable final OnAsyncUpdateListener onAsyncUpdateListener, boolean detectMoves) { - groups = newGroups; // incrementing generation means any currently-running diffs are discarded when they finish final int runGeneration = ++maxScheduledGeneration; + final DiffCallback diffUtilCallback = new DiffCallback(oldGroups, newGroups); new DiffTask(this, diffUtilCallback, runGeneration, detectMoves, onAsyncUpdateListener).execute(); } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/xwray/groupie/DiffCallback.java b/library/src/main/java/com/xwray/groupie/DiffCallback.java index 88188cb..5169b46 100644 --- a/library/src/main/java/com/xwray/groupie/DiffCallback.java +++ b/library/src/main/java/com/xwray/groupie/DiffCallback.java @@ -1,21 +1,28 @@ package com.xwray.groupie; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; class DiffCallback extends DiffUtil.Callback { private final int oldBodyItemCount; private final int newBodyItemCount; - private final Collection oldGroups; - private final Collection newGroups; + private final List oldList; + private final List newList; + + private final Map changeMap = new HashMap<>(); DiffCallback(Collection oldGroups, Collection newGroups) { this.oldBodyItemCount = GroupUtils.getItemCount(oldGroups); this.newBodyItemCount = GroupUtils.getItemCount(newGroups); - this.oldGroups = oldGroups; - this.newGroups = newGroups; + this.oldList = flatToItem(oldGroups); + this.newList = flatToItem(newGroups); } @Override @@ -30,23 +37,56 @@ public int getNewListSize() { @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - Item oldItem = GroupUtils.getItem(oldGroups, oldItemPosition); - Item newItem = GroupUtils.getItem(newGroups, newItemPosition); + Item oldItem = GroupUtils.getItem(oldList, oldItemPosition); + Item newItem = GroupUtils.getItem(newList, newItemPosition); return newItem.isSameAs(oldItem); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - Item oldItem = GroupUtils.getItem(oldGroups, oldItemPosition); - Item newItem = GroupUtils.getItem(newGroups, newItemPosition); - return newItem.hasSameContentAs(oldItem); + Item oldItem = GroupUtils.getItem(oldList, oldItemPosition); + Item newItem = GroupUtils.getItem(newList, newItemPosition); + boolean sameContent = newItem.hasSameContentAs(oldItem); + + //false means changed + if (sameContent) { + changeMap.put(newItemPosition, oldItemPosition); + } + return sameContent; } @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) { - Item oldItem = GroupUtils.getItem(oldGroups, oldItemPosition); - Item newItem = GroupUtils.getItem(newGroups, newItemPosition); + Item oldItem = GroupUtils.getItem(oldList, oldItemPosition); + Item newItem = GroupUtils.getItem(newList, newItemPosition); return oldItem.getChangePayload(newItem); } + + @NonNull + public List mergeGroups() { + ArrayList resultList = new ArrayList<>(); + + for (int i = 0; i < newList.size(); i++) { + Integer position = changeMap.get(i); + + if (position == null) { + resultList.add(newList.get(i)); + } else { + resultList.add(oldList.get(position)); + } + } + return resultList; + } + + private List flatToItem(Collection groups) { + List result = new ArrayList<>(); + for (Group group : groups) { + final int size = group.getItemCount(); + for (int i = 0; i < size; i++) { + result.add(group.getItem(i)); + } + } + return result; + } } \ No newline at end of file diff --git a/library/src/main/java/com/xwray/groupie/DiffTask.java b/library/src/main/java/com/xwray/groupie/DiffTask.java index 7b1c10e..968a5f6 100644 --- a/library/src/main/java/com/xwray/groupie/DiffTask.java +++ b/library/src/main/java/com/xwray/groupie/DiffTask.java @@ -1,30 +1,32 @@ package com.xwray.groupie; import android.os.AsyncTask; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import java.lang.ref.WeakReference; import java.util.Collection; +import java.util.List; /** * An async task implementation that runs {@link DiffUtil#calculateDiff(DiffUtil.Callback)} - * in a background thread. This task will call {@link AsyncDiffUtil.Callback#onDispatchAsyncResult(Collection)} - * passing the new list just before dispatching the diff result to the provided - * {@link DiffUtil.Callback} so that the new list. - *

This task is executed via {@link AsyncDiffUtil#calculateDiff(Collection, DiffUtil.Callback, OnAsyncUpdateListener, boolean)}. + * in a background thread. + *

This task is executed via {@link AsyncDiffUtil#calculateDiff(List, List, OnAsyncUpdateListener, boolean)}. */ class DiffTask extends AsyncTask { - @NonNull private final DiffUtil.Callback diffCallback; + @NonNull + private final DiffCallback diffCallback; private final WeakReference asyncListDiffer; private final int runGeneration; private final boolean detectMoves; - @Nullable private WeakReference onAsyncUpdateListener; + @Nullable + private WeakReference onAsyncUpdateListener; private Exception backgroundException = null; DiffTask(@NonNull AsyncDiffUtil asyncDiffUtil, - @NonNull DiffUtil.Callback callback, + @NonNull DiffCallback callback, int runGeneration, boolean detectMoves, @Nullable OnAsyncUpdateListener onAsyncUpdateListener) { @@ -54,16 +56,21 @@ protected void onPostExecute(@Nullable DiffUtil.DiffResult diffResult) { throw new RuntimeException(backgroundException); } AsyncDiffUtil async = asyncListDiffer.get(); - if (shouldDispatchResult(diffResult, async)) { - async.getAsyncDiffUtilCallback().onDispatchAsyncResult(async.getGroups()); - diffResult.dispatchUpdatesTo(async.getAsyncDiffUtilCallback()); - if (onAsyncUpdateListener != null && onAsyncUpdateListener.get() != null) { - onAsyncUpdateListener.get().onUpdateComplete(); - } + + if (!shouldDispatchResult(diffResult, async)) { + return; + } + + AsyncDiffUtil.Callback asyncDiffUtilCallback = async.asyncDiffUtilCallback; + asyncDiffUtilCallback.onDispatchAsyncResult(diffCallback.mergeGroups()); + + diffResult.dispatchUpdatesTo(asyncDiffUtilCallback); + if (onAsyncUpdateListener != null && onAsyncUpdateListener.get() != null) { + onAsyncUpdateListener.get().onUpdateComplete(); } } private boolean shouldDispatchResult(@Nullable DiffUtil.DiffResult diffResult, AsyncDiffUtil async) { return diffResult != null && async != null && runGeneration == async.getMaxScheduledGeneration(); } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/xwray/groupie/GroupAdapter.java b/library/src/main/java/com/xwray/groupie/GroupAdapter.java index 80434de..a078763 100644 --- a/library/src/main/java/com/xwray/groupie/GroupAdapter.java +++ b/library/src/main/java/com/xwray/groupie/GroupAdapter.java @@ -8,16 +8,16 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; -import java.util.Collection; import java.util.List; /** * An adapter that holds a list of Groups. */ -public class GroupAdapter extends RecyclerView.Adapter implements GroupDataObserver { +public class GroupAdapter extends RecyclerView.Adapter implements GroupDataObserver, AsyncDiffUtil.Callback { private final List groups = new ArrayList<>(); private OnItemClickListener onItemClickListener; @@ -25,34 +25,7 @@ public class GroupAdapter extends RecyclerView.Ada private int spanCount = 1; private Item lastItemForViewTypeLookup; - private AsyncDiffUtil.Callback diffUtilCallbacks = new AsyncDiffUtil.Callback() { - @Override - public void onDispatchAsyncResult(@NonNull Collection newGroups) { - setNewGroups(newGroups); - } - - @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, Object payload) { - notifyItemRangeChanged(position, count, payload); - } - }; - - private AsyncDiffUtil asyncDiffUtil = new AsyncDiffUtil(diffUtilCallbacks); + private final AsyncDiffUtil asyncDiffUtil = new AsyncDiffUtil(this); private final GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.SpanSizeLookup() { @Override @@ -82,38 +55,36 @@ public int getSpanCount() { /** * Updates the adapter with a new list that will be diffed on a background thread * and displayed once diff results are calculated. - * + *

* NOTE: This update method is NOT compatible with partial updates (change notifications * driven by individual groups and items). If you update using this method, all partial * updates will no longer work and you must use this method to update exclusively. *

* If you want to receive a callback once the update is complete call the * {@link #updateAsync(List, boolean, OnAsyncUpdateListener)} version - * + *

* This will default detectMoves to true. * * @param newGroups List of {@link Group} */ - @SuppressWarnings("unused") - public void updateAsync(@NonNull final List newGroups) { + public void updateAsync(@NonNull final List newGroups) { this.updateAsync(newGroups, true, null); } /** * Updates the adapter with a new list that will be diffed on a background thread * and displayed once diff results are calculated. - * + *

* NOTE: This update method is NOT compatible with partial updates (change notifications * driven by individual groups and items). If you update using this method, all partial * updates will no longer work and you must use this method to update exclusively. *

- * + *

* This will default detectMoves to true. * - * @see #updateAsync(List, boolean, OnAsyncUpdateListener) * @param newGroups List of {@link Group} + * @see #updateAsync(List, boolean, OnAsyncUpdateListener) */ - @SuppressWarnings("unused") public void updateAsync(@NonNull final List newGroups, @Nullable final OnAsyncUpdateListener onAsyncUpdateListener) { this.updateAsync(newGroups, true, onAsyncUpdateListener); } @@ -121,17 +92,16 @@ public void updateAsync(@NonNull final List newGroups, @Nullabl /** * Updates the adapter with a new list that will be diffed on a background thread * and displayed once diff results are calculated. - * + *

* NOTE: This update method is NOT compatible with partial updates (change notifications * driven by individual groups and items). If you update using this method, all partial * updates will no longer work and you must use this method to update exclusively. * - * @param newGroups List of {@link Group} + * @param newGroups List of {@link Group} * @param onAsyncUpdateListener Optional callback for when the async update is complete - * @param detectMoves Boolean is passed to {@link DiffUtil#calculateDiff(DiffUtil.Callback, boolean)}. Set to true - * if you want DiffUtil to detect moved items. + * @param detectMoves Boolean is passed to {@link DiffUtil#calculateDiff(DiffUtil.Callback, boolean)}. Set to true + * if you want DiffUtil to detect moved items. */ - @SuppressWarnings("unused") public void updateAsync(@NonNull final List newGroups, boolean detectMoves, @Nullable final OnAsyncUpdateListener onAsyncUpdateListener) { // Fast simple first insert if (groups.isEmpty()) { @@ -141,21 +111,18 @@ public void updateAsync(@NonNull final List newGroups, boolean } return; } - final List oldGroups = new ArrayList<>(groups); - final DiffCallback diffUtilCallback = new DiffCallback(oldGroups, newGroups); - asyncDiffUtil.calculateDiff(newGroups, diffUtilCallback, onAsyncUpdateListener, detectMoves); + asyncDiffUtil.calculateDiff(groups, newGroups, onAsyncUpdateListener, detectMoves); } /** * Replaces the groups within the adapter without using DiffUtil, and therefore without animations. - * - * For animation support, use {@link GroupAdapter#update(Collection)} or {@link GroupAdapter#updateAsync(List)} instead. + *

+ * For animation support, use {@link GroupAdapter#update(List)} or {@link GroupAdapter#updateAsync(List)} instead. * * @param newGroups List of {@link Group} */ - @SuppressWarnings("unused") - public void replaceAll(@NonNull final Collection newGroups) { + public void replaceAll(@NonNull final List newGroups) { setNewGroups(newGroups); notifyDataSetChanged(); } @@ -163,34 +130,34 @@ public void replaceAll(@NonNull final Collection newGroups) { /** * Updates the adapter with a new list that will be diffed on the main thread * and displayed once diff results are calculated. Not recommended for huge lists. - * + *

* This will default detectMoves to true. * * @param newGroups List of {@link Group} */ - @SuppressWarnings("unused") - public void update(@NonNull final Collection newGroups) { + public void update(@NonNull final List newGroups) { update(newGroups, true); } /** * Updates the adapter with a new list that will be diffed on the main thread * and displayed once diff results are calculated. Not recommended for huge lists. - * @param newGroups List of {@link Group} + * + * @param newGroups List of {@link Group} * @param detectMoves is passed to {@link DiffUtil#calculateDiff(DiffUtil.Callback, boolean)}. Set to false * if you don't want DiffUtil to detect moved items. */ - @SuppressWarnings("unused") - public void update(@NonNull final Collection newGroups, boolean detectMoves) { + public void update(@NonNull final List newGroups, boolean detectMoves) { final List oldGroups = new ArrayList<>(groups); + DiffCallback diffCallback = new DiffCallback(oldGroups, newGroups); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( - new DiffCallback(oldGroups, newGroups), + diffCallback, detectMoves ); - setNewGroups(newGroups); - - diffResult.dispatchUpdatesTo(diffUtilCallbacks); + setNewGroups(diffCallback.mergeGroups()); + diffResult.dispatchUpdatesTo((ListUpdateCallback) this); } /** @@ -213,6 +180,31 @@ public void setOnItemLongClickListener(@Nullable OnItemLongClickListener onItemL this.onItemLongClickListener = onItemLongClickListener; } + @Override + public void onDispatchAsyncResult(List mergedGroups) { + setNewGroups(mergedGroups); + } + + @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, Object payload) { + notifyItemRangeChanged(position, count, payload); + } + @Override @NonNull public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -264,8 +256,6 @@ public void onViewDetachedFromWindow(@NonNull VH holder) { @Override public int getItemViewType(int position) { lastItemForViewTypeLookup = getItem(position); - if (lastItemForViewTypeLookup == null) - throw new RuntimeException("Invalid position " + position); return lastItemForViewTypeLookup.getViewType(); } @@ -274,11 +264,13 @@ public long getItemId(int position) { return getItem(position).getId(); } - public @NonNull Item getItem(@NonNull VH holder) { + public @NonNull + Item getItem(@NonNull VH holder) { return holder.getItem(); } - public @NonNull Item getItem(int position) { + public @NonNull + Item getItem(int position) { return GroupUtils.getItem(groups, position); } @@ -294,9 +286,6 @@ public int getAdapterPosition(@NonNull Item contentItem) { /** * The position in the flat list of individual items at which the group starts - * - * @param group - * @return */ public int getAdapterPosition(@NonNull Group group) { int index = groups.indexOf(group); @@ -335,6 +324,7 @@ public int getItemCountForGroup(int groupIndex) { /** * This returns the total number of items contained in the top level group at the passed index + * * @deprecated This method has been deprecated in favour of {@link #getItemCountForGroup(int)}. Please use that method instead. */ @Deprecated @@ -351,7 +341,6 @@ public void clear() { } public void add(@NonNull Group group) { - if (group == null) throw new RuntimeException("Group cannot be null"); int itemCountBeforeGroup = getItemCount(); group.registerGroupDataObserver(this); groups.add(group); @@ -361,10 +350,8 @@ public void add(@NonNull Group group) { /** * Adds the contents of the list of groups, in order, to the end of the adapter contents. * All groups in the list must be non-null. - * - * @param groups */ - public void addAll(@NonNull Collection groups) { + public void addAll(@NonNull List groups) { if (groups.contains(null)) throw new RuntimeException("List of groups can't contain null!"); int itemCountBeforeGroup = getItemCount(); int additionalSize = 0; @@ -377,12 +364,11 @@ public void addAll(@NonNull Collection groups) { } public void remove(@NonNull Group group) { - if (group == null) throw new RuntimeException("Group cannot be null"); int position = groups.indexOf(group); remove(position, group); } - public void removeAll(@NonNull Collection groups) { + public void removeAll(@NonNull List groups) { for (Group group : groups) { remove(group); } @@ -390,6 +376,7 @@ public void removeAll(@NonNull Collection groups) { /** * Remove a Group at a raw adapter position + * * @param position raw adapter position of Group to remove */ public void removeGroupAtAdapterPosition(int position) { @@ -399,6 +386,7 @@ public void removeGroupAtAdapterPosition(int position) { /** * Remove a Group at a raw adapter position. + * * @param adapterPosition raw adapter position of Group to remove. * @deprecated This method has been deprecated in favor of {@link #removeGroupAtAdapterPosition(int)}. Please use that method instead. */ @@ -415,7 +403,6 @@ private void remove(int position, @NonNull Group group) { } public void add(int index, @NonNull Group group) { - if (group == null) throw new RuntimeException("Group cannot be null"); group.registerGroupDataObserver(this); groups.add(index, group); int itemCountBeforeGroup = getItemCountBeforeGroup(index); @@ -472,6 +459,7 @@ public Group getGroup(int adapterPosition) { /** * Returns the Group which contains this item or throws an {@link IndexOutOfBoundsException} if not present. * This is the item's direct parent, not necessarily one of the top level groups present in this adapter. + * * @param contentItem Item to find the parent group for. * @return Parent group of this item. */ @@ -581,7 +569,7 @@ private int getItemCountBeforeGroup(int groupIndex) { return count; } - private void setNewGroups(@NonNull Collection newGroups) { + private void setNewGroups(@NonNull List newGroups) { for (Group group : groups) { group.unregisterGroupDataObserver(this); } @@ -594,4 +582,4 @@ private void setNewGroups(@NonNull Collection newGroups) { } } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/xwray/groupie/GroupUtils.java b/library/src/main/java/com/xwray/groupie/GroupUtils.java index 5ad3dc0..5dd4afb 100644 --- a/library/src/main/java/com/xwray/groupie/GroupUtils.java +++ b/library/src/main/java/com/xwray/groupie/GroupUtils.java @@ -6,7 +6,7 @@ class GroupUtils { @NonNull - static Item getItem(Collection groups, int position) { + static Item getItem(Collection groups, int position) throws IndexOutOfBoundsException { int previousPosition = 0; for (Group group : groups) {