RecyclerView OnBindViewHolder call timing

Keywords: Java UI

1. Article 1 call timing

onLayout will be called during initialization loading and placement, but when was this onLayout called?

The onLayout method of recyclerView is as follows:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

This method will be called during placement. Continue to call dispatchLayout. The code of dispatchLayout is as follows:

void dispatchLayout() {
        if (mAdapter == null) {
            Log.w(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;

        // If the last time we measured children in onMeasure, we skipped the measurement and layout
        // of RV children because the MeasureSpec in both dimensions was EXACTLY, and current
        // dimensions of the RV are not equal to the last measured dimensions of RV, we need to
        // measure and layout children one last time.
        boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact
                        && (mLastAutoMeasureNonExactMeasuredWidth != getWidth()
                        || mLastAutoMeasureNonExactMeasuredHeight != getHeight());
        mLastAutoMeasureNonExactMeasuredWidth = 0;
        mLastAutoMeasureNonExactMeasuredHeight = 0;
        mLastAutoMeasureSkippedDueToExact = false;

        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates()
                || needsRemeasureDueToExactSkip
                || 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.

            // TODO(shepshapard): Worth a note that I believe
            //  "mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()" above is
            //  not actually correct, causes unnecessary work to be done, and should be
            //  removed. Removing causes many tests to fail and I didn't have the time to
            //  investigate. Just a note for the a future reader or bug fixer.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
}

This method is well understood from the method name, which is to distribute the layout. The key codes are as follows:

if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates()
                || needsRemeasureDueToExactSkip
                || 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.

            // TODO(shepshapard): Worth a note that I believe
            //  "mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()" above is
            //  not actually correct, causes unnecessary work to be done, and should be
            //  removed. Removing causes many tests to fail and I didn't have the time to
            //  investigate. Just a note for the a future reader or bug fixer.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();

To explain, there are several states:

(1) The Step of this State is Step_ In the State of start, execute dispatchLayoutStep1(), and then dispatchLayoutStep2(), (question: what is this State? Why is this State executed?)

(2) If there is an update, dispatchLayoutStep2() will be executed when the width and height are different,

(3) In other cases, execute dispatchLayoutStep3(), (question: what is done in this method?)

The code of dispatchLayoutStep2 is as follows:

private void dispatchLayoutStep2() {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    if (mPendingSavedState != null && mAdapter.canRestoreState()) {
        if (mPendingSavedState.mLayoutState != null) {
            mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
        }
        mPendingSavedState = null;
    }
    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
}

This method will also call mLayout.onLayoutChildren(mRecycler, mState) and mLayout method, because this variable is actually set in, that is, LayoutManager. LayoutManager is an abstract class and needs to be implemented. This is why a LayoutManager needs to be set during the initialization of RecyclerView. It can't run without setting it, This is only one reason. There should be code calls for many other reasons. You can find an implementation class, such as LinearLayoutManager, to meet the requirements. Take a look at the specific contents executed in his onLayoutChildren() method. Because this method is too long, only some contents are intercepted and the rest are omitted,

/**
* {@inheritDoc}
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    ......
    if (mAnchorInfo.mLayoutFromEnd) {
    	......
    	fill(recycler, mLayoutState, state, false);
    	......
    } else {
    	......
    	fill(recycler, mLayoutState, state, false);
    	......
    }
    ......
}

The fill method will be used in this,

/**
* The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
* independent from the rest of the {@link LinearLayoutManager}
* and with little change, can be made publicly available as a helper class.
*
* @param recycler        Current recycler that is attached to RecyclerView
* @param layoutState     Configuration on how we should fill out the available space.
* @param state           Context passed by the RecyclerView to control scroll steps.
* @param stopOnFocusable If true, filling stops in the first focusable new child
* @return Number of pixels that it added. Useful for scroll functions.
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
         RecyclerView.State state, boolean stopOnFocusable){
}

Magic method to fill a given layout. According to the layoutState setting, this method can be set as an independent static method, but it needs to be changed a little and can be used as a help class.

The specific contents of the fill method are as follows:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

There is a while loop. The judgment condition is this:

(layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)

The first condition is the or relationship. When the current layoutManager attribute is infinite, this infinite is actually the current linearLayout attribute, because it is passed layer by layer from the above methods. The parameter header is actually the current class, or the remaining space is > 0. How is the remaining space calculated?

int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;

It is the sum of the current available space and the remaining filled space. It's easy to understand the available space. What is the remaining space for?

/**
* Used if you want to pre-layout items that are not yet visible.
* The difference with {@link #mAvailable} is that, when recycling, distance laid out for
* {@link #mExtraFillSpace} is not considered to avoid recycling visible children.
*/
int mExtraFillSpace = 0;

This attribute is said like this. The difference between this attribute and M available is that when it is recycled, the space filled sub items will not be recycled, because this may lead to the recycling of some visible sub items.

The question comes again: what does the available space and the remaining filled space refer to on the screen? Which area does it refer to? I don't know. It's just digging a pit. I'll have a chance to see it again in the future.

The following & & conditions are easy to understand. After clicking, it is found that:

/**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
	return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}

This method is easy to understand, that is, when there are more data not loaded,

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
}

This method also has a lot of contents, which is probably in the computational space, but it has a next method at the beginning. The contents of this next method are as follows:

/**
* Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
    return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

This will be called directly to getViewForPosition(mCurrentPosition) and jump back to recyclerView.

@NonNull
public View getViewForPosition(int position) {
	return getViewForPosition(position, false);
}

This is the method in recyclerView, and then jump:

View getViewForPosition(int position, boolean dryRun) {
	return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

Then jump to the deadline method inside,

@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs){
    ......
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
        throw new IllegalStateException("Removed holder should be bound and it should"
        + " come here only in pre-layout. Holder: " + holder
        + exceptionLabel());
    	}
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ......
}

The tryBindViewHolderByDeadline method must be used here, which roughly means that the view must be completely bound in the end,

private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, int position, long deadlineNs) {
    holder.mBindingAdapter = null;
    holder.mOwnerRecyclerView = RecyclerView.this;
    final int viewType = holder.getItemViewType();
    long startBindNs = getNanoTime();
    if (deadlineNs != FOREVER_NS
        && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
        // abort - we have a deadline we can't meet
        return false;
    }
    mAdapter.bindViewHolder(holder, offsetPosition);
    long endBindNs = getNanoTime();
    mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
    attachAccessibilityDelegateOnBind(holder);
    if (mState.isPreLayout()) {
        holder.mPreLayoutPosition = position;
    }
    return true;
}

Then, the following methods are more common. First, go to the mAdapter.bindViewHolder(holder, offsetPosition) and look at the code in turn, because they will be executed without judgment conditions,

* @param holder   The view holder whose contents should be updated
* @param position The position of the holder with respect to this adapter
* @see #onBindViewHolder(ViewHolder, int)
*/
public final void bindViewHolder(@NonNull VH holder, int position) {
    boolean rootBind = holder.mBindingAdapter == null;
    if (rootBind) {
    	holder.mPosition = position;
    if (hasStableIds()) {
    	holder.mItemId = getItemId(position);
    }
    holder.setFlags(ViewHolder.FLAG_BOUND,
    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
    TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
    }
    holder.mBindingAdapter = this;
    onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
    if (rootBind) {
        holder.clearPayload();
        final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
        if (layoutParams instanceof RecyclerView.LayoutParams) {
        	((LayoutParams) layoutParams).mInsetsDirty = true;
        }
        TraceCompat.endSection();
    }
}

Then go to onbindviewholder (holder, position, holder. Getunmodified payloads (), and then

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

Then go to onBindViewHolder,

* @param holder   The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
public abstract void onBindViewHolder(@NonNull VH holder, int position);

It seems that this is the method that needs to be rewritten when writing code.

2. Article 2 call timing

This is called by sliding in onTouchEvent,

@Override
public boolean onTouchEvent(MotionEvent e) {
	......
    switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            }
            break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            }
            break;
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e, TYPE_TOUCH)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            }
            break;
            ......
        }
    ......
}

When moving to MotionEvent.ACTION_MOVE, there is a judgment, if (mscrollstate = = scroll_state_driving). When the current state is dragging, it will go to the method of scrollByInternal(), and then jump inside,

boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                type, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];

        if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

Call the scrollstep (x, y, mrusableintpair) method from this method, and then continue,

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();

    TraceCompat.beginSection(TRACE_SCROLL_TAG);
    fillRemainingScrollValues(mState);

    int consumedX = 0;
    int consumedY = 0;
    if (dx != 0) {
    consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
    consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    TraceCompat.endSection();
    repositionShadowingViews();

    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);

    if (consumed != null) {
    consumed[0] = consumedX;
    consumed[1] = consumedY;
    }
}

The calls in this method can be divided into sliding up and down or sliding left and right according to dx and dy. dx and Dy are common, which are the parameters during normal sliding. If dy is not equal to 0, the current recyclerView should be in the dragging state in the vertical direction. If fx is not equal to 0, the current recyclerView should be in the dragging state in the horizontal direction.

The next step is to call the scrollVerticallyBy method in the layoutManager. The layoutManager is used in many places. If it is the vertical direction, take the linearLayoutManager as an example, because the manager may be different in practice,

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
    	return 0;
    }
    return scrollBy(dy, recycler, state);
}

This method is to override the implementation method of the parent class by linearLayoutManager, and then call the scrollBy method,

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
    	return 0;
    }
    ensureLayoutState();
    mLayoutState.mRecycle = true;
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    updateLayoutState(layoutDirection, absDelta, true, state);
    final int consumed = mLayoutState.mScrollingOffset
    + fill(recycler, mLayoutState, state, false);
    if (consumed < 0) {
        if (DEBUG) {
        Log.d(TAG, "Don't have any more elements to scroll");
        }
        return 0;
    }
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    mOrientationHelper.offsetChildren(-scrolled);
    if (DEBUG) {
    	Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
    }
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
}

The fill method will still be called. Final int consumed = mlayoutstate. Mscollingoffset + fill (recycler, mlayoutstate, state, false);

But what does this calculation specifically calculate? It literally means what has been consumed, but where is it consumed? It is not clear for the time being.

After going to the fill method, it is similar to the previous methods. The previous methods are almost the same. It feels like this onBindViewHolder will be triggered when dragging, but the specific calculation is not clear.

3. Article 3 call timing

Or go back to the onTouchEvent mentioned earlier and drag it,

case MotionEvent.ACTION_MOVE: {
    final int index = e.findPointerIndex(mScrollPointerId);
    if (index < 0) {
        Log.e(TAG, "Error processing scroll; pointer index for id "
        + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
        return false;
    }

    final int x = (int) (e.getX(index) + 0.5f);
    final int y = (int) (e.getY(index) + 0.5f);
    int dx = mLastTouchX - x;
    int dy = mLastTouchY - y;

    if (mScrollState != SCROLL_STATE_DRAGGING) {
    	boolean startScroll = false;
        if (canScrollHorizontally) {
            if (dx > 0) {
                dx = Math.max(0, dx - mTouchSlop);
            } else {
                dx = Math.min(0, dx + mTouchSlop);
            }
            if (dx != 0) {
                startScroll = true;
            }
        }
        if (canScrollVertically) {
            if (dy > 0) {
                dy = Math.max(0, dy - mTouchSlop);
            } else {
                dy = Math.min(0, dy + mTouchSlop);
            }
            if (dy != 0) {
                startScroll = true;
            }
        }
        if (startScroll) {
        	setScrollState(SCROLL_STATE_DRAGGING);
        }
    }

    if (mScrollState == SCROLL_STATE_DRAGGING) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        if (dispatchNestedPreScroll(
            canScrollHorizontally ? dx : 0,
            canScrollVertically ? dy : 0,
            mReusableIntPair, mScrollOffset, TYPE_TOUCH
            )) {
            dx -= mReusableIntPair[0];
            dy -= mReusableIntPair[1];
            // Updated the nested offsets
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
            // Scroll has initiated, prevent parents from intercepting
            getParent().requestDisallowInterceptTouchEvent(true);
        }

        mLastTouchX = x - mScrollOffset[0];
        mLastTouchY = y - mScrollOffset[1];

        if (scrollByInternal(
            canScrollHorizontally ? dx : 0,
            canScrollVertically ? dy : 0,
            e, TYPE_TOUCH)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        if (mGapWorker != null && (dx != 0 || dy != 0)) {
            mGapWorker.postFromTraversal(this, dx, dy);
        }
    }
}
break;

There is an mGapWorker here. What does this mGapWorker do? You can take a look at the contents,

final class GapWorker implements Runnable

In this way, it is easier to understand. It is a package of runnable business. Basically, you can only look at the run method, because the run method should be the core content,

@Override
public void run() {
    try {
        TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

        if (mRecyclerViews.isEmpty()) {
        // abort - no work to do
    	return;
    }

    // Query most recent vsync so we can predict next one. Note that drawing time not yet
    // valid in animation/input callbacks, so query it here to be safe.
    //Here, a collection of recyclerviews will be traversed to get the millisecond time difference of the latest frame,
    final int size = mRecyclerViews.size();
    long latestFrameVsyncMs = 0;
    for (int i = 0; i < size; i++) {
        RecyclerView view = mRecyclerViews.get(i);
        if (view.getWindowVisibility() == View.VISIBLE) {
        	latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
        }
    }

	//If it is the latest, it will be returned directly without processing
    if (latestFrameVsyncMs == 0) {
        // abort - either no views visible, or couldn't get last vsync for estimating next
        return;
    }

	//This is the time value of the next time. It will add a time interval, which is calculated according to the frame rate,
    long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;

    prefetch(nextFrameNs);

    // TODO: consider rescheduling self, if there's more work to do
    } finally {
        mPostTimeNs = 0;
        TraceCompat.endSection();
    }
}

The frame rate intervals mentioned in it are added. mFrameIntervalNs, the code of its assignment is here,

float refreshRate = 60.0f;
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);

The intercepted code can be found in RecyclerView. If so, it means that this is actually a timing detection mechanism, because he will go to the prefetch(nextFrameNs) method until the next frame. Note that the unit is nanosecond, so this is actually 1s60 frames, so the result should be how many nanoseconds a frame has gone, So I feel that the calculation level in the source code is still very strong.

The method of prefetch(nextFrameNs) is as follows:

void prefetch(long deadlineNs) {
    buildTaskList();
    flushTasksWithDeadline(deadlineNs);
}

Then go inside,

private void flushTasksWithDeadline(long deadlineNs) {
    for (int i = 0; i < mTasks.size(); i++) {
        final Task task = mTasks.get(i);
        if (task.view == null) {
            break; // done with populated tasks
        }
        flushTaskWithDeadline(task, deadlineNs);
        task.clear();
    }
}

Take out the contents from each task, which roughly means that many tasks can be defined, and each task can be associated with the view, right? Follow this method and go inside,

private void flushTaskWithDeadline(Task task, long deadlineNs) {
    long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
    RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
    task.position, taskDeadlineNs);
    if (holder != null
    && holder.mNestedRecyclerView != null
    && holder.isBound()
    && !holder.isInvalid()) {
    	prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
    }
}

This roughly means that the recyclerView is inversely taken from the holder, and then called,

private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
            long deadlineNs) {
        if (innerView == null) {
            return;
        }

        if (innerView.mDataSetHasChangedAfterLayout
                && innerView.mChildHelper.getUnfilteredChildCount() != 0) {
            // RecyclerView has new data, but old attached views. Clear everything, so that
            // we can prefetch without partially stale data.
            innerView.removeAndRecycleViews();
        }

        // do nested prefetch!
        final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
        innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);

        if (innerPrefetchRegistry.mCount != 0) {
            try {
                TraceCompat.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
                innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
                for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
                    // Note that we ignore immediate flag for inner items because
                    // we have lower confidence they're needed next frame.
                    final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
                    prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
                }
            } finally {
                TraceCompat.endSection();
            }
        }
}

Prefetchposition with deadline (innerview, innerposition, deadline NS) will be called again;

private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
            int position, long deadlineNs) {
        if (isPrefetchPositionAttached(view, position)) {
            // don't attempt to prefetch attached views
            return null;
        }

        RecyclerView.Recycler recycler = view.mRecycler;
        RecyclerView.ViewHolder holder;
        try {
            view.onEnterLayoutOrScroll();
            //When you transfer here, you will return to the previous place. It is equivalent to a method called from another entry, and the following things will be repeated
            holder = recycler.tryGetViewHolderForPositionByDeadline(
                    position, false, deadlineNs);

            if (holder != null) {
                if (holder.isBound() && !holder.isInvalid()) {
                    // Only give the view a chance to go into the cache if binding succeeded
                    // Note that we must use public method, since item may need cleanup
                    recycler.recycleView(holder.itemView);
                } else {
                    // Didn't bind, so we can't cache the view, but it will stay in the pool until
                    // next prefetch/traversal. If a View fails to bind, it means we didn't have
                    // enough time prior to the deadline (and won't for other instances of this
                    // type, during this GapWorker prefetch pass).
                    recycler.addViewHolderToRecycledViewPool(holder, false);
                }
            }
        } finally {
            view.onExitLayoutOrScroll(false);
        }
        return holder;
}

4. Conclusion

So when the recyclerView slides,

1. During initialization, you will go to onBindViewHolder.

2. When touching, onTouch will also trigger onBindViewHolder, but it is limited and calculated according to.

3. The GapWorker created in the onattachedtowindow method will also go to the onBindViewHolder, which is built-in nested

Posted by Dax on Tue, 30 Nov 2021 08:31:57 -0800