RecycleView源码浅析之Recycler+滑动

来源:互联网 发布:ping 域名 ip不一样 编辑:程序博客网 时间:2024/06/05 00:17

概述

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.javapublic void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,                @Nullable ItemHolderInfo postInfo) {            mRecycler.unscrapView(viewHolder);            animateDisappearance(viewHolder, info, postInfo);        }//RV.javavoid 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.javaprivate 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的文章。

原创粉丝点击