前语

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

上章咱们讲了右半部分,本章咱们讲解左半部分;

怎样复用原理

咱们在滑动的时分,才会触发 RecyclerView 的收回复用,所以咱们从 RecyclerView 的 onTouchEvent 办法下手;咱们来看下滑动的时分,是怎样和 LayoutManager 关联起来的;

咱们进入 onTouchEvent 的 ACTION_MOVE 看下:

public boolean onTouchEvent(MotionEvent e) {
    //
    ...
    // 省掉部分代码
    case MotionEvent.ACTION_MOVE:
        if(scrollByInternal(xxxx)){}
        break;
}

咱们进入scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e) 这个办法看下:

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    //
    ...
    // 省掉部分代码
    scrollStep(x, y, mResuableIntPair);
}

咱们进入这个 scrollStep 办法看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

根据滑动方向,别离调用了 LayoutManager 不同的办法,咱们挑选其间一个进入看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们挑选 LinearLayoutManager 的 scrollVerticalcallBy 办法看下:

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
        RecyclerView.State state) {    
    if (mOrientation == HORIZONTAL) {        
        return 0;    
    }    
    return scrollBy(dy, recycler, state);
}

这儿直接调用了 scrollBy 办法,咱们进入这个办法看一下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们进入这个 fill 办法看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

可以看到,咱们在一个 while 循环中多次调用 layoutChunk 办法,这个 layoutChunk 办法便是获取 view 填充咱们的 RecyclerView 的,咱们进入这个办法看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

从缓存中获取 View 并添加到 RecyclerView 中,咱们进入这个 next 办法看下:

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {        
        return nextViewFromScrapList();    
    }    
    final View view = recycler.getViewForPosition(mCurrentPosition);    
    mCurrentPosition += mItemDirection;    
    return view;
}

从 Recycler 中根据方位获取一个 View,咱们进入这个 getViewForPosition 看下:

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

拿到 ViewHolder 之后,直接获取它的 itemView 并返回,一切的将 ViewHolder 从缓存取出来复用的逻辑都在这儿,咱们来看下 ViewHolder 是怎样复用的:

这儿总共包含了四级缓存,对应着四级复用:

  1. mChangeScrp 和 mAttachedScrp;用来缓存还在屏幕内的 ViewHolder

  2. mCachedViews;用来缓存移除屏幕外地 ViewHolder

  3. mViewCacheExtension;开发给用户的自定义扩展缓存,需要用户自己办理 View 的创立和缓存

  4. RecyclerViewPool;ViewHolder 缓存池

第一次缓存复用

// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {    
    holder = getChangedScrapViewForPosition(position);    
    fromScrapOrHiddenOrCache = holder != null;
}

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

与动画相关的,经过方位从 mChangeScrp 中获取;

第2次缓存复用

// 1) Find by position from scrap/hidden list/cache
if (holder == null) {    
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

针对方位的,经过方位从 mAttachedScrap 和 mCachedViews 中获取;

holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
    type, dryRun);

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

仍是针对mAttachedScrap 和 mCachedViews 中获取的,经过 ViewType 和 ItemId来区别,所以这个也属于第2次缓存复用;

第三次缓存复用

if (holder == null && mViewCacheExtension != null) {
    // We are NOT sending the offsetPosition because LayoutManager does not    
    // know it.    
    final View view = mViewCacheExtension            
            .getViewForPositionAndType(this, position, type);    
    if (view != null) {        
        holder = getChildViewHolder(view);
    }
    // 省掉部分代码
    ...
    // 
}

开发给开发中的自定义扩展缓存,需要开发者自己办理 View 的创立和缓存,一般用不到;

第四次缓存复用

holder = getRecycledViewPool().getRecycledView(type);

从缓存池中获取;

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

复用的流程咱们已经整理通了,那么拿到复用的 ItemView 之后,又是怎样调用到 onBindViewHolder 以及怎样调用的 onCreateViewHolder 呢?咱们继续分析:

假如四级缓存中都没有可以复用的 ViewHolder 的话,那么就需要进行 ViewHolder 的创立流程了;

if (holder == null) {
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
}

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

创立之后,便是进行 ViewHolder 的绑定流程了;

bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

终究就会调用到 onBindViewHolder 办法;

全体时序图如下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

怎样缓存原理

缓存发生了 RecyclerView 的 onLayout 办法中,咱们进入看一下:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);    
    dispatchLayout();    
    TraceCompat.endSection();    
    mFirstLayoutComplete = true;
}

咱们进入 dispatchLayout 办法看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们进入这个 dispatchLayoutStep2 办法看下,这个办法终究调用到了

mLayout.onLayoutChildren(mRecycler, mState);

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们进入 LinearLayoutManager 的 onLayoutChildren 办法看下,这个办法终究调用到了detachAndScrapAttachedViews 这个办法,咱们进入这个办法看下:

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    final int childCount = getChildCount();    
    for (int i = childCount - 1; i >= 0; i--) {        
        final View v = getChildAt(i);        
        scrapOrRecycleView(recycler, i, v);    
    }
}

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

这儿调用了recycler.recycleViewHolderInternal(viewHolder); 和 recycler.scrapView(view); 咱们别离看下;

recycler.recycleViewHolderInternal(viewHolder) 主要用来处理 mCacheView 和 RecyclerViewPool 的缓存;

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

假如 ViewHolder remove、update 等发生变化的时分,不履行缓存逻辑;

recycleCachedViewAt 处理的便是 mCacheView;

// 假如 mCacheView 当前的巨细大于等于 mViewCacheMax(默认的mCacheView的巨细)
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
   recycleCachedViewAt(0);
   cachedViewSize--;
}
void recycleCachedViewAt(int cachedViewIndex) {    
    if (DEBUG) {        
        Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);    
    }    
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);    
    if (DEBUG) {        
        Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);    
    }    
    addViewHolderToRecycledViewPool(viewHolder, true);    
    mCachedViews.remove(cachedViewIndex);
}

addViewHolderToRecycledViewPool(viewHolder, true); 和mCachedViews.remove(cachedViewIndex); 这两个办法履行之后,会将 ViewHolder 添加到 RecycledViewPool 中(调用 addViewHolderToRecycledViewPool 办法),一起从 mCachedViews 中移除,也便是说 RecyclerViewPool 中的数据是从 mCachedView 中来的;

当 mCachedViews 中存满之后(默认寄存2个),就会把第 0 个方位的 View 添加到 RecyclerViewPool 中并从本身移除掉,第 1 个方位的 ViewHolder 移动到第 0 个方位,新进来的放到第 1 个方位;

咱们接下来看下addViewHolderToRecycledViewPool 办法的完成;

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们进入 putRecycledView 办法中看下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

先获取 viewType,然后根据 viewType 获取 ScrapData,然后获取它的 scrapHeap 调集;也便是咱们的 ViewHolder 是寄存在 ScrapData 中了;

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们来看下 getScrapDataForType 的办法完成:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
    return;
} 

数据满了之后,直接 return 不进行缓存,也便是同一种 ViewType 的 ViewHolder 只保存 5 个;

scrap.resetInternal();

清空 ViewHolder,也便是缓存池中保存的只是 ViewHolder 类型,不保存数据;

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

咱们接下来看下recycler.scrapView(view);

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

这儿处理了 mAttachedScrap 和 mChangedScrap 用来缓存 ViewHolder;

全体时序图如下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

自定义LayoutManager

咱们假如想完成探探的左滑右滑作用,需要自定义 LayoutManager;

public class SlideLayoutManager extends RecyclerView.LayoutManager {
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }
    // 布局
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ViewHodler收回复用
        detachAndScrapAttachedViews(recycler);
        int bottomPosition;
        int itemCount = getItemCount();
        if (itemCount < CardConfig.MAX_SHOW_COUNT) {
            bottomPosition = 0;
        } else {
            // 布局了四张卡片
            bottomPosition = itemCount - CardConfig.MAX_SHOW_COUNT;
        }
        for (int i = bottomPosition; i < itemCount; i++) {
            // 复用
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
            int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
            // 布局 -> draw -> onDraw ,onDrawOver, onLayout
            layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
                    widthSpace / 2 + getDecoratedMeasuredWidth(view),
                    heightSpace / 2 + getDecoratedMeasuredHeight(view));
            int level = itemCount - i - 1;
            if (level > 0) {
                if (level < CardConfig.MAX_SHOW_COUNT - 1) {
                    view.setTranslationY(CardConfig.TRANS_Y_GAP * level);
                    view.setScaleX(1 - CardConfig.SCALE_GAP * level);
                    view.setScaleY(1 - CardConfig.SCALE_GAP * level);
                } else {
                    // 最下面的那个view 与前一个view 布局相同
                    view.setTranslationY(CardConfig.TRANS_Y_GAP * (level - 1));
                    view.setScaleX(1 - CardConfig.SCALE_GAP * (level - 1));
                    view.setScaleY(1 - CardConfig.SCALE_GAP * (level - 1));
                }
            }
        }
    }
}

一个简略的自定义 LayoutManager, generateDefaultLayoutParams 直接抄系统的完成即可;

终究完成的作用如下:

怎样应对Android面试官->我用RecyclerView完成了探探的滑动作用

仿 探探 的作用,gif 好卡…..;

简历润色

深度理解 RecyclerView 的缓存复用原理,可深度定制 LayoutManager;

下一章预告

带你玩转 ViewPager,完成炫酷 Banner;

欢迎三连

来都来了,点个关注,点个赞吧,你的支撑是我最大的动力~~~