diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2020-02-07 20:01:19 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-02-07 20:01:19 +0000 |
commit | b33d46b14166c25a7b8ca81c01334a6dbc7582eb (patch) | |
tree | 4d861e241f9c3ca10a5d38f9b146c6730d39c631 | |
parent | 7639caa34a0ac21f64327ecbc3ce1a7da8730c09 (diff) | |
parent | 4c715c2f54bc171e7850b86f86015c05d6d8a5e3 (diff) | |
download | support-b33d46b14166c25a7b8ca81c01334a6dbc7582eb.tar.gz |
Merge "RecyclerView MergeAdapter" into androidx-master-dev
50 files changed, 3180 insertions, 154 deletions
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java index 5f774ae3da7..73df6275b47 100644 --- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java +++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java @@ -237,7 +237,7 @@ public class LeanbackListPreferenceDialogFragment extends LeanbackPreferenceDial @Override public void onItemClick(ViewHolder viewHolder) { - final int index = viewHolder.getAdapterPosition(); + final int index = viewHolder.getAbsoluteAdapterPosition(); if (index == RecyclerView.NO_POSITION) { return; } @@ -298,7 +298,7 @@ public class LeanbackListPreferenceDialogFragment extends LeanbackPreferenceDial @Override public void onItemClick(ViewHolder viewHolder) { - final int index = viewHolder.getAdapterPosition(); + final int index = viewHolder.getAbsoluteAdapterPosition(); if (index == RecyclerView.NO_POSITION) { return; } diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java index 88fd21db81e..5dbf27dba56 100644 --- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java +++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java @@ -240,7 +240,7 @@ public class LeanbackListPreferenceDialogFragmentCompat extends @Override public void onItemClick(ViewHolder viewHolder) { - final int index = viewHolder.getAdapterPosition(); + final int index = viewHolder.getAbsoluteAdapterPosition(); if (index == RecyclerView.NO_POSITION) { return; } @@ -295,7 +295,7 @@ public class LeanbackListPreferenceDialogFragmentCompat extends @Override public void onItemClick(ViewHolder viewHolder) { - final int index = viewHolder.getAdapterPosition(); + final int index = viewHolder.getAbsoluteAdapterPosition(); if (index == RecyclerView.NO_POSITION) { return; } diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java index 408ef9495f6..34764dadf91 100644 --- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java +++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java @@ -237,7 +237,7 @@ public class GridActivity extends Activity { } if (mRequestLayoutOnFocus) { RecyclerView.ViewHolder vh = mGridView.getChildViewHolder(v); - int position = vh.getAdapterPosition(); + int position = vh.getAbsoluteAdapterPosition(); updateSize(v, position); } } @@ -393,7 +393,7 @@ public class GridActivity extends Activity { if (mRequestLayoutOnFocus) { if (v == view) { RecyclerView.ViewHolder vh = mGridView.getChildViewHolder(v); - int position = vh.getAdapterPosition(); + int position = vh.getAbsoluteAdapterPosition(); updateSize(v, position); } view.requestLayout(); diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java index a14925b8eaa..8b44059add3 100644 --- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java +++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java @@ -1581,7 +1581,7 @@ public class GridWidgetTest { int scrollPos = 0; while (true) { final View view = mGridView.getChildAt(mGridView.getChildCount() - 1); - final int pos = mGridView.getChildViewHolder(view).getAdapterPosition(); + final int pos = mGridView.getChildViewHolder(view).getAbsoluteAdapterPosition(); if (scrollPos != pos) { scrollPos = pos; mActivityTestRule.runOnUiThread(new Runnable() { @@ -2509,7 +2509,8 @@ public class GridWidgetTest { @Override public void run() { final int removeIndex = mGridView.getChildViewHolder( - mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition(); + mGridView.getChildAt( + mGridView.getChildCount() - 1)).getAbsoluteAdapterPosition(); mActivity.removeItems(removeIndex, 1); } }); @@ -2682,7 +2683,8 @@ public class GridWidgetTest { @Override public void run() { final int removeIndex = mGridView.getChildViewHolder( - mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition(); + mGridView.getChildAt( + mGridView.getChildCount() - 1)).getAbsoluteAdapterPosition(); mActivity.removeItems(removeIndex, 1); } }); diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java index 36cac480503..8e2d5bdb958 100644 --- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java +++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java @@ -667,7 +667,7 @@ public class DetailsFragment extends BaseFragment { RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter(); onSetRowStatus(rowPresenter, rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()), - bridgeViewHolder.getAdapterPosition(), + bridgeViewHolder.getAbsoluteAdapterPosition(), selectedPosition, selectedSubPosition); } } diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java index 9b0fae62d0c..e889c3cd6e1 100644 --- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java +++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java @@ -662,7 +662,7 @@ public class DetailsSupportFragment extends BaseSupportFragment { RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter(); onSetRowStatus(rowPresenter, rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()), - bridgeViewHolder.getAdapterPosition(), + bridgeViewHolder.getAbsoluteAdapterPosition(), selectedPosition, selectedSubPosition); } } diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java index 379587baa3e..de3270ca55d 100644 --- a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java +++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java @@ -2120,7 +2120,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } int totalItems = 0; for (int i = 0; i < scrapSize; i++) { - int pos = scrapList.get(i).getAdapterPosition(); + int pos = scrapList.get(i).getAbsoluteAdapterPosition(); if (pos >= 0) { mDisappearingPositions[totalItems++] = pos; } @@ -3658,7 +3658,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } void onChildRecycled(RecyclerView.ViewHolder holder) { - final int position = holder.getAdapterPosition(); + final int position = holder.getAbsoluteAdapterPosition(); if (position != NO_POSITION) { mChildrenStates.saveOffscreenView(holder.itemView, position); } diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/RecyclerViewParallax.java b/leanback/leanback/src/main/java/androidx/leanback/widget/RecyclerViewParallax.java index 8a1fd146da3..b36f1590c84 100644 --- a/leanback/leanback/src/main/java/androidx/leanback/widget/RecyclerViewParallax.java +++ b/leanback/leanback/src/main/java/androidx/leanback/widget/RecyclerViewParallax.java @@ -155,7 +155,7 @@ public class RecyclerViewParallax extends Parallax<RecyclerViewParallax.ChildPos } View firstChild = recyclerView.getLayoutManager().getChildAt(0); ViewHolder vh = recyclerView.findContainingViewHolder(firstChild); - int firstPosition = vh.getAdapterPosition(); + int firstPosition = vh.getAbsoluteAdapterPosition(); if (firstPosition < mAdapterPosition) { source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER); } else { diff --git a/recyclerview/recyclerview/api/1.2.0-alpha01.txt b/recyclerview/recyclerview/api/1.2.0-alpha01.txt index 89f44795785..344616858bd 100644 --- a/recyclerview/recyclerview/api/1.2.0-alpha01.txt +++ b/recyclerview/recyclerview/api/1.2.0-alpha01.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -888,7 +920,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt index 89f44795785..344616858bd 100644 --- a/recyclerview/recyclerview/api/current.txt +++ b/recyclerview/recyclerview/api/current.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -888,7 +920,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/api/public_plus_experimental_1.2.0-alpha01.txt b/recyclerview/recyclerview/api/public_plus_experimental_1.2.0-alpha01.txt index 89f44795785..344616858bd 100644 --- a/recyclerview/recyclerview/api/public_plus_experimental_1.2.0-alpha01.txt +++ b/recyclerview/recyclerview/api/public_plus_experimental_1.2.0-alpha01.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -888,7 +920,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/api/public_plus_experimental_current.txt b/recyclerview/recyclerview/api/public_plus_experimental_current.txt index 89f44795785..344616858bd 100644 --- a/recyclerview/recyclerview/api/public_plus_experimental_current.txt +++ b/recyclerview/recyclerview/api/public_plus_experimental_current.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -888,7 +920,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/api/restricted_1.2.0-alpha01.txt b/recyclerview/recyclerview/api/restricted_1.2.0-alpha01.txt index 9a8954b2d5c..4d87eb87c6d 100644 --- a/recyclerview/recyclerview/api/restricted_1.2.0-alpha01.txt +++ b/recyclerview/recyclerview/api/restricted_1.2.0-alpha01.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, @androidx.recyclerview.widget.RecyclerView.Orientation int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -891,7 +923,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt index 9a8954b2d5c..4d87eb87c6d 100644 --- a/recyclerview/recyclerview/api/restricted_current.txt +++ b/recyclerview/recyclerview/api/restricted_current.txt @@ -323,6 +323,35 @@ package androidx.recyclerview.widget { method public void onRemoved(int, int); } + public final class MergeAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder> { + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor @java.lang.SafeVarargs public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!...); + ctor public MergeAdapter(java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + ctor public MergeAdapter(androidx.recyclerview.widget.MergeAdapter.Config, java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!>); + method public boolean addAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public boolean addAdapter(int, androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + method public java.util.List<androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>!> getCopyOfAdapters(); + method public int getItemCount(); + method public void onBindViewHolder(androidx.recyclerview.widget.RecyclerView.ViewHolder, int); + method public androidx.recyclerview.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int); + method public boolean onFailedToRecycleView(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewAttachedToWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewDetachedFromWindow(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public void onViewRecycled(androidx.recyclerview.widget.RecyclerView.ViewHolder); + method public boolean removeAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>); + } + + public static class MergeAdapter.Config { + field public static final androidx.recyclerview.widget.MergeAdapter.Config DEFAULT; + field public final boolean isolateViewTypes; + } + + public static class MergeAdapter.Config.Builder { + ctor public MergeAdapter.Config.Builder(); + method public androidx.recyclerview.widget.MergeAdapter.Config build(); + method public androidx.recyclerview.widget.MergeAdapter.Config.Builder setIsolateViewTypes(boolean); + } + public abstract class OrientationHelper { method public static androidx.recyclerview.widget.OrientationHelper! createHorizontalHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!); method public static androidx.recyclerview.widget.OrientationHelper! createOrientationHelper(androidx.recyclerview.widget.RecyclerView.LayoutManager!, @androidx.recyclerview.widget.RecyclerView.Orientation int); @@ -469,6 +498,7 @@ package androidx.recyclerview.widget { ctor public RecyclerView.Adapter(); method public final void bindViewHolder(VH, int); method public final VH createViewHolder(android.view.ViewGroup, int); + method public int findRelativeAdapterPositionIn(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>, androidx.recyclerview.widget.RecyclerView.ViewHolder, int); method public abstract int getItemCount(); method public long getItemId(int); method public int getItemViewType(int); @@ -760,7 +790,9 @@ package androidx.recyclerview.widget { ctor public RecyclerView.LayoutParams(android.view.ViewGroup.MarginLayoutParams!); ctor public RecyclerView.LayoutParams(android.view.ViewGroup.LayoutParams!); ctor public RecyclerView.LayoutParams(androidx.recyclerview.widget.RecyclerView.LayoutParams!); - method public int getViewAdapterPosition(); + method public int getAbsoluteAdapterPosition(); + method public int getBindingAdapterPosition(); + method @Deprecated public int getViewAdapterPosition(); method public int getViewLayoutPosition(); method @Deprecated public int getViewPosition(); method public boolean isItemChanged(); @@ -891,7 +923,10 @@ package androidx.recyclerview.widget { public abstract static class RecyclerView.ViewHolder { ctor public RecyclerView.ViewHolder(android.view.View); - method public final int getAdapterPosition(); + method public final int getAbsoluteAdapterPosition(); + method @Deprecated public final int getAdapterPosition(); + method public final androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getBindingAdapter(); + method public final int getBindingAdapterPosition(); method public final long getItemId(); method public final int getItemViewType(); method public final int getLayoutPosition(); diff --git a/recyclerview/recyclerview/res/values/ids.xml b/recyclerview/recyclerview/res/values/ids.xml index fba1db4d74e..4ed0ecd89a7 100644 --- a/recyclerview/recyclerview/res/values/ids.xml +++ b/recyclerview/recyclerview/res/values/ids.xml @@ -16,4 +16,4 @@ <resources> <!-- ItemTouchHelper uses this id to save a View's original elevation. --> <item type="id" name="item_touch_helper_previous_elevation"/> -</resources> +</resources>
\ No newline at end of file diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java index 0ed5a9f8bb9..16e231735d8 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java @@ -381,6 +381,7 @@ abstract public class BaseRecyclerViewInstrumentationTest { @Override public void putRecycledView(RecyclerView.ViewHolder scrap) { assertNull(scrap.mOwnerRecyclerView); + assertNull(scrap.getBindingAdapter()); super.putRecycledView(scrap); } }; @@ -395,7 +396,7 @@ abstract public class BaseRecyclerViewInstrumentationTest { if (!vh.isRemoved()) { assertNotSame("If getItemOffsets is called, child should have a valid" + " adapter position unless it is removed : " + vh, - vh.getAdapterPosition(), RecyclerView.NO_POSITION); + vh.getAbsoluteAdapterPosition(), RecyclerView.NO_POSITION); } } }); @@ -911,7 +912,8 @@ abstract public class BaseRecyclerViewInstrumentationTest { @Override public void onBindViewHolder(@NonNull TestViewHolder holder, int position) { assertNotNull(holder.mOwnerRecyclerView); - assertEquals(position, holder.getAdapterPosition()); + assertSame(this, holder.getBindingAdapter()); + assertEquals(position, holder.getAbsoluteAdapterPosition()); final Item item = mItems.get(position); getTextViewInHolder(holder).setText(item.getDisplayText()); holder.itemView.setBackgroundColor(position % 2 == 0 ? 0xFFFF0000 : 0xFF0000FF); @@ -932,7 +934,7 @@ abstract public class BaseRecyclerViewInstrumentationTest { @Override public void onViewRecycled(@NonNull TestViewHolder holder) { super.onViewRecycled(holder); - final int adapterPosition = holder.getAdapterPosition(); + final int adapterPosition = holder.getAbsoluteAdapterPosition(); final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && !holder.isAdapterPositionUnknown() && !holder.isInvalid(); String log = "Position check for " + holder.toString(); diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerCacheTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerCacheTest.java index 8306bf22165..94db40d6d68 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerCacheTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerCacheTest.java @@ -70,7 +70,7 @@ public class GridLayoutManagerCacheTest extends BaseGridLayoutManagerTest { private boolean cachedViewsContains(int position) { // Note: can't make assumptions about order here, so just check all cached views for (int i = 0; i < cachedViews().size(); i++) { - if (cachedViews().get(i).getAdapterPosition() == position) return true; + if (cachedViews().get(i).getAbsoluteAdapterPosition() == position) return true; } return false; } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java index 3ca041c05fb..d667e964065 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java @@ -138,6 +138,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecation") // used for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -148,7 +149,6 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -177,7 +177,8 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { waitForIdleScroll(recyclerView); focusedView = recyclerView.getFocusedChild(); assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1), - recyclerView.getChildViewHolder(focusedView).getAdapterPosition()); + recyclerView + .getChildViewHolder(focusedView).getAbsoluteAdapterPosition()); pos += 3; } } @@ -335,6 +336,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -343,7 +345,6 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -425,6 +426,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -433,7 +435,6 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -520,6 +521,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { new GridTestAdapter(itemCount, 1) { @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -528,7 +530,6 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -611,6 +612,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { .orientation(HORIZONTAL).reverseLayout(false), new GridTestAdapter(itemCount, 1) { @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -619,7 +621,6 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -1158,7 +1159,7 @@ public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { for (int i = 0; i < childCount; i++) { View child = mGlm.getChildAt(i); RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); - if (holder.getAdapterPosition() == removePos) { + if (holder.getAbsoluteAdapterPosition() == removePos) { toBeRemoved = holder; } else { toBeMoved.add(holder); diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ItemTouchHelperTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ItemTouchHelperTest.java index 90e7ee27103..eb54e5411b4 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ItemTouchHelperTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ItemTouchHelperTest.java @@ -433,8 +433,8 @@ public class ItemTouchHelperTest extends BaseRecyclerViewInstrumentationTest { MoveRecord(RecyclerView.ViewHolder from, RecyclerView.ViewHolder to) { this.from = from; this.to = to; - fromPos = from.getAdapterPosition(); - toPos = to.getAdapterPosition(); + fromPos = from.getAbsoluteAdapterPosition(); + toPos = to.getAbsoluteAdapterPosition(); } } } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerBaseConfigSetTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerBaseConfigSetTest.java index 70d87ff3a99..d5a7937f459 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerBaseConfigSetTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerBaseConfigSetTest.java @@ -259,7 +259,7 @@ public class LinearLayoutManagerBaseConfigSetTest extends BaseLinearLayoutManage scrollBy(size * 2); assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); - assertThat(vh.getAdapterPosition(), is(500)); + assertThat(vh.getAbsoluteAdapterPosition(), is(500)); scrollBy(size * 2); assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); } @@ -293,7 +293,7 @@ public class LinearLayoutManagerBaseConfigSetTest extends BaseLinearLayoutManage scrollBy(-size * 2); assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); - assertThat(vh.getAdapterPosition(), is(500)); + assertThat(vh.getAbsoluteAdapterPosition(), is(500)); scrollBy(-size * 2); assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerCacheTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerCacheTest.java index 6a79e4ef099..bc34cb9b46a 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerCacheTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerCacheTest.java @@ -109,12 +109,14 @@ public class LinearLayoutManagerCacheTest extends BaseLinearLayoutManagerTest { int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); assertEquals(1, cachedViews().size()); - int prefetchedPosition = cachedViews().get(0).getAdapterPosition(); + int prefetchedPosition = cachedViews().get(0).getAbsoluteAdapterPosition(); if (mConfig.mReverseLayout == reverseScroll) { - // Pos scroll on pos layout, or reverse scroll on reverse layout = toward last + // Pos scroll on pos layout, or reverse scroll on reverse layout = toward + // last assertEquals(lastVisibleItemPosition + 1, prefetchedPosition); } else { - // Pos scroll on reverse layout, or reverse scroll on pos layout = toward first + // Pos scroll on reverse layout, or reverse scroll on pos layout = toward + // first assertEquals(firstVisibleItemPosition - 1, prefetchedPosition); } } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java index 42f95207915..9a497d83852 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java @@ -209,6 +209,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // Good to have colors for debugging @@ -216,7 +217,6 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -295,6 +295,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // Good to have colors for debugging @@ -302,7 +303,6 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -387,6 +387,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // Good to have colors for debugging @@ -394,7 +395,6 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -482,6 +482,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // Good to have colors for debugging @@ -489,7 +490,6 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -576,6 +576,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // Good to have colors for debugging @@ -583,7 +584,6 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -772,7 +772,7 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { for (int i = 0; i < childCount; i++) { View child = mLayoutManager.getChildAt(i); RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); - if (holder.getAdapterPosition() == removePos) { + if (holder.getAbsoluteAdapterPosition() == removePos) { toBeRemoved = holder; } else { toBeMoved.add(holder); diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterSubject.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterSubject.kt new file mode 100644 index 00000000000..f884f95e473 --- /dev/null +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterSubject.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat + +/** + * Helper subject to write nicer looking MergeAdapter tests. + */ +internal class MergeAdapterSubject( + metadata: FailureMetadata, + private val adapter: MergeAdapter +) : Subject( + metadata, + adapter +) { + fun hasItemCount(itemCount: Int) { + assertThat(adapter.itemCount).isEqualTo(itemCount) + } + + fun hasStateRestorationStrategy(strategy: RecyclerView.Adapter.StateRestorationStrategy) { + assertThat(adapter.stateRestorationStrategy).isEqualTo(strategy) + } + + fun bindView( + recyclerView: RecyclerView, + globalPosition: Int + ): BindingSubject { + if (recyclerView.adapter == null) { + recyclerView.adapter = adapter + } else { + check(recyclerView.adapter == adapter) { + "recyclerview is bound to another adapter" + } + } + // clear state + recyclerView.mState.apply { + mItemCount = adapter.itemCount + mLayoutStep = RecyclerView.State.STEP_LAYOUT + } + return assertAbout( + BindingSubject.Factory( + recyclerView = recyclerView + ) + ).that(globalPosition) + } + + fun canRestoreState() { + assertThat(adapter.canRestoreState()).isTrue() + } + + fun cannotRestoreState() { + assertThat(adapter.canRestoreState()).isFalse() + } + + object Factory : Subject.Factory<MergeAdapterSubject, MergeAdapter> { + override fun createSubject(metadata: FailureMetadata, actual: MergeAdapter): + MergeAdapterSubject { + return MergeAdapterSubject( + metadata = metadata, + adapter = actual + ) + } + } + + companion object { + fun assertThat(mergeAdapter: MergeAdapter) = + assertAbout(Factory).that(mergeAdapter) + } + + class BindingSubject( + metadata: FailureMetadata, + recyclerView: RecyclerView, + globalPosition: Int + ) : Subject( + metadata, + globalPosition + ) { + private val viewHolder by lazy { + val view = recyclerView.mRecycler.getViewForPosition(globalPosition) + val layoutParams = view.layoutParams + check(layoutParams is RecyclerView.LayoutParams) + val viewHolder = layoutParams.mViewHolder + viewHolder as MergeAdapterTest.MergeAdapterViewHolder + } + + internal fun verifyBoundTo( + adapter: MergeAdapterTest.NestedTestAdapter, + localPosition: Int + ) { + assertThat(viewHolder.boundItem()).isEqualTo(adapter.getItemAt(localPosition)) + assertThat(viewHolder.boundLocalPosition()).isEqualTo(localPosition) + assertThat(viewHolder.boundAdapter()).isSameInstanceAs(adapter) + } + + class Factory( + private val recyclerView: RecyclerView + ) : Subject.Factory<BindingSubject, Int> { + override fun createSubject( + metadata: FailureMetadata, + globalPosition: Int + ): + BindingSubject { + return BindingSubject( + metadata = metadata, + recyclerView = recyclerView, + globalPosition = globalPosition + ) + } + } + } +}
\ No newline at end of file diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterTest.kt new file mode 100644 index 00000000000..ab179d78086 --- /dev/null +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/MergeAdapterTest.kt @@ -0,0 +1,1296 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget + +import android.content.Context +import android.view.View +import android.view.View.MeasureSpec.AT_MOST +import android.view.ViewGroup +import androidx.recyclerview.widget.MergeAdapterSubject.Companion.assertThat +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.Changed +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.DataSetChanged +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.Inserted +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.Moved +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.Removed +import androidx.recyclerview.widget.MergeAdapterTest.LoggingAdapterObserver.Event.StateRestorationStrategy +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.ALLOW +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.PREVENT +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.PREVENT_WHEN_EMPTY +import androidx.recyclerview.widget.RecyclerView.LayoutParams +import androidx.recyclerview.widget.RecyclerView.LayoutParams.MATCH_PARENT +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import androidx.test.annotation.UiThreadTest +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +@RunWith(AndroidJUnit4::class) +@SmallTest +class MergeAdapterTest { + private lateinit var recyclerView: RecyclerView + + @Before + fun init() { + val context = ApplicationProvider.getApplicationContext<Context>() + recyclerView = RecyclerView( + context + ).also { + it.layoutManager = LinearLayoutManager(context) + it.itemAnimator = null + } + } + + @UiThreadTest + @Test + fun attachAndDetachAll() { + val merge = MergeAdapter() + val adapter1 = NestedTestAdapter(10, + getLayoutParams = { + LayoutParams(MATCH_PARENT, 3) + }) + merge.addAdapter(adapter1) + recyclerView.adapter = merge + measureAndLayout(100, 50) + assertThat(recyclerView.childCount).isEqualTo(10) + assertThat(adapter1.attachedViewHolders()).hasSize(10) + measureAndLayout(100, 0) + assertThat(recyclerView.childCount).isEqualTo(0) + assertThat(adapter1.attachedViewHolders()).isEmpty() + + val adapter2 = NestedTestAdapter(5, + getLayoutParams = { + LayoutParams(MATCH_PARENT, 3) + }) + merge.addAdapter(adapter2) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 200) + assertThat(recyclerView.childCount).isEqualTo(15) + assertThat(adapter1.attachedViewHolders()).hasSize(10) + assertThat(adapter2.attachedViewHolders()).hasSize(5) + merge.removeAdapter(adapter1) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 200) + assertThat(recyclerView.childCount).isEqualTo(5) + assertThat(adapter1.attachedViewHolders()).isEmpty() + assertThat(adapter2.attachedViewHolders()).hasSize(5) + measureAndLayout(100, 0) + assertThat(adapter2.attachedViewHolders()).isEmpty() + } + + @Test + @UiThreadTest + fun mergeInsideMerge() { + val merge = MergeAdapter() + val adapter1 = NestedTestAdapter(10) + merge.addAdapter(adapter1) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(10) + merge.removeAdapter(adapter1) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 100) + assertThat(adapter1.attachedViewHolders()).isEmpty() + } + + @UiThreadTest + @Test + fun recycleOnRemoval() { + val merge = MergeAdapter() + val adapter1 = NestedTestAdapter(10) + merge.addAdapter(adapter1) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(10) + adapter1.removeItems(3, 2) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 100) + assertThat(adapter1.recycledViewHolders()).hasSize(2) + assertThat(adapter1.attachedViewHolders()).hasSize(8) + assertThat(adapter1.attachedViewHolders()).containsNoneIn(adapter1.recycledViewHolders()) + } + + @UiThreadTest + @Test + fun checkAttachDetach_adapterAdditions() { + val merge = MergeAdapter() + val adapter1 = NestedTestAdapter(0) + merge.addAdapter(adapter1) + recyclerView.adapter = merge + measureAndLayout(100, 100) + adapter1.addItems(0, 3) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 100) + assertThat(adapter1.attachedViewHolders()).hasSize(3) + assertThat(adapter1.recycledViewHolders()).hasSize(0) + } + + @UiThreadTest + @Test + fun failedToRecycleTest() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + measureAndLayout(100, 200) + val viewHolder = recyclerView.findViewHolderForAdapterPosition(12) + check(viewHolder != null) { + "should have that view holder for position 12" + } + assertThat(adapter2.attachedViewHolders()).contains(viewHolder) + // give it transient state so that it won't be recycled + viewHolder.itemView.setHasTransientState(true) + adapter2.removeItems(2, 2) + assertThat(recyclerView.isLayoutRequested).isTrue() + measureAndLayout(100, 200) + assertThat(adapter2.attachedViewHolders()).hasSize(3) + assertThat(adapter2.failedToRecycleViewHolders()).contains(viewHolder) + assertThat(adapter2.failedToRecycleViewHolders()).hasSize(1) + assertThat(adapter2.attachedViewHolders()).doesNotContain(viewHolder) + } + + @Suppress("DEPRECATION") + @UiThreadTest + @Test + fun localAdapterPositions() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(4) + val adapter3 = NestedTestAdapter(8) + val merge = MergeAdapter(adapter1, adapter2, adapter3) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(22) + (0 until 22).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(recyclerView.getChildAdapterPosition(viewHolder.itemView)).isEqualTo(it) + assertThat(viewHolder.absoluteAdapterPosition).isEqualTo(it) + } + (0 until 10).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter1) + } + + (10 until 14).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it - 10) + assertThat(viewHolder.adapterPosition).isEqualTo(it - 10) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter2) + } + + (14 until 22).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it - 14) + assertThat(viewHolder.adapterPosition).isEqualTo(it - 14) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter3) + } + } + + @Suppress("LocalVariableName") + @UiThreadTest + @Test + fun localAdapterPositions_nested() { + val adapter1_1 = NestedTestAdapter(10) + val adapter1_2 = NestedTestAdapter(5) + val adapter1 = MergeAdapter(adapter1_1, adapter1_2) + val adapter2_1 = NestedTestAdapter(3) + val adapter2_2 = NestedTestAdapter(6) + val adapter2 = MergeAdapter(adapter2_1, adapter2_2) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(24) + (0 until 24).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.absoluteAdapterPosition).isEqualTo(it) + assertThat(recyclerView.getChildAdapterPosition(viewHolder.itemView)).isEqualTo(it) + } + (0 until 10).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter1_1) + } + (10 until 15).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it - 10) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter1_2) + } + (15 until 18).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it - 15) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter2_1) + } + (18 until 24).forEach { + val viewHolder = checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + assertThat(viewHolder.bindingAdapterPosition).isEqualTo(it - 18) + assertThat(viewHolder.bindingAdapter).isSameInstanceAs(adapter2_2) + } + } + + @UiThreadTest + @Test + fun localAdapterPositions_notIncluded() { + val adapter1 = NestedTestAdapter(10) + val merge = MergeAdapter(adapter1) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(10) + val vh = checkNotNull(recyclerView.findViewHolderForAdapterPosition(3)) + assertThat(vh.bindingAdapterPosition).isEqualTo(3) + + val toBeRemoved = checkNotNull(recyclerView.findViewHolderForAdapterPosition(4)) + adapter1.removeItems(4, 1) + assertThat(toBeRemoved.bindingAdapterPosition).isEqualTo(NO_POSITION) + assertThat(toBeRemoved.absoluteAdapterPosition).isEqualTo(NO_POSITION) + measureAndLayout(100, 100) + assertThat(toBeRemoved.bindingAdapter).isNull() + + recyclerView.adapter = null + measureAndLayout(100, 100) + assertThat(vh.bindingAdapterPosition).isEqualTo(NO_POSITION) + assertThat(vh.absoluteAdapterPosition).isEqualTo(NO_POSITION) + assertThat(vh.bindingAdapter).isNull() + } + + @UiThreadTest + @Test + fun attachDetachTest() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + assertThat(adapter1.attachedRecyclerViews()).containsExactly(recyclerView) + assertThat(adapter2.attachedRecyclerViews()).containsExactly(recyclerView) + val adapter3 = NestedTestAdapter(3) + merge.addAdapter(adapter3) + assertThat(adapter3.attachedRecyclerViews()).containsExactly(recyclerView) + merge.removeAdapter(adapter3) + assertThat(adapter3.attachedRecyclerViews()).isEmpty() + recyclerView.adapter = null + assertThat(adapter1.attachedRecyclerViews()).isEmpty() + assertThat(adapter2.attachedRecyclerViews()).isEmpty() + } + + @UiThreadTest + @Test + fun attachDetachTest_multipleRecyclerViews() { + val recyclerView2 = RecyclerView(ApplicationProvider.getApplicationContext()) + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + recyclerView2.adapter = merge + assertThat(adapter1.attachedRecyclerViews()).containsExactly(recyclerView, recyclerView2) + assertThat(adapter2.attachedRecyclerViews()).containsExactly(recyclerView, recyclerView2) + val adapter3 = NestedTestAdapter(3) + merge.addAdapter(adapter3) + assertThat(adapter3.attachedRecyclerViews()).containsExactly(recyclerView, recyclerView2) + merge.removeAdapter(adapter3) + assertThat(adapter3.attachedRecyclerViews()).isEmpty() + recyclerView.adapter = null + assertThat(adapter1.attachedRecyclerViews()).containsExactly(recyclerView2) + assertThat(adapter2.attachedRecyclerViews()).containsExactly(recyclerView2) + recyclerView2.adapter = null + assertThat(adapter1.attachedRecyclerViews()).isEmpty() + assertThat(adapter2.attachedRecyclerViews()).isEmpty() + assertThat(adapter3.attachedRecyclerViews()).isEmpty() + } + + @Test + @UiThreadTest + fun adapterRemoval() { + val adapter1 = NestedTestAdapter(3) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(8) + assertThat(merge.removeAdapter(adapter1)).isTrue() + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(5) + assertThat(merge.removeAdapter(adapter1)).isFalse() + assertThat(merge.removeAdapter(adapter2)).isTrue() + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(0) + } + + @Test + @UiThreadTest + fun boundAdapter() { + val adapter1 = NestedTestAdapter(3) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + recyclerView.adapter = merge + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(8) + val adapter1ViewHolders = (0 until 3).map { + checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + } + val adapter2ViewHolders = (3 until 8).map { + checkNotNull(recyclerView.findViewHolderForAdapterPosition(it)) + } + adapter1ViewHolders.forEach { + assertThat(it.bindingAdapter).isSameInstanceAs(adapter1) + } + adapter2ViewHolders.forEach { + assertThat(it.bindingAdapter).isSameInstanceAs(adapter2) + } + assertThat(merge.removeAdapter(adapter1)).isTrue() + // even when position is invalid, we should still be able to find the bound adapter + adapter1ViewHolders.forEach { + assertThat(it.bindingAdapter).isSameInstanceAs(adapter1) + } + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(5) + adapter1ViewHolders.forEach { + assertThat(it.bindingAdapter).isNull() + } + assertThat(merge.removeAdapter(adapter1)).isFalse() + assertThat(merge.removeAdapter(adapter2)).isTrue() + measureAndLayout(100, 100) + assertThat(recyclerView.childCount).isEqualTo(0) + adapter2ViewHolders.forEach { + assertThat(it.bindingAdapter).isNull() + } + } + + private fun measureAndLayout(@Suppress("SameParameterValue") width: Int, height: Int) { + measure(width, height) + layout(width, height) + } + + private fun measure(width: Int, height: Int) { + recyclerView.measure(AT_MOST or width, AT_MOST or height) + } + + private fun layout(width: Int, height: Int) { + recyclerView.layout(0, 0, width, height) + } + + @Test + fun size() { + val merge = MergeAdapter() + val observer = LoggingAdapterObserver(merge) + assertThat(merge).hasItemCount(0) + merge.addAdapter(NestedTestAdapter(0)) + observer.assertEventsAndClear( + "Empty adapter shouldn't cause notify" + ) + + val adapter1 = NestedTestAdapter(3) + merge.addAdapter(adapter1) + assertThat(merge).hasItemCount(3) + observer.assertEventsAndClear( + "adapter with count should trigger notify", + Inserted( + positionStart = 0, + itemCount = 3 + ) + ) + + val adapter2 = NestedTestAdapter(5) + merge.addAdapter(adapter2) + assertThat(merge).hasItemCount(8) + observer.assertEventsAndClear( + "appended non-empty adapter should trigger insert event", + Inserted( + positionStart = 3, + itemCount = 5 + ) + ) + + val adapter3 = NestedTestAdapter(2) + merge.addAdapter(2, adapter3) + assertThat(merge).hasItemCount(10) + observer.assertEventsAndClear( + "appended non-empty adapter should trigger insert event in right index", + Inserted( + positionStart = 3, + itemCount = 2 + ) + ) + + merge.addAdapter(NestedTestAdapter(0)) + assertThat(merge).hasItemCount(10) + observer.assertEventsAndClear( + "empty new adapter shouldn't trigger events" + ) + } + + @Test + fun nested_addition() { + val merge = MergeAdapter() + val observer = LoggingAdapterObserver(merge) + + val adapter1 = NestedTestAdapter(0) + merge.addAdapter(adapter1) + observer.assertEventsAndClear("empty adapter triggers no events") + + adapter1.addItems(positionStart = 0, itemCount = 3) + observer.assertEventsAndClear( + "non-empty adapter triggers an event", + Inserted( + positionStart = 0, + itemCount = 3 + ) + ) + assertThat(merge).hasItemCount(3) + adapter1.addItems(positionStart = 1, itemCount = 2) + observer.assertEventsAndClear( + "inner adapter change should trigger an event", + Inserted( + positionStart = 1, + itemCount = 2 + ) + ) + assertThat(merge).hasItemCount(5) + val adapter2 = NestedTestAdapter(2) + merge.addAdapter(adapter2) + observer.assertEventsAndClear( + "added adapter should trigger an event", + Inserted( + positionStart = 5, + itemCount = 2 + ) + ) + assertThat(merge).hasItemCount(7) + + adapter2.addItems(positionStart = 0, itemCount = 3) + observer.assertEventsAndClear( + "nested adapter prepends data", + Inserted( + positionStart = 5, + itemCount = 3 + ) + ) + assertThat(merge).hasItemCount(10) + + adapter2.addItems(positionStart = 2, itemCount = 4) + observer.assertEventsAndClear( + "nested adapter adds items with inner offset", + Inserted( + positionStart = 7, + itemCount = 4 + ) + ) + assertThat(merge).hasItemCount(14) + } + + @Test + fun nested_removal() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(15) + val adapter3 = NestedTestAdapter(20) + + val merge = MergeAdapter(adapter1, adapter2, adapter3) + val observer = LoggingAdapterObserver(merge) + assertThat(merge).hasItemCount(45) + + adapter1.removeItems(positionStart = 0, itemCount = 2) + observer.assertEventsAndClear( + "removal from first adapter top", + Removed( + positionStart = 0, + itemCount = 2 + ) + ) + assertThat(merge).hasItemCount(43) + adapter1.removeItems(positionStart = 2, itemCount = 1) + observer.assertEventsAndClear( + "removal from first adapter inner", + Removed( + positionStart = 2, + itemCount = 1 + ) + ) + assertThat(merge).hasItemCount(42) + // now first adapter has size 7 + adapter2.removeItems(positionStart = 0, itemCount = 3) + observer.assertEventsAndClear( + "removal from second adapter should be offset", + Removed( + positionStart = adapter1.itemCount, + itemCount = 3 + ) + ) + assertThat(merge).hasItemCount(39) + adapter2.removeItems(positionStart = 6, itemCount = 4) + observer.assertEventsAndClear( + "inner item removal from middle adapter should be offset", + Removed( + positionStart = adapter1.itemCount + 6, + itemCount = 4 + ) + ) + assertThat(merge).hasItemCount(35) + + adapter3.removeItems(positionStart = 0, itemCount = 3) + observer.assertEventsAndClear( + "removal from last adapter should be offset by adapter 1 and 2", + Removed( + positionStart = adapter1.itemCount + adapter2.itemCount, + itemCount = 3 + ) + ) + + adapter3.removeItems(positionStart = 2, itemCount = 5) + observer.assertEventsAndClear( + "removal from inner items from last adapter should be offset by adapter 1 & 2", + Removed( + positionStart = adapter1.itemCount + adapter2.itemCount + 2, + itemCount = 5 + ) + ) + + merge.removeAdapter(adapter2) + observer.assertEventsAndClear( + "removing an adapter should trigger removal", + Removed( + positionStart = adapter1.itemCount, + itemCount = adapter2.itemCount + ) + ) + assertThat(merge).hasItemCount(adapter1.itemCount + adapter3.itemCount) + merge.removeAdapter(adapter1) + observer.assertEventsAndClear( + "removing first adapter should trigger removal", + Removed( + positionStart = 0, + itemCount = adapter1.itemCount + ) + ) + assertThat(merge).hasItemCount(adapter3.itemCount) + merge.removeAdapter(adapter3) + observer.assertEventsAndClear( + "removing last adapter should trigger a removal", + Removed( + positionStart = 0, + itemCount = adapter3.itemCount + ) + ) + assertThat(merge).hasItemCount(0) + } + + @Test + fun nested_move() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(15) + val adapter3 = NestedTestAdapter(20) + val merge = MergeAdapter(adapter1, adapter2, adapter3) + val observer = LoggingAdapterObserver(merge) + adapter1.moveItem(fromPosition = 3, toPosition = 5) + observer.assertEventsAndClear( + "move from first adapter should come as is", + Moved( + fromPosition = 3, + toPosition = 5 + ) + ) + assertThat(merge).hasItemCount(45) + adapter2.moveItem(fromPosition = 2, toPosition = 4) + observer.assertEventsAndClear( + "move in adapter 2 should be offset", + Moved( + fromPosition = adapter1.itemCount + 2, + toPosition = adapter1.itemCount + 4 + ) + ) + adapter3.moveItem(fromPosition = 7, toPosition = 2) + observer.assertEventsAndClear( + "move in adapter 3 should be offset by adapter 1 & 2", + Moved( + fromPosition = adapter1.itemCount + adapter2.itemCount + 7, + toPosition = adapter1.itemCount + adapter2.itemCount + 2 + ) + ) + assertThat(merge).hasItemCount(45) + } + + @Test + fun nested_itemChange_withPayload() = nested_itemChange("payload") + + @Test + fun nested_itemChange_withoutPayload() = nested_itemChange(null) + + fun nested_itemChange(payload: Any? = null) { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(15) + val adapter3 = NestedTestAdapter(20) + val merge = MergeAdapter(adapter1, adapter2, adapter3) + val observer = LoggingAdapterObserver(merge) + + adapter1.changeItems(positionStart = 3, itemCount = 5, payload = payload) + observer.assertEventsAndClear( + "change from first adapter should come as is", + Changed( + positionStart = 3, + itemCount = 5, + payload = payload + ) + ) + assertThat(merge).hasItemCount(45) + adapter2.changeItems(positionStart = 2, itemCount = 4, payload = payload) + observer.assertEventsAndClear( + "change in adapter 2 should be offset", + Changed( + positionStart = adapter1.itemCount + 2, + itemCount = 4, + payload = payload + ) + ) + adapter3.changeItems(positionStart = 7, itemCount = 2, payload = payload) + observer.assertEventsAndClear( + "change in adapter 3 should be offset by adapter 1 & 2", + Changed( + positionStart = adapter1.itemCount + adapter2.itemCount + 7, + itemCount = 2, + payload = payload + ) + ) + assertThat(merge).hasItemCount(45) + } + + @Test + fun notifyDataSetChanged() { + // we could add some logic to make data set changes add/remove/itemChange events yet + // it is very hard to get right and might cause very undesired animations. Not doing it + // for V1. + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(15) + val adapter3 = NestedTestAdapter(20) + val merge = MergeAdapter(adapter1, adapter2, adapter3) + val observer = LoggingAdapterObserver(merge) + + adapter1.changeDataSet(3) + observer.assertEventsAndClear( + "data set change should come as is", + DataSetChanged + ) + assertThat(merge).hasItemCount(38) + adapter2.changeDataSet(20) + observer.assertEventsAndClear( + "data set change in adapter 2 should become full data set change", + DataSetChanged + ) + assertThat(merge).hasItemCount(43) + adapter3.changeDataSet(newSize = 0) + observer.assertEventsAndClear( + """when an adapter changes size to 0, it should still come as 0 as we cannot + |rely on itemCount changing immediately. In theory we would but adapter might be + |faulty and not update its size immediately, which would work fine in RV because + |everything is delayed but not here if we immediately read the item count + """.trimMargin(), + DataSetChanged + ) + assertThat(merge).hasItemCount(23) + } + + @Test + fun viewTypeMapping_allViewsHaveDifferentTypes() { + val adapter1 = NestedTestAdapter(10) { _, position -> + position + } + val merge = MergeAdapter(adapter1) + val adapter1ViewTypes = (0 until 10).map { + merge.getItemViewType(it) + }.toSet() + + assertWithMessage("all items have unique types") + .that(adapter1ViewTypes).hasSize(10) + repeat(adapter1.itemCount) { + assertThat(merge).bindView(recyclerView, it).verifyBoundTo( + adapter = adapter1, + localPosition = it + ) + } + val adapter2 = NestedTestAdapter(5) { _, position -> + position + } + merge.addAdapter(adapter2) + repeat(adapter2.itemCount) { + assertThat(merge).bindView(recyclerView, adapter1.itemCount + it).verifyBoundTo( + adapter = adapter2, + localPosition = it + ) + } + + merge.removeAdapter(adapter1) + repeat(adapter2.itemCount) { + assertThat(merge).bindView(recyclerView, it).verifyBoundTo( + adapter = adapter2, + localPosition = it + ) + } + } + + @Test + fun viewTypeMapping_shareTypesWithinAdapter() { + val adapter1 = NestedTestAdapter(10) { item, _ -> + item.id % 3 + } + val adapter2 = NestedTestAdapter(20) { item, _ -> + item.id % 4 + } + val merge = MergeAdapter(adapter1, adapter2) + val adapter1Types = (0 until adapter1.itemCount).map { + merge.getItemViewType(it) + }.toSet() + assertThat(adapter1Types).hasSize(3) + val adapter2Types = (adapter1.itemCount until adapter2.itemCount).map { + merge.getItemViewType(it) + }.toSet() + assertThat(adapter2Types).hasSize(4) + adapter2Types.forEach { + assertThat(adapter1Types).doesNotContain(it) + } + (0 until adapter1.itemCount).forEach { + assertThat(merge).bindView(recyclerView, it) + .verifyBoundTo( + adapter = adapter1, + localPosition = it + ) + } + + (0 until adapter2.itemCount).forEach { + assertThat(merge).bindView(recyclerView, adapter1.itemCount + it) + .verifyBoundTo( + adapter = adapter2, + localPosition = it + ) + } + + merge.removeAdapter(adapter1) + repeat(adapter2.itemCount) { + assertThat(merge).bindView(recyclerView, it).verifyBoundTo( + adapter = adapter2, + localPosition = it + ) + } + } + + @Test( + expected = IllegalArgumentException::class + ) + fun stableIdTest() { + val merge = MergeAdapter() + merge.setHasStableIds(true) + } + + @Test + fun stateRestrorationTest_callingPublicMerthodIsIgnored() { + val adapter = NestedTestAdapter(3).also { + it.stateRestorationStrategy = PREVENT + } + val merge = MergeAdapter(adapter) + assertThat(merge).hasStateRestorationStrategy(PREVENT) + merge.stateRestorationStrategy = ALLOW + assertThat(merge).hasStateRestorationStrategy(PREVENT) + merge.stateRestorationStrategy = PREVENT_WHEN_EMPTY + assertThat(merge).hasStateRestorationStrategy(PREVENT) + adapter.stateRestorationStrategy = ALLOW + assertThat(merge).hasStateRestorationStrategy(ALLOW) + } + + @Test + fun stateRestoration_subAdapterAllowsNonEmpty() { + val adapter1 = NestedTestAdapter(1).also { + it.stateRestorationStrategy = ALLOW + } + val adapter2 = NestedTestAdapter(0).also { + it.stateRestorationStrategy = PREVENT_WHEN_EMPTY + } + val merge = MergeAdapter(adapter1, adapter2) + assertThat(merge).cannotRestoreState() + adapter2.addItems(0, 1) + assertThat(merge).canRestoreState() + adapter2.removeItems(0, 1) + assertThat(merge).cannotRestoreState() + } + + @Test + fun stateRestoration_subAdapterAllowsNonEmpty_viaNotifyChange() { + val adapter1 = NestedTestAdapter(1).also { + it.stateRestorationStrategy = ALLOW + } + val adapter2 = NestedTestAdapter(0).also { + it.stateRestorationStrategy = PREVENT_WHEN_EMPTY + } + val merge = MergeAdapter(adapter1, adapter2) + assertThat(merge).cannotRestoreState() + adapter2.changeDataSet(1) + assertThat(merge).canRestoreState() + adapter2.changeDataSet(0) + assertThat(merge).cannotRestoreState() + } + + @Test + fun stateRestoration() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(5) + val adapter3 = NestedTestAdapter(20) + val merge = MergeAdapter(adapter1, adapter2, adapter3) + assertThat(merge).hasStateRestorationStrategy(ALLOW) + adapter2.stateRestorationStrategy = PREVENT + assertThat(merge).hasStateRestorationStrategy(PREVENT) + + adapter3.stateRestorationStrategy = PREVENT_WHEN_EMPTY + assertThat(merge).hasStateRestorationStrategy(PREVENT) + + adapter2.stateRestorationStrategy = ALLOW + assertThat(merge).hasStateRestorationStrategy(ALLOW) + + merge.removeAdapter(adapter3) + assertThat(merge).hasStateRestorationStrategy(ALLOW) + + val adapter4 = NestedTestAdapter(3).also { + it.stateRestorationStrategy = PREVENT + merge.addAdapter(it) + } + assertThat(merge).hasStateRestorationStrategy(PREVENT) + adapter4.stateRestorationStrategy = PREVENT_WHEN_EMPTY + assertThat(merge).hasStateRestorationStrategy(ALLOW) + merge.removeAdapter(adapter1) + assertThat(merge).hasStateRestorationStrategy(ALLOW) + adapter4.stateRestorationStrategy = ALLOW + assertThat(merge).hasStateRestorationStrategy(ALLOW) + } + + @Test + fun disposal() { + val adapter1 = NestedTestAdapter(10) + val adapter2 = NestedTestAdapter(5) + val merge = MergeAdapter(adapter1, adapter2) + assertThat(adapter1.observerCount()).isEqualTo(1) + assertThat(adapter2.observerCount()).isEqualTo(1) + merge.removeAdapter(adapter1) + assertThat(adapter1.observerCount()).isEqualTo(0) + assertThat(adapter2.observerCount()).isEqualTo(1) + + val adapter3 = NestedTestAdapter(2) + merge.addAdapter(adapter3) + assertThat(adapter3.observerCount()).isEqualTo(1) + merge.copyOfAdapters.forEach { + merge.removeAdapter(it) + } + listOf(adapter1, adapter2, adapter3).forEachIndexed { index, adapter -> + assertWithMessage("adapter ${index + 1}").apply { + that(adapter.observerCount()).isEqualTo(0) + that(adapter.attachedRecyclerViews()).isEmpty() + } + } + } + + /** + * Running only on 26 due to the getParameters method call and this is not API version + * dependent test so it is fine to only run it on new devices. + */ + @SdkSuppress(minSdkVersion = 26) + @Test + fun overrideTest() { + // custom method instead of using toGenericString to avoid having class name + fun Method.describe() = """ + $name(${parameters.map { + it.type.canonicalName + }}) : ${returnType.canonicalName} + """.trimIndent() + + val excludedMethods = setOf( + "getItemId([int]) : long", + "registerAdapterDataObserver(" + + "[androidx.recyclerview.widget.RecyclerView.AdapterDataObserver]) : void", + "unregisterAdapterDataObserver(" + + "[androidx.recyclerview.widget.RecyclerView.AdapterDataObserver]) : void", + "canRestoreState([]) : boolean", + "onBindViewHolder([androidx.recyclerview.widget.RecyclerView.ViewHolder, int, " + + "java.util.List]) : void" + ) + val adapterMethods = RecyclerView.Adapter::class.java.declaredMethods.filterNot { + Modifier.isPrivate(it.modifiers) || Modifier.isFinal(it.modifiers) + }.map { + it.describe() + }.filterNot { + excludedMethods.contains(it) + } + val mergeAdapterMethods = MergeAdapter::class.java.declaredMethods.map { + it.describe() + } + assertWithMessage( + """ + MergeAdapter should override all methods in RecyclerView.Adapter for future + compatibility. If you want to exclude a method, update the test. + """.trimIndent() + ).that(mergeAdapterMethods).containsAtLeastElementsIn(adapterMethods) + } + + @Test + fun getAdapters() { + val adapter1 = NestedTestAdapter(1) + val adapter2 = NestedTestAdapter(2) + val merge = MergeAdapter(adapter1, adapter2) + assertThat(merge.copyOfAdapters).isEqualTo(listOf(adapter1, adapter2)) + merge.removeAdapter(adapter1) + assertThat(merge.copyOfAdapters).isEqualTo(listOf(adapter2)) + } + + @Test + fun sharedTypes() { + val adapter1 = NestedTestAdapter(3) { _, pos -> + pos % 2 + } + val adapter2 = NestedTestAdapter(3) { _, pos -> + pos % 3 + } + val merge = MergeAdapter( + MergeAdapter.Config.Builder() + .setIsolateViewTypes(false) + .build(), adapter1, adapter2 + ) + assertThat(merge).bindView(recyclerView, 2) + .verifyBoundTo(adapter1, 2) + assertThat(merge).bindView(recyclerView, 3) + .verifyBoundTo(adapter2, 0) + assertThat(merge.getItemViewType(0)).isEqualTo(0) + assertThat(merge.getItemViewType(1)).isEqualTo(1) + assertThat(merge.getItemViewType(2)).isEqualTo(0) + // notice that it resets to 0 because type is based on position + assertThat(merge.getItemViewType(3)).isEqualTo(0) + assertThat(merge.getItemViewType(4)).isEqualTo(1) + assertThat(merge.getItemViewType(5)).isEqualTo(2) + // ensure we bind via the correct adapter when a type is limited to a specific adapter + assertThat(merge).bindView(recyclerView, 5) + .verifyBoundTo(adapter2, 2) + } + + @Test + fun sharedTypes_allUnique() { + val adapter1 = NestedTestAdapter(3) { item, _ -> + item.id + } + val adapter2 = NestedTestAdapter(3) { item, _ -> + item.id + } + val merge = MergeAdapter( + MergeAdapter.Config.Builder() + .setIsolateViewTypes(false) + .build(), adapter1, adapter2 + ) + assertThat(merge).bindView(recyclerView, 0) + .verifyBoundTo(adapter1, 0) + assertThat(merge).bindView(recyclerView, 1) + .verifyBoundTo(adapter1, 1) + assertThat(merge).bindView(recyclerView, 2) + .verifyBoundTo(adapter1, 2) + assertThat(merge).bindView(recyclerView, 3) + .verifyBoundTo(adapter2, 0) + assertThat(merge).bindView(recyclerView, 4) + .verifyBoundTo(adapter2, 1) + assertThat(merge).bindView(recyclerView, 5) + .verifyBoundTo(adapter2, 2) + } + + private var itemCounter = 0 + private fun produceItem(): TestItem = (itemCounter++).let { + TestItem(id = it, value = it) + } + + internal inner class NestedTestAdapter( + count: Int = 0, + val getLayoutParams: ((MergeAdapterViewHolder) -> LayoutParams)? = null, + val itemTypeLookup: ((TestItem, position: Int) -> Int)? = null + ) : RecyclerView.Adapter<MergeAdapterViewHolder>() { + private val attachedViewHolders = mutableListOf<MergeAdapterViewHolder>() + private val recycledViewHolders = mutableListOf<MergeAdapterViewHolder>() + private val failedToRecycleViewHolders = mutableListOf<MergeAdapterViewHolder>() + private var attachedRecyclerViews = mutableListOf<RecyclerView>() + private var observers = mutableListOf<RecyclerView.AdapterDataObserver>() + + private val items = mutableListOf<TestItem>().also { list -> + repeat(count) { + list.add(produceItem()) + } + } + + fun attachedViewHolders(): List<MergeAdapterViewHolder> = attachedViewHolders + + override fun onViewAttachedToWindow(holder: MergeAdapterViewHolder) { + assertThat(attachedViewHolders).doesNotContain(holder) + attachedViewHolders.add(holder) + } + + override fun onViewDetachedFromWindow(holder: MergeAdapterViewHolder) { + assertThat(attachedViewHolders).contains(holder) + attachedViewHolders.remove(holder) + } + + override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) { + assertThat(observers).doesNotContain(observer) + observers.add(observer) + super.registerAdapterDataObserver(observer) + } + + override fun unregisterAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) { + assertThat(observers).contains(observer) + observers.remove(observer) + super.unregisterAdapterDataObserver(observer) + } + + fun observerCount() = observers.size + + override fun getItemViewType(position: Int): Int { + itemTypeLookup?.let { + return it(items[position], position) + } + return super.getItemViewType(position) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + assertThat(attachedRecyclerViews).doesNotContain(recyclerView) + attachedRecyclerViews.add(recyclerView) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + assertThat(attachedRecyclerViews).contains(recyclerView) + attachedRecyclerViews.remove(recyclerView) + } + + fun attachedRecyclerViews(): List<RecyclerView> = attachedRecyclerViews + + fun addItems(positionStart: Int, itemCount: Int = 1) { + require(itemCount > 0) + require(positionStart >= 0 && positionStart <= items.size) + val newItems = (0 until itemCount).map { + produceItem() + } + items.addAll(positionStart, newItems) + notifyItemRangeInserted(positionStart, itemCount) + } + + fun removeItems(positionStart: Int, itemCount: Int = 1) { + require(positionStart >= 0) + require(positionStart + itemCount <= items.size) + require(itemCount > 0) + repeat(itemCount) { + items.removeAt(positionStart) + } + notifyItemRangeRemoved(positionStart, itemCount) + } + + fun moveItem(fromPosition: Int, toPosition: Int) { + require(fromPosition >= 0 && fromPosition < items.size) + require(toPosition >= 0 && toPosition < items.size) + if (fromPosition == toPosition) return + items.add(toPosition, items.removeAt(fromPosition)) + notifyItemMoved(fromPosition, toPosition) + } + + fun changeDataSet(newSize: Int = items.size) { + require(newSize >= 0) + val newItems = (0 until newSize).map { + produceItem() + } + items.clear() + items.addAll(newItems) + notifyDataSetChanged() + } + + fun changeItems(positionStart: Int, itemCount: Int, payload: Any? = null) { + require(positionStart >= 0 && positionStart < items.size) + require(positionStart + itemCount <= items.size) + (positionStart until positionStart + itemCount).forEach { + val prev = items[it] + items[it] = prev.copy(value = prev.value + 1) + } + if (payload == null) { + notifyItemRangeChanged(positionStart, itemCount) + } else { + notifyItemRangeChanged(positionStart, itemCount, payload) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MergeAdapterViewHolder { + return MergeAdapterViewHolder(parent.context, viewType).also { holder -> + getLayoutParams?.invoke(holder)?.let { + holder.itemView.layoutParams = it + } + } + } + + override fun onBindViewHolder(holder: MergeAdapterViewHolder, position: Int) { + assertThat(getItemViewType(position)).isEqualTo(holder.localViewType) + holder.bindTo(this, items[position], position) + } + + override fun onViewRecycled(holder: MergeAdapterViewHolder) { + recycledViewHolders.add(holder) + holder.onRecycled() + } + + override fun getItemCount() = items.size + + override fun onFailedToRecycleView(holder: MergeAdapterViewHolder): Boolean { + failedToRecycleViewHolders.add(holder) + return super.onFailedToRecycleView(holder) + } + + fun getItemAt(localPosition: Int) = items[localPosition] + fun recycledViewHolders(): List<MergeAdapterViewHolder> = recycledViewHolders + fun failedToRecycleViewHolders(): List<MergeAdapterViewHolder> = failedToRecycleViewHolders + } + + class MergeAdapterViewHolder( + context: Context, + val localViewType: Int + ) : RecyclerView.ViewHolder(View(context)) { + private var boundItem: Any? = null + private var boundAdapter: RecyclerView.Adapter<*>? = null + private var boundPosition: Int? = null + fun bindTo(adapter: RecyclerView.Adapter<*>, item: Any, position: Int) { + boundAdapter = adapter + boundPosition = position + boundItem = item + } + + fun boundItem() = boundItem + fun boundLocalPosition() = boundPosition + fun boundAdapter() = boundAdapter + fun onRecycled() { + boundItem = null + boundPosition = -1 + boundAdapter = null + } + } + + class LoggingAdapterObserver( + private val src: RecyclerView.Adapter<*> + ) : RecyclerView.AdapterDataObserver() { + init { + src.registerAdapterDataObserver(this) + } + + private val events = mutableListOf<Event>() + + fun assertEventsAndClear( + message: String, + vararg expected: Event + ) { + assertWithMessage(message).that(events).isEqualTo(expected.toList()) + events.clear() + } + + override fun onChanged() { + events.add(DataSetChanged) + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + events.add( + Changed( + positionStart = positionStart, + itemCount = itemCount + ) + ) + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + events.add( + Changed( + positionStart = positionStart, + itemCount = itemCount, + payload = payload + ) + ) + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + events.add( + Inserted( + positionStart = positionStart, + itemCount = itemCount + ) + ) + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + events.add( + Removed( + positionStart = positionStart, + itemCount = itemCount + ) + ) + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + require(itemCount == 1) { + "RV does not support moving more than 1 item at a time" + } + events.add( + Moved( + fromPosition = fromPosition, + toPosition = toPosition + ) + ) + } + + override fun onStateRestorationStrategyChanged() { + events.add( + StateRestorationStrategy( + newValue = src.stateRestorationStrategy + ) + ) + } + + sealed class Event { + object DataSetChanged : Event() + data class Changed( + val positionStart: Int, + val itemCount: Int, + val payload: Any? = null + ) : Event() + + data class Inserted( + val positionStart: Int, + val itemCount: Int + ) : Event() + + data class Removed( + val positionStart: Int, + val itemCount: Int + ) : Event() + + data class Moved( + val fromPosition: Int, + val toPosition: Int + ) : Event() + + data class StateRestorationStrategy( + val newValue: RecyclerView.Adapter.StateRestorationStrategy + ) : Event() + } + } + + internal data class TestItem( + val id: Int, + val value: Int, + val viewType: Int = 0 + ) +}
\ No newline at end of file diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAnimationsTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAnimationsTest.java index 77246c58f43..7843a3cb862 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAnimationsTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAnimationsTest.java @@ -243,7 +243,7 @@ public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest { RecyclerView.State state) { mLayoutItemCount = 7; View targetView = recycler - .getViewForPosition(target.getAdapterPosition()); + .getViewForPosition(target.getAbsoluteAdapterPosition()); assertSame(targetView, target.itemView); super.beforePostLayout(recycler, layoutManager, state); } @@ -479,7 +479,7 @@ public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest { mLayoutManager.expectLayouts(1); target.mBoundItem.mType += 2; mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9; - mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1); + mTestAdapter.changeAndNotify(target.getAbsoluteAdapterPosition(), 1); requestLayoutOnUIThread(mRecyclerView); mLayoutManager.waitForLayout(2); diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewCacheTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewCacheTest.java index bec138e4621..330f659e700 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewCacheTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewCacheTest.java @@ -494,7 +494,7 @@ public class RecyclerViewCacheTest { @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { // verify unbound view doesn't get - assertNotEquals(RecyclerView.NO_POSITION, holder.getAdapterPosition()); + assertNotEquals(RecyclerView.NO_POSITION, holder.getAbsoluteAdapterPosition()); } }; mRecyclerView.setAdapter(adapter); @@ -519,7 +519,7 @@ public class RecyclerViewCacheTest { assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1); RecyclerView.ViewHolder pooledHolder = mRecyclerView.getRecycledViewPool() .mScrap.get(0).mScrapHeap.get(0); - assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAdapterPosition()); + assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAbsoluteAdapterPosition()); } @Test @@ -853,7 +853,7 @@ public class RecyclerViewCacheTest { @Override public void onViewRecycled(@NonNull ViewHolder holder) { - mSavedStates.set(holder.getAdapterPosition(), + mSavedStates.set(holder.getAbsoluteAdapterPosition(), holder.mRecyclerView.getLayoutManager().onSaveInstanceState()); } @@ -1194,8 +1194,8 @@ public class RecyclerViewCacheTest { @Override public void onViewRecycled(@NonNull ViewHolder holder) { - if (holder.getAdapterPosition() >= 0) { - mSavedStates.set(holder.getAdapterPosition(), + if (holder.getAbsoluteAdapterPosition() >= 0) { + mSavedStates.set(holder.getAbsoluteAdapterPosition(), holder.mRecyclerView.getLayoutManager().onSaveInstanceState()); } } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java index 94ead611b57..9a86462b193 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java @@ -513,7 +513,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest if (mRecyclerView.mRecycler.mCachedViews.contains(holder)) { assertThat("ViewHolder's getAdapterPosition should be " + "RecyclerView.NO_POSITION", - holder.getAdapterPosition(), + holder.getAbsoluteAdapterPosition(), is(RecyclerView.NO_POSITION)); cachedRecycleCount.incrementAndGet(); } @@ -2388,6 +2388,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest assertThat(recycledViewCount.get(), is(9)); assertTrue(failedToRecycle.get()); assertNull(vh.mOwnerRecyclerView); + assertNull(vh.getBindingAdapter()); checkForMainThreadException(); } @@ -2422,6 +2423,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest }); assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); assertNull(vh.mOwnerRecyclerView); + assertNull(vh.getBindingAdapter()); checkForMainThreadException(); } @@ -2476,7 +2478,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest for (int i = 0; i < recyclerView.getChildCount(); i ++) { View view = recyclerView.getChildAt(i); RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); - if (vh.getAdapterPosition() == 2) { + if (vh.getAbsoluteAdapterPosition() == 2) { if (mRecyclerView.mChildHelper.isHidden(view)) { assertThat(hidden, CoreMatchers.nullValue()); hidden = vh; @@ -2600,7 +2602,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest assertEquals("should be able to find VH with adapter position " + index, vh, recyclerView.findViewHolderForAdapterPosition(index)); assertEquals("get adapter position should return correct index", index, - vh.getAdapterPosition()); + vh.getAbsoluteAdapterPosition()); layoutPositions.put(view, vh.mPosition); } if (adapterChanges != null) { @@ -2616,7 +2618,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest recyclerView.findViewHolderForAdapterPosition(index)); } assertSame("get adapter position should return correct index", index, - vh.getAdapterPosition()); + vh.getAbsoluteAdapterPosition()); assertSame("should be able to find view with layout position", vh, mRecyclerView.findViewHolderForLayoutPosition( layoutPositions.get(view))); @@ -3891,7 +3893,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest try { TestViewHolder tvh = (TestViewHolder) parent.getChildViewHolder(view); Object data = tvh.getData(); - int adapterPos = tvh.getAdapterPosition(); + int adapterPos = tvh.getAbsoluteAdapterPosition(); assertThat(adapterPos, is(not(NO_POSITION))); if (state.isPreLayout()) { preLayoutData.put(adapterPos, data); @@ -5458,7 +5460,7 @@ public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest if (pendingScrollPosition != NO_POSITION) { assertEquals(pendingScrollPosition, getChildViewHolderInt(getChildAt(0)) - .getAdapterPosition()); + .getAbsoluteAdapterPosition()); } action.jumpTo(pendingScrollPosition + 2); } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewPrefetchTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewPrefetchTest.java index 38a0f4e36fa..6ad0fc89095 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewPrefetchTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewPrefetchTest.java @@ -107,6 +107,6 @@ public class RecyclerViewPrefetchTest extends BaseRecyclerViewInstrumentationTes layout.waitForPrefetch(10); assertThat(cachedViews().size(), is(1)); - assertThat(cachedViews().get(0).getAdapterPosition(), is(6)); + assertThat(cachedViews().get(0).getAbsoluteAdapterPosition(), is(6)); } }
\ No newline at end of file diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java index 800248bd210..47aecfb1893 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java @@ -774,7 +774,7 @@ public class StaggeredGridLayoutManagerBaseConfigSetTest scrollBy(size * 2); assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); - assertThat(vh.getAdapterPosition(), is(pos)); + assertThat(vh.getAbsoluteAdapterPosition(), is(pos)); scrollBy(size * 2); assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); } @@ -811,7 +811,7 @@ public class StaggeredGridLayoutManagerBaseConfigSetTest scrollBy(-size * 2); assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); - assertThat(vh.getAdapterPosition(), is(pos)); + assertThat(vh.getAbsoluteAdapterPosition(), is(pos)); scrollBy(-size * 2); assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerCacheTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerCacheTest.java index 1545d5880f8..dd7781b4449 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerCacheTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerCacheTest.java @@ -71,7 +71,7 @@ public class StaggeredGridLayoutManagerCacheTest extends BaseStaggeredGridLayout private boolean cachedViewsContains(int position) { // Note: can't make assumptions about order here, so just check all cached views for (int i = 0; i < cachedViews().size(); i++) { - if (cachedViews().get(i).getAdapterPosition() == position) return true; + if (cachedViews().get(i).getAbsoluteAdapterPosition() == position) return true; } return false; } diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java index 888d2e54d4c..d298a916091 100644 --- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java @@ -17,8 +17,7 @@ package androidx.recyclerview.widget; import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; -import static androidx.recyclerview.widget.StaggeredGridLayoutManager - .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; +import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE; import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL; import static androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams; @@ -353,6 +352,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag } @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public void onBindViewHolder(@NonNull TestViewHolder holder, int position) { Item item = mItems.get(position); holder.mBoundItem = item; @@ -363,7 +363,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation using this for kitkat tests holder.itemView.setBackgroundDrawable(stl); if (mOnBindCallback != null) { mOnBindCallback.onBoundItem(holder, position); @@ -392,9 +391,10 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder( focusedChild); assertTrue("new focused view should have a larger position " - + lastViewHolder.getAdapterPosition() + " vs " - + containingViewHolder.getAdapterPosition(), - lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition()); + + lastViewHolder.getAbsoluteAdapterPosition() + " vs " + + containingViewHolder.getAbsoluteAdapterPosition(), + lastViewHolder.getAbsoluteAdapterPosition() + < containingViewHolder.getAbsoluteAdapterPosition()); } public void focusSearchFailure(boolean scrollDown) throws Throwable { @@ -404,6 +404,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -414,7 +415,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -460,23 +460,27 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag focusSearchAndWaitForScroll(focusedView, focusDir); focusedView = mRecyclerView.getFocusedChild(); assertEquals(pos + 3, - mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); + mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition()); pos += 3; } for (int i : new int[]{18, 19, 20, 21, 23, 24}) { focusSearchAndWaitForScroll(focusedView, focusDir); focusedView = mRecyclerView.getFocusedChild(); - assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); + assertEquals(i, mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition()); } // now move right focusSearch(focusedView, View.FOCUS_RIGHT); waitForIdleScroll(mRecyclerView); focusedView = mRecyclerView.getFocusedChild(); - assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); + assertEquals(25, + mRecyclerView.getChildViewHolder(focusedView).getAbsoluteAdapterPosition()); for (int i : new int[]{28, 30}) { focusSearchAndWaitForScroll(focusedView, focusDir); focusedView = mRecyclerView.getFocusedChild(); - assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); + assertEquals(i, mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition()); } } @@ -497,6 +501,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -507,7 +512,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -576,7 +580,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{4, 6}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); assertEquals("Focused view should be at adapter position " + i + " whereas it's at " + actualFocusIndex, i, actualFocusIndex); } @@ -587,7 +592,9 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{9, 11, 11, 11}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = + mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); assertEquals("Focused view should not be changed, whereas it's now at " @@ -611,6 +618,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag RecyclerView mAttachedRv; @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -621,7 +629,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -689,7 +696,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{4, 6}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); assertEquals("Focused view should be at adapter position " + i + " whereas it's at " + actualFocusIndex, i, actualFocusIndex); } @@ -700,7 +708,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{9, 11, 11, 11}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); assertEquals("Focused view should not be changed, whereas it's now at " @@ -729,6 +738,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag new GridTestAdapter(18, 1) { @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -739,7 +749,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -806,7 +815,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{4, 6}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); assertEquals("Focused view should be at adapter position " + i + " whereas it's at " + actualFocusIndex, i, actualFocusIndex); } @@ -817,7 +827,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{9, 11, 11, 11}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); assertEquals("Focused view should not be changed, whereas it's now at " @@ -845,6 +856,7 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag new GridTestAdapter(18, 1) { @Override + @SuppressWarnings("deprecated") // using this for kitkat tests public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); @@ -855,7 +867,6 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag stl.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(Color.RED)); stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); - //noinspection deprecation used to support kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @@ -923,7 +934,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{4, 6}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); assertEquals("Focused view should be at adapter position " + i + " whereas it's at " + actualFocusIndex, i, actualFocusIndex); } @@ -934,7 +946,8 @@ public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManag for (int i : new int[]{9, 11, 11, 11}) { focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT); focusedView = mRecyclerView.getFocusedChild(); - actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); + actualFocusIndex = mRecyclerView.getChildViewHolder( + focusedView).getAbsoluteAdapterPosition(); toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); assertEquals("Focused view should not be changed, whereas it's now at " diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java index eacf384ad86..e08fdd158f8 100644 --- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java @@ -705,7 +705,8 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration public void run() { if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() && !anim.mOverridden - && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) { + && anim.mViewHolder.getAbsoluteAdapterPosition() + != RecyclerView.NO_POSITION) { final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); // if animator is running or we have other active recover animations, we try // not to call onSwiped because DefaultItemAnimator is not good at merging @@ -879,8 +880,8 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration mDistances.clear(); return; } - final int toPosition = target.getAdapterPosition(); - final int fromPosition = viewHolder.getAdapterPosition(); + final int toPosition = target.getAbsoluteAdapterPosition(); + final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); if (mCallback.onMove(mRecyclerView, viewHolder, target)) { // keep target visible mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, @@ -1635,8 +1636,8 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration * <p> * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved * to the adapter position of {@code target} ViewHolder - * ({@link ViewHolder#getAdapterPosition() - * ViewHolder#getAdapterPosition()}). + * ({@link ViewHolder#getAbsoluteAdapterPosition() + * ViewHolder#getAdapterPositionInRecyclerView()}). * <p> * If you don't support drag & drop, this method will never be called. * diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapter.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapter.java new file mode 100644 index 00000000000..2362a449cb2 --- /dev/null +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapter.java @@ -0,0 +1,348 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget; + +import android.util.Log; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.util.Preconditions; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.util.Arrays; +import java.util.List; + +/** + * An {@link Adapter} implementation that presents the contents of multiple adapters in sequence. + * + * <pre> + * MyAdapter adapter1 = ...; + * AnotherAdapter adapter2 = ...; + * MergeAdapter merged = new MergeAdapter(adapter1, adapter2); + * recyclerView.setAdapter(mergedAdapter); + * </pre> + * <p> + * By default, {@link MergeAdapter} isolates view types of nested adapters from each other such that + * it will change the view type before reporting it back to the {@link RecyclerView} to avoid any + * conflicts between the view types of added adapters. This also means each added adapter will have + * its own isolated pool of {@link ViewHolder}s, with no re-use in between added adapters. + * <p> + * If your {@link Adapter}s share the same view types, and can support sharing {@link ViewHolder} + * s between added adapters, provide an instance of {@link Config} where you set + * {@link Config#isolateViewTypes} to {@code false}. A common usage pattern for this is to return + * the {@code R.layout.<layout_name>} from the {@link Adapter#getItemViewType(int)} method. + * <p> + * When an added adapter calls one of the {@code notify} methods, {@link MergeAdapter} properly + * offsets values before reporting it back to the {@link RecyclerView}. + * If an adapter calls {@link Adapter#notifyDataSetChanged()}, {@link MergeAdapter} also calls + * {@link Adapter#notifyDataSetChanged()} as calling + * {@link Adapter#notifyItemRangeChanged(int, int)} will confuse the {@link RecyclerView}. + * You are highly encouraged to to use {@link SortedList} or {@link ListAdapter} to avoid + * calling {@link Adapter#notifyDataSetChanged()}. + * <p> + * {@link MergeAdapter} does not yet support stable ids. Even if one of the added adapters have + * stable ids, {@link MergeAdapter} will not use it. + * Calling {@link Adapter#setHasStableIds(boolean)} with {@code true} on a {@link MergeAdapter} + * will result in an {@link IllegalArgumentException}. + * Similar to the case above, you are highly encouraged to use {@link ListAdapter}, which will + * automatically calculate the changes in the data set for you. + * <p> + * It is common to find the adapter position of a {@link ViewHolder} to handle user action on the + * {@link ViewHolder}. For those cases, instead of calling {@link ViewHolder#getAdapterPosition()}, + * use {@link ViewHolder#getBindingAdapterPosition()}. If your adapters share {@link ViewHolder}s, + * you can use the {@link ViewHolder#getBindingAdapter()} method to find the adapter which last + * bound that {@link ViewHolder}. + */ +@SuppressWarnings("unchecked") +public final class MergeAdapter extends Adapter<ViewHolder> { + private static final String TAG = "MergeAdapter"; + /** + * Bulk of the logic is in the controller to keep this class isolated to the public API. + */ + private final MergeAdapterController mController; + + /** + * Creates a MergeAdapter with {@link Config#DEFAULT} and the given adapters in the given order. + * + * @param adapters The list of adapters to add + */ + @SafeVarargs + public MergeAdapter(@NonNull Adapter<? extends ViewHolder>... adapters) { + this(Config.DEFAULT, adapters); + } + + /** + * Creates a MergeAdapter with the given config and the given adapters in the given order. + * + * @param config The configuration for this MergeAdapter + * @param adapters The list of adapters to add + * @see Config.Builder + */ + @SafeVarargs + public MergeAdapter( + @NonNull Config config, + @NonNull Adapter<? extends ViewHolder>... adapters) { + this(config, Arrays.asList(adapters)); + } + + /** + * Creates a MergeAdapter with {@link Config#DEFAULT} and the given adapters in the given order. + * + * @param adapters The list of adapters to add + */ + public MergeAdapter(@NonNull List<Adapter<? extends ViewHolder>> adapters) { + this(Config.DEFAULT, adapters); + } + + /** + * Creates a MergeAdapter with the given config and the given adapters in the given order. + * + * @param config The configuration for this MergeAdapter + * @param adapters The list of adapters to add + * @see Config.Builder + */ + public MergeAdapter( + @NonNull Config config, + @NonNull List<Adapter<? extends ViewHolder>> adapters) { + mController = new MergeAdapterController(this, config); + for (Adapter<? extends ViewHolder> adapter : adapters) { + addAdapter(adapter); + } + } + + /** + * Appends the given adapter to the existing list of adapters and notifies the observers of + * this {@link MergeAdapter}. + * + * @param adapter The new adapter to add + * @return {@code true} if the adapter is successfully added because it did not already exist, + * {@code false} otherwise. + * @see #addAdapter(int, Adapter) + * @see #removeAdapter(Adapter) + */ + public boolean addAdapter(@NonNull Adapter<? extends ViewHolder> adapter) { + return mController.addAdapter((Adapter<ViewHolder>) adapter); + } + + /** + * Adds the given adapter to the given index among other adapters that are already added. + * + * @param index The index into which to insert the adapter. MergeAdapter will throw an + * {@link IndexOutOfBoundsException} if the index is not between 0 and current + * adapter count (inclusive). + * @param adapter The new adapter to add to the adapters list. + * @return {@code true} if the adapter is successfully added because it did not already exist, + * {@code false} otherwise. + * @see #addAdapter(Adapter) + * @see #removeAdapter(Adapter) + */ + public boolean addAdapter(int index, @NonNull Adapter<? extends ViewHolder> adapter) { + return mController.addAdapter(index, (Adapter<ViewHolder>) adapter); + } + + /** + * Removes the given adapter from the adapters list if it exists + * + * @param adapter The adapter to remove + * @return {@code true} if the adapter was previously added to this {@code MergeAdapter} and + * now removed or {@code false} if it couldn't be found. + */ + public boolean removeAdapter(@NonNull Adapter<? extends ViewHolder> adapter) { + return mController.removeAdapter((Adapter<ViewHolder>) adapter); + } + + @Override + public int getItemViewType(int position) { + return mController.getItemViewType(position); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return mController.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + mController.onBindViewHolder(holder, position); + } + + /** + * MergeAdapter does not support stable ids yet. + * Calling this method with {@code true} will result in an {@link IllegalArgumentException}. + * + * @param hasStableIds Whether items in data set have unique identifiers or not. + */ + @Override + public void setHasStableIds(boolean hasStableIds) { + Preconditions.checkArgument(!hasStableIds, + "Stable ids are not supported for MergeAdapter yet"); + //noinspection ConstantConditions + super.setHasStableIds(hasStableIds); + } + + /** + * Calling this method on the MergeAdapter has no impact as the MergeAdapter infers this + * value from added sub adapters. + * + * {@link MergeAdapter} picks the least allowing strategy from given adapters. So if a nested + * adapter sets this to + * {@link androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy#PREVENT}, + * merge adapter will report that. + * + * @param strategy The saved state restoration strategy for this Adapter. + */ + @Override + public void setStateRestorationStrategy(@NonNull StateRestorationStrategy strategy) { + // do nothing + Log.w(TAG, "Calling setStateRestorationStrategy has no impact on the MergeAdapter as" + + " it derives its state restoration strategy from nested adapters"); + } + + /** + * Internal method to be called from the wrappers. + */ + void internalSetStateRestorationStrategy(@NonNull StateRestorationStrategy strategy) { + super.setStateRestorationStrategy(strategy); + } + + @Override + public int getItemCount() { + return mController.getTotalCount(); + } + + @Override + public boolean onFailedToRecycleView(@NonNull ViewHolder holder) { + return mController.onFailedToRecycleView(holder); + } + + @Override + public void onViewAttachedToWindow(@NonNull ViewHolder holder) { + mController.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(@NonNull ViewHolder holder) { + mController.onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + mController.onViewRecycled(holder); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + mController.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mController.onDetachedFromRecyclerView(recyclerView); + } + + /** + * Returns a copy of the list of adapters in this {@link MergeAdapter}. + * Note that this is a copy hence future changes in the MergeAdapter are not reflected in + * this list. + * + * @return A copy of the list of adapters in this MergeAdapter. + */ + @NonNull + public List<Adapter<? extends ViewHolder>> getCopyOfAdapters() { + return mController.getCopyOfAdapters(); + } + + /** + * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. + * + * If the given {@link Adapter} is not part of this {@link MergeAdapter}, + * {@link RecyclerView#NO_POSITION} is returned. + * + * @param adapter The adapter which is a sub adapter of this MergeAdapter or itself. + * @param viewHolder The view holder whose local position in the given adapter will be returned. + * @return The local position of the given {@link ViewHolder} in the given {@link Adapter} or + * {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item or the + * given {@link Adapter} is not part of this MergeAdapter. + */ + @Override + public int findRelativeAdapterPositionIn( + @NonNull Adapter<? extends ViewHolder> adapter, + @NonNull ViewHolder viewHolder, + int globalPosition) { + return mController.getLocalAdapterPosition(adapter, viewHolder, globalPosition); + } + + /** + * The configuration object for a {@link MergeAdapter}. + */ + public static class Config { + /** + * If {@code false}, {@link MergeAdapter} assumes all assigned adapters share a global + * view type pool such that they use the same view types to refer to the same + * {@link ViewHolder}s. + * <p> + * Setting this to {@code false} will allow nested adapters to share {@link ViewHolder}s but + * it also means these adapters should not have conflicting view types + * ({@link Adapter#getItemViewType(int)}) such that two different adapters return the same + * view type for different {@link ViewHolder}s. + * + * By default, it is set to {@code true} which means {@link MergeAdapter} will isolate + * view types across adapters, preventing them from using the same {@link ViewHolder}s. + */ + public final boolean isolateViewTypes; + @NonNull + public static final Config DEFAULT = new Config(true); + + Config(boolean isolateViewTypes) { + this.isolateViewTypes = isolateViewTypes; + } + + /** + * The builder for {@link Config} class. + */ + public static class Builder { + private boolean mIsolateViewTypes; + + /** + * Sets whether {@link MergeAdapter} should isolate view types of nested adapters from + * each other. + * + * @param isolateViewTypes {@code true} if {@link MergeAdapter} should override view + * types of nested adapters to avoid view type + * conflicts, {@code false} otherwise. + * Defaults to true. + * @return this + * @see Config#isolateViewTypes + */ + @NonNull + public Builder setIsolateViewTypes(boolean isolateViewTypes) { + mIsolateViewTypes = isolateViewTypes; + return this; + } + + /** + * @return A new instance of {@link Config} with the given parameters. + */ + @NonNull + public Config build() { + return new Config(mIsolateViewTypes); + } + } + } +} diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapterController.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapterController.java new file mode 100644 index 00000000000..ab003d3428a --- /dev/null +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/MergeAdapterController.java @@ -0,0 +1,464 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.ALLOW; +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.PREVENT; +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.PREVENT_WHEN_EMPTY; +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; + +/** + * All logic for the {@link MergeAdapter} is here so that we can clearly see a separation + * between an adapter implementation and merging logic. + */ +class MergeAdapterController implements NestedAdapterWrapper.Callback { + private final MergeAdapter mMergeAdapter; + + /** + * Holds the mapping from the view type to the adapter which reported that type. + */ + private final ViewTypeStorage mViewTypeStorage; + + /** + * We hold onto the list of attached recyclerviews so that we can dispatch attach/detach to + * any adapter that was added later on. + * Probably does not need to be a weak reference but playing safe here. + */ + private List<WeakReference<RecyclerView>> mAttachedRecyclerViews = new ArrayList<>(); + + /** + * Keeps the information about which ViewHolder is bound by which adapter. + * It is set in onBind, reset at onRecycle. + */ + private final IdentityHashMap<ViewHolder, NestedAdapterWrapper> + mBinderLookup = new IdentityHashMap<>(); + + private List<NestedAdapterWrapper> mWrappers = new ArrayList<>(); + + // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯ + private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition(); + + MergeAdapterController( + MergeAdapter mergeAdapter, + MergeAdapter.Config config) { + mMergeAdapter = mergeAdapter; + if (config.isolateViewTypes) { + mViewTypeStorage = new ViewTypeStorage.IsolatedViewTypeStorage(); + } else { + mViewTypeStorage = new ViewTypeStorage.SharedIdRangeViewTypeStorage(); + } + } + + @Nullable + private NestedAdapterWrapper findWrapperFor(Adapter<ViewHolder> adapter) { + final int index = indexOfWrapper(adapter); + if (index == -1) { + return null; + } + return mWrappers.get(index); + } + + private int indexOfWrapper(Adapter<ViewHolder> adapter) { + final int limit = mWrappers.size(); + for (int i = 0; i < limit; i++) { + if (mWrappers.get(i).adapter == adapter) { + return i; + } + } + return -1; + } + + /** + * return true if added, false otherwise. + * + * @see MergeAdapter#addAdapter(Adapter) + */ + boolean addAdapter(Adapter<ViewHolder> adapter) { + return addAdapter(mWrappers.size(), adapter); + } + + /** + * return true if added, false otherwise. + * throws exception if index is out of bounds + * + * @see MergeAdapter#addAdapter(int, Adapter) + */ + boolean addAdapter(int index, Adapter<ViewHolder> adapter) { + if (index < 0 || index > mWrappers.size()) { + throw new IndexOutOfBoundsException("Index must be between 0 and " + + mWrappers.size() + ". Given:" + index); + } + NestedAdapterWrapper existing = findWrapperFor(adapter); + if (existing != null) { + return false; + } + NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this, mViewTypeStorage); + mWrappers.add(index, wrapper); + // notify attach for all recyclerview + for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { + RecyclerView recyclerView = reference.get(); + if (recyclerView != null) { + adapter.onAttachedToRecyclerView(recyclerView); + } + } + // new items, notify add for them + if (wrapper.getCachedItemCount() > 0) { + mMergeAdapter.notifyItemRangeInserted( + countItemsBefore(wrapper), + wrapper.getCachedItemCount() + ); + } + // reset state restoration strategy + calculateAndUpdateStateRestorationStrategy(); + return true; + } + + boolean removeAdapter(Adapter<ViewHolder> adapter) { + final int index = indexOfWrapper(adapter); + if (index == -1) { + return false; + } + NestedAdapterWrapper wrapper = mWrappers.get(index); + int offset = countItemsBefore(wrapper); + mWrappers.remove(index); + mMergeAdapter.notifyItemRangeRemoved(offset, wrapper.getCachedItemCount()); + // notify detach for all recyclerviews + for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { + RecyclerView recyclerView = reference.get(); + if (recyclerView != null) { + adapter.onDetachedFromRecyclerView(recyclerView); + } + } + wrapper.dispose(); + calculateAndUpdateStateRestorationStrategy(); + return true; + } + + private int countItemsBefore(NestedAdapterWrapper wrapper) { + int count = 0; + for (NestedAdapterWrapper item : mWrappers) { + if (item != wrapper) { + count += item.getCachedItemCount(); + } else { + break; + } + } + return count; + } + + @Override + public void onChanged(@NonNull NestedAdapterWrapper wrapper) { + // TODO should we notify more cleverly, maybe in v2 + mMergeAdapter.notifyDataSetChanged(); + calculateAndUpdateStateRestorationStrategy(); + } + + @Override + public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mMergeAdapter.notifyItemRangeChanged( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount, @Nullable Object payload) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mMergeAdapter.notifyItemRangeChanged( + positionStart + offset, + itemCount, + payload + ); + } + + @Override + public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mMergeAdapter.notifyItemRangeInserted( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + int offset = countItemsBefore(nestedAdapterWrapper); + mMergeAdapter.notifyItemRangeRemoved( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int fromPosition, int toPosition) { + int offset = countItemsBefore(nestedAdapterWrapper); + mMergeAdapter.notifyItemMoved( + fromPosition + offset, + toPosition + offset + ); + } + + @Override + public void onStateRestorationStrategyChanged(NestedAdapterWrapper nestedAdapterWrapper) { + calculateAndUpdateStateRestorationStrategy(); + } + + private void calculateAndUpdateStateRestorationStrategy() { + StateRestorationStrategy newStrategy = computeStateRestorationStrategy(); + if (newStrategy != mMergeAdapter.getStateRestorationStrategy()) { + mMergeAdapter.internalSetStateRestorationStrategy(newStrategy); + } + } + + private StateRestorationStrategy computeStateRestorationStrategy() { + for (NestedAdapterWrapper wrapper : mWrappers) { + StateRestorationStrategy strategy = + wrapper.adapter.getStateRestorationStrategy(); + if (strategy == PREVENT) { + // one adapter can block all + return PREVENT; + } else if (strategy == PREVENT_WHEN_EMPTY && wrapper.getCachedItemCount() == 0) { + // an adapter wants to allow w/ size but we need to make sure there is no prevent + return PREVENT; + } + } + return ALLOW; + } + + public int getTotalCount() { + // should we cache this as well ? + int total = 0; + for (NestedAdapterWrapper wrapper : mWrappers) { + total += wrapper.getCachedItemCount(); + } + return total; + } + + public int getItemViewType(int globalPosition) { + WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); + int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition); + releaseWrapperAndLocalPosition(wrapperAndPos); + return itemViewType; + } + + public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) { + NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType); + return wrapper.onCreateViewHolder(parent, globalViewType); + } + + /** + * Always call {@link #releaseWrapperAndLocalPosition(WrapperAndLocalPosition)} when you are + * done with it + */ + @NonNull + private WrapperAndLocalPosition findWrapperAndLocalPosition( + int globalPosition + ) { + WrapperAndLocalPosition result; + if (mReusableHolder.mInUse) { + result = new WrapperAndLocalPosition(); + } else { + mReusableHolder.mInUse = true; + result = mReusableHolder; + } + int localPosition = globalPosition; + for (NestedAdapterWrapper wrapper : mWrappers) { + if (wrapper.getCachedItemCount() > localPosition) { + result.mWrapper = wrapper; + result.mLocalPosition = localPosition; + break; + } + localPosition -= wrapper.getCachedItemCount(); + } + if (result.mWrapper == null) { + throw new IllegalArgumentException("Cannot find wrapper for " + globalPosition); + } + return result; + } + + private void releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition) { + wrapperAndLocalPosition.mInUse = false; + wrapperAndLocalPosition.mWrapper = null; + wrapperAndLocalPosition.mLocalPosition = -1; + mReusableHolder = wrapperAndLocalPosition; + } + + public void onBindViewHolder(ViewHolder holder, int globalPosition) { + WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); + mBinderLookup.put(holder, wrapperAndPos.mWrapper); + wrapperAndPos.mWrapper.onBindViewHolder(holder, wrapperAndPos.mLocalPosition); + releaseWrapperAndLocalPosition(wrapperAndPos); + } + + public boolean canRestoreState() { + for (NestedAdapterWrapper wrapper : mWrappers) { + if (!wrapper.adapter.canRestoreState()) { + return false; + } + } + return true; + } + + public void onViewAttachedToWindow(ViewHolder holder) { + NestedAdapterWrapper wrapper = getWrapper(holder); + wrapper.adapter.onViewAttachedToWindow(holder); + } + + public void onViewDetachedFromWindow(ViewHolder holder) { + NestedAdapterWrapper wrapper = getWrapper(holder); + wrapper.adapter.onViewDetachedFromWindow(holder); + } + + public void onViewRecycled(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.remove(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + wrapper.adapter.onViewRecycled(holder); + } + + public boolean onFailedToRecycleView(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.remove(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + return wrapper.adapter.onFailedToRecycleView(holder); + } + + @NonNull + private NestedAdapterWrapper getWrapper(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + return wrapper; + } + + private boolean isAttachedTo(RecyclerView recyclerView) { + for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { + if (reference.get() == recyclerView) { + return true; + } + } + return false; + } + + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + if (isAttachedTo(recyclerView)) { + return; + } + mAttachedRecyclerViews.add(new WeakReference<>(recyclerView)); + for (NestedAdapterWrapper wrapper : mWrappers) { + wrapper.adapter.onAttachedToRecyclerView(recyclerView); + } + } + + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + for (int i = mAttachedRecyclerViews.size() - 1; i >= 0; i--) { + WeakReference<RecyclerView> reference = mAttachedRecyclerViews.get(i); + if (reference.get() == null) { + mAttachedRecyclerViews.remove(i); + } else if (reference.get() == recyclerView) { + mAttachedRecyclerViews.remove(i); + break; // here we can break as we don't keep duplicates + } + } + for (NestedAdapterWrapper wrapper : mWrappers) { + wrapper.adapter.onDetachedFromRecyclerView(recyclerView); + } + } + + public int getLocalAdapterPosition( + Adapter<? extends ViewHolder> adapter, + ViewHolder viewHolder, + int globalPosition + ) { + NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); + if (wrapper == null) { + return NO_POSITION; + } + int itemsBefore = countItemsBefore(wrapper); + // local position is globalPosition - itemsBefore + int localPosition = globalPosition - itemsBefore; + // sanity check to detect errors early on + if (localPosition < 0 || localPosition >= wrapper.adapter.getItemCount()) { + throw new IllegalStateException("Detected inconsistent adapter updates. The" + + " local position of the view holder maps to " + localPosition + " which" + + " is out of bounds for the adapter with size " + + wrapper.getCachedItemCount() + "." + + "Make sure to immediately call notify methods in your adapter when you " + + "change the backing data" + + "viewHolder:" + viewHolder + + "adapter:" + adapter); + } + return wrapper.adapter.findRelativeAdapterPositionIn(adapter, viewHolder, localPosition); + } + + + @Nullable + public Adapter<? extends ViewHolder> getBoundAdapter(ViewHolder viewHolder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); + if (wrapper == null) { + return null; + } + return wrapper.adapter; + } + + public List<Adapter<? extends ViewHolder>> getCopyOfAdapters() { + if (mWrappers.isEmpty()) { + return Collections.emptyList(); + } + List<Adapter<? extends ViewHolder>> adapters = new ArrayList<>(mWrappers.size()); + for (NestedAdapterWrapper wrapper : mWrappers) { + adapters.add(wrapper.adapter); + } + return adapters; + } + + /** + * Helper class to hold onto wrapper and local position without allocating objects as this is + * a very common call. + */ + static class WrapperAndLocalPosition { + NestedAdapterWrapper mWrapper; + int mLocalPosition; + boolean mInUse; + } +} diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java new file mode 100644 index 00000000000..ff1dfe56fb8 --- /dev/null +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationStrategy.PREVENT_WHEN_EMPTY; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Preconditions; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * Wrapper for each adapter in {@link MergeAdapter}. + */ +class NestedAdapterWrapper { + private final ViewTypeStorage.ViewTypeLookup mViewTypeLookup; + public final Adapter<ViewHolder> adapter; + @SuppressWarnings("WeakerAccess") + final Callback mCallback; + // we cache this value so that we can know the previous size when change happens + // this is also important as getting real size while an adapter is dispatching possibly a + // a chain of events might create inconsistencies (as it happens in DiffUtil). + // Instead, we always calculate this value based on notify events. + @SuppressWarnings("WeakerAccess") + int mCachedItemCount; + + private RecyclerView.AdapterDataObserver mAdapterObserver = + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + mCachedItemCount = adapter.getItemCount(); + mCallback.onChanged(NestedAdapterWrapper.this); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + mCallback.onItemRangeChanged( + NestedAdapterWrapper.this, + positionStart, + itemCount, + null + ); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + mCallback.onItemRangeChanged( + NestedAdapterWrapper.this, + positionStart, + itemCount, + payload + ); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mCachedItemCount += itemCount; + mCallback.onItemRangeInserted( + NestedAdapterWrapper.this, + positionStart, + itemCount); + if (mCachedItemCount > 0 + && adapter.getStateRestorationStrategy() == PREVENT_WHEN_EMPTY) { + mCallback.onStateRestorationStrategyChanged(NestedAdapterWrapper.this); + } + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + mCachedItemCount -= itemCount; + mCallback.onItemRangeRemoved( + NestedAdapterWrapper.this, + positionStart, + itemCount + ); + if (mCachedItemCount < 1 + && adapter.getStateRestorationStrategy() == PREVENT_WHEN_EMPTY) { + mCallback.onStateRestorationStrategyChanged(NestedAdapterWrapper.this); + } + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + Preconditions.checkArgument(itemCount == 1, + "moving more than 1 item is not supported in RecyclerView"); + mCallback.onItemRangeMoved( + NestedAdapterWrapper.this, + fromPosition, + toPosition + ); + } + + @Override + public void onStateRestorationStrategyChanged() { + mCallback.onStateRestorationStrategyChanged( + NestedAdapterWrapper.this + ); + } + }; + + NestedAdapterWrapper( + Adapter<ViewHolder> adapter, + final Callback callback, + ViewTypeStorage viewTypeStorage) { + this.adapter = adapter; + mCallback = callback; + mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this); + mCachedItemCount = this.adapter.getItemCount(); + this.adapter.registerAdapterDataObserver(mAdapterObserver); + } + + + void dispose() { + adapter.unregisterAdapterDataObserver(mAdapterObserver); + mViewTypeLookup.dispose(); + } + + int getCachedItemCount() { + return mCachedItemCount; + } + + int getItemViewType(int localPosition) { + return mViewTypeLookup.localToGlobal(adapter.getItemViewType(localPosition)); + } + + ViewHolder onCreateViewHolder( + ViewGroup parent, + int globalViewType) { + int localType = mViewTypeLookup.globalToLocal(globalViewType); + return adapter.onCreateViewHolder(parent, localType); + } + + void onBindViewHolder(ViewHolder viewHolder, int localPosition) { + adapter.bindViewHolder(viewHolder, localPosition); + } + + interface Callback { + void onChanged(@NonNull NestedAdapterWrapper wrapper); + + void onItemRangeChanged( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount + ); + + void onItemRangeChanged( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount, + @Nullable Object payload + ); + + void onItemRangeInserted( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount); + + void onItemRangeRemoved( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount + ); + + void onItemRangeMoved( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int fromPosition, + int toPosition + ); + + void onStateRestorationStrategyChanged(NestedAdapterWrapper nestedAdapterWrapper); + } + +} diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java index 5ae3d825492..201fb2f572e 100644 --- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java @@ -146,14 +146,15 @@ import java.util.List; * is seeing. * <p> * The other set of position related methods are in the form of - * <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAdapterPosition()}, + * <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAbsoluteAdapterPosition()}, + * {@link ViewHolder#getBindingAdapterPosition()}, * {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to * work with up-to-date adapter positions even if they may not have been reflected to layout yet. * For example, if you want to access the item in the adapter on a ViewHolder click, you should use - * {@link ViewHolder#getAdapterPosition()}. Beware that these methods may not be able to calculate - * adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new layout has - * not yet been calculated. For this reasons, you should carefully handle {@link #NO_POSITION} or - * <code>null</code> results from these methods. + * {@link ViewHolder#getBindingAdapterPosition()}. Beware that these methods may not be able to + * calculate adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new + * layout has not yet been calculated. For this reasons, you should carefully handle + * {@link #NO_POSITION} or <code>null</code> results from these methods. * <p> * When writing a {@link LayoutManager} you almost always want to use layout positions whereas when * writing an {@link Adapter}, you probably want to use adapter positions. @@ -3879,7 +3880,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, // removed item. mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION : (focusedVh.isRemoved() ? focusedVh.mOldPosition - : focusedVh.getAdapterPosition()); + : focusedVh.getAbsoluteAdapterPosition()); mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView); } } @@ -4841,7 +4842,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, */ public int getChildAdapterPosition(@NonNull View child) { final ViewHolder holder = getChildViewHolderInt(child); - return holder != null ? holder.getAdapterPosition() : NO_POSITION; + return holder != null ? holder.getAbsoluteAdapterPosition() : NO_POSITION; } /** @@ -4893,7 +4894,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * Note that when Adapter contents change, ViewHolder positions are not updated until the * next layout calculation. If there are pending adapter updates, the return value of this * method may not match your adapter contents. You can use - * #{@link ViewHolder#getAdapterPosition()} to get the current adapter position of a ViewHolder. + * #{@link ViewHolder#getBindingAdapterPosition()} to get the current adapter position + * of a ViewHolder. If the {@link Adapter} that is assigned to the RecyclerView is an adapter + * that combines other adapters (e.g. {@link MergeAdapter}), you can use the + * {@link ViewHolder#getBindingAdapter()}) to find the position relative to the {@link Adapter} + * that bound the {@link ViewHolder}. * <p> * When the ItemAnimator is running a change animation, there might be 2 ViewHolders * with the same layout position representing the same Item. In this case, the updated @@ -4935,7 +4940,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, for (int i = 0; i < childCount; i++) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.isRemoved() - && getAdapterPositionFor(holder) == position) { + && getAdapterPositionInRecyclerView(holder) == position) { if (mChildHelper.isHidden(holder.itemView)) { hidden = holder; } else { @@ -6020,6 +6025,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, @SuppressWarnings("unchecked") private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, int position, long deadlineNs) { + holder.mBindingAdapter = null; holder.mOwnerRecyclerView = RecyclerView.this; final int viewType = holder.getItemViewType(); long startBindNs = getNanoTime(); @@ -6527,6 +6533,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { + holder.mBindingAdapter = null; holder.mOwnerRecyclerView = null; } } @@ -6556,6 +6563,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, if (dispatchRecycled) { dispatchViewRecycled(holder); } + holder.mBindingAdapter = null; holder.mOwnerRecyclerView = null; getRecycledViewPool().putRecycledView(holder); } @@ -7039,8 +7047,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * invalidated or the new position cannot be determined. For this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. If you need the position of an item later - * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will - * have the updated adapter position. + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. * * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can * handle efficient partial bind. @@ -7061,8 +7069,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * invalidated or the new position cannot be determined. For this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. If you need the position of an item later - * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will - * have the updated adapter position. + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. * <p> * Partial bind vs full bind: * <p> @@ -7086,6 +7094,31 @@ public class RecyclerView extends ViewGroup implements ScrollingView, } /** + * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. + * + * If the given {@link Adapter} is not part of this {@link Adapter}, + * {@link RecyclerView#NO_POSITION} is returned. + * + * @param adapter The adapter which is a sub adapter of this adapter or itself. + * @param viewHolder The ViewHolder whose local position in the given adapter will be + * returned. + * @return The local position of the given {@link ViewHolder} in this {@link Adapter} + * or {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item + * or the given {@link Adapter} is not part of this Adapter (if this Adapter merges other + * adapters). + */ + public int findRelativeAdapterPositionIn( + @NonNull Adapter<? extends ViewHolder> adapter, + @NonNull ViewHolder viewHolder, + int localPosition + ) { + if (adapter == this) { + return localPosition; + } + return NO_POSITION; + } + + /** * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new * {@link ViewHolder} and initializes some private fields to be used by RecyclerView. * @@ -7113,24 +7146,38 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * {@link ViewHolder} contents with the item at the given position and also sets up some * private fields to be used by RecyclerView. * + * Adapters that merge other adapters should use + * {@link #bindViewHolder(ViewHolder, int)} when calling nested adapters so that + * RecyclerView can track which adapter bound the {@link ViewHolder} to return the correct + * position from {@link ViewHolder#getBindingAdapterPosition()} method. + * They should also override + * the {@link #findRelativeAdapterPositionIn(Adapter, ViewHolder, int)} method. + * @param holder The view holder whose contents should be updated + * @param position The position of the holder with respect to this adapter * @see #onBindViewHolder(ViewHolder, int) */ public final void bindViewHolder(@NonNull VH holder, int position) { - holder.mPosition = position; - if (hasStableIds()) { - holder.mItemId = getItemId(position); - } - holder.setFlags(ViewHolder.FLAG_BOUND, - ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID - | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); - TraceCompat.beginSection(TRACE_BIND_VIEW_TAG); + boolean rootBind = holder.mBindingAdapter == null; + if (rootBind) { + holder.mPosition = position; + if (hasStableIds()) { + holder.mItemId = getItemId(position); + } + holder.setFlags(ViewHolder.FLAG_BOUND, + ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + TraceCompat.beginSection(TRACE_BIND_VIEW_TAG); + } + holder.mBindingAdapter = this; onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); - holder.clearPayload(); - final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); - if (layoutParams instanceof RecyclerView.LayoutParams) { - ((LayoutParams) layoutParams).mInsetsDirty = true; + if (rootBind) { + holder.clearPayload(); + final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); + if (layoutParams instanceof RecyclerView.LayoutParams) { + ((LayoutParams) layoutParams).mInsetsDirty = true; + } + TraceCompat.endSection(); } - TraceCompat.endSection(); } /** @@ -7207,7 +7254,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * <p> * RecyclerView calls this method right before clearing ViewHolder's internal data and * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information - * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get * its adapter position. * * @param holder The ViewHolder for the view being recycled @@ -11007,7 +11054,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * * RecyclerView calls this method right before clearing ViewHolder's internal data and * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information - * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get * its adapter position. * * @param holder The ViewHolder containing the view that was recycled @@ -11186,6 +11233,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, */ RecyclerView mOwnerRecyclerView; + // The last adapter that bound this ViewHolder. It is cleaned before VH is recycled. + Adapter<? extends ViewHolder> mBindingAdapter; + public ViewHolder(@NonNull View itemView) { if (itemView == null) { throw new IllegalArgumentException("itemView may not be null"); @@ -11232,11 +11282,13 @@ public class RecyclerView extends ViewGroup implements ScrollingView, /** * @deprecated This method is deprecated because its meaning is ambiguous due to the async - * handling of adapter updates. You should use {@link #getLayoutPosition()} or - * {@link #getAdapterPosition()} depending on your use case. + * handling of adapter updates. You should use {@link #getLayoutPosition()}, + * {@link #getBindingAdapterPosition()} or {@link #getAbsoluteAdapterPosition()} + * depending on your use case. * * @see #getLayoutPosition() - * @see #getAdapterPosition() + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() */ @Deprecated public final int getPosition() { @@ -11259,18 +11311,33 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * of the item. * <p> * If LayoutManager needs to call an external method that requires the adapter position of - * the item, it can use {@link #getAdapterPosition()} or + * the item, it can use {@link #getAbsoluteAdapterPosition()} or * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. * * @return Returns the adapter position of the ViewHolder in the latest layout pass. - * @see #getAdapterPosition() + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() */ public final int getLayoutPosition() { return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; } + /** - * Returns the Adapter position of the item represented by this ViewHolder. + * @deprecated This method is confusing when adapters nest other adapters. + * If you are calling this in the context of an Adapter, you probably want to call + * {@link #getBindingAdapterPosition()} or if you want the position as {@link RecyclerView} + * sees it, you should call {@link #getAbsoluteAdapterPosition()}. + * @return {@link #getBindingAdapterPosition()} + */ + @Deprecated + public final int getAdapterPosition() { + return getBindingAdapterPosition(); + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link Adapter} that bound it. * <p> * Note that this might be different than the {@link #getLayoutPosition()} if there are * pending adapter updates but a new layout pass has not happened yet. @@ -11285,17 +11352,89 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * <p> * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the * next layout pass, the return value of this method will be {@link #NO_POSITION}. - * + * <p> + * If the {@link Adapter} that bound this {@link ViewHolder} is inside another + * {@link Adapter} (e.g. {@link MergeAdapter}), this position might be different than + * {@link #getAbsoluteAdapterPosition()}. If you would like to know the position that + * {@link RecyclerView} considers (e.g. for saved state), you should use + * {@link #getAbsoluteAdapterPosition()}. * @return The adapter position of the item if it still exists in the adapter. * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last * layout pass or the ViewHolder has already been recycled. + * @see #getAbsoluteAdapterPosition() + * @see #getLayoutPosition() */ - public final int getAdapterPosition() { + public final int getBindingAdapterPosition() { + if (mBindingAdapter == null) { + return NO_POSITION; + } if (mOwnerRecyclerView == null) { return NO_POSITION; } - return mOwnerRecyclerView.getAdapterPositionFor(this); + @SuppressWarnings("unchecked") + Adapter<? extends ViewHolder> rvAdapter = mOwnerRecyclerView.getAdapter(); + if (rvAdapter == null) { + return NO_POSITION; + } + int globalPosition = mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + if (globalPosition == NO_POSITION) { + return NO_POSITION; + } + return rvAdapter.findRelativeAdapterPositionIn(mBindingAdapter, this, globalPosition); + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link RecyclerView}'s {@link Adapter}. If the {@link Adapter} that bound this + * {@link ViewHolder} is inside another adapter (e.g. {@link MergeAdapter}), this + * position might be different and will include + * the offsets caused by other adapters in the {@link MergeAdapter}. + * <p> + * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + * <p> + * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + * <p> + * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + * <p> + * Note that if you are querying the position as {@link RecyclerView} sees, you should use + * {@link #getAbsoluteAdapterPosition()} (e.g. you want to use it to save scroll + * state). If you are querying the position to access the {@link Adapter} contents, + * you should use {@link #getBindingAdapterPosition()}. + * + * @return The adapter position of the item from {@link RecyclerView}'s perspective if it + * still exists in the adapter and bound to a valid item. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + * @see #getBindingAdapterPosition() + * @see #getLayoutPosition() + */ + public final int getAbsoluteAdapterPosition() { + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + return mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + } + + /** + * Returns the {@link Adapter} that last bound this {@link ViewHolder}. + * Might return {@code null} if this {@link ViewHolder} is not bound to any adapter. + * + * @return The {@link Adapter} that last bound this {@link ViewHolder} or {@code null} if + * this {@link ViewHolder} is not bound by any adapter (e.g. recycled). + */ + @Nullable + public final Adapter<? extends ViewHolder> getBindingAdapter() { + return mBindingAdapter; } /** @@ -11596,7 +11735,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, mPendingAccessibilityImportanceChange.clear(); } - int getAdapterPositionFor(ViewHolder viewHolder) { + int getAdapterPositionInRecyclerView(ViewHolder viewHolder) { if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) || !viewHolder.isBound()) { @@ -11805,15 +11944,41 @@ public class RecyclerView extends ViewGroup implements ScrollingView, } /** + * @deprecated This method is confusing when nested adapters are used. + * If you are calling from the context of an {@link Adapter}, + * use {@link #getBindingAdapterPosition()}. If you need the position that + * {@link RecyclerView} sees, use {@link #getAbsoluteAdapterPosition()}. + */ + @Deprecated + public int getViewAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); + } + + /** * Returns the up-to-date adapter position that the view this LayoutParams is attached to - * corresponds to. + * corresponds to in the {@link RecyclerView}. If the {@link RecyclerView} has an + * {@link Adapter} that merges other adapters, this position will be with respect to the + * adapter that is assigned to the {@link RecyclerView}. * - * @return the up-to-date adapter position this view. It may return - * {@link RecyclerView#NO_POSITION} if item represented by this View has been removed or + * @return the up-to-date adapter position this view with respect to the RecyclerView. It + * may return {@link RecyclerView#NO_POSITION} if item represented by this View has been + * removed or * its up-to-date position cannot be calculated. */ - public int getViewAdapterPosition() { - return mViewHolder.getAdapterPosition(); + public int getAbsoluteAdapterPosition() { + return mViewHolder.getAbsoluteAdapterPosition(); + } + + /** + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to with respect to the {@link Adapter} that bound this View. + * + * @return the up-to-date adapter position this view relative to the {@link Adapter} that + * bound this View. It may return {@link RecyclerView#NO_POSITION} if item represented by + * this View has been removed or its up-to-date position cannot be calculated. + */ + public int getBindingAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); } } @@ -13237,7 +13402,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, } if ((flags & FLAG_INVALIDATED) == 0) { final int oldPos = viewHolder.getOldPosition(); - final int pos = viewHolder.getAdapterPosition(); + final int pos = viewHolder.getAbsoluteAdapterPosition(); if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) { flags |= FLAG_MOVED; } diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java new file mode 100644 index 00000000000..0f131299f14 --- /dev/null +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java @@ -0,0 +1,197 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.recyclerview.widget; + +import android.util.SparseArray; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used by {@link MergeAdapter} to isolate view types between nested adapters, if necessary. + */ +interface ViewTypeStorage { + @NonNull + NestedAdapterWrapper getWrapperForGlobalType(int globalViewType); + + @NonNull + ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper + ); + + /** + * Api given to {@link NestedAdapterWrapper}s. + */ + interface ViewTypeLookup { + int localToGlobal(int localType); + + int globalToLocal(int globalType); + + void dispose(); + } + + class SharedIdRangeViewTypeStorage implements ViewTypeStorage { + // we keep a list of nested wrappers here even though we only need 1 to create because + // they might be removed. + SparseArray<List<NestedAdapterWrapper>> mGlobalTypeToWrapper = new SparseArray<>(); + + @NonNull + @Override + public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) { + List<NestedAdapterWrapper> nestedAdapterWrappers = mGlobalTypeToWrapper.get( + globalViewType); + if (nestedAdapterWrappers == null || nestedAdapterWrappers.isEmpty()) { + throw new IllegalArgumentException("Cannot find the wrapper for global view" + + " type " + globalViewType); + } + // just return the first one since they are shared + return nestedAdapterWrappers.get(0); + } + + @NonNull + @Override + public ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper) { + return new WrapperViewTypeLookup(wrapper); + } + + void removeWrapper(@NonNull NestedAdapterWrapper wrapper) { + for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) { + List<NestedAdapterWrapper> wrappers = mGlobalTypeToWrapper.valueAt(i); + if (wrappers.remove(wrapper)) { + if (wrappers.isEmpty()) { + mGlobalTypeToWrapper.removeAt(i); + } + } + } + } + + class WrapperViewTypeLookup implements ViewTypeLookup { + final NestedAdapterWrapper mWrapper; + + WrapperViewTypeLookup(NestedAdapterWrapper wrapper) { + mWrapper = wrapper; + } + + @Override + public int localToGlobal(int localType) { + // register it first + List<NestedAdapterWrapper> wrappers = mGlobalTypeToWrapper.get( + localType); + if (wrappers == null) { + wrappers = new ArrayList<>(); + mGlobalTypeToWrapper.put(localType, wrappers); + } + if (!wrappers.contains(mWrapper)) { + wrappers.add(mWrapper); + } + return localType; + } + + @Override + public int globalToLocal(int globalType) { + return globalType; + } + + @Override + public void dispose() { + removeWrapper(mWrapper); + } + } + } + + class IsolatedViewTypeStorage implements ViewTypeStorage { + SparseArray<NestedAdapterWrapper> mGlobalTypeToWrapper = new SparseArray<>(); + + int mNextViewType = 0; + + int obtainViewType(NestedAdapterWrapper wrapper) { + int nextId = mNextViewType++; + mGlobalTypeToWrapper.put(nextId, wrapper); + return nextId; + } + + @NonNull + @Override + public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) { + NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get( + globalViewType); + if (wrapper == null) { + throw new IllegalArgumentException("Cannot find the wrapper for global" + + " view type " + globalViewType); + } + return wrapper; + } + + @Override + @NonNull + public ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper) { + return new WrapperViewTypeLookup(wrapper); + } + + void removeWrapper(@NonNull NestedAdapterWrapper wrapper) { + for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) { + NestedAdapterWrapper existingWrapper = mGlobalTypeToWrapper.valueAt(i); + if (existingWrapper == wrapper) { + mGlobalTypeToWrapper.removeAt(i); + } + } + } + + class WrapperViewTypeLookup implements ViewTypeLookup { + private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1); + private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1); + final NestedAdapterWrapper mWrapper; + + WrapperViewTypeLookup(NestedAdapterWrapper wrapper) { + mWrapper = wrapper; + } + + @Override + public int localToGlobal(int localType) { + int index = mLocalToGlobalMapping.indexOfKey(localType); + if (index > -1) { + return mLocalToGlobalMapping.valueAt(index); + } + // get a new key. + int globalType = obtainViewType(mWrapper); + mLocalToGlobalMapping.put(localType, globalType); + mGlobalToLocalMapping.put(globalType, localType); + return globalType; + } + + @Override + public int globalToLocal(int globalType) { + int index = mGlobalToLocalMapping.indexOfKey(globalType); + if (index < 0) { + throw new IllegalStateException("requested global type " + globalType + " does" + + " not belong to the adapter:" + mWrapper.adapter); + } + return mGlobalToLocalMapping.valueAt(index); + } + + @Override + public void dispose() { + removeWrapper(mWrapper); + } + } + } +} diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/util/SortedListActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/util/SortedListActivity.java index 761c3838033..7f4377d4014 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/util/SortedListActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/util/SortedListActivity.java @@ -121,7 +121,7 @@ public class SortedListActivity extends AppCompatActivity { mLayoutInflater.inflate(R.layout.sorted_list_item_view, parent, false)) { @Override void onDoneChanged(boolean isDone) { - int adapterPosition = getAdapterPosition(); + int adapterPosition = getBindingAdapterPosition(); if (adapterPosition == RecyclerView.NO_POSITION) { return; } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/AnimatedRecyclerView.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/AnimatedRecyclerView.java index 8b7a4782f6c..3c857c52e52 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/AnimatedRecyclerView.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/AnimatedRecyclerView.java @@ -359,7 +359,7 @@ public class AnimatedRecyclerView extends Activity { public void itemClicked(View view) { ViewGroup parent = (ViewGroup) view; MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent); - final int position = holder.getAdapterPosition(); + final int position = holder.getBindingAdapterPosition(); if (position == RecyclerView.NO_POSITION) { return; } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java index 98ae6c441ce..ff1f343bb89 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java @@ -87,7 +87,7 @@ abstract public class BaseLayoutManagerActivity<T extends RecyclerView.LayoutMan vh.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final int pos = vh.getAdapterPosition(); + final int pos = vh.getBindingAdapterPosition(); if (pos != RecyclerView.NO_POSITION && pos + 1 < getItemCount()) { swap(pos, pos + 1); } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/GridLayoutManagerActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/GridLayoutManagerActivity.java index 9ac08f2dc24..fd1f2e0a20e 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/GridLayoutManagerActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/GridLayoutManagerActivity.java @@ -122,7 +122,7 @@ public class GridLayoutManagerActivity extends BaseLayoutManagerActivity<GridLay vh.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final int pos = vh.getAdapterPosition(); + final int pos = vh.getBindingAdapterPosition(); if (pos == RecyclerView.NO_POSITION) { return; } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java index 106c9d3ea50..366028ac230 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java @@ -170,7 +170,7 @@ public class NestedRecyclerViewActivity extends BaseLayoutManagerActivity<Linear @Override public void onViewRecycled(@NonNull ViewHolder holder) { - mSavedStates.set(holder.getAdapterPosition(), + mSavedStates.set(holder.getBindingAdapterPosition(), holder.mRecyclerView.getLayoutManager().onSaveInstanceState()); } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewActivity.java index 220049dd4ae..a80ea810e7d 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewActivity.java @@ -56,7 +56,7 @@ public class RecyclerViewActivity extends Activity { vh.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final int pos = vh.getAdapterPosition(); + final int pos = vh.getBindingAdapterPosition(); if (pos == RecyclerView.NO_POSITION) { return; } diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/StableIdActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/StableIdActivity.java index f4990477180..7d88692293e 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/StableIdActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/StableIdActivity.java @@ -115,7 +115,7 @@ public class StableIdActivity extends BaseLayoutManagerActivity<LinearLayoutMana viewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final int pos = viewHolder.getAdapterPosition(); + final int pos = viewHolder.getBindingAdapterPosition(); if (pos != RecyclerView.NO_POSITION) { // swap item to top, and notify data set changed Pair<Integer, String> d = mData.remove(pos); diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyItemHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyItemHolder.java index c7761b230a3..944e200acc3 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyItemHolder.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyItemHolder.java @@ -45,7 +45,7 @@ final class FancyItemHolder extends FancyHolder { mDetails = new ItemDetails<Uri>() { @Override public int getPosition() { - return FancyItemHolder.this.getAdapterPosition(); + return FancyItemHolder.this.getBindingAdapterPosition(); } @Override diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java index 975990591f4..9edbdd8bba0 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java @@ -41,7 +41,7 @@ final class DemoHolder extends RecyclerView.ViewHolder { mDetails = new ItemDetails<Long>() { @Override public int getPosition() { - return DemoHolder.this.getAdapterPosition(); + return DemoHolder.this.getBindingAdapterPosition(); } @Override diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/touch/ItemTouchHelperActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/touch/ItemTouchHelperActivity.java index 1730c9ca093..5a2cd9e92db 100644 --- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/touch/ItemTouchHelperActivity.java +++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/touch/ItemTouchHelperActivity.java @@ -115,7 +115,8 @@ abstract public class ItemTouchHelperActivity extends Activity { public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - mAdapter.move(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + mAdapter.move(viewHolder.getBindingAdapterPosition(), + target.getBindingAdapterPosition()); return true; } diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/PagedListViewActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/PagedListViewActivity.java index a4a8ae88129..f38262c6ae6 100644 --- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/PagedListViewActivity.java +++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/PagedListViewActivity.java @@ -133,7 +133,7 @@ public class PagedListViewActivity extends Activity { @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition()); } } } diff --git a/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/widget/RecyclerViewUsage.java b/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/widget/RecyclerViewUsage.java index 2bbcddcde21..4f0b18f73fc 100644 --- a/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/widget/RecyclerViewUsage.java +++ b/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/widget/RecyclerViewUsage.java @@ -68,7 +68,7 @@ public class RecyclerViewUsage extends TransitionUsageBase { @Override public void onBindViewHolder(@NonNull TransitionHolder holder, int position) { holder.itemView.setOnClickListener(v -> { - final int clickedPosition = holder.getAdapterPosition(); + final int clickedPosition = holder.getBindingAdapterPosition(); if (clickedPosition == RecyclerView.NO_POSITION) return; TransitionManager.beginDelayedTransition(mRecyclerView, diff --git a/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/ParallelNestedScrollingActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/ParallelNestedScrollingActivity.kt index c20b77dd576..bdd594ddd9b 100644 --- a/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/ParallelNestedScrollingActivity.kt +++ b/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/ParallelNestedScrollingActivity.kt @@ -60,7 +60,8 @@ class ParallelNestedScrollingActivity : Activity() { override fun onBindViewHolder(holder: ViewHolder, position: Int) { with(holder) { - title.text = title.context.getString(R.string.page_position, adapterPosition) + title.text = + title.context.getString(R.string.page_position, absoluteAdapterPosition) itemView.setBackgroundResource(PAGE_COLORS[position % PAGE_COLORS.size]) } } @@ -99,7 +100,8 @@ class ParallelNestedScrollingActivity : Activity() { override fun onBindViewHolder(holder: ViewHolder, position: Int) { with(holder) { - tv.text = tv.context.getString(R.string.item_position, adapterPosition) + tv.text = + tv.context.getString(R.string.item_position, absoluteAdapterPosition) tv.setBackgroundResource(CELL_COLORS[position % CELL_COLORS.size]) } } |