Preface: If a man has no dream, what is the difference between him and salted fish? — Shaolin Football

1. Drawing process

RecyclerView support all kinds of layout effect, its core is the key to RecyclerView. LayoutManager, is we need when using setLayoutManager () sets the layout manager. RecyclerView has been part of the function out, in the layout manager for additional processing, but also convenient for developers to expand. LayoutManager is responsible for the measurement and layout of RecyclerView and the recycling and reuse of itemView. Today here mainly combined with LinearLayoutManager to analyze the drawing process of RecyclerView.

RecyclerView provides three types of layout managers:

  • The LinearLayoutManager displays items in a list. There is a HORIZONTAL RecyclerView.HORIZONTAL and VERTICAL RecyclerView.
  • The GridLayoutManager presents items as a grid, horizontal and vertical;
  • StaggeredGridLayoutManager display item, in the form of cascade flow with horizontal and vertical direction.

Here take LinearLayoutManager as an example to analyze the drawing process of RecyclerView.

Warm prompt: in this paper, the source code based on androidx. Recyclerview: recyclerview: 1.2.0 – alpha01

1.1. Three steps of RecyclerView

RecyclerView set LayoutManager, this step is necessary, what kind of LayoutManager to draw RecyclerView, otherwise RecyclerView does not know how to draw.

 recyclerView.setLayoutManager(manager);
Copy the code

SetLayoutManager () sets the RecyclerView layout manager to use:

   public void setLayoutManager(@Nullable LayoutManager layout) {
        if (layout == mLayout) {// Just like the previous manager, return directly
            return;
        }
        stopScroll();// Stop scrolling
        if(mLayout ! =null) {// Every time you set layoutManager, reset the initial parameters of recyclerView
        	if(mItemAnimator ! =null) {
                mItemAnimator.endAnimations();// End the animation
            }
            mLayout.removeAndRecycleAllViews(mRecycler);// Remove and recycle all ItemViews
            mLayout.removeAndRecycleScrapInt(mRecycler);// Remove and recycle all discarded ItemViews
            mRecycler.clear();// Clear all caches
            
            mLayout.setRecyclerView(null);/ / reset RecyclerView
            mLayout = null;
        } else{ mRecycler.clear(); } · · · · · · · mLayout. SetRecyclerView (this);//LayoutManager is associated with RecyclerView
        mRecycler.updateViewCacheSize();// Update the cache size
        requestLayout();// Request a redraw
    }
Copy the code

Here the reset recycle is done first, then the LayoutManager is associated with the RecyclerView, and finally the request is redrawn. RequestLayout () = onMeasure(), onLayout(), onDraw() = onMeasure();

    public void requestLayout(a) {
        if(mRecyclerView ! =null) {
            mRecyclerView.requestLayout();// Request a redraw}}Copy the code

1. onMeasure()

Take a look at the RecyclerView onMeasure() method:

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {// If mLayout is empty, take the default measurement and finish
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {// For automatic measurement, the default is true
        	final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
        	mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);// Measure the width of RecyclerView
        	 // Whether the current width and height of RecyclerView is accurate
        	final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {// If the width and height of RecyclerView is exact or the mAdapter is empty, end
                return;
            }
            MeasureSpecModeIsExactly = false when the width and height of RecyclerView is wrAP_content
            // Because the width and height of RecyclerView is wrAP_content, you need to measure the width and height of itemView first to know the width and height of RecyclerView
            if (mState.mLayoutStep == State.STEP_START) {// It has not been measured
                dispatchLayoutStep1();//1. The adapter updates, animates, saves information about the current view, and runs the predictive layout
            }
            dispatchLayoutStep2();//2. The final actual layout view will be run multiple times if necessary
            // Get the width and height of RecyclerView according to itemViewmLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); }}Copy the code

OnMeasure () is mainly RecyclerView width and height measurement work, mainly in two cases:

  • (1) When the width and height of RecyclerView is match_parent or an exact value, that ismeasureSpecModeIsExactly = true, at this time only need to measure its width and height to know the width and height of RecyclerView, the measurement method ends;
  • (2) When the width and height of RecyclerView is WRAP_content, that ismeasureSpecModeIsExactly = false, will execute downdispatchLayoutStep1()anddispatchLayoutStep2(), is traversed to measure the size of ItemView to determine the width and height of RecyclerView. In this case, the real measurement operation is indispatchLayoutStep2()In the finish.

DispatchLayoutStep1 () and dispatchLayoutStep2() will be explained below.

2. onLayout()

In the onLayout() method, call the dispatchLayout() method directly:

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout(); // Call the dispatchLayout() method layout directly
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }
Copy the code

DispatchLayout () is a wrapper around layoutChildren(), which handles dynamic changes caused by the layout:

  void dispatchLayout(a) {· · · · · · mState. MIsMeasuring =false;// Set RecyclerView layout to complete state.
        if (mState.mLayoutStep == State.STEP_START) {// If the subitemView is not measured in advance in the OnMeasure phase
            dispatchLayoutStep1();// Layout step 1: Adapter update, animate run, save current view information, run predictive layout
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) {// The first two steps complete the measurement, but have to run the following code again because of the size change
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();// Layout step 2: The final actual layout view, run multiple times if necessary
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();// Layout step 3: The layout of the last step, save the view animation, trigger animation and unnecessary cleanup.
    }
Copy the code

You can see that the three steps of the dispatchLayout() phase selectively measure the layout as in the onMeasure() phase:

  • 1. If the sub-ItemView is not measured in advance in onMeasure stage, that is, the width and height of RecyclerView ismatch_parentOr exact value when calleddispatchLayoutStep1()anddispatchLayoutStep2()Measure the width and height of itemView
  • 2. If the subItemView is measured in advance during the onMeasure phase, but the subview changes or the expected width is inconsistent with the actual width and height, it will be calleddispatchLayoutStep2()Remeasure;
  • 3. It will be executed at the enddispatchLayoutStep3()Methods.

DispatchLayoutStep1 () is mainly for pre-layout, adapter update, animation operation, save the information of the current view and so on;

  private void dispatchLayoutStep1(a) {
        mState.assertLayoutStep(State.STEP_START);
        fillRemainingScrollValues(mState);
        mState.mIsMeasuring = false;
        startInterceptRequestLayout();// Intercept layout requests
        mViewInfoStore.clear();//itemView information is cleared
        onEnterLayoutOrScroll();
        // When measuring and dispatching layouts, update adapters and calculate which type of animation to run
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();// Save focus information
        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
        mItemsAddedOrRemoved = mItemsChanged = false;
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
        mState.mItemCount = mAdapter.getItemCount();
        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);// Find the minimum and maximum position to draw itemView

        if (mState.mRunSimpleAnimations) {
            // Get the number of items that can be displayed on the interface
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                // Animation information
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                // Save the holder and animation information to the pre-layoutmViewInfoStore.addToPreLayout(holder, animationInfo); }}// Run with layout, will use the old item position, layout manager layout all
        if (mState.mRunPredictiveAnimations) {
            // Save the logic that the old manager can run
            saveOldPositions();
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            / / layout itemView
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mStructureChanged = didStructureChange;
        }
        stopInterceptRequestLayout(false);// Restore draw lock
        mState.mLayoutStep = State.STEP_LAYOUT;
    }
Copy the code

(2) dispatchLayoutStep2() indicates the actual layout of the view of the final state:

  private void dispatchLayoutStep2(a) {
        startInterceptRequestLayout();// Intercept the request layout
        onEnterLayoutOrScroll();
        // Set the layout state and animation state
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // The prelayout is complete, and the layout of itemView is started
        mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); · · · · · · stopInterceptRequestLayout (false);// Stop intercepting layout requests
    }
Copy the code

(3) dispatchLayoutStep3() is the last step of the layout, save the animation information of the view, execute the animation, and do some necessary cleaning:

   private void dispatchLayoutStep3(a) {
        mState.assertLayoutStep(State.STEP_ANIMATIONS);
        startInterceptRequestLayout();// Start intercepting layout requests

        mState.mLayoutStep = State.STEP_START;// The layout starts
        if (mState.mRunSimpleAnimations) {
            // Step 3: Find out where things are now and process the change animation.
            // Reverse traverse the list, because we might call animateChange in the loop, which might remove the target view holder.
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                // Run a change animation. If an item is changed, but the updated version is disappearing, a conflict situation can occur.
                // Since views marked as disappearing may be out of bounds, we run a change animation. Both views will be cleared automatically after the animation is complete.
                // On the other hand, if it is the same viewholder instance, we will run a vanishing animation because we will not rebind the updated VH unless it is enforced by the layout manager.

                // Run disappear animation instead of change
                mViewInfoStore.addToPostLayout(holder, animationInfo);
                final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
                // We add and delete so that any layout information is merged
                mViewInfoStore.addToPostLayout(holder, animationInfo);

                ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }

            // Handle view information lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        // Recycle obsolete views
        mLayout.removeAndRecycleScrapInt(mRecycler);
        // Reset the state
        mState.mPreviousLayoutItemCount = mState.mItemCount;
        mDataSetHasChangedAfterLayout = false;

        // Clear data from mChangedScrap
        mRecycler.mChangedScrap.clear();
        mRecycler.updateViewCacheSize();// Update the cache size

        mLayout.onLayoutCompleted(mState);// The layout is complete
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);// Stop intercepting layout requests
        mViewInfoStore.clear();//itemView information is cleared

        recoverFocusFromState();// Return to focus
        resetFocusInfo();// Reset the focus information
    }
Copy the code

Summarize the three steps of the distribution layout:

  • DispatchLayoutStep1 () : pre-layout, adapter update, animation run, save current view information, etc.
  • DispatchLayoutStep2 () : indicates the actual layout of the final state view, several times if necessary;
  • DispatchLayoutStep3 () : indicates the last step of layout, saves and triggers information about animation, related cleanup, etc.

3. onDraw()

In the last step of the onDraw() method, TextView and ImageView controls are already drawn if no special effects are needed.

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);// All ItemViews are drawn first
		// Draw ItemDecoration separately
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState); }}Copy the code

4. Summary of drawing process:

1. The itemView of RecyclerView may be measured for many times. If the width and height of RecyclerView is fixed or match_parent, then inonMeasure()In the stage, ItemView layout will not be measured in advance. If the width and height of RecyclerView is WRAP_content, the actual width and height of RecyclerView will be measured in advanceonMeasure()Stage traversal measurement of itemView layout to determine the width and height value of the content display area to determine the actual width and height of RecyclerView;

2. DispatchLayoutStep1 (), dispatchLayoutStep2(), dispatchLayoutStep3() these three methods will be executed. When the actual width and height of RecyclerView is uncertain, DispatchLayoutStep1 (), dispatchLayoutStep2(), and finally dispatchLayoutStep3() in onLayout(). If the itemView changes, dispatchLayoutStep2() will be executed again.

3. The measurement and layout of the itemView is actually in the dispatchLayoutStep2() method.

RecyclerView drawing three step flow chart:

2.2 LinearLayoutManager filling, measuring and layout processes

The drawing of RecyclerView goes through measure, layout and draw, but the real layout of itemView is entrusted to each LayoutManager. The LinearLayoutManager knows that dispatchLayoutStep2() is the actual layout view step, and the LayoutManager calls onLayoutChildren() to layout the itemView. It is the core method for drawing an itemView, which lists all related subviews from a given adapter.

1. OnLayoutChildren () layout itemView

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1) Check subclasses and other variables to find trace point coordinates and trace point positions
        // 2) Fill from the beginning, stack from the bottom
        // 3) Fill from the bottom, stack from the top
        // 4) Stack from the bottom to meet the demand
        // Create the layout state
        if(mPendingSavedState ! =null|| mPendingScrollPosition ! = RecyclerView.NO_POSITION) {if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);// Remove all child views
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;// Disallow collection
        // Draw the layout upside down
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();// Get the child currently in focus
        if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
            mAnchorInfo.reset();// Reset the anchor information
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            //1. Calculate the position and coordinates of the update point
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true; } · · · · · · ·// Calculate the direction of the first layout
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;

        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        detachAndScrapAttachedViews(recycler);// Temporarily detach the attached view, detach all child detach and recycle it through Scrap
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
      
        mLayoutState.mNoRecycleSpace = 0;
        //2. Start filling, stack from bottom;
        if (mAnchorInfo.mLayoutFromEnd) {
            Fill in the ItemView layout from the start position
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);// Populate all ItemViews
           
            Fill in the ItemView layout from the end position
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);// Populate all ItemViews
            endOffset = mLayoutState.mOffset;
        }else { //3. Bottom fill, stack from top to bottom;
            Fill in the ItemView layout from the end position
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
 
            Fill in the ItemView layout from the start position
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
        //4. Calculate the scroll offset and call the fill method to fill the new ItemView if necessary
         layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    }
Copy the code

First state judgment and some preparation work, and to trace point selection and update information, detachAndScrapAttachedViews (recycler) temporary will have additional view separation, cache Scrap, next time to fill out the reuse directly. Then calculate which direction to start the layout. The layout algorithm is as follows:

  • 1. Find an anchor point coordinate and the position of an anchor point item by examining child elements and other variables;
  • 2. Start filling and stack from the bottom.
  • 3. Bottom filling, stacked from top to bottom;
  • 4. Scroll to meet requirements, such as stack from the bottom.

2. Fill () starts filling the itemView

The filling layout is handed to the fill() method, which fills the given layout defined by layoutState. Why fill it twice? Let’s look at the fill() method:

  // Fill method, which returns the pixels to fill the itemView for subsequent scrolling
  int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
       recycleByLayoutState(recycler, layoutState);// Retrieve the view that slides out of the screen
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        // Core == while() loop ==
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// Loop until there is no data
            layoutChunkResult.resetInternal();
            // Populate the core method of itemViewlayoutChunk(recycler, state, layoutState, layoutChunkResult); · · · · · ·if (layoutChunkResult.mFinished) {// The layout ends, and the loop exits
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;// Based on the height offset of the added child} · · · · · ·return start - layoutState.mAvailable;// Returns the size of the region to be filled this time
    }
Copy the code

The fill() core is a while() loop that executes layoutChunk() to fill an itemView to the screen and returns the size of the filled area. So we’re going to start with how much remaining remainingSpace we have on the screen, and we’re going to subtract the amount of space that the sub-views are taking up from that, and we’re going to finish the sub-view layout when it’s less than 0, and if we haven’t already surpassed the remainingSpace, Call layoutChunk() to position the View.

3. LayoutChunk () creates, populates, measures, and lays out itemView

LayoutChunk () is the method to fill the layout of the itemView. There are mainly the following steps for creating, filling, measuring and laying out the itemView:

  • 1.layoutState.next(recycler)Get an itemView from the cache, if not, create an itemView;
  • 2. According to the actual situation to add itemView to RecyclerView, the final call or ViewGroupaddView()Methods;
  • 3.measureChildWithMargins()Measuring itemView size includes padding for the parent view, item decoration, and margins for the child view.
  • B: Left, top, right, bottomlayoutDecoratedWithMargins()Layout the given itemView in RecyclerView using coordinates.
   void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        //1. Get or create an itemView from the cache
        View view = layoutState.next(recycler);// Get the View to display for the current postion· · · · · ·// Add itemView to RecyclerView; // Add itemView to RecyclerView
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0); }}//3. Measuring the size of the child View includes the padding of the parent View, the item decoration, and the margins of the child View
        measureChildWithMargins(view, 0.0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        // Calculate the left, top, right, and bottom coordinates of an ItemView
        intleft, top, right, bottom; · · · · · ·//4. Use the coordinates in RecyclerView to create the given itemView
        // Calculate the correct layout position, subtract margin, calculate all view boundary box (including margin and decoration)
        layoutDecoratedWithMargins(view, left, top, right, bottom);// Call child.layout for layout
    }
Copy the code

Get an itemView from the cache by layoutstate.next (). If not, create a new itemView. Then addView() adds an itemView to RecyclerView as needed. The final call to the ViewGroup’s addView() method is followed by measureChildWithMargins(), which measures the size of the child views including the parent View’s padding, the item trim, and the margins of the child views. Finally getDecoratedMeasuredWidth () by computing the good left, top, right, bottom values in RecyclerView coordinates given itemView layout, note that there is the width of the total width of the item + decoration.

   View next(RecyclerView.Recycler recycler) {
            if(mScrapList ! =null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
Copy the code

Get the itemView and use the cached View if there is a cached View in mScrapList, or create the View if there is no mScrapList and add it to mScrapList. The getViewForPosition() method is a RecyclerView cache mechanism that will be explained in subsequent articles.

LinearLayoutManager filling, measuring, layout process summary:

onLayoutChildren()Represents a list of all related subviews from a given adapter, and the fill layout is handed infill()Method to populate the given layout defined by layoutState, executed through the while() looplayoutChunk()Fill an itemView to the screen as a way to finally fill the layout itemView,layoutState.next(recycler)Get or create an itemView from the cache, add an itemView to RecyclerView by addView(), and then call the ViewGroupaddView()Method,measureChildWithMargins()Measuring the itemView size includes the padding of the parent view, the item trim, and the margins of the child view, and finallylayoutDecoratedWithMargins()According to the calculated left, top, right, bottom through the use of coordinates in RecyclerView layout of the given itemView.

The flow chart is as follows:

Two, sliding principle

RecyclerView, as a list control, has its own sliding function, which is often used in the actual development. Its sliding principle is also what we need to master. It is called “know what it is, know what it is”. The sliding event processing of RecyclerView is still responded by onTouchEvent() touch event. The difference is that RecyclerView uses nested sliding mechanism, which will notify the sliding event to the parent View supporting nested sliding to make a decision first. This article may involve the knowledge of nested sliding in the process of introducing ordinary sliding (the next article will analyze nested sliding). Let’s take a look at the effect picture of ordinary sliding first:

2.1, onTouchEvent ()

RecyclerView event processing is still by onTouchEvent() touch event response, here to add a little knowledge of onTouchEvent(), familiar can be skipped.

  • Boolean onTouchEvent(MotionEvent event) : Implement this method to handle touch screen motion events, return true for event processing, false for event processing;
  • Motionevent. ACTION_DOWN: finger down, a down gesture has started, the action includes the initial starting position;
  • Motionevent.action_move: The movement of the finger, which changes when the gesture is pressed (between down and up), contains the most recent point, and any intermediate points since the last down or move event;
  • Motionevent. ACTION_UP: The finger leaves, a pressed gesture has been completed, and the action contains a final post position and any intermediate points since the last down or move event;
  • MotionEvent.ACTION_CANCEL: The gesture is cancelled, the current gesture is terminated, you don’t get any more coordinates, you can treat this as an up event, but don’t do anything you would normally do;
  • Motionevent. ACTION_POINTER_DOWN: When multiple fingers are pressed, a non-primary touch point is falling;
  • Motionevent. ACTION_POINTER_UP: Multiple fingers leave and one non-primary touch point rises;
  • Motionevent.action_outside: The finger touches outside the normal boundaries, and the movement occurs outside the normal range of UI elements. This does not provide a complete gesture, but only the initial position of the motion touch; Note that because the location of any event is outside the boundaries of the view hierarchy, it is not assigned to any child element of the ViewGroup by default;
  • Motionevent. ACTION_SCROLL: non-touch sliding, MotionEvent contains relative vertical/horizontal scroll offset, this action is not a touch event.

RecyclerView onTouchEvent();

  	@Override
    public boolean onTouchEvent(MotionEvent e) {
     	// Assigns slide events to OnItemTouchListener or provides interceptions for OnItemTouchListeners. True is returned if touch events are intercepted
     	if (findInterceptingOnItemTouchListener(e)) {
            cancelScroll();
            return true;
        }
        
     	// Determine the sliding direction according to the layout direction
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();// Can you support horizontal sliding
        final boolean canScrollVertically = mLayout.canScrollVertically();// Can you support vertical sliding
       	
       	// Get a new VelocityTracker object to observe the sliding speed
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(e);
		// Returns the operation being performed, without touchpoint index information. This is the event type, such as motionEvent.action_down
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();/ / the Action of the index
        // Copy the event information to create a new event
		final MotionEvent vtev = MotionEvent.obtain(e);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
        
        switch (action) {
            case MotionEvent.ACTION_DOWN: {// Press your finger down
             	mScrollPointerId = e.getPointerId(0);// Get the id of the first touchpoint associated with a specific touchpoint
             	// Record the X and Y coordinates of the Down event
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5 f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5 f);
                
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;// Indicates sliding along the horizontal axis
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;// Indicates sliding along the vertical axis
                }
                // Start a new nested scroll, if a collaborative parent View is found, and start the nested scroll
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;
            
  			case MotionEvent.ACTION_POINTER_DOWN: {// Multiple finger press
  				// Update mScrollPointerId to respond only to recently pressed gesture events
                mScrollPointerId = e.getPointerId(actionIndex);
                // Update the most recent gesture coordinates
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5 f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5 f);
               } break;
                
            case MotionEvent.ACTION_MOVE: {// Move your finger
            	// Get the touchpoint subscript according to mScrollPointerId
             	final int index = e.findPointerIndex(mScrollPointerId);
              
              	// Calculate the offset dx, dy from the x, y generated by the move event
                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) {// Is not touched to move state
                    boolean startScroll = false;
                    if (canScrollHorizontally) {// Horizontal sliding direction
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if(dx ! =0) {
                            startScroll = true; }}if (canScrollVertically) {// Vertical sliding direction
                        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); }}// Being touched moves the state, where the sliding is really handled
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;//mReusableIntPair The sliding distance consumed by the parent view
                    mReusableIntPair[1] = 0;
					//mScrollOffset indicates the scrolling position of RecyclerView
					// Assigns a step of the nested pre-slide operation to the currently nested slide parent. True indicates that the parent View takes precedence over the slide event.
					// if consumed, dx dx will subtract the distance consumed by the parent View
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];// Subtract the distance consumed by the parent View
                        dx -= mReusableIntPair[1];
                        // Update nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Sliding has started to prevent the parent View from being blocked
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
					// The final sliding effect
                    if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    // Prefetch a ViewHolder from the cache
                    if(mGapWorker ! =null&& (dx ! =0|| dy ! =0)) {
                        mGapWorker.postFromTraversal(this, dx, dy); }}}break;

            case MotionEvent.ACTION_POINTER_UP: {// Multiple fingers leave
            	// Select a new touch point to process the ending and reprocess the coordinates
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {// When the finger leaves, the sliding event ends
             	mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                // Calculate the sliding speed
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                // The last X/Y slip speed
                final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                 // Handle inertial sliding
                if(! ((xvel ! =0|| yvel ! =0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);// Set the sliding state
                }
                resetScroll();// Reset the slider
            } break;
            
            case MotionEvent.ACTION_CANCEL: {// Cancel the gesture to release various resources
                cancelScroll();// Exit the slide
            } break;
        }
        
        if(! eventAddedToVelocityTracker) { mVelocityTracker.addMovement(vtev); } vtev.recycle();// Recycle the sliding event for reuse. You can no longer touch the event by calling this method
        
        return true;// Return true to RecyclerView to process events
    }
Copy the code

ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL are the basic events of View. ACTION_POINTER_DOWN, The ACTION_POINTER_UP two events are related to multi-finger sliding.

There are three main things going on here,

  • A slide event is assigned to OnItemTouchListener or intercepted by OnItemTouchListeners. True is returned if intercepted, indicating that the event is consumed.
  • Second, initialize gesture coordinates, sliding direction, event information and other data;
  • OnItemTouchListeners or OnItemTouchListeners do not consume the current event process.

There are a lot of details, let’s break them down case by case:

1. The Down event

    case MotionEvent.ACTION_DOWN:{// Press your finger down
        mScrollPointerId = e.getPointerId(0);// Get the id of the first touchpoint associated with a specific touchpoint
        //1. Record the X and Y coordinates of the Down event
        mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5 f);
        mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5 f);

        if (canScrollHorizontally) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;// Indicates sliding along the horizontal axis
        }
        if (canScrollVertically) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;// Indicates sliding along the vertical axis
        }
        //2. Start a new nested scroll, if a collaborative parent View is found, and start the nested scroll
        startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
    } break;
Copy the code

The Down event first fetches the id of the first touchpoint. A Pointer is a touchpoint. Down is the start of a series of events.

  • 1. Record the X and Y coordinates of the Down event.
  • 2. CallstartNestedScroll()A new nested slide is launched, and if the nested parent View is found, the nested slide is launched, that is, the event is processed.

2. Move events

   case MotionEvent.ACTION_MOVE:{// Move your finger
   		// Get the touchpoint subscript according to mScrollPointerId
        final int index = e.findPointerIndex(mScrollPointerId);

        //1. Calculate offset dx, dy based on x, y generated by move event
        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) {// Is not touched to move state
            boolean startScroll = false;
            if (canScrollHorizontally) {// Horizontal sliding direction......}if (canScrollVertically) {// Vertical sliding direction......}// Set the sliding state, SCROLL_STATE_DRAGGING indicates sliding
            if (startScroll) setScrollState(SCROLL_STATE_DRAGGING);
        }
        // Being touched moves the state, where the sliding is really handled
        if (mScrollState == SCROLL_STATE_DRAGGING) {
            mReusableIntPair[0] = 0;//mReusableIntPair The sliding distance consumed by the parent view
            mReusableIntPair[1] = 0;
            //2. Assign a step of the nested pre-slide operation to the currently nested scroll parent. If true, the parent View takes precedence over the slide event.
            // if it consumes, dx dy will subtract the distance consumed by the parent View, mScrollOffset indicates the scrolling position of RecyclerView
            if (dispatchNestedPreScroll(
                    canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
                    mReusableIntPair, mScrollOffset, TYPE_TOUCH
            )) {
                dx -= mReusableIntPair[0];// Subtract the distance consumed by the parent View
                dy -= mReusableIntPair[1];
                // Update nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
                // Start sliding to prevent the parent View from being blocked
                getParent().requestDisallowInterceptTouchEvent(true);
            }

            mLastTouchX = x - mScrollOffset[0];
            mLastTouchY = y - mScrollOffset[1];
            //3. The final implementation of the scrolling effect
            if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            //4. Prefetch a ViewHolder from cache
            if(mGapWorker ! =null&& (dx ! =0|| dy ! =0)) {
                mGapWorker.postFromTraversal(this, dx, dy); }}}break;
Copy the code

Move event is the core of the sliding event processing, the code is relatively long, but the structure is simple, mainly divided into the following steps:

  • 1. Calculate offset dx and dy according to x and y generated by move event;
  • 2.dispatchNestedPreScroll()Dispatch a step that asks the parent View if it needs to handle the sliding event first. If so, dx and dy will subtract the distance consumed by the parent View.
  • 3. Determine the sliding direction and callscrollByInternal()Finally achieve the scrolling effect;
  • 4. CallmGapWorker.postFromTraversal()Prefetch a ViewHolder from the RecyclerView cache.

ScrollByInternal () is the final implementation of sliding effect, which will be analyzed in detail later. GapWorker prefetchviewholder by adding Runnable to RecyclerView task queue. Final call RecyclerView. Recycler tryGetViewHolderForPositionByDeadline get ViewHolder (), it is the core of the whole RecyclerView recycling reuse caching mechanism method. Here is not a detailed analysis, RecyclerView recycle cache mechanism detailed explanation “hope to provide you with help.

3. Up event

   case MotionEvent.ACTION_UP: {// When the finger leaves, the sliding event ends
        mVelocityTracker.addMovement(vtev);
        eventAddedToVelocityTracker = true;
        //1. Calculate the current sliding speed according to the past points
        mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
        // The last X/Y slip speed
        final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
        final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
        // Handle inertial sliding
        if(! ((xvel ! =0|| yvel ! =0) && fling((int) xvel, (int) yvel))) {
            setScrollState(SCROLL_STATE_IDLE);// Set the sliding state
        }
        resetScroll();//2. Reset slider
    } break;
Copy the code

The Up event ends when the finger leaves. Two main things were done:

  • 1. BycomputeCurrentVelocity()Compute the sliding speed and compute the final sliding speed of the X and Y axes,fling()It deals with inertial sliding;
  • 2. Set the sliding state and reset the sliding information after the inertia sliding.

First, computeCurrentVelocity() is used to calculate the sliding speed and the final sliding speed of X and Y axes. If the final sliding speed is greater than the given value of the system when lifting, the inertia is maintained and then the sliding distance is maintained. Finally, the nested sliding View is notified to finish sliding and the data is reset. Fling () is the core method for handling inertial slides, as discussed below.

4. Cancel the event

    case MotionEvent.ACTION_CANCEL:{// Cancel the gesture to release various resources
        cancelScroll();// Exit the slide
    } break;
    
 	private void cancelScroll(a) {
 		//1. Reset slider, whether resource
        resetScroll();
        //2. Set the sliding state to no sliding state
        setScrollState(SCROLL_STATE_IDLE);
    }
Copy the code

The Cancel event indicates that the gesture event has been cancelled, resetting the sliding status and other information. Two main things were done:

  • 1.resetScroll()Stop nesting slides in progress, freeing resources
  • 2. Set the sliding status toSCROLL_STATE_IDLEThere’s no sliding.

When the event is consumed by the parent View, it will respond to the Cancel event. For example, when RecyclerView receives the Down event, but is subsequently intercepted by the parent View, the RecyclerView will respond to the Cancel event.

5. Pointer_Down events

    case MotionEvent.ACTION_POINTER_DOWN:{// Multiple finger press
        // Update mScrollPointerId to respond only to recently pressed gesture events
        mScrollPointerId = e.getPointerId(actionIndex);
        // Update the most recent gesture coordinates
        mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5 f);
        mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5 f);
    } break;
Copy the code

The Pointer_Down event mainly updates the mScrollPointerId and the pressed coordinates immediately when multiple fingers are pressed. Respond to the new gesture, no longer respond to the old gesture, all events and coordinates to the new event and coordinates prevail.

Note: the RecyclerView does not respond to multiple finger swiping, but RecyclerView does not respond to the old finger gesture, but responds to the latest finger gesture when the old finger is not released and another new finger is pressed.

6. Pointer_Up events

    case MotionEvent.ACTION_POINTER_UP:{// Multiple fingers leave
        // Select the latest coordinate point to process the outcome, reprocess the coordinates
        onPointerUp(e);
    } break;
    
   private void onPointerUp(MotionEvent e) {
        final int actionIndex = e.getActionIndex();
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            final int newIndex = actionIndex == 0 ? 1 : 0;
            mScrollPointerId = e.getPointerId(newIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5 f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5 f); }}Copy the code

The Pointer_Up event selects the latest pointer to handle the outcome when multiple fingers leave. OnPointerUp () determines whether the leaving event coordinate ID is consistent with the current sliding coordinate ID, if so, the gesture coordinate and the current coordinate point ID will be updated.

2.2. Sliding process

RecyclerView’s onTouchEvent() method is used to change the event type of the RecyclerView. The last article combined with the source code of LinearLayoutManager analyzed the drawing process of RecyclerView, here also take the vertical direction of LinearLayoutManager as an example to analyze the vertical direction of RecyclerView sliding, sliding in other ways are all changes. Get the slide direction when you start responding to onTouchEvent() :

	// Determine the sliding direction according to the layout direction
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();// Can you support horizontal sliding
    final boolean canScrollVertically = mLayout.canScrollVertically();// Can you support vertical sliding
Copy the code

Above is obtained through LinearLayoutManager whether can slide in horizontal and vertical direction, here the callback mLayout. CanScrollHorizontally () and mLayout canScrollVertically () method, So if canScrollHorizontally = true, I can swipe left and right, and if canScrollHorizontally = true, I can swipe up and down.

    @Override
    public boolean canScrollHorizontally(a) {
        return mOrientation == HORIZONTAL;// The linear direction is horizontal, can slide horizontally
    }

    @Override
    public boolean canScrollVertically(a) {
        return mOrientation == VERTICAL;// The linear direction is vertical direction, can slide vertically
    }
Copy the code

1. Plain sliding

As the Move event analysis above knows, calculate the distance of the slide in ACTION_MOVE and then call scrollByInternal() to handle the itemView sliding with the gesture. The core method is scrollByInternal() :

   boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0; int unconsumedY = 0;
        int consumedX = 0; int consumedY = 0;
		//1. Use deferred changes to avoid crashes that may be caused by adapter changes during sliding
        consumePendingUpdateOperations();
        if(mAdapter ! =null) {
        	//2. Slide step
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
   		//3. Assign a step of the nested pre-slide operation to the currently nested slide parent View. If true, the parent View takes precedence over the slide event.
        // if consumed, dx dy subtracts the distance consumed by the parent View
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        // Update the last touch coordinates taking into account the slip offset
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
		//4. Slide callback
        if(consumedX ! =0|| consumedY ! =0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
    
        // Whether there is sliding consumption
        returnconsumedNestedScroll || consumedX ! =0|| consumedY ! =0;
    }
Copy the code

The above code does four main things:

  • 1.consumePendingUpdateOperations()Delay is used to avoid crashes that may be caused by adapter changes during a slide, because the slide assumes that no data has changed when in fact the data has changed;
  • 2.scrollStep()The core slide step is handed over to the layout manager to handle its own slide;
  • 3. The nested sliding mechanism is still used to notify the parent View to preferentially handle sliding events after its own sliding is completed;
  • 4.dispatchOnScrolled()Notify the slide callback listener of RecyclerView.

ScrollStep () is handled its sliding method, through the dx, dy slide RecyclerView, horizontal slip. Call the mLayout scrollHorizontallyBy (), vertical sliding. Call the mLayout scrollVerticallyBy ();

   void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        if(dx ! =0) {
        	// Slide the dx pixel horizontally in the screen coordinates and return the distance moved, default is not moved, return 0
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if(dy ! =0) {
        	// Slide the dy pixel vertically in the screen coordinates and return the distance moved, default is not moved, return 0consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); }}Copy the code

The final sliding distance is handled by LayoutManager in the sliding function. Here’s the vertical sliding scrollVerticallyBy() :

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {// If it is horizontal, the vertical sliding distance is 0, that is, no sliding
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }
Copy the code

ScrollVerticallyBy () is a vertical slide, if the linear direction is HORIZONTAL, the slide distance is 0, that is, no slide, otherwise scrollBy() is called to achieve a vertical slide:

  int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
          if (getChildCount() == 0 || delta == 0) {// Do not slide if there is no data or the slide distance is 0
            return 0;
        }
        1. Update the layout status
        updateLayoutState(layoutDirection, absDelta, true, state);
        //2. First call fill() to bring in the view that has been slid in, and reclaim the view that has been slid out to return the space of the current layout view
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
    	// Calculate the sliding distance
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        //3. Add offsets to all child views and move the View position according to the calculated sliding distance
        mOrientationHelper.offsetChildren(-scrolled);/ / move
        mLayoutState.mLastScrollDelta = scrolled;// Record the sliding distance
        return scrolled;
    }
Copy the code

These two methods do three main things,

  • 1. ByupdateLayoutState()[Fixed] some states, such as where the stroke point is, whether there is animation, etc.
  • 2. Byfill()First check which views are out of bounds, recycle them, then refill the new view and return the offset of the fill;
  • Through 3.offsetChildren()Add offsets to all child views and move the View’s position according to the sliding distance.

The logic for scrollBy() to handle sliding is to update the state of the layout, then call fill() to return the fill distance, and if there is a slide distance, the View layout is put in, and if a View is completely removed from the screen, it is recycled into the cache. OffsetChildren () is called to offset all child Views.

Note: The sliding event does not re-request the layout, it does not re-request onLayoutChildren(), updates to the layout are either re-retrieved from the cache by fill() or by creating an itemView to populate the screen.

OrientationHelper () : offsetChildren() : offsetChildren() : offsetChildren() : offsetChildren() : offsetChildren() : offsetChildren() :

	// Move all child views by the given distance
	public abstract void offsetChildren(int amount);
Copy the code

In LinerLayoutManager source code implementation of OrientationHelper class and implementation of abstract methods:

  public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            public void offsetChildren(int amount) {
                mLayoutManager.offsetChildrenVertical(amount);// Offset all child views vertically to append to RecyclerView}}; }Copy the code

OffsetChildrenVertical (int dx) offsetChildrenVertical(int dx) offsetChildrenVertical(int dx) offsetChildrenVertical(int dx)

   public void offsetChildrenVertical(@Px int dy) {
      if(mRecyclerView ! =null) { mRecyclerView.offsetChildrenVertical(dy); }}Copy the code

RecyclerView offsetChildrenVertical() :

	public void offsetChildrenVertical(@Px int dy) {
		// Get the number of RecyclerView itemViews
        final int childCount = mChildHelper.getChildCount();
        // Swipe through all itemViews by calling View offsetTopAndBottom()
        for (int i = 0; i < childCount; i++) {
            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);// How many pixels to move the view}}Copy the code

Finally, I found the source code of the core moving sub-view: iterating through all itemViews, and finally sliding through each sub-view by calling the offsetTopAndBottom() or offsetLeftAndRight() method of the underlying View. Get the total number of ItemViews, and then move each itemView by the specified distance dy by traversing.

General sliding summary: In RecyclerView Move touch event dispatch sliding event responsescrollByInternal()Method that handles nested sliding of the parent View and actually calls the LayoutManagerscrollHorizontallyBy()orscrollVerticallyBy()Method to calculatescrollBy()In thefill()Filling the layout handles the actual sliding distance, iterating through all itemViews, and finally calling the underlying View’s through each child ViewoffsetTopAndBottom()oroffsetLeftAndRight()Method to achieve sliding.

2. Inertial sliding

We slide the list quickly and then release our finger. The list continues to slide inertia for a while, RecyclerView inertia slide (), while onTouchEvent() processes ACTION_UP events:

 case MotionEvent.ACTION_UP: {// When the finger leaves, the sliding event ends
        mVelocityTracker.addMovement(vtev);
        //1. Calculate the current sliding speed according to the past points
        mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
        // The last X/Y slip speed
        final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
        final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
        // Handle inertial sliding
        if(! ((xvel ! =0|| yvel ! =0) && fling((int) xvel, (int) yvel))) {
            setScrollState(SCROLL_STATE_IDLE);// Set the sliding state
        }
        resetScroll();//2. Reset slider
    } break;
Copy the code

First, computeCurrentVelocity() is used to calculate the sliding speed and the final sliding speed of X and Y axes. If the final speed is greater than the given value of the system when lifting, the inertia will be maintained and the sliding distance will be maintained. Finally, the nested sliding View will be notified that the sliding has ended and the sliding information will be reset. Inertia sliding core method Fling () :

    public boolean fling(int velocityX, int velocityY) {
    	//1. Can you slide horizontally and vertically
        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
        final boolean canScrollVertical = mLayout.canScrollVertically();

        if(! canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX =0;// If you cannot slide horizontally, or if the slide speed is less than the system's slide speed, the horizontal slide speed is set to 0
        }
        if(! canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { velocityY =0;// If you cannot slide vertically, or if the slide speed is less than the system slide speed, the vertical slide speed is set to 0
        }
        
        // No sliding speed, returns false, does not handle inertial sliding
        if (velocityX == 0 && velocityY == 0) return false;

		// Whether the parent View handles nested pre-inertial sliding
        if(! dispatchNestedPreFling(velocityX, velocityY)) {final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);
			//2. The client handles inertial sliding as required by the developer
            if(mOnFlingListener ! =null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {// If you can slide
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);// Start nesting slides
                //3.RecyclerView process its own inertia sliding
                mViewFlinger.fling(velocityX, velocityY);
                return true; }}return false;
    }
Copy the code

Fling () does three things:

  • 1. According to the sliding direction and the sliding speed compared with the system speed, judge whether it can slip inertia;
  • 2. The client handles inertia sliding and passes as required by the developerOnFlingListener.onFling()Method To judge whether RecyclerView is good to deal with the inertia motion required by developers, and to decide whether it can deal with inertia sliding;
  • 3.RecyclerView itself to handle inertia sliding, call ViewFlingerfling()Methods.

RecyclerView is a Runnable class inside of RecyclerView. Then The Fling () of ViewFlinger is RecyclerView.

    public void fling(int velocityX, int velocityY) {
    	setScrollState(SCROLL_STATE_SETTLING);// Set the scroll state to inertial slide
        mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
        // Start sliding based on a swinging gesture, the distance covered depends on the initial speed.
        mOverScroller.fling(0.0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        // Make Runnable run on the next animation step, Runnable will run on the user interface thread.
        postOnAnimation();
    }
Copy the code

Moverscroll.fling () just calculates the parameters for inertial sliding, and finally calls postOnAnimation(), which finally calls back to ViewFlinger’s run method:

    @Override
    public void run(a) {· · · · · ·final OverScroller scroller = mOverScroller;
        //1. Update the sliding position information to determine whether the current sliding is complete. True indicates that the sliding is not complete
        if (scroller.computeScrollOffset()) {
            final int x = scroller.getCurrX();
            final int y = scroller.getCurrY();
            // Calculate the rolling distance
            int unconsumedX = x - mLastFlingX;
            int unconsumedY = y - mLastFlingY;
            mLastFlingX = x;
            mLastFlingY = y;
            int consumedX = 0;
            int consumedY = 0; · · · · · ·if(mAdapter ! =null) {// Local slide
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                //2. Slide step by dX, dY slide RecyclerView
                scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
                consumedX = mReusableIntPair[0];
                consumedY = mReusableIntPair[1];
                unconsumedX -= consumedX;
                unconsumedY -= consumedY;
            }

            // slide after nesting
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            // Whether the parent View handles nested sliding events
            dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                    TYPE_NON_TOUCH, mReusableIntPair);
            unconsumedX -= mReusableIntPair[0];
            unconsumedY -= mReusableIntPair[1];
			
            boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX();
            boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY();
			// Whether the slide is complete (the slide ends or the x,y distance is complete or no further slide is possible)
            final booleandoneScrolling = scroller.isFinished() || ((scrollerFinishedX || unconsumedX ! =0) && (scrollerFinishedY || unconsumedY ! =0));

			//4. End of slide
            if(! smoothScrollerPending && doneScrolling) {if(getOverScrollMode() ! = View.OVER_SCROLL_NEVER) {final int vel = (int) scroller.getCurrVelocity();
                    int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0;
                    int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0;
                    absorbGlows(velX, velY);
                }

                if(ALLOW_THREAD_GAP_WORK) { mPrefetchRegistry.clearPrefetchPositions(); }}else {
                //3. Otherwise continue to slide (recursively execute the run method until the slide ends)
                postOnAnimation();
                // Prefetch ViewHolder(cache fetch or create)
                if(mGapWorker ! =null) {
                    mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY); }}}// re-slide
        if (mReSchedulePostAnimationCallback) {
            internalPostOnAnimation();
        } else {// Set the slide end statesetScrollState(SCROLL_STATE_IDLE); stopNestedScroll(TYPE_NON_TOUCH); }}Copy the code

There are three main things it does:

  • 1. Update the sliding position information to determine whether the current sliding is complete. True indicates that the sliding is not complete;
  • 2. Calculate the relevant information of sliding distance and call backscrollStep()By dX, dY sliding RecyclerView;
  • 3. If the sliding is not complete, perform the operationpostOnAnimation()Recursive run method until the slide ends;
  • 4. After the slide ends, clear the data and set the slide end state.

Inertial sliding summary: RecyclerView responseonTouchEvent()According to the final sliding speed, it can judge whether there is inertia sliding. If there is inertia sliding, it will passfling()First deal with the developers required to deal with inertia sliding, otherwise directly RecyclerView itself to deal with inertia sliding, in ViewFlingerfling()Calculate the slide related coordinate data information, and then inpostOnAnimation()In the callbackrun()Handle slides, which are also calledscrollStep()Complete the slide, and recursively execute if the slide does not endpostOnAnimation()Methods the callbackrun()Straight slide done.

2.3. Summary of sliding principle

RecyclerView sliding event processing is still through onTouchEvent() touch event response, calculation update touch coordinates and sliding direction and other related information, processing parent View nested sliding, sliding event response scrollByInternal() method, Actually call LayoutManager’s scrollHorizontallyBy() or scrollVerticallyBy() method to calculate the actual sliding distance that the fill() fills the layout while processing in scrollBy(), Finally, RecyclerView iterates through all itemViews, and finally through each child View calls the bottom View offsetTopAndBottom() or offsetLeftAndRight() method to achieve sliding.

The sliding flow chart of RecyclerView is as follows (double-click to open higher clarity) :

Pay attention and don’t get lost


Well everyone, that’s all for this article. Thank you very much for reading it. I am Suming, thank you for your support and recognition, your praise is the biggest motivation for my creation. Landscape has to meet, we will see you in the next article!

If there are any mistakes in this blog, please comment, thank you very much!

I hope we can be friends inGithub,The Denver nuggetsShare knowledge together and encourage each other! Keep Moving!