Antecedents to review

Android RecycleView easy to achieve drop-down refresh, load more

Android RecyclerView custom single and multiple selection mode

SwipeRefreshLayout in RecyclerView blank pull failure analysis

It has been more than two years since the PowerAdapter and SelectPowerAdapter were created. At the beginning, I put SwipeRefreshLayout and RecyclerView directly coupled together, layout to write a control, cool. Because the business scenario was that way, there was no consideration for flexibility. SwipeRefreshLayout can not be used directly in the head of the business, suddenly realized that such limitations are too big. As a result, Android has created more of its own pull-down refresh loading, which does have practical application scenarios. I even went crazy about doing a drop-down refresh of the UC browser. After that, I split the header, and the PowerAdapter stuff was used to simplify the implementation of loading more already multi-layout fills. SelectPowerAdapter inherits from PowerAdapter and is extremely poorly written. Used to implement simple single and multiple selections.

update

Warehouse please stamp PowerRecyclerView

These two Adapters are a decoration to the existing Adapter functions, so that the caller can implement some common functions. Before this time, some functions have been gradually enriched. For example, SelectAdaper was really simple before, but now it has gradually realized basic functions such as single, multiple, reverse, select, delete, limit, maximum number of selected. PowerAdapter will add, delete, change, check and other basic functions perfect.

Because there was no specification, some methods were arbitrary. Now some methods was abolished, such as to make the adapter before. AttachRecyclerView (), why want to scrap it, because there’s onAttachedToRecyclerView adapter directly () method, so, don’t need to add this method. That is sparse (CAI) suddenly (JI) and take it for granted to add directly.

And the most important thing is to add the local refresh this good thing, at first is not considered this place, later read the document, found that such a good function almost forgotten. How to understand local refresh and usage will be the focus of the next article.

And we know that in RecyclerView there are notifyItemChange() notifyItemRemove() and so on, This is a partial refresh compared to the ListView or notifyDataChange() method. In addition, these added refresh methods, in fact, are the default animation effect, the specific effect is DefaultItemAnimator control and processing, because of the animation effect, let the development of some unexpected conditions.

Suppose we now have a scene for uploading photos. Each ViewHolder has an image and then a progress bar on it that returns in real time based on the upload progress. If you use notifyItemChange() to update the animation, there are two problems. First, you will find that the entire layout flashes every time you refresh it. Second, how should I transfer the value of this progress? Add a progress temporary field to the ViewHolder Bean object?

In view of the above two questions, I would like to ask an additional question, which is also a pre-question. If we call notifyItemChange() multiple times, will the entry be refreshed multiple times?

NotifyItemChange () is in the same position as the actual local refresh. Is the ViewHolder the same object? Second, is there no animation set for partial refresh?

With these problems, began to RecyclerView this part of the source code exploration, all the following source code based on Android API 27 Platform.

NotifyItemChange () The second argument

RecyclerView is a real local refresh, but how in the end is a local refresh? It’s actually quite simple. Look at notifyItemChange(int Position), another overloaded function.

    public final void notifyItemChanged(int position, @Nullable Object payload) {
        mObservable.notifyItemRangeChanged(position, 1, payload);
    }
Copy the code

Also, there is an overloaded function corresponding to onBindViewHolder(@nonNULL VH holder, int Position) in the Adapter. The default implementation is this method.

    public void onBindViewHolder(@NonNull VH holder, int position,
            @NonNull List<Object> payloads) {
        onBindViewHolder(holder, position);
    }
Copy the code

Ok, so that’s the difference between RecyclerView local refresh apis. In fact, for the first additional question (if we call notifyItemChange() multiple times, does the item refresh more than once?) Payload is an Object. When onBindViewHolder() is passed in, the parameters are a collection. The RecyclerView RecyclerView controls requestLayout() calls only when the notify function is used for multiple times. The result is really like this, directly look at the relevant source code.

//RecyclerView 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); } //RecyclerViewDataObserver @Override public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { assertNotInLayoutOrScroll(null); if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { triggerUpdateProcessor(); } } // AdapterHelper void triggerUpdateProcessor() { if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); } else { mAdapterUpdateDuringMeasure = true; requestLayout(); }}Copy the code

After calling the notifyItemChange() method (with either one or two arguments), NotifyItemRangeChanged (int positionStart, int itemCount,@Nullable Object Payload) The last callback to RecyclerViewDataObserver. OnItemRangeChanged () method. Within that method, there is a triggerUpdateProcessor() method, which essentially requests a relayout. RequestLayout () will only be requested if the condition is true, and then you can answer the question if the condition is true.

/**
 * @return True if updates should be processed.
 */
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if (itemCount < 1) {
        return false;
    }
    mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
    mExistingUpdateTypes |= UpdateOp.UPDATE;
    return mPendingUpdates.size() == 1;
}
Copy the code

Size ==1 specifies that requestLayout() is triggered only after the first call returns true. Second, the payload parameter is wrapped as an object here and put into the mPendingUpdates collection. The first discovery, which proves once and for all that the appeal guess is correct, is that even if you call Notify multiple times, requestLayout() will only fire the first time.

Measure Layout cannot escape

Now that there’s a requestLayout() call, go back to the onMeasure() and onLayout() methods.

@Override protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; } if (mLayout.isAutoMeasureEnabled()) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; }... }... }Copy the code

Suppose our RecyclerView layout is two match_parent or has an exact value, then the code snippet executed looks like this. Then look at onLayout().

@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { dispatchLayout(); mFirstLayoutComplete = true; } void dispatchLayout() { ... mState.mIsMeasuring = false; if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) { // First 2 steps are done in onMeasure but looks like we have to run again due to // changed size. mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { // always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } dispatchLayoutStep3(); }Copy the code

In RecyclerView’s onLayout() method, three steps are performed. It’s pretty clear from the nomenclature. For each of the three steps, there are detailed comments above each method. The first step is to process Adapter updates, determine the animation effects to execute, save the current Views, and finally, if necessary, predict the layout and save the related information. In the second step, the layout is executed based on the final state and may be executed multiple times; Third, save the View information, perform the animation, and finally do some cleanup and reset operations.

I know the truth, but I still can’t live this life. This is another minimalist translation

Due to the article (Neng) (LI) has (BU) limit (XING), the next step method only to do the analysis of the relevant and local refresh relevant code (only concerned about the above mentioned several problems), RecyclerView code too TM, a is not finished, or a lifetime may also not finish.

dispatchLayoutStep1

Handle Adapter updates, determine animation effects to perform, save current Views, and finally, if necessary, predict layout and save related information.

private void dispatchLayoutStep1() { mState.assertLayoutStep(State.STEP_START); startInterceptRequestLayout(); / / 1. Update mRunSimpleAnimations and mRunPredictiveAnimations flag in fact there are some other SAO processAdapterUpdatesAndSetAnimationFlags operation (); / / 2. MInPreLayout useful mState. Set to true behind mInPreLayout = mState. MRunPredictiveAnimations; . if (mState.mRunSimpleAnimations) { // Step 0: Find out where all non-removed items are, pre-layout 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()); / / 5. Save the animation information related mViewInfoStore. AddToPreLayout (holder, animationInfo); if (mState.mTrackOldChangeHolders && holder.isUpdated() && ! holder.isRemoved() && ! holder.shouldIgnore() && ! Holder.isinvalid ()) {//3. If holder is certain to update, add it to oldChangeHolders. mViewInfoStore.addToOldChangeHolders(key, holder); } } } if (mState.mRunPredictiveAnimations) { ... //4. It is important that LayoutManager start to work 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 (viewHolder.shouldIgnore()) { continue; } if (! mViewInfoStore.isInPreLayout(viewHolder)) { ... / / 5. Save the animation information related mViewInfoStore. AddToAppearedInPreLayoutHolders (viewHolder animationInfo); }}... }... onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT; }Copy the code

There are five additional annotations. First, update the animations associated with mRunSimpleAnimations and mRunPredictiveAnimations, which the rest of the actions depend on. Second, synchronize the state of mInPreLayout with mRunPredictiveAnimations. This will also be used in later steps. Third, save the ViewHolder that needs to be updated into the oldChangeHolder collection. Fourth, call LayoutManager.onLayoutChildren (). Fifth, save relevant animation information.

There are three things we need to focus on right now. One,payloadHow are parameters passed toViewHolder; Second, whether the animation effect andpayloadHave a relationship; The thirdViewHolderWhether or not the same object is refreshed.

//RecyclerView 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 ... mAdapterHelper.preProcess(); . boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; / / is often true mState. MRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator! = null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); / / is often true mState. MRunPredictiveAnimations = mState. MRunSimpleAnimations && animationTypeSupported &&! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); } //AdapterHelper void preProcess() { mOpReorderer.reorderOps(mPendingUpdates); final int count = mPendingUpdates.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { ... case UpdateOp.UPDATE: applyUpdate(op); break; }... } mPendingUpdates.clear(); } //AdapterHelper private void postponeAndUpdateViewHolders(UpdateOp op) { if (DEBUG) { Log.d(TAG, "postponing " + op); } mPostponedList.add(op); switch (op.cmd) { ... case UpdateOp.UPDATE: mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); break; default: throw new IllegalArgumentException("Unknown update op type for " + op); }}Copy the code

The above several methods, involving RecyclerView interact with the Adapter, executed first mAdapterHelper. The preProcess (after), The payload in the onItemRangeChanged() method described above is wrapped as an UpdateOp object, which is where you start processing the object.

    UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
        this.cmd = cmd;
        this.positionStart = positionStart;
        this.itemCount = itemCount;
        this.payload = payload;
    }
Copy the code

CMD corresponds to our operation, which is update, followed by the corresponding parameters in the notifyItemRangeChange() method. The AdapterHelper finally uses callback to RecyclerView, where the viewRangeUpdate() method is executed. This callback is created when RecyclerView is created.

//RecyclerView is initialized by calling void initAdapterManager() {mAdapterHelper = new AdapterHelper(new adapterHelper.callback () {....  @Override public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) { viewRangeUpdate(positionStart, itemCount, payload); mItemsChanged = true; }}); } //RecyclerView void viewRangeUpdate(int positionStart, int itemCount, Object payload) { final int childCount = mChildHelper.getUnfilteredChildCount(); final int positionEnd = positionStart + itemCount; for (int i = 0; i < childCount; i++) { final View child = mChildHelper.getUnfilteredChildAt(i); final ViewHolder holder = getChildViewHolderInt(child); if (holder == null || holder.shouldIgnore()) { continue; } if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {// Very important, Here we update the Flag and then pass the payload into the Viewholder holder.addflags (viewholder.flag_update); holder.addChangePayload(payload); // lp cannot be null since we get ViewHolder from it. ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; } } mRecycler.viewRangeUpdate(positionStart, itemCount); }Copy the code

At this point, we can see that in the viewRangeUpdate() method, the ** holder is followed by the FLAG_UPDATE flag, which is important to remember. Payload is added directly to the corresponding holder using the addChangePayload() method. ** A core problem has been solved.

Then return to processAdapterUpdatesAndSetAnimationFlags () after the part, setting mRunSimpleAnimations and mRunPredictiveAnimations two identifiers to true. Return to the third point in the dispatchLayoutStep1() method.

if (mState.mTrackOldChangeHolders && holder.isUpdated() && ! holder.isRemoved() && ! holder.shouldIgnore() && ! Holder.isinvalid ()) {//3. If holder is certain to update, add it to oldChangeHolders. mViewInfoStore.addToOldChangeHolders(key, holder); }Copy the code

FLAG_UPDATE is added when ** holder needs to be updated, and the holder.isupdated () method returns true. So if the third condition is met, it will be implemented. **

Then comes the fourth point:

if (mState.mRunPredictiveAnimations) { ... //4. It is important that LayoutManager start to work mlayout. onLayoutChildren(mRecycler, mState);Copy the code

In LayoutManger onLayoutChildren () there is a lot of code, and here is two lines of code, we care about is actually two methods, detachAndScrapAttachedViews () and the fill () method.

//LinearLayoutManger @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... detachAndScrapAttachedViews(recycler); fill(recycler, mLayoutState, state, false); . }Copy the code

DetachAndScrapAttachedViews () this method will eventually reverse iterate through all the View, in turn, calls the Recycler scrapView () method. Recycler is one of the core RecyclerView. It’s not too much to write an article about recycling by caching. Here we are concerned about recycling.

void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); / / if you don't need to update into mAttachedScrap if (holder. HasAnyOfTheFlags (ViewHolder. FLAG_REMOVED | ViewHolder. FLAG_INVALID) | |! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { ... holder.setScrapContainer(this, false); mAttachedScrap.add(holder); } else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); }}Copy the code

Note that if you don’t need to update it, it will be added to mAttachedScrap, and if you need to update it, it will be added to mChangedScrap. Why do you want to add to these collections? Because later, when fill() is used to find the corresponding holder, the corresponding View is produced and finally added to RecyclerView control.

In the fill () method, you will call tryGetViewHolderForPositionByDeadline find ViewHolder () method, get the corresponding View, then addView ().

//Recycler @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ... boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; / / set to in dispatchLayoutStep1 mState. MInPreLayout = mState. MRunPredictiveAnimations / / 0. From the mChangedScrap collection to find the if (mState. IsPreLayout ()) {holder = getChangedScrapViewForPosition (position); fromScrapOrHiddenOrCache = holder ! = null; } // 1. Find by position from scrap/hidden list/cache if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); . } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); . final int type = mAdapter.getItemViewType(offsetPosition); // 2. Find from scrap/cache via stable ids, if exists if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder ! = null) { // update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache = true; MViewCacheExtension defaults to null if (holder == null && mViewCacheExtension! = null) { // We are NOT sending the offsetPosition because LayoutManager does not // know it. final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view ! = null) { holder = getChildViewHolder(view); . } } if (holder == null) { // fallback to pool ... Holder = getRecycledViewPool().getrecycledView (type); . } if (holder == null) { ... / / 5. Honesty create holder = mAdapter. CreateViewHolder (RecyclerView. This type); . }}Copy the code

The ViewHolder will be retrieved from the mChangedScrap collection only if isPreLayout() is true. In dispatchLayoutStep1(), MState. MInPreLayout = mState. MRunPredictiveAnimations default will be set to true, so will perform here. The first step is to find mAttachedScrap, mHiddenViews, mCachedViews. The second step is to search again through the independent ID. The third step, if possible, is to extend the lookup by mViewCacheExtension, which can be set by RecyclerView; The fourth part, search from RecycledViewPool; Finally, create through adapter.

At this point, the dispatchLayoutStep1() method is almost finished.

dispatchLayoutStep2

The layout is executed based on the final state and may be executed multiple times.

private void dispatchLayoutStep2() { ... // mInPreLayout is set to false mstate.minpreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; . }Copy the code

The second step is much easier than the first. The core is to set mInPreLayout to false and then re-call the onLayoutChildren() method of LayoutManager. The mInPreLayout field is false and ViewHolder is added to the mChangedScrap collection. The mInPreLayout field is false. It won’t go to mChangedScrap lookup ViewHolder (tryGetViewHolderForPositionByDeadline () method of step 0 will not perform operation, so the old ViewHolder cannot be reused, It retrieves the ViewHolder return from the following steps. That is, when we normally use notifyItemChangge(pconfisition) as a parameter, it does not use the same ViewHolder when refreshed.

dispatchLayoutStep3

Save the View information, perform the animation, and finally do some cleanup.

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)); . long key = getChangedHolderKey(holder); / / get the current holder, animation information final ItemHolderInfo animationInfo = mItemAnimator. RecordPostLayoutInformation (mState, holder); / / get olderViewHolder ViewHolder oldChangeViewHolder = mViewInfoStore. GetFromOldChangeHolders (key); if (oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); // If the old and the new are the same, both disappear, If (oldChangeViewHolder == holder) {// Run disappear animation instead of change mViewInfoStore.addToPostLayout(holder, animationInfo); } else {/ / get information kept before final ItemHolderInfo preInfo = mViewInfoStore. PopFromPreLayout (oldChangeViewHolder); / / here a deposit a get complete information cover mViewInfoStore addToPostLayout (holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); if (preInfo == null) { handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); } else {// Add implement animateChange(oldChangeViewHolder, Holder, preInfo, postInfo, oldBsellers, newbsellers); } } } else { mViewInfoStore.addToPostLayout(holder, animationInfo); Mviewinfostore.process (mViewInfoProcessCallback); } // Recycle and reset... }Copy the code

In dispatchLayoutStep1(), we saved some information that is now finally coming in handy. Comments have been added about the animation logic. Finally, note that if you need to execute an animation, the animateChange() method will be executed. This method does the animateChange creation, not directly, until the final call to mViewinFoStore.process (mViewInfoProcessCallback), Start to really get the relevant animation information, and execute.

// RecyclerView private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, boolean oldHolderDisappearing, boolean newHolderDisappearing) { oldHolder.setIsRecyclable(false); if (oldHolderDisappearing) { addAnimatingView(oldHolder); } if (oldHolder ! = newHolder) { if (newHolderDisappearing) { addAnimatingView(newHolder); } / / here is very interesting, a bit like a tongue twister, this set is behind to do relevant release oldHolder mShadowedHolder = newHolder; addAnimatingView(oldHolder); mRecycler.unscrapView(oldHolder); newHolder.setIsRecyclable(false); newHolder.mShadowingHolder = oldHolder; } the if (mItemAnimator animateChange (oldHolder newHolder, preInfo, postInfo)) {/ / here actually perform related animation postAnimationRunner (); } } // DefaultItemAnimator @Override public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, Int fromY, int toX, int toY) {if (oldHolder == newHolder) { Return animateMove(oldHolder, fromX, fromY, toX, toY); } / / if it is not the same as the old and new You will need to calculate the offset, and then create a ChangeInfo final float prevTranslationX = oldHolder. ItemView. GetTranslationX (); final float prevTranslationY = oldHolder.itemView.getTranslationY(); final float prevAlpha = oldHolder.itemView.getAlpha(); resetAnimation(oldHolder); int deltaX = (int) (toX - fromX - prevTranslationX); int deltaY = (int) (toY - fromY - prevTranslationY); // recover prev translation state after ending animation oldHolder.itemView.setTranslationX(prevTranslationX); oldHolder.itemView.setTranslationY(prevTranslationY); oldHolder.itemView.setAlpha(prevAlpha); if (newHolder ! = null) { // carry over translation values resetAnimation(newHolder); newHolder.itemView.setTranslationX(-deltaX); newHolder.itemView.setTranslationY(-deltaY); newHolder.itemView.setAlpha(0); // Add (new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); // Add (new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY); return true; } @Override public boolean animateMove(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { final View view = holder.itemView; fromX += (int) holder.itemView.getTranslationX(); fromY += (int) holder.itemView.getTranslationY(); resetAnimation(holder); int deltaX = toX - fromX; int deltaY = toY - fromY; If (deltaX == 0 && deltaY == 0) {dispatchMoveFinished(holder); // return false; } if (deltaX ! = 0) { view.setTranslationX(-deltaX); } if (deltaY ! = 0) { view.setTranslationY(-deltaY); } // Add (new MoveInfo(holder, fromX, fromY, toX, toY)); return true; }Copy the code

If oldHolder and newHolder are equal and the associated offset is zero, the associated animation effect will not be added or executed.

If relevant animation effects, will create to join DefaultItemAnimator collection, and mItemAnimator animateChange () method returns true, the last call postAnimationRunner () method is carried out.

In postAnimationRunner () method of the Runnable calls last mItemAnimator. RunPendingAnimations (), finally will be executed to animateChangeImpl () method.

//DefaultItemAnimator void animateChangeImpl(final ChangeInfo changeInfo) { final ViewHolder holder = changeInfo.oldHolder; final View view = holder == null ? null : holder.itemView; final ViewHolder newHolder = changeInfo.newHolder; final View newView = newHolder ! = null ? newHolder.itemView : null; // If the old View exists, execute the related animation if (View! = null) { final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( getChangeDuration()); mChangeAnimations.add(changeInfo.oldHolder); oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { dispatchChangeStarting(changeInfo.oldHolder, true); } @override public void onAnimationEnd(Animator Animator) {oldViewanim.setListener (null); view.setAlpha(1); view.setTranslationX(0); view.setTranslationY(0); // RecyclerView dispatchChangeFinished(changeInfo.oldHolder, true); mChangeAnimations.remove(changeInfo.oldHolder); dispatchFinishedWhenDone(); } }).start(); } // newView executes related animation if (newView! = null) { final ViewPropertyAnimator newViewAnimation = newView.animate(); mChangeAnimations.add(changeInfo.newHolder); newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) .alpha(1).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { dispatchChangeStarting(changeInfo.newHolder, false); } @ Override public void onAnimationEnd (Animator Animator) {/ / recycling release newViewAnimation setListener (null); newView.setAlpha(1); newView.setTranslationX(0); newView.setTranslationY(0); // RecyclerView dispatchChangeFinished(changeInfo.newholder, false); mChangeAnimations.remove(changeInfo.newHolder); dispatchFinishedWhenDone(); } }).start(); } } // RecyclerView inner private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { ItemAnimatorRestoreListener() { } @Override public void onAnimationFinished(ViewHolder item) { item.setIsRecyclable(true); // The ViewHolder is set as a twister, where the final release is made if (item.mshadowedholder! = null && item.mShadowingHolder == null) { // old vh item.mShadowedHolder = null; } // always null this because an OldViewHolder can never become NewViewHolder w/o being // recycled. item.mShadowingHolder = null; if (! item.shouldBeKeptAsChild()) { if (! removeAnimatingView(item.itemView) && item.isTmpDetached()) { removeDetachedView(item.itemView, false); }}}}Copy the code

In the animateChangeImpl() method, perform animations for oldView and newView, and then call back to onAnimationFinished() in RecyclerView. Complete the release of the holder associated with the ViewHolder’s previous animation.

At this point, the third analysis is almost complete. Going back to the two problems with local flushing that were raised earlier:

First, are notifyItemChange() and local refresh ViewHolder the same object? Second, is there no animation set for partial refresh?

Based on the conclusion of the analysis in step 3 above, if oldHolder and newHolder are equal and the offset is zero, no animation effects will be added and executed. Given the actual situation, we can take a bold guess at the answer to the first question: they use the same ViewHolder object. Then for the second problem, if the same object is used and the offset is 0, the associated animation will not be performed.

However, this conclusion does not seem to be consistent with our analysis of step 2: The ViewHolder we modified was added to the mChangedScrap collection, but since mInPreLayout is now set to false, It won’t go to mChangedScrap lookup ViewHolder (tryGetViewHolderForPositionByDeadline () method of 0 step operation, so the old ViewHolder cannot be reused, It retrieves the ViewHolder return from the following steps. That is, when we normally use the notifyItemChangge() method, it does not use the same ViewHolder.

What went wrong? NotifyItemChangge (position) is a notifyItemChangge(position) method. For a partial refresh, we use a two-parameter approach.

void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); / / if you don't need to update into mAttachedScrap if (holder. HasAnyOfTheFlags (ViewHolder. FLAG_REMOVED | ViewHolder. FLAG_INVALID) / / update or not Can be reused | |! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { ... holder.setScrapContainer(this, false); mAttachedScrap.add(holder); } else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); }}Copy the code

Looking again at the scrapView() method called in step 2, we can see that in the if judgment, if the holder does need to be updated, it may also be added to the mAttachedScrap collection. As long as anReuseUpdatedViewHolder(holder) returns true.

//RecyclerView
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
    return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
            viewHolder.getUnmodifiedPayloads());
}
Copy the code

RecyclerView finally invokes ItemAnimator canReuseUpdatedViewHolder in () method, we look at the concrete implementation:

@Override public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, @NonNull List<Object> payloads) { return ! payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); }Copy the code

The kagyu, when we use local refresh, content is not null, at this time, if the ViewHolder needs to be updated, its update logo will be joining indeed, but at the same time canReuseUpdatedViewHolder () will return true, so, Instead of adding ViewHolder to the mChangedScrap collection, the ViewHolder will be added to the mAttachedScrap collection.

So, when you do a partial refresh, it’s the same thingViewHolderIf the position does not change, the animation will not be performed; And when you don’t use local refresh, it’s not the sameViewHolderRegardless of whether the position changes or not, the relevant animation will be performed, so what you seeitemViewIt flashes a little bit. When we call it multiple timesnotifyItemChange()Method is not fired more than oncerequestLayout()And the callbackbindViewHolder()

You can use a local refresh for that scene, because you don’t have any flicker, because you’re executing the animation, and you can use the payload as the payload. It’s a programmer’s trick to get the last value in the payloads (the latest progress) and then update it to the progress bar.