RecyclerView, Android self study materials

Here, we find that the internal processing of RecyclerView is different for different situations. Therefore, in order to solve practical problems, it is essential to look at the source code. Next, we will follow the source code together. Let's take a look at the specific scrolling implementation of RecyclerView( I need to remind you that I use LinearLayoutManager here. This article is based on LinearLayoutManager for analysis)

 public void smoothScrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.smoothScrollToPosition(this, mState, position);
    } 

When mRecycler.smoothScrollToPosition() method is used, the smoothScrollToPosition method of LayoutManager is called internally. smoothScrollToPosition in LayoutManager is not implemented. The specific implementation is in its subclass. Here we use LinearLayoutManager, so let's see how it is implemented internally.

 @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
        scroller.setTargetPosition(position);//Set target position
        startSmoothScroll(scroller);
    } 

Here, we can see that the RecyclerView slides due to linearsmoothcoller, and the parent class of linearsmoothcoller is RecyclerView.SmoothScroller. I'm sure everyone will feel a little familiar, because when we move the content in the control, we will all use a class, Scroller. Here, RecyclerView also defines a sliding Scroller. It must be related to sliding its internal view.

 public void startSmoothScroll(SmoothScroller smoothScroller) {
            if (mSmoothScroller != null && smoothScroller != mSmoothScroller
                    && mSmoothScroller.isRunning()) {
                mSmoothScroller.stop();
            }
            mSmoothScroller = smoothScroller;
            mSmoothScroller.start(mRecyclerView, this);
        } 

Continue to walk startSmoothScroll, the method of internal judgment if the calculation of coordinates is stopped, then call start() method to start calculating the coordinates. Then start looking at the start() method.

 void start(RecyclerView recyclerView, LayoutManager layoutManager) {
            mRecyclerView = recyclerView;
            mLayoutManager = layoutManager;
            if (mTargetPosition == RecyclerView.NO_POSITION) {
                throw new IllegalArgumentException("Invalid target position");
            }
            mRecyclerView.mState.mTargetPosition = mTargetPosition;
            mRunning = true;//Setting the current scroller has started to execute
            mPendingInitialRun = true;
            mTargetView = findViewByPosition(getTargetPosition());//Find the corresponding View according to the target location,
            onStart();
            mRecyclerView.mViewFlinger.postOnAnimation();
        } 

In the start method, the execution status of the current scroller will be identified, and the corresponding target view will be found according to the scrolling position. Here we need to highlight the findViewByPosition() method. This method will query whether there is a view corresponding to the target location within the visible range of the Recycler. For example, if the visible range of the RecyclerView is 1-9 and the target location is 10, then mTargetView =null. If the visible range is 9-20 and the target location is 1, then mTargetView =null.

Finally, the postOnAnimation() method of the internal class ViewFlinger of RecyclerView is called.

 class ViewFlinger implements Runnable {
	   ....Omit some codes
     void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }
   } 

Here we find that ViewFlinger is actually a Runnable, which is sent out inside postOnAnimation(). Let's just focus on the run() method of ViewFlinger.

 @Override
        public void run() {
		           ...Omit some codes
            final OverScroller scroller = mScroller;
            //Get SmoothScroller in layoutManger
            final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
            if (scroller.computeScrollOffset()) {//If it is the first time to go, it will return false
	               ...Omit some codes
             }
            if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            }
              ...Omit some codes
        } 

The internal implementation of ViewFlinger's run() method is complex. When the method is executed for the first time, it will execute if (scroller.computeScrollOffset()), where scroller is a reference to the attribute mscoller in ViewFlinger, where mscoller will be initialized by default when ViewFlinger creates an object. In the first judgment, the if statement block will not be entered because the calculation has not been started. Then, the following statement will be directly followed:

 if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            } 

Finally, I found that I just walked an onAnimation (0, 0) and continued to walk the method.

 private void onAnimation(int dx, int dy) {
            final RecyclerView recyclerView = mRecyclerView;
            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
                stop();
            }
            mPendingInitialRun = false;
            if (mTargetView != null) {//Judge whether the target view exists. If so, calculate the distance to move to the position
                if (getChildPosition(mTargetView) == mTargetPosition) {
                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                    mRecyclingAction.runIfNecessary(recyclerView);
                    stop();
                } else {
                    Log.e(TAG, "Passed over target position while smooth scrolling.");
                    mTargetView = null;
                }
            }
            if (mRunning) {//If it doesn't exist, keep looking
                onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
                boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
                mRecyclingAction.runIfNecessary(recyclerView);
                if (hadJumpTarget) {
                    // It is not stopped so needs to be restarted
                    if (mRunning) {
                        mPendingInitialRun = true;
                        recyclerView.mViewFlinger.postOnAnimation();
                    } else {
                        stop(); // done
                    }
                }
            }
        } 

In the onAnimation method, we judge whether the target view is empty. You should remember our search for the target view above. If the current position is not within the visible range, then mTargetView =null, the corresponding judgment statement will not be returned. Continue viewing onSeekTargetStep().

 protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
        if (getChildCount() == 0) {
            stop();
            return;
        }
        //noinspection PointlessBooleanExpression
        if (DEBUG && mTargetVector != null
                && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
            throw new IllegalStateException("Scroll happened in the opposite direction"
                    + " of the target. Some calculations are wrong");
        }
        mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
        mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);

        if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
            updateActionForInterimTarget(action);
        } // everything is valid, keep going

    } 

Through the code directly, we find that we don't understand what the modified function should do. Here, we only know that when the scrolling occurs for the first time, mminterimtargetdx = 0 and mminterimtargetdy = 0, we will use the updateActionForInterimTarget() method.

 protected void updateActionForInterimTarget(Action action) {
        // find an interim target position
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
		...Omit some codes
        normalize(scrollVector);
        mTargetVector = scrollVector;

        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
        
		//Calculate the time to scroll, default scroll distance, TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
		  
		//In order to avoid pausing during scrolling, we will track the callback distance in onSeekTargetStep. In fact, we will not scroll beyond the actual distance
        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                //The time saved here is a little longer than it actually takes.
                (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    } 

Translation according to official documents: when the view corresponding to the target scrolling position is not within the visible range of RecyclerView, this method calculates the direction vector towards the view and triggers smooth scrolling. The default scrolling distance is 12000 (unit: px) (that is, in order to scroll to the target position, the Recycler will scroll up to 12000 pixels).

Since this method calculates the time, let's take a look at the calculateTimeForScrolling() method. Through the method name, we should know that this method is to calculate the time that a given distance needs to roll at the default speed.

 protected int calculateTimeForScrolling(int dx) {
	    //The time is rounded here. 
        return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
    } 

Including MILLISECONDS_PER_PX will be created when LinearSmoothScroller is initialized.

 public LinearSmoothScroller(Context context) {
      MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
    } 

View the calculateSpeedPerPixel() method

 private static final float MILLISECONDS_PER_INCH = 25f;// By default, it takes 25ms to move an inch
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
    } 

In other words, the current scrolling speed is related to the pixel density of the screen. By obtaining the pixel density per inch of the current mobile phone screen and the time required to move per inch, the time required to move a pixel density can be calculated by dividing the time required to move per inch by the pixel density. OK, now that we have calculated the time required to move a pixel density, we can directly multiply it by the pixel to calculate the time required to move the pixel.

Now that we have calculated the time, we only need to care about what the update() method of Action does,

 //Save information about SmoothScroller sliding distance
        public static class Action {
		       ...Omit code
	         public void update(int dx, int dy, int duration, Interpolator interpolator) {
                mDx = dx;
                mDy = dy;
                mDuration = duration;
                mInterpolator = interpolator;
                mChanged = true;
            }
	     } 

Here, we find that Action is just a class that stores sliding information about SmoothScroller. Initially, it saves the horizontal and vertical sliding distance (12000px), sliding time and interpolator. At the same time, record the status of the current data change.

Now that we have finished the onSeekTargetStep method of Action, let's continue to look at the runIfNecessary() method of Action.

 void runIfNecessary(RecyclerView recyclerView) {
		         ....Omit code
                if (mChanged) {
                    validate();
                    if (mInterpolator == null) {
                        if (mDuration == UNDEFINED_DURATION) {
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
                        } else {
	                        //The mDx,mDy,mDuration. Passed in here are the update() method before the Action. Saved information
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
                        }
                    } else {
                        recyclerView.mViewFlinger.smoothScrollBy(
                                mDx, mDy, mDuration, mInterpolator);
                    }
              mChanged = false;
                ....Omit code
            } 

TNND, calling around, and finally passing the information stored in the Action to the smoothScrollBy() method of ViewFlinger. Note here: once this method is called, mChanged will be set to false. The next time you enter this method, the sliding method of ViewFlinger will not be called.

 public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
			 //Determine whether it is the same interpolator. If not, recreate the mscoller
            if (mInterpolator != interpolator) {
                mInterpolator = interpolator;
                mScroller = new OverScroller(getContext(), interpolator);
            }
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            mScroller.startScroll(0, 0, dx, dy, duration);
            if (Build.VERSION.SDK_INT < 23) {
                mScroller.computeScrollOffset();
            }
            postOnAnimation();
        } 

Here, after mScroller receives the sliding information passed in by Acttion and starts sliding. Finally, postOnAnimation() will be called and the run() method of ViewFiinger will be sent out. Finally, we return to the run() method of ViewFiinger.

 public void run() {
         ...Omit some codes
   if (scroller.computeScrollOffset()) {
                final int[] scrollConsumed = mScrollConsumed;
                final int x = scroller.getCurrX();
                final int y = scroller.getCurrY();
                int dx = x - mLastFlingX;
                int dy = y - mLastFlingY;
                int hresult = 0;
                int vresult = 0;
                mLastFlingX = x;
                mLastFlingY = y;
                int overscrollX = 0, overscrollY = 0;
		        ...Omit some codes
                if (mAdapter != null) {
                    startInterceptRequestLayout();
                    onEnterLayoutOrScroll();
                    TraceCompat.beginSection(TRACE_SCROLL_TAG);
                    fillRemainingScrollValues(mState);
                    if (dx != 0) {//If the lateral direction is greater than 0, start scrolling RecyclerView
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {//If the vertical direction is greater than 0, start scrolling RecyclerView to obtain the current scrolling distance
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }
                    TraceCompat.endSection();
                    repositionShadowingViews();

                    onExitLayoutOrScroll();
                    stopInterceptRequestLayout(false);
                    if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
                            && smoothScroller.isRunning()) {
                        final int adapterSize = mState.getItemCount();
                        if (adapterSize == 0) {
                            smoothScroller.stop();
                        } else if (smoothScroller.getTargetPosition() >= adapterSize) {
                            smoothScroller.setTargetPosition(adapterSize - 1);
                            //Pass in the scrolling distance dx dy of the current recelerview
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        } else {
                            //Pass in the scrolling distance dx dy of the current recelerview
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        }
                    }
                }
                enableRunOnAnimationRequests();
             } 

Here, the scroller (get the sliding distance information passed in from the previous Action) has started sliding, so if (scroller.computeScrollOffset()) condition is true, then the scroller gets the value of the current vertical direction and starts scrolling RecyclerView, that is, the code mLayout.scrollVerticallyBy(dy, mRecycler, mState); Then let smoothScroller execute the onAnimation() method. The parameter passed in is the scrolling distance of RecyclerView. Now let's continue to look at the onAnimation method.

 private void onAnimation(int dx, int dy) {
            final RecyclerView recyclerView = mRecyclerView;
            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
                stop();
            }
            mPendingInitialRun = false;
            if (mTargetView != null) {
                // verify target position
                if (getChildPosition(mTargetView) == mTargetPosition) {
                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                    mRecyclingAction.runIfNecessary(recyclerView);
                    stop();
                } else {
                    Log.e(TAG, "Passed over target position while smooth scrolling.");
                    mTargetView = null;
                }
            }
            if (mRunning) {//Get the distance that the current Recycler needs to scroll
                onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
                boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
                mRecyclingAction.runIfNecessary(recyclerView);
                if (hadJumpTarget) {
                    // It is not stopped so needs to be restarted
                    if (mRunning) {
                        mPendingInitialRun = true;
                        recyclerView.mViewFlinger.postOnAnimation();
                    } else {
 

Tags: Android Design Pattern

Posted on Thu, 02 Sep 2021 19:35:00 -0400 by spetstnelis