1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.recyclerview.widget; 18 19 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS; 20 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS; 21 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS; 22 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW; 23 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT; 24 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY; 25 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; 26 27 import android.util.Log; 28 import android.util.Pair; 29 import android.view.ViewGroup; 30 31 import androidx.core.util.Preconditions; 32 import androidx.recyclerview.widget.RecyclerView.Adapter; 33 import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy; 34 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 35 36 import org.jspecify.annotations.NonNull; 37 import org.jspecify.annotations.Nullable; 38 39 import java.lang.ref.WeakReference; 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.IdentityHashMap; 43 import java.util.List; 44 45 /** 46 * All logic for the {@link ConcatAdapter} is here so that we can clearly see a separation 47 * between an adapter implementation and merging logic. 48 */ 49 class ConcatAdapterController implements NestedAdapterWrapper.Callback { 50 private final ConcatAdapter mConcatAdapter; 51 52 /** 53 * Holds the mapping from the view type to the adapter which reported that type. 54 */ 55 private final ViewTypeStorage mViewTypeStorage; 56 57 /** 58 * We hold onto the list of attached recyclerviews so that we can dispatch attach/detach to 59 * any adapter that was added later on. 60 * Probably does not need to be a weak reference but playing safe here. 61 */ 62 private List<WeakReference<RecyclerView>> mAttachedRecyclerViews = new ArrayList<>(); 63 64 /** 65 * Keeps the information about which ViewHolder is bound by which adapter. 66 * It is set in onBind, reset at onRecycle. 67 */ 68 private final IdentityHashMap<ViewHolder, NestedAdapterWrapper> 69 mBinderLookup = new IdentityHashMap<>(); 70 71 private List<NestedAdapterWrapper> mWrappers = new ArrayList<>(); 72 73 // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯ 74 private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition(); 75 76 private final ConcatAdapter.Config.@NonNull StableIdMode mStableIdMode; 77 78 /** 79 * This is where we keep stable ids, if supported 80 */ 81 private final StableIdStorage mStableIdStorage; 82 ConcatAdapterController( ConcatAdapter concatAdapter, ConcatAdapter.Config config)83 ConcatAdapterController( 84 ConcatAdapter concatAdapter, 85 ConcatAdapter.Config config) { 86 mConcatAdapter = concatAdapter; 87 88 // setup view type handling 89 if (config.isolateViewTypes) { 90 mViewTypeStorage = new ViewTypeStorage.IsolatedViewTypeStorage(); 91 } else { 92 mViewTypeStorage = new ViewTypeStorage.SharedIdRangeViewTypeStorage(); 93 } 94 95 // setup stable id handling 96 mStableIdMode = config.stableIdMode; 97 if (config.stableIdMode == NO_STABLE_IDS) { 98 mStableIdStorage = new StableIdStorage.NoStableIdStorage(); 99 } else if (config.stableIdMode == ISOLATED_STABLE_IDS) { 100 mStableIdStorage = new StableIdStorage.IsolatedStableIdStorage(); 101 } else if (config.stableIdMode == SHARED_STABLE_IDS) { 102 mStableIdStorage = new StableIdStorage.SharedPoolStableIdStorage(); 103 } else { 104 throw new IllegalArgumentException("unknown stable id mode"); 105 } 106 } 107 findWrapperFor(Adapter<ViewHolder> adapter)108 private @Nullable NestedAdapterWrapper findWrapperFor(Adapter<ViewHolder> adapter) { 109 final int index = indexOfWrapper(adapter); 110 if (index == -1) { 111 return null; 112 } 113 return mWrappers.get(index); 114 } 115 indexOfWrapper(Adapter<ViewHolder> adapter)116 private int indexOfWrapper(Adapter<ViewHolder> adapter) { 117 final int limit = mWrappers.size(); 118 for (int i = 0; i < limit; i++) { 119 if (mWrappers.get(i).adapter == adapter) { 120 return i; 121 } 122 } 123 return -1; 124 } 125 126 /** 127 * return true if added, false otherwise. 128 * 129 * @see ConcatAdapter#addAdapter(Adapter) 130 */ addAdapter(Adapter<ViewHolder> adapter)131 boolean addAdapter(Adapter<ViewHolder> adapter) { 132 return addAdapter(mWrappers.size(), adapter); 133 } 134 135 /** 136 * return true if added, false otherwise. 137 * throws exception if index is out of bounds 138 * 139 * @see ConcatAdapter#addAdapter(int, Adapter) 140 */ addAdapter(int index, Adapter<ViewHolder> adapter)141 boolean addAdapter(int index, Adapter<ViewHolder> adapter) { 142 if (index < 0 || index > mWrappers.size()) { 143 throw new IndexOutOfBoundsException("Index must be between 0 and " 144 + mWrappers.size() + ". Given:" + index); 145 } 146 if (hasStableIds()) { 147 Preconditions.checkArgument(adapter.hasStableIds(), 148 "All sub adapters must have stable ids when stable id mode " 149 + "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS"); 150 } else { 151 if (adapter.hasStableIds()) { 152 Log.w(ConcatAdapter.TAG, "Stable ids in the adapter will be ignored as the" 153 + " ConcatAdapter is configured not to have stable ids"); 154 } 155 } 156 NestedAdapterWrapper existing = findWrapperFor(adapter); 157 if (existing != null) { 158 return false; 159 } 160 NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this, 161 mViewTypeStorage, mStableIdStorage.createStableIdLookup()); 162 mWrappers.add(index, wrapper); 163 // notify attach for all recyclerview 164 for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { 165 RecyclerView recyclerView = reference.get(); 166 if (recyclerView != null) { 167 adapter.onAttachedToRecyclerView(recyclerView); 168 } 169 } 170 // new items, notify add for them 171 if (wrapper.getCachedItemCount() > 0) { 172 mConcatAdapter.notifyItemRangeInserted( 173 countItemsBefore(wrapper), 174 wrapper.getCachedItemCount() 175 ); 176 } 177 // reset state restoration strategy 178 calculateAndUpdateStateRestorationPolicy(); 179 return true; 180 } 181 removeAdapter(Adapter<ViewHolder> adapter)182 boolean removeAdapter(Adapter<ViewHolder> adapter) { 183 final int index = indexOfWrapper(adapter); 184 if (index == -1) { 185 return false; 186 } 187 NestedAdapterWrapper wrapper = mWrappers.get(index); 188 int offset = countItemsBefore(wrapper); 189 mWrappers.remove(index); 190 mConcatAdapter.notifyItemRangeRemoved(offset, wrapper.getCachedItemCount()); 191 // notify detach for all recyclerviews 192 for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { 193 RecyclerView recyclerView = reference.get(); 194 if (recyclerView != null) { 195 adapter.onDetachedFromRecyclerView(recyclerView); 196 } 197 } 198 wrapper.dispose(); 199 calculateAndUpdateStateRestorationPolicy(); 200 return true; 201 } 202 countItemsBefore(NestedAdapterWrapper wrapper)203 private int countItemsBefore(NestedAdapterWrapper wrapper) { 204 int count = 0; 205 for (NestedAdapterWrapper item : mWrappers) { 206 if (item != wrapper) { 207 count += item.getCachedItemCount(); 208 } else { 209 break; 210 } 211 } 212 return count; 213 } 214 getItemId(int globalPosition)215 public long getItemId(int globalPosition) { 216 WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); 217 long globalItemId = wrapperAndPos.mWrapper.getItemId(wrapperAndPos.mLocalPosition); 218 releaseWrapperAndLocalPosition(wrapperAndPos); 219 return globalItemId; 220 } 221 222 @Override onChanged(@onNull NestedAdapterWrapper wrapper)223 public void onChanged(@NonNull NestedAdapterWrapper wrapper) { 224 // TODO should we notify more cleverly, maybe in v2 225 mConcatAdapter.notifyDataSetChanged(); 226 calculateAndUpdateStateRestorationPolicy(); 227 } 228 229 @Override onItemRangeChanged(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)230 public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, 231 int positionStart, int itemCount) { 232 final int offset = countItemsBefore(nestedAdapterWrapper); 233 mConcatAdapter.notifyItemRangeChanged( 234 positionStart + offset, 235 itemCount 236 ); 237 } 238 239 @Override onItemRangeChanged(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount, @Nullable Object payload)240 public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, 241 int positionStart, int itemCount, @Nullable Object payload) { 242 final int offset = countItemsBefore(nestedAdapterWrapper); 243 mConcatAdapter.notifyItemRangeChanged( 244 positionStart + offset, 245 itemCount, 246 payload 247 ); 248 } 249 250 @Override onItemRangeInserted(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)251 public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper, 252 int positionStart, int itemCount) { 253 final int offset = countItemsBefore(nestedAdapterWrapper); 254 mConcatAdapter.notifyItemRangeInserted( 255 positionStart + offset, 256 itemCount 257 ); 258 } 259 260 @Override onItemRangeRemoved(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)261 public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, 262 int positionStart, int itemCount) { 263 int offset = countItemsBefore(nestedAdapterWrapper); 264 mConcatAdapter.notifyItemRangeRemoved( 265 positionStart + offset, 266 itemCount 267 ); 268 } 269 270 @Override onItemRangeMoved(@onNull NestedAdapterWrapper nestedAdapterWrapper, int fromPosition, int toPosition)271 public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, 272 int fromPosition, int toPosition) { 273 int offset = countItemsBefore(nestedAdapterWrapper); 274 mConcatAdapter.notifyItemMoved( 275 fromPosition + offset, 276 toPosition + offset 277 ); 278 } 279 280 @Override onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper)281 public void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper) { 282 calculateAndUpdateStateRestorationPolicy(); 283 } 284 calculateAndUpdateStateRestorationPolicy()285 private void calculateAndUpdateStateRestorationPolicy() { 286 StateRestorationPolicy newPolicy = computeStateRestorationPolicy(); 287 if (newPolicy != mConcatAdapter.getStateRestorationPolicy()) { 288 mConcatAdapter.internalSetStateRestorationPolicy(newPolicy); 289 } 290 } 291 computeStateRestorationPolicy()292 private StateRestorationPolicy computeStateRestorationPolicy() { 293 for (NestedAdapterWrapper wrapper : mWrappers) { 294 StateRestorationPolicy strategy = 295 wrapper.adapter.getStateRestorationPolicy(); 296 if (strategy == PREVENT) { 297 // one adapter can block all 298 return PREVENT; 299 } else if (strategy == PREVENT_WHEN_EMPTY && wrapper.getCachedItemCount() == 0) { 300 // an adapter wants to allow w/ size but we need to make sure there is no prevent 301 return PREVENT; 302 } 303 } 304 return ALLOW; 305 } 306 getTotalCount()307 public int getTotalCount() { 308 // should we cache this as well ? 309 int total = 0; 310 for (NestedAdapterWrapper wrapper : mWrappers) { 311 total += wrapper.getCachedItemCount(); 312 } 313 return total; 314 } 315 getItemViewType(int globalPosition)316 public int getItemViewType(int globalPosition) { 317 WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); 318 int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition); 319 releaseWrapperAndLocalPosition(wrapperAndPos); 320 return itemViewType; 321 } 322 onCreateViewHolder(ViewGroup parent, int globalViewType)323 public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) { 324 NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType); 325 return wrapper.onCreateViewHolder(parent, globalViewType); 326 } 327 getWrappedAdapterAndPosition( int globalPosition)328 public Pair<Adapter<? extends ViewHolder>, Integer> getWrappedAdapterAndPosition( 329 int globalPosition) { 330 WrapperAndLocalPosition wrapper = findWrapperAndLocalPosition(globalPosition); 331 Pair<Adapter<? extends ViewHolder>, Integer> pair = new Pair<>(wrapper.mWrapper.adapter, 332 wrapper.mLocalPosition); 333 releaseWrapperAndLocalPosition(wrapper); 334 return pair; 335 } 336 337 /** 338 * Always call {@link #releaseWrapperAndLocalPosition(WrapperAndLocalPosition)} when you are 339 * done with it 340 */ findWrapperAndLocalPosition( int globalPosition )341 private @NonNull WrapperAndLocalPosition findWrapperAndLocalPosition( 342 int globalPosition 343 ) { 344 WrapperAndLocalPosition result; 345 if (mReusableHolder.mInUse) { 346 result = new WrapperAndLocalPosition(); 347 } else { 348 mReusableHolder.mInUse = true; 349 result = mReusableHolder; 350 } 351 int localPosition = globalPosition; 352 for (NestedAdapterWrapper wrapper : mWrappers) { 353 if (wrapper.getCachedItemCount() > localPosition) { 354 result.mWrapper = wrapper; 355 result.mLocalPosition = localPosition; 356 break; 357 } 358 localPosition -= wrapper.getCachedItemCount(); 359 } 360 if (result.mWrapper == null) { 361 throw new IllegalArgumentException("Cannot find wrapper for " + globalPosition); 362 } 363 return result; 364 } 365 releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition)366 private void releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition) { 367 wrapperAndLocalPosition.mInUse = false; 368 wrapperAndLocalPosition.mWrapper = null; 369 wrapperAndLocalPosition.mLocalPosition = -1; 370 mReusableHolder = wrapperAndLocalPosition; 371 } 372 onBindViewHolder(ViewHolder holder, int globalPosition)373 public void onBindViewHolder(ViewHolder holder, int globalPosition) { 374 WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); 375 mBinderLookup.put(holder, wrapperAndPos.mWrapper); 376 wrapperAndPos.mWrapper.onBindViewHolder(holder, wrapperAndPos.mLocalPosition); 377 releaseWrapperAndLocalPosition(wrapperAndPos); 378 } 379 canRestoreState()380 public boolean canRestoreState() { 381 for (NestedAdapterWrapper wrapper : mWrappers) { 382 if (!wrapper.adapter.canRestoreState()) { 383 return false; 384 } 385 } 386 return true; 387 } 388 onViewAttachedToWindow(ViewHolder holder)389 public void onViewAttachedToWindow(ViewHolder holder) { 390 NestedAdapterWrapper wrapper = getWrapper(holder); 391 wrapper.adapter.onViewAttachedToWindow(holder); 392 } 393 onViewDetachedFromWindow(ViewHolder holder)394 public void onViewDetachedFromWindow(ViewHolder holder) { 395 NestedAdapterWrapper wrapper = getWrapper(holder); 396 wrapper.adapter.onViewDetachedFromWindow(holder); 397 } 398 onViewRecycled(ViewHolder holder)399 public void onViewRecycled(ViewHolder holder) { 400 NestedAdapterWrapper wrapper = mBinderLookup.get(holder); 401 if (wrapper == null) { 402 throw new IllegalStateException("Cannot find wrapper for " + holder 403 + ", seems like it is not bound by this adapter: " + this); 404 } 405 wrapper.adapter.onViewRecycled(holder); 406 mBinderLookup.remove(holder); 407 } 408 onFailedToRecycleView(ViewHolder holder)409 public boolean onFailedToRecycleView(ViewHolder holder) { 410 NestedAdapterWrapper wrapper = mBinderLookup.get(holder); 411 if (wrapper == null) { 412 throw new IllegalStateException("Cannot find wrapper for " + holder 413 + ", seems like it is not bound by this adapter: " + this); 414 } 415 final boolean result = wrapper.adapter.onFailedToRecycleView(holder); 416 mBinderLookup.remove(holder); 417 return result; 418 } 419 getWrapper(ViewHolder holder)420 private @NonNull NestedAdapterWrapper getWrapper(ViewHolder holder) { 421 NestedAdapterWrapper wrapper = mBinderLookup.get(holder); 422 if (wrapper == null) { 423 throw new IllegalStateException("Cannot find wrapper for " + holder 424 + ", seems like it is not bound by this adapter: " + this); 425 } 426 return wrapper; 427 } 428 isAttachedTo(RecyclerView recyclerView)429 private boolean isAttachedTo(RecyclerView recyclerView) { 430 for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) { 431 if (reference.get() == recyclerView) { 432 return true; 433 } 434 } 435 return false; 436 } 437 onAttachedToRecyclerView(RecyclerView recyclerView)438 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 439 if (isAttachedTo(recyclerView)) { 440 return; 441 } 442 mAttachedRecyclerViews.add(new WeakReference<>(recyclerView)); 443 for (NestedAdapterWrapper wrapper : mWrappers) { 444 wrapper.adapter.onAttachedToRecyclerView(recyclerView); 445 } 446 } 447 onDetachedFromRecyclerView(RecyclerView recyclerView)448 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 449 for (int i = mAttachedRecyclerViews.size() - 1; i >= 0; i--) { 450 WeakReference<RecyclerView> reference = mAttachedRecyclerViews.get(i); 451 if (reference.get() == null) { 452 mAttachedRecyclerViews.remove(i); 453 } else if (reference.get() == recyclerView) { 454 mAttachedRecyclerViews.remove(i); 455 break; // here we can break as we don't keep duplicates 456 } 457 } 458 for (NestedAdapterWrapper wrapper : mWrappers) { 459 wrapper.adapter.onDetachedFromRecyclerView(recyclerView); 460 } 461 } 462 getLocalAdapterPosition( Adapter<? extends ViewHolder> adapter, ViewHolder viewHolder, int globalPosition )463 public int getLocalAdapterPosition( 464 Adapter<? extends ViewHolder> adapter, 465 ViewHolder viewHolder, 466 int globalPosition 467 ) { 468 NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); 469 if (wrapper == null) { 470 return NO_POSITION; 471 } 472 int itemsBefore = countItemsBefore(wrapper); 473 // local position is globalPosition - itemsBefore 474 int localPosition = globalPosition - itemsBefore; 475 // Early error detection: 476 int itemCount = wrapper.adapter.getItemCount(); 477 if (localPosition < 0 || localPosition >= itemCount) { 478 throw new IllegalStateException("Detected inconsistent adapter updates. The" 479 + " local position of the view holder maps to " + localPosition + " which" 480 + " is out of bounds for the adapter with size " 481 + itemCount + "." 482 + "Make sure to immediately call notify methods in your adapter when you " 483 + "change the backing data" 484 + "viewHolder:" + viewHolder 485 + "adapter:" + adapter); 486 } 487 return wrapper.adapter.findRelativeAdapterPositionIn(adapter, viewHolder, localPosition); 488 } 489 490 getBoundAdapter(ViewHolder viewHolder)491 public @Nullable Adapter<? extends ViewHolder> getBoundAdapter(ViewHolder viewHolder) { 492 NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); 493 if (wrapper == null) { 494 return null; 495 } 496 return wrapper.adapter; 497 } 498 499 @SuppressWarnings("MixedMutabilityReturnType") getCopyOfAdapters()500 public List<Adapter<? extends ViewHolder>> getCopyOfAdapters() { 501 if (mWrappers.isEmpty()) { 502 return Collections.emptyList(); 503 } 504 List<Adapter<? extends ViewHolder>> adapters = new ArrayList<>(mWrappers.size()); 505 for (NestedAdapterWrapper wrapper : mWrappers) { 506 adapters.add(wrapper.adapter); 507 } 508 return adapters; 509 } 510 hasStableIds()511 public boolean hasStableIds() { 512 return mStableIdMode != NO_STABLE_IDS; 513 } 514 515 /** 516 * Helper class to hold onto wrapper and local position without allocating objects as this is 517 * a very common call. 518 */ 519 static class WrapperAndLocalPosition { 520 NestedAdapterWrapper mWrapper; 521 int mLocalPosition; 522 boolean mInUse; 523 } 524 } 525