How to read this passage

This paper mainly explains the principle of changing RecyclerView Layout trigger animation execution. The first part focuses on the explanation of principles and codes, and the second part explains the execution process of each stage through pictures and pictures combined with scenes.

It is recommended to read the first half of the principle and code, so as to have the concept in mind, with theoretical knowledge to read the second half of the scene. Finally, combined with the full text of knowledge, with the problem to read the source code, the effect will be better.

The principle of article

1. Notify the Adapter

Adapter has several notify related methods, which are:

  • notifyDataSetChanged()
  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
  • notifyItemMoved(int, int)

For those of you with a little development experience, the notifyDataSetChanged() method is a bit heavier than the others in that it causes the entire list to refresh, while the others do not. Those of you with more development experience may also know that the notifyDataSetChanged() method does not trigger the RecyclerView animation mechanism, and that the other methods trigger various types of animations.

2. RecyclerView layout logic

2.1 RecyclerViewçš„dispatchLayout

DispatchLayout as the name suggests, of course, is the child View layout (add and place in the appropriate location) to RecyclerView. Open its source code and you can see a comment like this.

Wrapper around layoutChildren() that handles animating changes caused by layout. Animations work on the assumption that there are five different kinds of items in play:

  1. PERSISTENT: items are visible before and after layout

  2. REMOVED: items were visible before layout and were removed by the app

  3. ADDED: items did not exist before layout and were added by the app

  4. DISAPPEARING: items exist in the data set before/after, but changed from visible to non-visible in the process of layout (they were moved off screen as a side-effect of other changes)

  5. APPEARING: items exist in the data set before/after, but changed from non-visible to visible in the process of layout (they were moved on screen as a side-effect of other changes)

We know that from the notes. The dispatchLayout method not only gives child Views layouts, but also handles animations. There are five main types of animation:

  1. PERSISTENT: Animations for views on the mobile interface both before and after layout
  2. REMOVED: Visible to the user before the layout, but the data has been REMOVED from the data source
  3. ADDED: Adds data to the data source and makes it visible to users after layout
  4. Bsellers: data always exists in the data source, but changes from visible to invisible after layout
  5. Data persists in the data source but becomes visible after being laid out

So far, we can’t fully understand the specific differences between these five types of animation, and what kinds of scenes trigger these types of animation. But it provides us with good research ideas. For now, we just need to take a look at the five types of animation. Next, we have a look at the source code of dispatchLayout. In response to the title of the article, we have posted a simplified version of the source code:

void dispatchLayout(){ ... dispatchLayoutStep1(); dispatchLayoutStep2(); dispatchLayoutStep3(); . }Copy the code

About dispatchLayoutStepX method, I believe many people have heard or understood, I will make a detailed introduction later in the article, a brief introduction as follows:

From the dispatchLayout annotation, we notice the words “before” and “after”, meaning before and after, respectively. That makes it easy. DispatchLayoutStep1 = “before”, dispatchLayoutStep2 = “after”. Their functions are described as follows:

  1. dispatchLayoutStep1
    1. Determine whether to enable the animation function

    2. If animation is enabled, the Item related information on the current screen is saved for subsequent animation

    3. If animation is enabled, call the mLayoutChildren method to prearrange

    4. After pre-layout, save the new Item information into Appeared with the information saved in Step 2

The condensed code looks like this:

private void dispatchLayoutStep1() { ... / / the first step to determine whether need to open the animation function processAdapterUpdatesAndSetAnimationFlags (); . if (mState.mRunSimpleAnimations) { ... / / the second step The current Item related information stored on the screen for the use of the subsequent animation int count = mChildHelper. GetChildCount (); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); }... if (mState.mRunPredictiveAnimations) { saveOldPositions(); 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 (viewHolder.shouldIgnore()) { continue; // What items need to be included in the configuration before and after the configuration if (! mViewInfoStore.isInPreLayout(viewHolder)) { if (wasHidden) { recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); } else { mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); } } } clearOldPositions(); } else { clearOldPositions(); }}}Copy the code
  1. DispatchLayoutStep2 Displays the final interface based on the data in the data source
private void dispatchLayoutStep2() { ... // Step 2: Run layout mState.mInPreLayout = false; // Close mLayout.onLayoutChildren(mRecycler, mState); . }Copy the code
  1. DispatchLayoutStep3 Triggers animation
private void dispatchLayoutStep3() { ... if (mState.mRunSimpleAnimations) { // Step 3: Find out where things are now, and process change animations. // traverse list in reverse because we may call animateChange in the loop which may // remove the target view holder. for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore()) { continue; } long key = getChangedHolderKey(holder); final ItemHolderInfo animationInfo = mItemAnimator .recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); if (oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { // run a change animation ... } else { mViewInfoStore.addToPostLayout(holder, animationInfo); }} // Step 4: Process view info Lists and trigger animations // MViewinfostore. Process (mViewInfoProcessCallback); }... }Copy the code

From the code we can see that dispatchLayoutStep1 and dispatchLayoutStep2 call onLayoutChildren while dispatchLayoutStep3 does not.

2.2 onLayoutChildren method of LinearLayoutManager

Take vertical direction RecyclerView as an example, we can fill RecyclerView in two directions, from top to bottom and from bottom to top. Start filling position is not fixed, can start filling from any position of RecyclerView. I’ve simplified the functionality of this method into the following steps:

  1. Find filled anchors (eventually calling findReferenceChild method)
  2. Remove the Views on the screen (final call detachAndScrapAttachedViews method)
  3. Fill from the anchor from the top down (call the fill and layoutChunk methods)
  4. Populate from the bottom up at the anchor point (call the fill and layoutChunk methods)
  5. If there is extra space, continue to fill (call the fill and layoutChunk methods)
  6. The layout, will be extra ViewHolder scrapList fill layoutForPredictiveAnimations (call)

This paper only explains the main process of onLayoutChildren, please refer to the specific filling logicRecyclerView filling logic article

LinearLayoutManager#onLayoutChildren

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... / / 1. Looking for filling the anchor updateAnchorInfoForLayout (recycler, state, mAnchorInfo); . / / 2. Remove the Views on the screen detachAndScrapAttachedViews (recycler); . / / 3. From the anchor points from above fill updateLayoutStateToFillEnd (mAnchorInfo); mLayoutState.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); . / / 4. From the anchor point from down to up in filling / / the fill forward start updateLayoutStateToFillStart (mAnchorInfo); mLayoutState.mExtraFillSpace = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); . If (mlayoutstate.mavailable > 0) {extraForEnd = mlayoutState.mavailable; // start could not consume all it should. add more items towards end updateLayoutStateToFillEnd(lastElement, endOffset); mLayoutState.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; }}... / / 6. The preliminary layout, will be extra ViewHolder scrapList filling layoutForPredictiveAnimations (recycler, state, startOffset, endOffset); .Copy the code

LinearLayoutManager#layoutForPredictiveAnimations

private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, RecyclerView.State state, int startOffset, Int endOffset) {// Check whether the condition is satisfied, if it is prelayout directly return if (! state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout() || ! supportsPredictiveItemAnimations()) { return; } // View int scrapExtraStart = 0, scrapExtraEnd = 0; final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); final int scrapSize = scrapList.size(); final int firstChildPos = getPosition(getChildAt(0)); for (int i = 0; i < scrapSize; i++) { RecyclerView.ViewHolder scrap = scrapList.get(i); // If (scrap. IsRemoved ()) {continue; } / / calculate extra control scrapExtraEnd + = mOrientationHelper getDecoratedMeasurement (scrap. ItemView); } mLayoutState.mScrapList = scrapList; . If (scrapExtraEnd > 0) {View Anchor = getChildClosestToEnd(); updateLayoutStateToFillEnd(getPosition(anchor), endOffset); mLayoutState.mExtraFillSpace = scrapExtraEnd; mLayoutState.mAvailable = 0; mLayoutState.assignPositionFromScrapList(); fill(recycler, mLayoutState, state, false); } mLayoutState.mScrapList = null; }Copy the code

At this point, the logic of the layout is explained. As for the specific animation execution logic, due to space constraints. Not covered in this article

Scenario article

1. notifyItemRemoved

Let’s test how dispatchLayout is rearranged by removing the View from the screen and calling the notifyItemRemoved method. Assume the initial state as shown in the figure below. Assume that there are 100 Adapter data and Item1 ~ Item6 views on the screen. Delete Item1 and Item2.

  1. Set ViewHolder corresponding to Item1 Item2 to REMOVE
  2. Assign the mPreLayoutPosition field of the ViewHolder corresponding to all items to the current position

Let’s review the following steps of onLayoutChildren

  1. Find filled anchors (eventually calling findReferenceChild method)
  2. Remove the Views on the screen (final call detachAndScrapAttachedViews method)
  3. Fill from the anchor from the top down (call the fill and layoutChunk methods)
  4. Populate from the bottom up at the anchor point (call the fill and layoutChunk methods)
  5. If there is extra space, continue to fill (call the fill and layoutChunk methods)
  6. The layout, will be extra ViewHolder scrapList fill layoutForPredictiveAnimations (call)

1.1 dispatchLayoutStep1 phase

  1. Look for the filled anchor, and the logic for finding the anchor is to go from the top down and find the first Item that’s not in the remove state. In this Case, find Item3

  1. Remove Views from the screen and put their ViewHolder into the Recyclable mAttachedScrap cache. The advantage of this cache is that if positions are recyclable, they can be used directly without rebinding.

  1. Fill down from anchor Item3, mAttachedScrap just leaves ViewHolder2 and ViewHolder1

  1. Fill Item2 up from anchor Item3, Item1. Because Item2, Imte1, has been removed, the space it consumes will not be recorded, so it can be filled again in Step 5

  1. I have some extra space, so I’m going to go ahead and fill in Item7, Item8

  1. Because the current layout is pre-layout, return directly

At this point, the layout of Step1 is finished

1.2 dispatchLayoutStep2 phase

  1. Look for the filled anchor, and the logic for finding the anchor is to go from the top down and find the first Item that’s not in the remove state. In this Case, find Item3

  1. Remove Views from the screen and put their ViewHolder into the Recycler’s mAttachedScrap cache

  1. Fill from anchor Item3 down to Item6, there is not enough distance left, mAttachedScrap only ViewHolder8, ViewHolder7, ViewHolder2, ViewHolder1

  1. Fill it up, even though we still have the height of two views, but at this point, we don’t have any data on it, so we don’t fill it up here

  1. Now I have two views, so I’m going to fill it down

Notice that the layout is now complete but there is a GAP between the top of the screen and the first one, which will be fixed

if (getChildCount() > 0) { // because layout from end may be changed by scroll to position // we re-calculate it. // find which side we should check for gaps. if (mShouldReverseLayout ^ mStackFromEnd) { int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } else { int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; }}Copy the code

The effect after repair is as follows

  1. The current layout is not pre-layout, but because ViewHolder1 and ViewHolder2 are removed, it is skipped

2. notifyItemInserted

Let’s say I insert two pieces of data under Item1: AddItem1, AddItem2

2.1 dispatchLayoutStep1 phase

  1. Find the anchor point and find Item1

2. Remove the Views from the screen and add them to mAttachedScrap3. Fill the anchor point from top to bottom4. Fill the anchor point from bottom to top. As can be seen from the figure above, there is no space on the anchor point. Check whether there is any space left. If there is any space left at the end, do not fill. 6. Because the current stage is pre-layout, no padding is done

2.2 dispatchLayoutStep2 phase

  1. Find the anchor point and find Item1

2. Remove the Views from the screen and add them to mAttachedScrap3. Fill in the anchor point from the top down. At this point, the changed data is filled in the screen. AddItem1 and addItem2 are filled under Item14. Anchor points are filled from bottom to top. As can be seen from the figure, there is no space to fill. The current layoutStep2 phase will fill the contents of mAttachScrap to the end of the screen, and the ItemView corresponding to ViewHolder5 and ViewHolder6 will be filled

2.3 dispatchLayoutStep3 phase

The animation starts, and when the animation ends, Item5 and Item6 are recycled, which is then recycled into the mCachedViews cache pool

This article doesn’t cover exactly how animations are executed, but we’ll listen for the breakdown next time.

If you have any questions, please contact me in the following ways