RecyclerView中提供的方法解析

关于RecyclerView滑动到指定位置,它提供了scrollTo(),scrollBy(),scrollToPosition(),smoothScrollBy(),smoothScrollToPosition()方法,下面将详细解释这些方法的作用。

1.scrollTo(int x, int y)

public void scrollTo(int x, int y) {
        Log.w("RecyclerView", "RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead");
    }

该方法在View中的意思是滑动到绝对位置,比如当前位置为(10,10),通过scrollTo(30,30),最后的位置为(30,30)。但从上面scrollTo方法源代码中可以看出,RecyclerView并不支持滑动到绝对位置,因为该方法为空实现,而是使用scrollToPosition(position)方法来代替。

2.scrollBy(int x, int y)

public void scrollBy(int x, int y) {
        if(this.mLayout == null) {
            Log.e("RecyclerView", "Cannot scroll without a LayoutManager set. Call setLayoutManager with a non-null argument.");
        } else if(!this.mLayoutFrozen) {
            boolean canScrollHorizontal = this.mLayout.canScrollHorizontally();
            boolean canScrollVertical = this.mLayout.canScrollVertically();
            if(canScrollHorizontal || canScrollVertical) {
                this.scrollByInternal(canScrollHorizontal?x:0, canScrollVertical?y:0, (MotionEvent)null);
            }

        }
    }

该方法为滑动到相对位置,比如当前位置为(10,10),通过scrollBy(30,30),最后的位置为(40,40)。在该方法中,首先会判断mLayout是否为空,mLayout就是RecyclerView持有的LayoutManager,然后再判断RecyclerView是横向还是垂直滑动,如果是横向滑动则取(x,0)传递到scrollByInternal方法中,反之垂直则取(0,y)。

在scrollByInternal方法中有几行滑动关键的代码:

if(x != 0) {
                consumedX = this.mLayout.scrollHorizontallyBy(x, this.mRecycler, this.mState);
                unconsumedX = x - consumedX;
            }

            if(y != 0) {
                consumedY = this.mLayout.scrollVerticallyBy(y, this.mRecycler, this.mState);
                unconsumedY = y - consumedY;
            }
            //......
 if(!this.mItemDecorations.isEmpty()) {
            this.invalidate();//刷新View
        }

上面有两个重要的值consumedX/consumedY,它们决定了滑动到的具体位置。而consumedX/consumedY是由LayoutManager中scrollHorizontallyBy或scrollVerticallyBy方法计算出来的。有此看来,RecyclerView中的scrollBy方法,实际是委托给了
LayoutManager中scrollHorizontallyBy和scrollVerticallyBy方法。但是LayoutManager并不是具体实现类,而是交给了它的子类去实现相关方法。
比如经常用到的LinearLayoutManager,下面为
LinearLayoutManager中scrollHorizontallyBy和scrollVerticallyBy方法:

public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
        return this.mOrientation == 1?0:this.scrollBy(dx, recycler, state);
    }

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
        return this.mOrientation == 0?0:this.scrollBy(dy, recycler, state);
    }

上面的方法又交给了LinearLayoutManager的scrollBy方法去处理。
由此得出它们的流程是:
RecyclerView–>scrollBy–>scrollByInternal–>LayoutManager–>
scrollHorizontallyBy/scrollVerticallyBy–>LinearLayoutManager–>scrollBy–>RecyclerView

当得到新的位置后,最后RecyclerView会调用invalidate()方法,重新执行draw过程。

3.scrollToPosition(int position)

public void scrollToPosition(int position) {
        if(!this.mLayoutFrozen) {
            this.stopScroll();
            if(this.mLayout == null) {
                Log.e("RecyclerView", "Cannot scroll to position a LayoutManager set. Call setLayoutManager with a non-null argument.");
            } else {
                this.mLayout.scrollToPosition(position);
                this.awakenScrollBars();
            }
        }
    }

该方法为滑动到RecyclerView中的指定位置(滑动过程中没有动画),由上面代码可以看出,实际上是委托给了LayoutManager的scrollToPosition方法,在LinearLayoutManager的scrollToPosition方法为:

public void scrollToPosition(int position) {
        this.mPendingScrollPosition = position;
        this.mPendingScrollPositionOffset = -2147483648;
        if(this.mPendingSavedState != null) {
            this.mPendingSavedState.invalidateAnchor();
        }
        //重新请求布局,会导致执行measure,layout,draw过程
        this.requestLayout();
    }

其实直接调用了requestLayout,requestLayout会导致View重新执行
measure、layout、draw过程。

4.smoothScrollBy(int dx, int dy)和smoothScrollBy(int dx, int dy, Interpolator interpolator)

public void smoothScrollBy(int dx, int dy) {
        this.smoothScrollBy(dx, dy, (Interpolator)null);
    }

    public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
        if(this.mLayout == null) {
            Log.e("RecyclerView", "Cannot smooth scroll without a LayoutManager set. Call setLayoutManager with a non-null argument.");
        } else if(!this.mLayoutFrozen) {
            if(!this.mLayout.canScrollHorizontally()) {
                dx = 0;
            }

            if(!this.mLayout.canScrollVertically()) {
                dy = 0;
            }

            if(dx != 0 || dy != 0) {
                this.mViewFlinger.smoothScrollBy(dx, dy, interpolator);
            }

        }
    }

这个两个方法都是平滑滑动到相对位置。smoothScrollBy(int dx, int dy)方法间接调用的smoothScrollBy(int dx, int dy, Interpolator interpolator)方法,参数interpolator为插值器,它是一个接口,和动画使用的插值器一样。它的实现有:

RecyclerView滑动到item android recyclerview滑动到指定位置底部_sed

常用的有LinearInterpolator(线性插值器)、 AccelerateInterpolator(加速插值器) 、DecelerateInterpolator(减速插值器)等。

在滑动之前会先通过LayoutManager来判断是横向滑动或垂直滑动,如果是横向滑动则取(dx,0)传递到scrollByInternal方法中,反之垂直则取(0,dy)。

在这个方法的最后调用了mViewFlinger的smoothScrollBy方法,

class ViewFlinger implements Runnable {
        private int mLastFlingX;
        private int mLastFlingY;
        private OverScroller mScroller;
        Interpolator mInterpolator;
        private boolean mEatRunOnAnimationRequest;
        private boolean mReSchedulePostAnimationCallback;
        ///......

        public void smoothScrollBy(int dx, int dy, int duration) {
            this.smoothScrollBy(dx, dy, duration, RecyclerView.sQuinticInterpolator);
        }

        public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
            this.smoothScrollBy(dx, dy, this.computeScrollDuration(dx, dy, 0, 0), interpolator == null?RecyclerView.sQuinticInterpolator:interpolator);
        }

        public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
            if(this.mInterpolator != interpolator) {
                this.mInterpolator = interpolator;
                this.mScroller = new OverScroller(RecyclerView.this.getContext(), interpolator);
            }

            RecyclerView.this.setScrollState(2);
            this.mLastFlingX = this.mLastFlingY = 0;
            this.mScroller.startScroll(0, 0, dx, dy, duration);
            if(VERSION.SDK_INT < 23) {
                this.mScroller.computeScrollOffset();
            }

            this.postOnAnimation();//post到主线程中去执行
        }

ViewFlinger实现了Runnable接口,它是一个线程。在调用它的smoothScrollBy方法时候,会把刚才的interpolator传递进来,另外再给mScroller赋值。mScroller是一个OverScroller对像,这个类封装了滑动的能力,它可以超过滑动边界。它是新版本引入用来替换android.widget.Scroller类的。

OverScroller和Scroller一样,先通过调用startScroll(int startX, int startY, int dx, int dy, int duration)方法来设置要滑动的新位置相关参数:

  • startX和startY表示滑动的起点位置,当startX为正值,View内容会向左边滑动,反之向右边滑动;当startY为正值,View内容会向上边滑动,反之向下边滑动。
  • dx和dy表示滑动的距离,当dx为正值,View内容会向左边滑动,反之向右边滑动;当dy为正值,View内容会向上边滑动,反之向下边滑动。
  • duration表示的是滑动时间,整个滑动过程完成所需要的时间。

当设置新位置参数后,再调用computeScrollOffset方法:

public boolean computeScrollOffset() {
        if (isFinished()) {
            return false;
        }

        switch (mMode) {
            case SCROLL_MODE:
                long time = AnimationUtils.currentAnimationTimeMillis();
                // Any scroller can be used for time, since they were started
                // together in scroll mode. We use X here.
                final long elapsedTime = time - mScrollerX.mStartTime;

                final int duration = mScrollerX.mDuration;
                if (elapsedTime < duration) {
                    final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
                    //根据时间的流逝来计算当前的位置
                    mScrollerX.updateScroll(q);
                    mScrollerY.updateScroll(q);
                } else {
                    abortAnimation();
                }
                break;
                //......

    void updateScroll(float q) {
            mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));

该方法主要是根据时间的流逝来计算当前的位置(mCurrentPosition),当返回true时,表示滑动还未结束,否则结束。

最后的postOnAnimation()方法,是将ViewFlinger线程post到主线程中去执行,当开始运行ViewFlinger线程时,会调用ViewFlinger的run()方法:

if(!RecyclerView.this.mItemDecorations.isEmpty()) {
                        RecyclerView.this.invalidate();
                    }

RecyclerView调用invalidate()方法,会重新执行draw过程,并不会执行measure和layout过程。

最后,就是通过这样不断的计算新位置然后重绘实现了平滑滑动的效果

5.smoothScrollToPosition(int position)

if(!this.mLayoutFrozen) {
            if(this.mLayout == null) {
                Log.e("RecyclerView", "Cannot smooth scroll without a LayoutManager set. Call setLayoutManager with a non-null argument.");
            } else {
                this.mLayout.smoothScrollToPosition(this, this.mState, position);
            }
        }
    }

该方法主要是平滑滑动到指定位置。其实也是交给LayoutManager来处理的。下面为LinearLayoutManager中的smoothScrollToPosition方法:

public void smoothScrollToPosition(RecyclerView recyclerView, State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        this.startSmoothScroll(linearSmoothScroller);
    }

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

            this.mSmoothScroller = smoothScroller;
            this.mSmoothScroller.start(this.mRecyclerView, this);
        }

在smoothScrollToPosition方法中,主要是通过
LinearSmoothScroller来实现平滑滑动的。LinearSmoothScroller继承自SmoothScroller类,SmoothScroller是平滑滑动的基类,它不仅提供了处理目标View位置的基本跟踪,还提供了触发滑动的方法。

LinearSmoothScroller使用了LinearInterpolator和DecelerateInterpolator两个插值器,在没有滑动到RecyclerView的目标position前,也就是目标position并不可见之前,使用LinearInterpolator来实现平滑滑动,在目标position可见的时候,再使用DecelerateInterpolator来实现平滑滑动。

最后调用SmoothScroller的start方法:

void start(RecyclerView recyclerView, RecyclerView.LayoutManager layoutManager) {
            this.mRecyclerView = recyclerView;
            this.mLayoutManager = layoutManager;
            if(this.mTargetPosition == -1) {
                throw new IllegalArgumentException("Invalid target position");
            } else {
                this.mRecyclerView.mState.mTargetPosition = this.mTargetPosition;
                this.mRunning = true;
                this.mPendingInitialRun = true;
                this.mTargetView = this.findViewByPosition(this.getTargetPosition());
                this.onStart();
                this.mRecyclerView.mViewFlinger.postOnAnimation();//将mViewFlinger线程post到主线程中去执行
            }
        }

start方法中的关键执行mViewFlinger线程。

6.总结

  • 虽然RecyclerView提供了scrollTo()方法,但是并没有提供scrollTo()方法的具体实现,而是使用scrollToPosition()方法来代替。
  • scrollBy()和smoothScrollBy()都是通过指定相对位置来滑动,而scrollToPosition()和smoothScrollToPosition()方法是通过RecyclerView中的position来滑动,其中smoothScrollBy()和smoothScrollToPosition()方法实现了平滑滑动的效果。
  • 虽然smoothScrollBy()和smoothScrollToPosition()方法都实现了平滑滑动的效果,但是它们的实现方式是不一样的。smoothScrollBy()方法是通过
    OverScroller类实现的,而smoothScrollToPosition()是通过SmoothScroller类实现的。
  • scrollBy(),scrollToPosition()和smoothScrollToPosition()方法都是通过LayoutManager来计算位置的,只有smoothScrollBy()是通过OverScroller来计算位置。
  • 使用scrollBy(),smoothScrollBy()和smoothScrollToPosition()方法滑动到指定位置,只会重新执行draw过程,而scrollToPosition()方法滑动到指定位置,会重新执行measure,layout,draw过程,这将是一个消耗性能的操作,所以建议不使用。