preface

The wholeRecyclerViewThis is the third article in a series designed to help Android developers improve RecyclerView.

In the previous series of articles, we looked at each from the source point of view:

  • RecyclerView(Overall structure)
  • Recycler
  • LayoutManger
  • ItemDecoration(A little understanding)

Throughout RecyclerView, it seems that there are still ItemAnimator and Adapter, so this article as the last of the RecyclerView series, naturally to complete the rest of the analysis (at the end of the article has the link to the previous article).

directory

RecyclerView in the magician – Adapter

I call Adapter the magician in RecyclerView, why do I call it the magician? It turns the data into a concrete view, but this is the adapter pattern we talk about a lot.

Adapter’s main functions are data rotor view and data management and notification, so before we look at the source, we need to know about the related classes of Adpater:

The name of the role
AdapterDataObservable The class that is actually processed when data changes
ViewHolder depositChild viewsAnd the current location information, you should be very familiar with ~

1. Data rotor view

In our previous article about Recycler, we learned that if we don’t recycle Recycler by caching ViewHolder, Create a ViewHolder by calling Adapter#onCreateViewHolder. In our usual implementation of this method, we would normally:

View root = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.xxx,viewGroup,false);
return new ViewHolder(root);
Copy the code

The Recycler can then be recycled by calling Adapter#onBindViewHolder to display the Recycler data in the controls. However, both methods are implemented by the consumer of the control.

2. Data management

Every time the data changes, we need to call Adapt # NotifyXXX to notify RecyclerView that the data set has changed. This time we delete as an example to analyze the source code.

2.1 Setting an Adapter

The code to set the adapter is RecyclerView#setAdapter:

public void setAdapter(Adapter adapter) { // ... // Focus on methodssetAdapterInternal(adapter, false.true);
	// ...
}

private void setAdapterInternal(Adapter adapter, Boolean compatibleWithPrevious,
            Boolean removeAndRecycleViews) {
	if(mAdapter ! = null) {/ / old remove adapter registration mAdapter unregisterAdapterDataObserver (mObserver); mAdapter.onDetachedFromRecyclerView(this); } / /... final Adapter oldAdapter = mAdapter; // mAdapter = adapter;if(adapter ! = null) { adapter.registerAdapterDataObserver(mObserver); adapter.onAttachedToRecyclerView(this); } / /... }Copy the code

This code does two things:

  • The old adapter is unregistered
  • Registers notification objects for data changes in the new adapter

The data change notification object is this mObserver. Let’s see what this mObserver is:

private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); // RecyclerViewDataObserver AdapterDataObserver public abstract static class AdapterDataObserver { public voidonChanged() {
		// Do nothing
	}
	public void onItemRangeChanged(int positionStart, int itemCount) {
		// do nothing
	}
	public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
		// fallback to onItemRangeChanged(positionStart, itemCount) if app
		// does not override this method.
		onItemRangeChanged(positionStart, itemCount);
	}
	public void onItemRangeInserted(int positionStart, int itemCount) {
		// do nothing
	}
	public void onItemRangeRemoved(int positionStart, int itemCount) {
		// do nothing
	}
	public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
		// do nothing
	}
}
Copy the code

And the RecyclerViewDataObserver is inherited from the AdapterDataObserver abstract class, the specific implementation details we will discuss later.

2.2 Data Deletion

The usage scenario looks like this:

btnDeleteOne.setOnClickListener(new View.OnClickListener() {
    @Override
	public void onClick(View v) {
		List<String> strings = mAdapter.getValues();
		if(strings.size() == 0)
		return; // remove the first data strings.remove(0); / / adapter inform delete mAdapter. NotifyItemRemoved (0); }Copy the code
2.3 Adapter Notification

It is important to note that Adapter and RecyclerViewDataObserver are internal classes of RecyclerView, so they can directly use resources within RecyclerView.

To remove data in RecyclerView, we call the adaptive #notifyRemoved method:

public final void notifyItemRemoved(int position) {
	mObservable.notifyItemRangeRemoved(position, 1);
}
Copy the code

RecyclerViewDataObserver#notifyItemRemoved

private class RecyclerViewDataObserver extends AdapterDataObserver { //... Omit some methods @ Override public void onItemRangeRemoved (int positionStart, int itemCount) {assertNotInLayoutOrScroll (null);if(mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { triggerUpdateProcessor(); }} // refresh the interface or animation directly // delete this is called refresh interface voidtriggerUpdateProcessor() {
		if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
			ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
		} else {
			mAdapterUpdateDuringMeasure = true; requestLayout(); }}}Copy the code

In the RecyclerViewDataObserver notification delete method, it again to delete to the AdapterHelper, call AdapterHelper#onItemRangeRemoved:

/ * * * @return True if updates should be processed.
 */
Boolean onItemRangeRemoved(int positionStart, int itemCount) {
	if (itemCount < 1) {
		return false; } // mPendingUpdates is a List<UpdateOp> // here is adding a deleted UpdateOp to mPendingUpdates mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null)); mExistingUpdateTypes |= UpdateOp.REMOVE;return mPendingUpdates.size() == 1;
}
Copy the code

AdapterHelper is so confident that it doesn’t let anyone else handle it that it adds an update to its mPendingUpdates. What does this update do? We also introduce it when we use it. RecyclerViewDataObserver#notifyItemRemoved In RecyclerViewDataObserver It immediately calls requestLayout again to refresh the interface.

2.4 Some details of the interface drawing process

Interface rendering has been the focus of our previous blogs, so in this chapter we’ll look at data notifications in more detail.

In RecyclerView#dispatchLayoutStep1, RecyclerView invokes RecyclerView# processAdapterUpdatesAndSetAnimationFlags handle your updates and Adapter set markers for animation, here we only see the Adapter data update relevant:

private void processAdapterUpdatesAndSetAnimationFlags() {
	//...
  
	// simple animations are a subset of advanced animations (which will cause a
	// pre-layout step)
	// If layout supports predictive animations, pre-process to decide if we want to run them
	if (predictiveItemAnimationsEnabled()) {
		mAdapterHelper.preProcess();
	} else {
		mAdapterHelper.consumeUpdatesInOnePass();
	}
  
	// ...
}

private Boolean predictiveItemAnimationsEnabled() {/ / RecyclerView set the default mItemAnimator, / / and LinearLayout supportsPredictiveItemAnimations ()true// The method returns astrue
	return(mItemAnimator ! = null && mLayout.supportsPredictiveItemAnimations()); }Copy the code

Because RecyclerView# predictiveItemAnimationsEnabled usually returns true, then we jump AdapterHelper, see AdapterHelper# the preProcess methods:

void preProcess() {/ /... final int count = mPendingUpdates.size();for(int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { // ... Add to omitcase UpdateOp.REMOVE:
			    applyRemove(op);
				break; }} mpendingupdates.clear (); }Copy the code

MPendingUpdates is an ArrayList

. The above method consumes the deleted UpdateOp that we added to mPendingUpdates earlier. In the treatment of the delete attribute UpdateOp AdapterHelper# applyRemove method and call AdapterHelper# postponeAndUpdateViewHolders:

private void postponeAndUpdateViewHolders(UpdateOp op) { mPostponedList.add(op); switch (op.cmd) { // ... Omit add, update, movecase UpdateOp.REMOVE:
		    mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,op.itemCount);
			break;
		default:
		   	throw new IllegalArgumentException("Unknown update op type for "+ op); }}Copy the code

The RecyclerView of mCallback is also used in RecyclerView. The RecyclerView of mCallback is also used in RecyclerView.

void initAdapterManager() {
	mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {/ /... Omit the other method / / displays only remove @ Override public void offsetPositionsForRemovingLaidOutOrNewView (int positionStart, int itemCount) { offsetPositionRecordsForRemove(positionStart, itemCount,false);
			mItemsAddedOrRemoved = true; } / /... Omit other methods}); } void offsetPositionRecordsForRemove(int positionStart, int itemCount, Boolean applyToPreLayout) { final int positionEnd = positionStart + itemCount; final int childCount = mChildHelper.getUnfilteredChildCount();for (int i = 0; i < childCount; i++) {
		final ViewHolder holder = getChildViewHolderint(mChildHelper.getUnfilteredChildAt(i));
		if(holder ! = null && ! holder.shouldIgnore()) {ifHolder.mposition >= positionEnd {// Update the position of the ViewHOlder that was not deleted holder.offsetPosition(-itemCount, applyToPreLayout); mState.mStructureChanged =true;
			} else if(holder. MPosition > = positionStart) {/ / delete with new 逇 ViewHolder location information holder. FlagRemovedAndOffsetPosition (positionStart - 1, -itemCount, applyToPreLayout); mState.mStructureChanged =true;
			}
		}
	}
	mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
	requestLayout();
}
Copy the code

The above code does two things:

  • For those to be deletedViewHolderPlus deletedflagTo updateViewHolderThe location of the
  • The position will changeViewHolderUpdate the location

After the data is deleted, the Adapter is used to add delete tags and update location information to the ViewHolder. Subsequent processing is left to the LayoutManager and ItemAnimator, which we will examine in the following animation

2. Interface interaction glue – ItemAnimator

Good animation will make the interface interaction is very natural, RecyclerView as a powerful UI control, naturally also support animation, yes, RecyclerView sub-view animation is realized by ItemAnimator.

The Gif above is not suitable for explaining, so I changed the chat image and also deleted the first message:

Adapter
The ViewHolder
flag
flag
ViewHolder

Before we do that, a quick look at the animation-related class ViewInfoStore:

1. Perform pre-layout

What is the pre-layout? In simple terms, RecyclerView is used to generate the actual RecyclerView once, that is, LayoutManager#onLayoutChildren is used twice, so why is it used twice? Let’s take our time.

Pre-layout is an important process that gets triggered when there’s a simple subview animation going on, and we need to review RecyclerView#dispatchLayoutStep1, Direct access to the RecyclerView# processAdapterUpdatesAndSetAnimationFlags method:

private void processAdapterUpdatesAndSetAnimationFlags() {
	// simple animations are a subset of advanced animations (which will cause a
	// pre-layout step)
	// If layout supports predictive animations, pre-process to decide if we want to run them
	if (predictiveItemAnimationsEnabled()) {
		mAdapterHelper.preProcess();
	} else{ mAdapterHelper.consumeUpdatesInOnePass(); } Boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; // mFirstLayoutComplete is set to after the first layout is completetruemState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator ! = null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && ! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); }Copy the code

From the above code, we can see:

  • The first comment indicates that a simple animation triggers a pre-layout
  • whenRecyclerViewYou can’t trigger an animation until the first layout is complete,mFirstLayoutCompleteIs set to after the first layout is completetrue
  • mState.mRunSimpleAnimationsfortrueismState.mRunPredictiveAnimationsfortrueIs sufficient and necessary,mState.mRunPredictiveAnimationsThis property is important because it determines whether or not to prearrange

RecyclerView#dispatchLayoutStep1

private void dispatchLayoutStep1() {/ /... mViewInfoStore.clear(); processAdapterUpdatesAndSetAnimationFlags(); // reset some states //... MItemsAddedOrRemoved = mItemsChanged =false; . / / whether the preliminary layout depends on mState mRunPredictiveAnimations mState. MInPreLayout = mState. MRunPredictiveAnimations;if (mState.mRunSimpleAnimations) {
		int count = mChildHelper.getChildCount();
		for (int i = 0; i < count; ++i) {
			final ViewHolder holder = getChildViewHolderint(mChildHelper.getChildAt(i));
			if(holder.shouldIgnore() || (holder.isInvalid() && ! mAdapter.hasStableIds())) {continue; } final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); / /... Omit}}if (mState.mRunPredictiveAnimations) {
		// Step 1: run prelayout: This will use the old positions of items. The layout manager
		// is expected to layout everything, even removed items (though not to add removed
		// items back to the container). This gives the pre-layout position of APPEARING views
		// whichCome into existence as part of the real layout. // Basically, layoutManager will layout each subview, including the subview added to it and the subview deleted. LayoutManager knows exactly what animation to execute saveOldPositions(); final Boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged =false;
		// temporarily disable flag because we are asking for previous layout
		mLayout.onLayoutChildren(mRecycler, mState);
		mState.mStructureChanged = didStructureChange;
		for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
			final View child = mChildHelper.getChildAt(i);
			final ViewHolder viewHolder = getChildViewHolderint(child);
			// ...
			if(! MViewInfoStore. IsInPreLayout (viewHolder)) {/ / for new viewHolder tag / /... Some methods omit mViewInfoStore. AddToAppearedInPreLayoutHolders (viewHolder animationInfo); }} instead! [](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/9/16/16d3a4d52bfc6558~tplv-t2oaga2asx-image.image ) clearOldPositions(); }else {
		clearOldPositions();
	}
	// ...
}
Copy the code

In addition to the method entered directly above, there are two if statements.

The first if statement: mState mRunSimpleAnimations to true

This is a simple matter of recording the position of the ViewHolder that existed before the pre-layout.

The second if statement: mState mRunPredictiveAnimations to true

The first call to LayoutManager#onLayoutChildren is called. I’m not going to explain the layout process here. If you want to know more about it, you can refer to my previous article: Recyclerview-layoutmanager.

Note that in the add child view, the LayoutManager#addViewInt method is called:

private void addViewint(View child, int index, Boolean disappearing) {
	// ...
	if (disappearing || holder.isRemoved()) {
		// these views will be hidden at the end of the layout pass.
		mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
	} else {
		// This may look like unnecessary but may happen if layout manager supports
		// predictive layouts and adapter removed then re-added the same item.
		// In this case, added version will be visible in the post layout (because add is
		// deferred) but RV will still bind it to the same View.
		// So if a View re-appears in post layout pass, remove it from disappearing list.
		mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
	}
	// ...
}
Copy the code

The purpose of this method is that if it is a deleted ViewHolder, it will add the deleted mark to the InfoRecord corresponding to the ViewHolder in the ViewInfoStore. In a real layout (non-pre-layout), the deleted ViewHolder will not be used, so say, Only the pre-layout will record the deletion animation.

Pre-layout completed, the interface looks like:

As you can see, after the first layout, the ViewHolder that needs to be deleted and the ViewHolder that needs to be auto-filled are added to RecyclerView. It calls ViewInfoStore and adds the corresponding InfoRecord to the newly added ViewHolder.

After completing this, RecyclerView is very familiar with which animations to process, which is also the meaning of pre-layout.

2. Real layout

Again, without going into the specific code, after the second layout is complete, the interface becomes:

Recycler

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, Boolean dryRun) {
	final int scrapCount = mAttachedScrap.size();
	// Try first for an exact, non-invalid match from scrap.
	for (int i = 0; i < scrapCount; i++) {
		final ViewHolder holder = mAttachedScrap.get(i);
		if(... && (mState.mInPreLayout || ! Holder.isremoved ())) {// In the first-level cache mAttachedScrap, if the ViewHolder // pre-layout is available, Real layouts cannot use holder.addflags (viewholder.flag_from_scrap);return holder;
		}
	}
	//...
	return null;
}
Copy the code

While the current problem is solved, you may have another problem: how does the deletion animation work without the deleted subview? ** Let’s see what happens next.

3. Perform the animation

We have recorded so much information about the ViewHolder neutron view before, now it is time to use it:

private void dispatchLayoutStep3() {/ /...if (mState.mRunSimpleAnimations) {
		// Step 3: Find out whereThings are now, and process change animations. // Find the current ViewHolder and perform the animations that need to be performedfor (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
			ViewHolder holder = getChildViewHolderint(mChildHelper.getChildAt(i));
			long key = getChangedHolderKey(holder);
			final ItemHolderInfo animationInfo = mItemAnimator
			                        .recordPostLayoutInformation(mState, holder);
			ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
			if(oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { // ...if (oldDisappearing && oldChangeViewHolder == holder) {
					// run disappear animation instead of change
					mViewInfoStore.addToPostLayout(holder, animationInfo);
				} else{/ /... mViewInfoStore.addToPostLayout(holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); / /... }}else{ mViewInfoStore.addToPostLayout(holder, animationInfo); }} // Step 4: Process view info Lists and trigger animations // Execute mViewinfostore. Process (mViewInfoProcessCallback); } // reset some animation-related classes //... mState.mRunSimpleAnimations =false;
	mState.mRunPredictiveAnimations = false;
	// ...
	mViewInfoStore.clear();
}
Copy the code

The main purpose of the first half of this function is to add a Post tag to the InfoRecord associated with ViewHolder in the ViewInfoStore, Mviewinfostore.process (mViewInfoProcessCallback) is our core function, animation execution. We will focus on this method:

void process(ProcessCallback callback) {
	for(int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); final InfoRecord record = mLayoutHolderMap.removeAt(index); // Execute different animations according to different flagsif ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
			callback.unused(viewHolder);
		} else if((record.flags & FLAG_DISAPPEARED) ! = 0) { // Set as"disappeared" by the LayoutManager (addDisappearingView)
			if (record.preInfo == null) {
				callback.unused(viewHolder);
			} else{ callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); }}else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
			// Appeared in the layout but not in the adapter (e.g. entered the viewport)
			callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
		} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
			// Persistent in both passes. Animate persistence
			callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
		} else if((record.flags & FLAG_PRE) ! = 0) { // Wasin pre-layout, never been added to post layout
			callback.processDisappeared(viewHolder, record.preInfo, null);
		} else if((record.flags & FLAG_POST) ! = 0) { // Was notin pre-layout, been added to post layout
			callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
		} else if((record.flags & FLAG_APPEAR) ! = 0) { // Scrap view. RecyclerView will handle removing/recycling this. }else if (DEBUG) {
			throw new IllegalStateException("record without any reasonable flag combination:/"); } InfoRecord.recycle(record); }} Interface ProcessCallback {void processAid (ViewHolder ViewHolder, @nonnull ItemHolderInfo preInfo, @Nullable ItemHolderInfo postInfo); void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo, ItemHolderInfo postInfo); void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo); void unused(ViewHolder holder); }Copy the code

In the key method ViewInfoStore#process, mLayoutHolderMap is iterated to get the InfoRecord bound to ViewHolder, and different methods are called back to handle different animations based on different flags. The implementation of the callback interface in RecyclerView:

/**
 * The callback to convert view info diffs into animations.
 */
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() { @Override public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, Nullable ItemHolderInfo postInfo) {// Remove ViewHolder mrecycler. unscrapView(ViewHolder); animateDisappearance(viewHolder, info, postInfo); } @Override public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) {// animateAppearance(viewHolder, preInfo, info); } @Override public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { viewHolder.setIsRecyclable(false);
		if (mDataSetHasChangedAfterLayout) {
			if(mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) { postAnimationRunner(); }}else if(mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { postAnimationRunner(); } } @Override public void unused(ViewHolder viewHolder) { mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); }};Copy the code

If you look at the delete method, you have a bigger question, delete the subview is gone, and perform the wool delete animation? Then I have to tell you: RecyclerView does not have a RecyclerView that needs to be deleted, but the current ViewInfoStore has a ViewHolder that needs to be added to RecyclerView before deleting. Look at the RecyclerView# animatepattern method above to see if it looks like this:

void animateDisappearance(@NonNull ViewHolder holder, @NonNull ItemHolderInfo preLayoutInfo, @nullable ItemHolderInfo postLayoutInfo) {// Add subview to interface addAnimatingView(holder); holder.setIsRecyclable(false); // mItemAnimator performs the delete animationif(mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { postAnimationRunner(); */ Private void addAnimatingView(ViewHolder ViewHolder) {final View View = viewHolder.itemView; final Boolean alreadyParented = view.getParent() == this; mRecycler.unscrapView(getChildViewHolder(view));if(viewHolder isTmpDetached ()) {/ / attach back interface mChildHelper. AttachViewToParent (view, 1, the getLayoutParams (),true);
	} else if(! AlreadyParented) {// addView McHildhelper. addView(view,true);
	} else{ mChildHelper.hide(view); }}Copy the code

View the state of ViewHolder and the animation to be performed:

mItemAnimator

4. DefaultItemAnimator mechanism

ItemAnimator is an abstract class, so some methods require a concrete class implementation. In the absence of a concrete ItemAnimator, the default DefaultItemAnimator is used. DefaultItemAnimator:

DefaultItemAnimator
ItemAnimator

In the delete animation of DefaultItemAnimator, a transparency 1-0 animation is performed on the deleted child view. When the animation is finished, the child view is deleted and the ViewHolder is recalled. Instead of being called after the transparency animation, the shift animation is invoked using a delay of the transparency animation execution time. So it looks like the subview was deleted before the subview started to move online.

After the animation is executed, the image looks like this:

The above is the RecyclerView delete part of the Adapter and ItemAnimator call principle, other methods students can analyze ~

Third, summary

RecyclerView

If you want to continue learning about RecyclcerView:

The first: “Cocooned recyclerview-whole into one” the second: “Cocooned recyclerview-layoutManager”

Special sharing:

GridLayoutManager, You probably haven’t tried it yet