RecyclerView滑动源码分析

http://blog.csdn.net/qq_23012315/article/details/50807224



接下来,我将分析ReyclerView的滑动流程。

滑动

  RecyclerView的滑动过程可以分为2个阶段:手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。现在先看看RecyclerView的触屏事件处理onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent e) {
    ...
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    ...
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {
            ...
            final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
            final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            ...
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                ...
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (startScroll) {
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
        ...
        case MotionEvent.ACTION_UP: {
            ...
            final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetTouch();
        } break;
        ...
    }
    ...
}

这里我以垂直方向的滑动来说明。当RecyclerView接收到ACTION_MOVE事件后,会先计算出手指移动距离(dy),并与滑动阀值(mTouchSlop)比较,当大于此阀值时将滑动状态设置为SCROLL_STATE_DRAGGING,而后调用scrollByInternal()方法,使RecyclerView滑动,这样RecyclerView的滑动的第一阶段scroll就完成了;当接收到ACTION_UP事件时,会根据之前的滑动距离与时间计算出一个初速度yvel,这步计算是由VelocityTracker实现的,然后再以此初速度,调用方法fling(),完成RecyclerView滑动的第二阶段fling。显然滑动过程中关键的方法就2个:scrollByInternal()与fling()。接下来同样以垂直线性布局来说明。先来说明scrollByInternal(),跟踪进入后,会发现它最终会调用到LinearLayoutManager.scrollBy()方法,这个过程很简单,我就不列出源码了,但是分析到这里先暂停下,去看看fling()方法:

public boolean fling(int velocityX, int velocityY) {
    ...
    mViewFlinger.fling(velocityX, velocityY);
    ...
}

有用的就这一行,其它乱七八糟的不看也罢。mViewFlinger是一个Runnable的实现ViewFlinger的对象,就是它来控件着ReyclerView的fling过程的算法的。下面来看下类ViewFlinger的一段代码:

void postOnAnimation() {
    if (mEatRunOnAnimationRequest) {
        mReSchedulePostAnimationCallback = true;
    } else {
        removeCallbacks(this);
        ViewCompat.postOnAnimation(RecyclerView.this, this);
    }
}

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    mScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    postOnAnimation();
}

可以看到,其实RecyclerView的fling是借助Scroller实现的;然后postOnAnimation()方法的作用就是在将来的某个时刻会执行我们给定的一个Runnable对象,在这里就是这个mViewFlinger对象,这部分原理我就不再深入分析了,它已经不属于本文的范围了。并且,关于Scroller的作用及原理,本文也不会作过多解释。对于这两点各位可以自行查阅,有很多文章对于作过详细阐述的。接下来看看ViewFlinger.run()方法:

public void run() {
    ...
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        final int dx = x - mLastFlingX;
        final int dy = y - mLastFlingY;
        ...
        if (mAdapter != null) {
            ...
            if (dy != 0) {
                vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                overscrollY = dy - vresult;
            }
            ...
        }
        ...
        if (!awakenScrollBars()) {
            invalidate();//刷新界面
        }
        ...
        if (scroller.isFinished() || !fullyConsumedAny) {
            setScrollState(SCROLL_STATE_IDLE);
        } else {
            postOnAnimation();
        }
    }
    ...
}

本段代码中有个方法mLayout.scrollVerticallyBy(),跟踪进入你会发现它最终也会走到LinearLayoutManager.scrollBy(),这样虽说RecyclerView的滑动可以分为两阶段,但是它们的实现最终其实是一样的。这里我先解释下上段代码。第一,dy表示滑动偏移量,它是由Scroller根据时间偏移量(Scroller.fling()开始时间到当前时刻)计算出的,当然如果是RecyclerView的scroll阶段,这个偏移量也就是手指滑动距离。第二,上段代码会多次执行,至到Scroller判断滑动结束或已经滑动到边界。再多说一下,postOnAnimation()保证了RecyclerView的滑动是流畅,这里涉及到著名的“android 16ms”机制,简单来说理想状态下,上段代码会以16毫秒一次的速度执行,这样其实,Scroller每次计算的滑动偏移量是很小的一部分,而RecyclerView就会根据这个偏移量,确定是平移ItemView,还是除了平移还需要再创建新ItemView。 
scroll offset 
  现在就来看看LinearLayoutManager.scrollBy()方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled);
    ...
}

如上文所讲到的fill()方法,作用就是向可绘制区间填充ItemView,那么在这里,可绘制区间就是滑动偏移量!再看方法mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整个滑动过程就分析完成了,当然RecyclerView的滑动还有个特性叫平滑滑动(smooth scroll),其实它的实现就是一个fling滑动,所以就不再赘述了。

Recycler

  Recycler的作用就是重用ItemView。在填充ItemView的时候,ItemView是从它获取的;滑出屏幕的ItemView是由它回收的。对于不同状态的ItemView存储在了不同的集合中,比如有scrapped、cached、exCached、recycled,当然这些集合并不是都定义在同一个类里。 
  回到之前的layoutChunk方法中,有行代码layoutState.next(recycler),它的作用自然就是获取ItemView,我们进入这个方法查看,最终它会调用到RecyclerView.Recycler.getViewForPosition()方法:

View getViewForPosition(int position, boolean dryRun) {
    ...
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrap = holder != null;
    }
    // 1) Find from scrap by position
    if (holder == null) {
        holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
        ...
    }
    if (holder == null) {
        ...
        // 2) Find from scrap via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) {
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        ...
        mAdapter.bindViewHolder(holder, offsetPosition);
        ...
    }
    ...
}

这个方法比较长,我先解释下它的逻辑吧。根据列表位置获取ItemView,先后从scrapped、cached、exCached、recycled集合中查找相应的ItemView,如果没有找到,就创建(Adapter.createViewHolder()),最后与数据集绑定。其中scrapped、cached和exCached集合定义在RecyclerView.Recycler中,分别表示将要在RecyclerView中删除的ItemView、一级缓存ItemView和二级缓存ItemView,cached集合的大小默认为2,exCached是需要我们通过RecyclerView.ViewCacheExtension自己实现的,默认没有;recycled集合其实是一个Map,定义在RecyclerView.RecycledViewPool中,将ItemView以ItemType分类保存了下来,这里算是RecyclerView设计上的亮点,通过RecyclerView.RecycledViewPool可以实现在不同的RecyclerView之间共享ItemView,只要为这些不同RecyclerView设置同一个RecyclerView.RecycledViewPool就可以了。 
  上面解释了ItemView从不同集合中获取的方式,那么RecyclerView又是在什么时候向这些集合中添加ItemView的呢?下面我逐个介绍下。 
  scrapped集合中存储的其实是正在执行REMOVE操作的ItemView,这部分会在后文进一步描述。 
  在fill()方法的循环体中有行代码recycleByLayoutState(recycler, layoutState);,最终这个方法会执行到RecyclerView.Recycler.recycleViewHolderInternal()方法:

void recycleViewHolderInternal(ViewHolder holder) {
        ...
        if (forceRecycle || holder.isRecyclable()) {
            if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE)) {
                // Retire oldest cached view
                final int cachedViewSize = mCachedViews.size();
                if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                }
                if (cachedViewSize < mViewCacheMax) {
                    mCachedViews.add(holder);
                    cached = true;
                }
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder);
                recycled = true;
            }
        }
        ...
    }

这个方法的逻辑是这样的:首先判断集合cached是否満了,如果已満就从cached集合中移出一个到recycled集合中去,再把新的ItemView添加到cached集合;如果不満就将ItemView直接添加到cached集合。 
  最后exCached集合是我们自己创建的,所以添加删除元素也要我们自己实现。

数据集、动画

  RecyclerView定义了4种针对数据集的操作,分别是ADD、REMOVE、UPDATE、MOVE,封装在了AdapterHelper.UpdateOp类中,并且所有操作由一个大小为30的对象池管理着。当我们要对数据集作任何操作时,都会从这个对象池中取出一个UpdateOp对象,放入一个等待队列中,最后调用RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,根据这个等待队列中的信息,对所有子控件重新测量、布局并绘制且执行动画。以上就是我们调用Adapter.notifyItemXXX()系列方法后发生的事。 
  显然当我们对某个ItemView做操作时,它很有可以会影响到其它ItemView。下面我以REMOVE为例来梳理下这个流程。 
remove example 
  首先调用Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():

public void onItemRangeRemoved(int positionStart, int itemCount) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
        triggerUpdateProcessor();
    }
}

这里的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待队列添加一个类型为REMOVE的UpdateOp对象, triggerUpdateProcessor()方法就是调用View.requestLayout()方法,这会导致界面重新布局,也就是说方法RecyclerView.onLayout()会随后调用,这之后的流程就和在绘制流程一节中所描述的一致了。但是动画在哪是执行的呢?查看之前所列出的onLayout()方法发现dispatchLayoutStepX方法共有3个,前文只解释了dispatchLayoutStep2()的作用,这里就其它2个方法作进一步说明。不过dispatchLayoutStep1()没有过多要说明的东西,它的作用只是初始化数据,需要详细说明的是dispatchLayoutStep3()方法:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        ...
        // Step 4: Process view info lists and trigger animations
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    ...
}

代码注释已经说明得很清楚了,这里我没有列出step 3相关的代码是因为这部分只是初始化或赋值一些执行动画需要的中间数据,process()方法最终会执行到RecyclerView.animateDisappearance()方法:

private void animateDisappearance(...) {
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

这里的animateDisappearance()会把一个动画与ItemView绑定,并添加到待执行队列中, postAnimationRunner()调用后就会执行这个队列中的动画,注意方法addAnimatingView():

private void addAnimatingView(ViewHolder viewHolder) {
    final View view = viewHolder.itemView;
    ...
    mChildHelper.addView(view, true);
    ...
}

这里最终会向ChildHelper中的一个名为mHiddenViews的集合添加给定的ItemView,那么这个mHiddenViews又是什么东西?上节中的getViewForPosition()方法中有个getScrapViewForPosition(),作用是从scrapped集合中获取ItemView:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
    ...
    View view = mChildHelper.findHiddenNonRemovedView(position, type);
    ...
}

接下来是findHiddenNonRemovedView()方法:

View findHiddenNonRemovedView(int position, int type) {
    final int count = mHiddenViews.size();
    for (int i = 0; i < count; i++) {
        final View view = mHiddenViews.get(i);
        RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
        if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
                && (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
            return view;
        }
    }
    return null;
}

Oops!看到这里就我之前所讲的scrapped集合联系起来了,虽然绕了个圈。所以这里就论证我之前对于scrapped集合的理解。 
  文章到这里也快结束了,最后关于动画,本节提到的对数据集的4种操作,在DefalutItemAnimator中给出了对应的默认实现,就是改变透明度,实现淡入淡出效果。如果要自定义ItemView的动画可以参考这里的实现来做。好了,以上就是我对于RecyclerView的全部剖析了,也许还有我没有提及的方面,或是我讲错的地方,欢迎指正。

结束语

  之所以写这篇文章,是因为之前一直没有找到关于RecyclerView实现原理上分析的文章,找到的都是怎么使用的,所有写下本文,希望能对此感兴趣的同学有些许帮助。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值