RecyclerView.smoothScrollToPosition瞭解一下

小狗狗.jpg

前言

最近開發中遇到了一個需求,須要RecyclerView滾動到指定位置後置頂顯示,當時遇到這個問題的時候,內心第一反應是直接使用RecyclerView的smoothScrollToPosition()方法,實現對應位置的平滑滾動。可是在實際使用中發現並無到底本身想要的效果。本想着偷懶直接從網上Copy下,可是發現效果並非很好。因而就本身去研究源碼。bash

該系列文章分爲兩篇文章。app

什麼是可見範圍?

在瞭解RecyclerView的smoothScrollToPosition方法以前,有個知識點,我以爲有必要給你們說一下,由於使用smoothScrollToPosition中遇到的問題都與可見範圍有關。less

可見範圍.png

這裏所說的可見範圍是,RecyclerView第一個可見item的位置與最後一個可見item的位置之間的範圍。ide

1、實際使用中碰見的問題

若是當前滾動位置在可見範圍內,是不會發生滾動的

不會滾動.gif

當前RecyclerView的可見範圍爲0到9,當咱們想要滾動到1位置時,發現當前RecyclerView並無發生滾動。函數

2、若是當前滾動位置在可見範圍以後,會滾動到底部

滾動到底部.gif

當前RecyclerView的可見範圍爲0到9,當咱們想要滾動到10位置時,發現RecyclerView滾動了,且當前位置對應的視圖在RecyclreView的底部。佈局

3、若是當前滾動位置在可見範圍以前,會滾動到頂部

滾動到頂部.gif

這裏咱們滾動RecyclerView,使其可見範圍爲10到19,當咱們分別滾動到一、3位置時,RecyclerView滾動了。且當前位置對應的視圖在RecyclerView的頂部。post

2、RecyclerView smoothScrollToPosition源碼解析

到了這裏咱們發現對於不一樣狀況,RecyclerView內部處理是不同的,因此爲了解決實際問題,看源碼是必不可少的,接下來咱們就一塊兒跟着源碼走一遍。來看看RecyclerView具體的滾動實現。(這裏須要提醒你們的是這裏我採用的是LinearLayoutManager,本文章都是基於LinearLayoutManager進行分析的)ui

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);
    }
複製代碼

mRecycler.smoothScrollToPosition()方法時,內部調用了LayoutManager的smoothScrollToPosition方法,LayoutManager中smoothScrollToPosition沒有實現,具體實如今其子類中,這裏咱們使用的是LinearLayoutManager,因此咱們來看看內部是怎麼實現的。this

@Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
        scroller.setTargetPosition(position);//設定目標位置
        startSmoothScroll(scroller);
    }

複製代碼

這裏咱們能夠看到,這裏致使RecyclerView滑動的是LinearSmoothScroller,而LinearSmoothScroller的父類是RecyclerView.SmoothScroller,看到這裏我相信你們都會感到一絲熟悉,由於咱們在對控件內內容進行移動的時候,咱們都會使用到一個類,那就是Scroller。這裏RecyclerView也自定了一個滑動Scroller。確定是與滑動其內部視圖相關的。spa

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

繼續走startSmoothScroll,方法內部判斷了若是正在計算座標值就中止,而後調用start()方法從新開始計算座標值。接着開始看start()方法。

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;//設置當前scroller已經開始執行
            mPendingInitialRun = true;
            mTargetView = findViewByPosition(getTargetPosition());//根據目標位置查找相應View,
            onStart();
            mRecyclerView.mViewFlinger.postOnAnimation();
        }
複製代碼

在start方法中,會標識當前scroller的執行狀態,同時會根據滾動的位置去尋找對應的目標視圖。這裏須要着重提示一下,findViewByPosition()這個方法,該方法會在Recycler的可見範圍內去查詢是否有目標位置對應的視圖,例如,如今RecyclerView的可見範圍爲1-9,目標位置爲10,那麼mTargetView =null,若是可見範圍爲9-20,目標位置爲1,那麼mTargetView =null。

最終調用RecyclerView的內部類 ViewFlinger的postOnAnimation()方法。

class ViewFlinger implements Runnable {
	   ....省略部分代碼
     void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }
   }
複製代碼

這裏咱們發現,ViewFlinger其實一個Runnable,在postOnAnimation()內部又將該Runnable發送出去了。那下面咱們只用關心ViewFlinger的run()方法就好了。

@Override
        public void run() {
		           ...省略部分代碼
            final OverScroller scroller = mScroller;
            //得到layoutManger中的SmoothScroller
            final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
            if (scroller.computeScrollOffset()) {//若是是第一次走,會返回false
	               ...省略部分代碼
             }
            if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            }
              ...省略部分代碼
        }
複製代碼

ViewFlinger的run()方法內部實現比較複雜, 在該方法第一次執行的時候,會執行,if (scroller.computeScrollOffset()) ,其中scroller是ViewFlinger中的屬性mScroller的引用,其中mScroller會在ViewFlinger建立對象的時候,就默認初始化了。那麼第一次判斷時候,由於尚未開始計算,因此不會進這個if語句塊,那麼接下來就會直接走下面的語句:

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

最後發現,只是走了一個onAnimation(0,0),繼續走該方法。

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) {//判斷目標視圖是否存在,若是存在則計算移動到位置須要移動的距離
                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) {//若是不存在,繼續去找
                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
                    }
                }
            }
        }
複製代碼

在onAnimation方法中,判斷了目標視圖是否爲空,你們應該還記得上文中,咱們對目標視圖的查找。若是當前位置不在可見範圍以內,那麼mTargetView =null,就不回走對應的判斷語句。繼續查看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

    }
複製代碼

直接經過代碼,發現並不理解改函數要作什麼樣的工做,這裏咱們只知道第一次發生滾動時,mInterimTargetDx=0與mInterimTargetDy =0,那麼會走updateActionForInterimTarget()方法。

protected void updateActionForInterimTarget(Action action) {
        // find an interim target position
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
		...省略部分代碼
        normalize(scrollVector);
        mTargetVector = scrollVector;

        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
        
		//計算須要滾動的時間,  默認滾動距離,TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
		  
		//爲了不在滾動的時候出現停頓,咱們會跟蹤onSeekTargetStep中的回調距離,實際上不會滾動超出實際的距離
        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                //這裏存入的時間要比實際花費的時間大一點。
                (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    }
複製代碼

根據官方文檔進行翻譯:當目標滾動位置對應視圖不在RecyclerView的可見範圍內,該方法計算朝向該視圖的方向向量並觸發平滑滾動。默認滾動的距離爲12000(單位:px),(也就是說了爲了滾動到目標位置,會讓Recycler至多滾動12000個像素)

既然該方法計算了時間,那麼咱們就看看calculateTimeForScrolling()方法,經過方法名咱們就應該瞭解了該方法是計算給定距離在默認速度下須要滾動的時間。

protected int calculateTimeForScrolling(int dx) {
	    //這裏對時間進行了四捨五入操做。 
        return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
    }

複製代碼

其中MILLISECONDS_PER_PX 會在LinearSmoothScroller初始化的時候建立。

public LinearSmoothScroller(Context context) {
      MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
    }
  
複製代碼

查看calculateSpeedPerPixel()方法

private static final float MILLISECONDS_PER_INCH = 25f;// 默認爲移動一英寸須要花費25ms
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
    }

複製代碼

也就是說,當前滾動的速度是與屏幕的像素密度相關, 經過獲取當前手機屏幕每英寸的像素密度,與每英寸移動所須要花費的時間,用每英寸移動所須要花費的時間除以像素密度就能計算出移動一個像素密度須要花費的時間。OK,既然咱們已經算出了移動一個像素密度須要花費的時間,那麼直接乘以像素,就能算出移動該像素所須要花費的時間了。

既然如今咱們算出了時間,咱們如今只用關心Action的update()方法究竟是幹什麼的就行了,

//保存關於SmoothScroller滑動距離信息
        public static class Action {
		       ...省略代碼
	         public void update(int dx, int dy, int duration, Interpolator interpolator) {
                mDx = dx;
                mDy = dy;
                mDuration = duration;
                mInterpolator = interpolator;
                mChanged = true;
            }
	     }
複製代碼

這裏咱們發現Action,只是存儲關於SmoothScroller滑動信息的一個類,那麼初始時保存了橫向與豎直滑動的距離(12000px)、滑動時間,插值器。同時記錄當前數據改變的狀態。

如今咱們已經把Action的onSeekTargetStep方法走完了,那接下來,咱們繼續看Action的runIfNecessary()方法。

void runIfNecessary(RecyclerView recyclerView) {
		         ....省略代碼
                if (mChanged) {
                    validate();
                    if (mInterpolator == null) {
                        if (mDuration == UNDEFINED_DURATION) {
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
                        } else {
	                        //這裏傳入的mDx,mDy,mDuration.是Action以前update()方法。保存的信息
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
                        }
                    } else {
                        recyclerView.mViewFlinger.smoothScrollBy(
                                mDx, mDy, mDuration, mInterpolator);
                    }
              mChanged = false;
                ....省略代碼
            }
複製代碼

TNND,調來調去最後又把Action存儲的信息傳給了ViewFlinger的smoothScrollBy()方法。這裏須要注意:一旦調用該方法會將mChanged置爲false,下次再次進入該方法時,那麼就不會調用ViewFlinger的滑動方法了。

public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
			 //判斷是不是同一插值器,若是不是,從新建立mScroller
            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();
        }
複製代碼

這裏mScroller接受到Acttion傳入的滑動信息開始滑動後。最後會調用postOnAnimation(),又將ViewFiinger的run()法發送出去。那麼最終咱們又回到了ViewFiinger的run()方法。

public void run() {
         ...省略部分代碼
   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;
		        ...省略部分代碼
                if (mAdapter != null) {
                    startInterceptRequestLayout();
                    onEnterLayoutOrScroll();
                    TraceCompat.beginSection(TRACE_SCROLL_TAG);
                    fillRemainingScrollValues(mState);
                    if (dx != 0) {//若是橫向方向大於0,開始讓RecyclerView滾動
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {//若是豎直方向大於0,開始讓RecyclerView滾動,得到當前滾動的距離
                        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);
                            //傳入當前RecylerView滾動的距離 dx dy
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        } else {
                            //傳入當前RecylerView滾動的距離 dx dy
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        }
                    }
                }
                enableRunOnAnimationRequests();
             }
複製代碼

這裏scroller(拿到以前Action傳入的滑動距離信息)已經開始滑動了,故 if (scroller.computeScrollOffset()) 條件爲true, 那麼scroller拿到當前豎直方向的值就開始讓RecyclerView滾動了,也就是代碼 mLayout.scrollVerticallyBy(dy, mRecycler, mState);接着又讓smoothScroller執行onAnimation()方法。其中傳入的參數是RecyclerView已經滾動的距離。那咱們如今繼續看onAnimation方法。

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) {//得到當前Recycler須要滾動的距離
                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
                    }
                }
            }
        }
複製代碼

那麼如今代碼就明瞭了,RecylerView會判斷在滾動的時候,目標視圖是否已經出現,若是沒有出現,會調用onSeekTargetStep保存當前RecylerView滾動距離,而後判斷RecyclerView是否須要滑動,而後又經過postOnAnimation()將ViewFlinger 發送出去了。那麼直到找到目標視圖纔會中止。

那什麼狀況下,目標視圖不爲空呢,其實在RecylerView內部滾動的時候。會判斷目標視圖是否存在,若是存在會對mTargetView進行賦值操做。因爲篇幅限制,這裏就不對目標視圖的查找進行介紹了,有興趣的小夥伴能夠本身看一下源碼。

那接下來,咱們就假如當前已經找到了目標視圖,那麼接下來程序會走onTargetFound()方法。

protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
        //計算讓目標視圖可見的,須要滾動的橫向距離
        final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
       //計算讓目標視圖可見的,須要滾動的橫向距離
        final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
        final int distance = (int) Math.sqrt(dx * dx + dy * dy);
        final int time = calculateTimeForDeceleration(distance);
        if (time > 0) {
            //更新須要滾動的距離。
            action.update(-dx, -dy, time, mDecelerateInterpolator);
        }
    }
複製代碼

當目標視圖被找到之後,會計算讓目標視圖出如今可見範圍內,須要移動的橫向與縱向距離。並計算所須要花費的時間。而後從新讓RecyclerView滾動一段距離。

這裏咱們着重看calculateDyToMakeVisible。

public int calculateDyToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollVertically()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        //獲取當前view在其父佈局的開始位置
        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
        //獲取當前View在其父佈局結束位置
        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
        //獲取當前佈局的開始位置 
        final int start = layoutManager.getPaddingTop();
        //獲取當前佈局的結束位置
        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
        return calculateDtToFit(top, bottom, start, end, snapPreference);
    }
複製代碼

這裏咱們會根據當前view的top、bottom及當前佈局的start、end等座標信息,而後調用了calculateDtToFit()方法。如今最重要的出現了,也是咱們那三個問題出現的緣由!!

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
            snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {//滾動位置在可見範圍以前
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {//滾動位置在可見範圍以後
                    return dtEnd;
                }
                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
        return 0;//在可見範圍以內,直接返回
    }

複製代碼

咱們會根據snapPreference對應的值來計算相應的距離,同時snapPreference的具體值與getVerticalSnapPreference(這裏咱們是豎直方向)因此咱們看該方法。

protected int getVerticalSnapPreference() {
        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
    }
複製代碼

其中mTargetVector與layoutManager.computeScrollVectorForPosition有關。

@Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }
複製代碼

也就是說在LinerlayoutManager爲豎直的狀況下,snapPreference默認爲SNAP_ANY,那麼咱們就能夠獲得,下面三種狀況。

  • 當滾動位置在可見範圍以內時 boxStart - viewStart<=0 boxEnd - viewEnd>0 滾動距離爲0,故不會滾動
  • 當滾動位置在可見範圍以前時 boxStart - viewStart> 0 那麼實際滾動距離爲正值,內容向上滾動,故只能滾動到頂部
  • 當滾動位置在可見範圍距離以外時 boxEnd - viewEnd<0 那麼實際滾動距離爲其差值,內容向下滾動,故只能滾動到底部

有可能你們如今看代碼已經看暈了,下面我就用一張圖來總結整個流程,結合流程圖再去看代碼,我相信你們能有更好的理解。

基本流程圖.png
相關文章
相關標籤/搜索