RecyclerView is a display list control whose child controls can be scrolled. How does this work? Take a look at the source code.

Entry point: Touch events

When reading source code, how to choose the right entry point in the vast source code is very important, choose a good can less detours.

The most obvious entry point for the scrolling scenario is touch events, where a finger slides on a RecyclerView and a list follows.

Starting with RecyclerView. OnTouchEvent (). The logic for sliding the list should be in ACTION_MOVE, and its source code is as follows (slightly longer can be skipped) :

public class RecyclerView {
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (action) {
            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.5 f);
                final int y = (int) (e.getY(index) + 0.5 f);
                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)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if(mGapWorker ! =null&& (dx ! =0|| dy ! =0)) {
                        mGapWorker.postFromTraversal(this, dx, dy); }}}break; }}}Copy the code

New way to read source code: Profiler method

Although the sliding-related logic has been accurately located, the source code in the ACTION_MOVE branch is still too long, headache!

How to quickly locate the key logic in the complex source code?

RecyclerView animation principle | change the posture to see the source code (pre – layout) introduced in a way: “a breakpoint debugging method”. Write a simple demo to simulate the scenario, and then use breakpoint debugging to determine the critical path along the source call chain.

Here’s a faster method: the Profiler method.

Or write a demo to load a list, open the performance debugging tool Profiler that comes with AndroidStudio, select the CPU bar, trigger the list scrolling with your finger, and then click the Record button to start recording the complete function call chain during the list scrolling process. After the list is scrolling, Click Stop to Stop recording. You get something like this:

The horizontal axis represents the time, the vertical axis represents the function call that occurred at that point in time, and the direction of the call chain is from top to bottom, that is, the caller is above, and the caller is below.

A red line at the top of the image indicates the user interaction that took place during this time. In the demo scenario, the interaction is a finger scrolling list. The logic that triggers the scrolling of the list should be contained within the time corresponding to the red line segment. Press the W key to zoom in on this call chain:

The call chain is really long, if you can’t see clearly click on the larger image.

At the top of the call chain is the looper.loop () method, because that’s where all the main thread logic is executed.

Moving down the call chain, Looper calls messagequeue.next () to fetch the next message in the MessageQueue, followed by hander.dispatchmessage () and hander.handlecallback (), Means to distribute and process the message.

Because the news is the processing of touch events, so the Choreographer and entrust ViewRootImpl distributing touch events, after a long distribution chain, finally saw a familiar way Activity. The dispatchTouchEvent (), Indicates that the touch event has been passed to the Activity. Layers and then according to the hierarchical structure of the interface, to distribute to RecyclerView. The onTouchEvent (), go here, we care about the list of sliding logic are all displayed in front of, amplify the layout and then look at:

A clear chain of calls came up:

RecyclerView.onTouchEvent()
RecyclerView.scrollByInternal()
RecyclerView.scrollStep()
LinearLayoutManager.scrollVerticallyBy()
LinearLayoutManager.scrollBy()
OrientationHelper.offsetChildren()
LayoutManager.offsetChildrenVertical()
RecyclerView.offsetChildrenVertical()
View.offsetTopAndBottom()
Copy the code

Already do not need and RecyclerView onTouchEvent () of confused logic to entwine, walk along this call to check, all the key information of a won’t be missed.

Follow the chain of key calls

With the key call chain above, you save a lot of time. It is now possible to RecyclerView. OnTouchEvent () the logic in the PiShaJianJin:

public class RecyclerView {
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                ...
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    // 1. Trigger nested scroll so that the parent control in nested scroll has priority to consume the scroll distance
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        getParent().requestDisallowInterceptTouchEvent(true); }...// 2. Trigger the list itself to scroll
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true); }... }}break; }}}Copy the code

Above the key call chain, scrollByInternal(), there is an unexpected logic to handle nested scrolling, which prioritized the consumption of its parent control before the list consumes the scroll distance.

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

        consumePendingUpdateOperations();
        if(mAdapter ! =null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            // Trigger list scrolling (finger slider distance is passed in)
            scrollStep(x, y, mReusableIntPair);
            // Record the number of pixels consumed by scrolling through the list and the remaining unconsumed pixels
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1]; unconsumedX = x - consumedX; unconsumedY = y - consumedY; }... mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // Leave the unconsumed scrolling distance of the list to its parent control
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1]; . }}Copy the code

ScrollByInternal () is the starting point of the list scrolling call chain. It calls scrollStep() to trigger the list itself, followed by dispatchNestedScroll() to give its parent control the scroll margin left over after its own consumption.

Further down the chain of key calls:

public class RecyclerView {
    LayoutManager mLayout;
    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        // Disallow relayouts before scrolling
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();

        int consumedX = 0;
        int consumedY = 0;
        // Scroll dx horizontally
        if(dx ! =0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        // Scroll dy vertically
        if(dy ! =0) { consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); }...// Pass the scroll consumption through the array
        if(consumed ! =null) {
            consumed[0] = consumedX;
            consumed[1] = consumedY; }}}Copy the code

ScrollStep () delegates the task of triggering the scroll to LayoutManager, calling its scrollVerticallyBy() :

public class RecyclerView {
    public abstract static class LayoutManager {
        / / empty implementation
        public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
            return 0; }}}public class LinearLayoutManager extends RecyclerView.LayoutManager {
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // Vertical scrolling does not occur if the layout is horizontal
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        // Trigger vertical scrolling
        return scrollBy(dy, recycler, state);
    }
    
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {...final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        // Calculate and scroll through various related data and save it in mLayoutState
        updateLayoutState(layoutDirection, absDelta, true, state);
        // Populate additional entries and calculate the scroll value actually consumed
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); .final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        // Pan the list in the opposite direction that all children want to scrollmOrientationHelper.offsetChildren(-scrolled); . mLayoutState.mLastScrollDelta = scrolled;returnscrolled; }}Copy the code

If read RecyclerView animation principle | (pre – layout), change the posture to see source code for LinearLayoutManager. The fill () method must not unfamiliar. It is used to fill the list with additional entries, as determined by the extra space mlayoutstate.mavailable, which is assigned by the absDelta, the scroll distance, in the updateLayoutState() method.

The source code for fill() is as follows:

public class LinearLayoutManager {
    // Fill the entry according to the remaining space
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {...// Calculate free space = free space + extra space
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // Fill more entries when the remaining space is greater than 0
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk() ... }}void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // Get the next entry view to be populatedView view = layoutState.next(recycler); . addView(view); . }}Copy the code

The fill() method circulates layoutChunk() to fill the list with entries based on the remaining space, which in a scrolling list scenario is determined by the scrolling distance.

On the list when rolling, filling, and reuse the details of the item table analysis can click RecyclerView interview questions | rolling table when the item is being filled or recycled?

Next () will eventually trigger onCreateViewHolder() and onBindViewHolder(), so the speed at which the two methods execute, i.e. the speed at which the entries are loaded, Also affects the fluency of slide list, on how to improve the loading speed table item can click RecyclerView performance optimization | to halve load time table item (a)

The scrollBy() method populates additional entries in the scrolling direction of the list, depending on the scrolling distance. Fill out, and then call mOrientationHelper. OffsetChildren () all the list items to scroll to the opposite direction of translation:

public abstract class OrientationHelper {
    // Abstract translation subentry
    public abstract void offsetChildren(int amount);
    
    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            public void offsetChildren(int amount) {
                // Delegate to LayoutManager to translate child entries verticallymLayoutManager.offsetChildrenVertical(amount); }... }}public class RecyclerView {
    public abstract static class LayoutManager {
        public void offsetChildrenVertical(@Px int dy) {
            if(mRecyclerView ! =null) {
                // Delegate to RecyclerView to translate child entries verticallymRecyclerView.offsetChildrenVertical(dy); }}}public void offsetChildrenVertical(@Px int dy) {
        // Iterate over all child entries
        final int childCount = mChildHelper.getChildCount();
        for (int i = 0; i < childCount; i++) {
            // Shift the child entry verticallymChildHelper.getChildAt(i).offsetTopAndBottom(dy); }}}Copy the code

After a series of calls, view.offsettopandBottom () is finally executed:

public class View {
    public void offsetTopAndBottom(int offset) {
        if(offset ! =0) {
            final boolean matrixIsIdentity = hasIdentityMatrix();
            if (matrixIsIdentity) {
                if (isHardwareAccelerated()) {
                    invalidateViewProperty(false.false);
                } else {
                    final ViewParent p = mParent;
                    if(p ! =null&& mAttachInfo ! =null) {
                        final Rect r = mAttachInfo.mTmpInvalRect;
                        int minTop;
                        int maxBottom;
                        int yLoc;
                        if (offset < 0) {
                            minTop = mTop + offset;
                            maxBottom = mBottom;
                            yLoc = offset;
                        } else {
                            minTop = mTop;
                            maxBottom = mBottom + offset;
                            yLoc = 0;
                        }
                        r.set(0, yLoc, mRight - mLeft, maxBottom - minTop);
                        p.invalidateChild(this, r); }}}else {
                invalidateViewProperty(false.false);
            }

            // Modify the top and bottom values of the view
            mTop += offset;
            mBottom += offset;
            mRenderNode.offsetTopAndBottom(offset);
            if (isHardwareAccelerated()) {
                invalidateViewProperty(false.false);
                invalidateParentIfNeededAndWasQuickRejected();
            } else {
                if(! matrixIsIdentity) { invalidateViewProperty(false.true); } invalidateParentIfNeeded(); } notifySubtreeAccessibilityStateChangedIfNeeded(); }}}Copy the code

This method modifies the mTop and mBottom values of the View and triggers a lightweight redraw.

At this point, the analysis can already answer the opening question:

RecyclerView calculates finger sliding distance when processing ACTION_MOVE event, which is used as rolling displacement value.

RecyclerView fills additional entries in the rolling direction according to the length of the rolling displacement, and then shifts all entries to the opposite direction of the rolling of the same displacement value, so as to achieve rolling.

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?