RecycleView源码浅析之Recycler+滑动

概述

Recycler解决了两个哲学问题,VH从哪里来以及VH到哪里去,前两篇讲到RV的绘制流程和动画都回避了View的获取以及回收问题,其实是因为Recycler帮我们完成了而且封装得很好。这一篇就来看看Recycler是如何帮我们做到这些的,顺带看一下RV这个ViewGroup对触摸事件的处理。

onTouchEvent()

和ViewPager差不多,RV分为拖动和fling,scrollByInternal()产生拖动,fling()产生滑动。

public boolean onTouchEvent(MotionEvent e) {
  ...
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                ...

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
              ...

                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);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;
        ...
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                        -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                        -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
              //fling
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;
            ...
        return true;
    }

首先看一下scrollByInternal()(看了好多博客貌似都是错的,内部并没有用到scrollBy而是用的layout产生滑动效果),最终调用到scrollBy(),这个方法并不是重写的View#scrollBy(签名不同),而是根据dy重新布局了一次,即用layout产生滑动的效果。那么也就是说滑动的时候VH的回收就是fill()中对VH的回收的逻辑,稍后再说。

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
       //将更新映射到VH上
        consumePendingUpdateOperations();
        if (mAdapter != null) {
            //y方向上scroll
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }

        }
       ...

        return consumedX != 0 || consumedY != 0;
    }

    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        ...
        return scrollBy(dy, recycler, state);
    }

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
          //确定布局方向
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
         //dy的绝对值
        final int absDy = Math.abs(dy);
         //更新LayoutState
        updateLayoutState(layoutDirection, absDy, true, state);
         //开始布局
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        ...
        return scrolled;
    }

接下来看看fling()

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

ViewFlinger是RV的一个内部类,实现了Runnable接口,有一个ScrollerCompat成员。其fling()调用了ScrollerCompat#fling(),这个方法和ScrollerCompat#startScroll()类似,初始化了一些值并设置了Scroller的模式,然后需要有不断地回调+计算+改变内容的过程,这是由下面的postOnAnimation()启动的,启动后会执行run()。computeScrollOffset()本质上就是在FLING模式下更新坐标。最终我们又看到了scrollVerticallyBy(),最终会进入LM的scrollBy(),利用fill()进行布局和回收。看来殊途同归。

class ViewFlinger implements Runnable{
    ...
    private ScrollerCompat mScroller;
      ...
    public void fling(int velocityX, int velocityY) {
            //设置了模式,还有一些变量,用于fling效果。
              mScroller.fling(0, 0, velocityX, velocityY,
                      Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            //把这个Runnable post
              postOnAnimation();
    }


    @Override
    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;
                int hresult = 0;
                int vresult = 0;
                mLastFlingX = x;
                mLastFlingY = y;
                int overscrollX = 0, overscrollY = 0;
                if (mAdapter != null) {
                    //重新布局
                    if (dy != 0) {
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }


                }
            ....
                if (scroller.isFinished() || !fullyConsumedAny) {
                    setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
                    if (ALLOW_THREAD_GAP_WORK) {
                        mPrefetchRegistry.clearPrefetchPositions();
                    }
                } else {
                  //没有结束继续调用
                    postOnAnimation();
                    if (mGapWorker != null) {
                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
                    }
                }
            }
            ...
        }
}

Recycler的一些概念

上面简单地把拖动和fling的过程过了一遍,最终都是利用fill()进行重新布局达到了滑动的效果。而fill()中不仅从Recycler中获取View,也把超出布局范围的View交给Recycler。

Recycler不仅是VH的回收者,也是View(VH)的提供者。我们先看一些关于Recycler的概念。

  • 三级缓存
    第一级:mAttachedScrap、mChangedScrap、mCachedViews

    第二级:ViewCacheExtension(可选,让使用者自己配置)

    第三级:RecycledViewPool(RV之间共享VH的缓存池)

  • View的detach和remove

    都是针对VG的

    被detach的View从VG的View[]数组(保存child)中移除(但在其他地方还有引用),这个轻量级的移除通常用来改变View在数组中的位置。

    被remove的View从VG中真正移除

  • Recycler的scrap和recycle

    recycle一般配合view的remove,被recycle的VH进入mCachedViews

    scrap一般配合View的detach,被scrap的VH进入mAttachedScrap或mChangedScrap

getViewForPosition()

这个Recycler的方法解释了View从哪来的问题,因为View的回收的地方很多,而提供View的地方很固定,就是在fill()方法中,所以我们先来讲这个问题。一切缘起都是因为下面这个方法,它在layout的时候被调用获取下一个应该布局的View,然后添加、测量、布局(这些大家可以看我的第一篇关于RV的文章):

 //LayoutState.java
        /**
         * Gets the view for the next element that we should layout.
         * Also updates current item index to the next item, based on {@link #mItemDirection}
         *
         * @return The next element that we should layout.
         */
        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

随即会调用到Recycler的一个方法getViewForPosition(),最终会调用到tryGetViewHolderForPositionByDeadline()。这个方法会根据position依次从各级缓存寻找VH或直接新建一个,我们先屏蔽“为什么VH会在这级缓存”这个问题,单单来看获得的逻辑。总体来说是这样的:

1.从mChangedScrapView一级缓存中寻找。

2.从mAttachedScrap一级缓存中寻找。

3.从mHiddenViews中寻找。

4.从mCachedViews中寻找。

5.从mViewCacheExtension中寻找。

6.从mRecyclerPool中寻找。

7.如果都没有找到,调用mAdapter.createViewHolder()新建一个VH。

8.检查VH是否需要重新绑定数据(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()),如果需要,调用mAdapter.bindViewHolder()绑定数据。

9.(VH已经持有itemView了)获取View的LayoutParams,并将其mViewHolder成员指向该VH,mPendingInvalidate成员赋值fromScrapOrHiddenOrCache && bound(如果是从scrap或者hidden或者cache中返回的VH或者是VH重新绑定了,那么需要重绘该VH)。

        /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * <p>
         */
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
           // 1.从mChangedScrapView一级缓存中寻找。
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
            }
            // 2.从mAttachedScrap一级缓存中寻找。
            //3.从mHiddenViews中寻找。
             //为什么会在mHiddenViews中寻找呢?是因为某些被移除但需要执行动画的View是被添加到mHiddenViews中的
            //4.从mCachedViews中寻找。
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            if (holder == null) {
                ...
                if (holder == null && mViewCacheExtension != null) {
                    //5.从mViewCacheExtension中寻找。
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    ...
                }
                if (holder == null) { // fallback to pool
                    //6.从mRecyclerPool中寻找。
                    holder = getRecycledViewPool().getRecycledView(type);

                }
                if (holder == null) {
                    //7.如果都没有找到,调用mAdapter.createViewHolder()新建一个VH。
                    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()) {
                //8.检查VH是否需要重新绑定数据(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()),如果需要,调用mAdapter.bindViewHolder()绑定数据。
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            //9.(VH已经持有itemView了)获取View的LayoutParams,并将其mViewHolder成员指向该VH,mPendingInvalidate成员赋值fromScrapOrHiddenOrCache && bound(如果是从scrap或者hidden或者cache中返回的VH或者是VH重新绑定了,那么需要重绘该VH)。
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }

VH的回收

如果光看上面的过程逻辑还是比较清晰的,但VH回收的地点却比较散,我们只能从我们已知的地方入手,随着学习的不断深入再慢慢补全。

首先我们已知的一个地方就是在LLM真正开始布局之前,会调用下面这个方法。看注释知道LM会先对已经存在的所有VH做一个scrap或者recycle处理。

 //layoutManager.java
        /**
         * Temporarily detach and scrap all currently attached child views. Views will be scrapped
         * into the given Recycler. The Recycler may prefer to reuse scrap views before
         * other views that were previously recycled.
         */
        public void detachAndScrapAttachedViews(Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }
        //layoutManager.java
        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            //从方法名上来看,如果VH是无效的 + 没有被移除的 + mAdapter没有StableId,
          //那么我们会remove + recycle这个VH
            if (viewHolder.isInvalid() && !viewHolder.isRemoved() &&
                    !mRecyclerView.mAdapter.hasStableIds()) {
                //前面解释概念的时候我们说了,remove是和VG相关,而且是compeletely remove,即和VG断绝一切关系
                removeViewAt(index);
                //和mmCachedViews有关
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
              //把View从VG中detach
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

下面我们来具体看一下recycleViewHolderInternal()和scrapView(),首先是recycleViewHolderInternal()。重点注释在下面,也就是说这个方法是和mCachedViews配合的,被VG remove掉的View,其VH是一定进入mCachedViews的。

void recycleViewHolderInternal(ViewHolder holder) {
            ...
            if (forceRecycle || holder.isRecyclable()) {
              //如果不是INVALID、REMOVED、UPDATE、ADAPTER_POSITION_UNKNOWN这几个状态,开始回收
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_REMOVED
                                | ViewHolder.FLAG_UPDATE
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                    int cachedViewSize = mCachedViews.size();
                  // 如果mCachedViews满了,淘汰一个去mRecyclerPool
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    ...
                      //添加到mCachedViews中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.
                //如果一个View正在执行动画却被要求回收,那么回收动作交给ItemAnimatorRestoreListener去做
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

接下来是scrapView(),这是和View的轻量级操作detach结合的。重点注释在下面。

      /**
         * Mark an attached view as scrap.
         *
         * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
         * for rebinding and reuse. Requests for a view for a given position may return a
         * reused or rebound scrap view instance.</p>
         *
         * @param view View to scrap
         */
        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            //如果这个VH有 REMOVED、INVALID其中的状态 或者 没有更新 或者 是可用的已更新的VH(和动画相关)
            //那么添加到mAttachedScrap中
           //这里对于第一个条件,我们知道在prelayout的时候被remove的VH还是会layout出来
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {

                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {//那么如果 没有REMOVED、INVALID其中的状态 且 更新了 且不能重用更新了的VH
                    //加入到mChangedScrap中
                    //何时会产生这种情况呢?也就是我们更改了数据后并调用Adapter.notifyItemChanged方法后
                    //VH会被标记为UPDATE,在scrap的时候有可能进入这个分支被添加到mChangedScrap中的。
                    //而且我们和上面获取的过程联系起来,如果一个VH从mChangedScrap获取,那么它就有UPDATE的flag,
              //会执行bindViewHolder()方法,且LayoutParams.mPendingInvalidate为真,稍后会执行到它的重绘。
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

然后我们能想起来的一个地方就是在拖动或者fling过程中的fill()方法中,我们会把布局后在屏幕外的VH回收了。

recycleByLayoutState(recycler, layoutState);

经过一系列跟踪,最后会调用到这个方法,View的操作是remove。

        /**
         * Remove a child view and recycle it using the given Recycler.
         *
         * @param index Index of child to remove and recycle
         * @param recycler Recycler to use to recycle child
         */
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            recycler.recycleView(view);
        }

继续看recycleView(),其思想就是,如果一个View移出了屏幕,那么它必然是进入mCachedViews这一级缓存的。也就是说和滑动相关的回收是和mCachedViews关联的。

        public void recycleView(View view) {
            // This public recycle method tries to make view recycle-able since layout manager
            // intended to recycle this view (e.g. even if it is in scrap or change cache)
            ViewHolder holder = getChildViewHolderInt(view);
          //如果这个VH有TmpDetached标志,我们将它完全移除
          //那么何时VH有TmpDetached标志呢?如果一个View被detach,它会给LayoutParams中的VH设置这个标志位。
            if (holder.isTmpDetached()) {
                removeDetachedView(view, false);
            }
          //如果VH在mChangedScrap或者mAttachedScrap中,我们“unScrap”它
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()){
                holder.clearReturnedFromScrapFlag();
            }
          //进行回收保存进mCachedViews,上面已经介绍过
            recycleViewHolderInternal(holder);
        }

还有一个地方就是在执行消失动画的时候,首先这个VH要unscrap,然后 addAnimatingView()这个方法保证了这个VH的View是作为hidden添加到VG中用于执行动画的。

//ViewInfoStore.ProcessCallback.java
public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                @Nullable ItemHolderInfo postInfo) {
            mRecycler.unscrapView(viewHolder);
            animateDisappearance(viewHolder, info, postInfo);
        }
//RV.java
void animateDisappearance(@NonNull ViewHolder holder,
            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        addAnimatingView(holder);
        holder.setIsRecyclable(false);
        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }
//RV.java
    private void addAnimatingView(ViewHolder viewHolder) {
        final View view = viewHolder.itemView;
        final boolean alreadyParented = view.getParent() == this;
        mRecycler.unscrapView(getChildViewHolder(view));
        //这个View以hidden的身份添加到VG中
        if (viewHolder.isTmpDetached()) {
            // re-attach
            mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
        } else if(!alreadyParented) {
            mChildHelper.addView(view, true);
        } else {
            mChildHelper.hide(view);
        }
    }

最终这里仅仅只是移除了VH并没有进行回收吗?我猜想是有回收的过程,但是没有找到,或者是前面已经对它进行了回收?

//DefaultItemAnimator.java
private void animateRemoveImpl(final ViewHolder holder) {
        ..
        animation.setDuration(getRemoveDuration())
                .alpha(0).setListener(new VpaListenerAdapter() {

            ...
            @Override
            public void onAnimationEnd(View view) {
                animation.setListener(null);
                ViewCompat.setAlpha(view, 1);
                dispatchRemoveFinished(holder);
                //移除VH
                mRemoveAnimations.remove(holder);
                dispatchFinishedWhenDone();
            }
        }).start();
    }

总结

这篇文章简单介绍了一下RV的滑动以及View的获取以及回收机制。

滑动依靠的是layout过程改变子View的位置。

Recycler承包了View(VH)的提供以及VH的回收。其中提供的时候会进行分级查找,如果找不到会进行新建,根据具体情况执行绑定数据。VH的回收有很多地方,比较典型的是布局前和滑动时,fill()相关的回收和mCachedViews关联。

由于本人水平有限,对RV的源码跟踪以及理解都还不能达到一个很高的层次,但至少自己心里已经有了一个大致的框架。有错误的地方或不足的地方欢迎大家一起来讨论,我会不断patch这几篇关于RV的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值