欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

RecycleView源码浅析之Recycler+滑动

程序员文章站 2022-05-05 10:28:07
...

概述

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的文章。