如何应对Android面试官->我用RecyclerView实现了探探的滑动效果

前言

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

如何复用原理

我们在滑动的时候,才会触发 RecyclerView 的回收复用,所以我们从 RecyclerView 的 onTouchEvent 方法入手;我们来看下滑动的时候,是怎么和 LayoutManager 关联起来的;

我们进入 onTouchEvent 的 ACTION_MOVE 看下:

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

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

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

我们进入这个 scrollStep 方法看下:

根据滑动方向,分别调用了 LayoutManager 不同的方法,我们选择其中一个进入看下:

我们选择 LinearLayoutManager 的 scrollVerticalcallBy 方法看下:

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

这里直接调用了 scrollBy 方法,我们进入这个方法看一下:

我们进入这个 fill 方法看下:

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

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

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

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

arduino 复制代码
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 缓存池

第一次缓存复用

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

与动画相关的,通过位置从 mChangeScrp 中获取;

第二次缓存复用

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

针对位置的,通过位置从 mAttachedScrap 和 mCachedViews 中获取;

ini 复制代码
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),        
    type, dryRun);

还是针对 mAttachedScrap 和 mCachedViews 中获取的,通过 ViewType 和 ItemId来区分,所以这个也属于第二次缓存复用;

第三次缓存复用

scss 复制代码
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 的创建和缓存,一般用不到;

第四次缓存复用

ini 复制代码
holder = getRecycledViewPool().getRecycledView(type);

从缓存池中获取;

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

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

ini 复制代码
if (holder == null) { 
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
}

创建之后,就是进行 ViewHolder 的绑定流程了;

ini 复制代码
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

最终就会调用到 onBindViewHolder 方法;

整体时序图如下:

如何缓存原理

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

arduino 复制代码
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 方法看下:

我们进入这个 dispatchLayoutStep2 方法看下,这个方法最终调用到了

ini 复制代码
mLayout.onLayoutChildren(mRecycler, mState);

我们进入 LinearLayoutManager 的 onLayoutChildren 方法看下,这个方法最终调用到了 detachAndScrapAttachedViews 这个方法,我们进入这个方法看下:

ini 复制代码
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);    
    }
}

这里调用了 recycler.recycleViewHolderInternal(viewHolder); 和 recycler.scrapView(view); 我们分别看下;

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

如果 ViewHolder remove、update 等发生变化的时候,不执行缓存逻辑;

recycleCachedViewAt 处理的就是 mCacheView;

scss 复制代码
// 如果 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 方法的实现;

我们进入 putRecycledView 方法中看下:

先获取 viewType,然后根据 viewType 获取 ScrapData,然后获取它的 scrapHeap 集合;也就是我们的 ViewHolder 是存放在 ScrapData 中了;

我们来看下 getScrapDataForType 的方法实现:

kotlin 复制代码
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
    return;
} 

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

ini 复制代码
scrap.resetInternal();

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

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

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

整体时序图如下:

自定义LayoutManager

我们如果想实现探探的左滑右滑效果,需要自定义 LayoutManager;

scss 复制代码
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 直接抄系统的实现即可;

最终实现的效果如下:

仿 探探 的效果,gif 好卡.....;

简历润色

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

下一章预告

带你玩转 ViewPager,实现炫酷 Banner;

欢迎三连

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

相关推荐
路在脚下@3 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
森屿Serien6 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
C4rpeDime1 小时前
自建MD5解密平台-续
android
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
ssr——ssss2 小时前
SSM-期末项目 - 基于SSM的宠物信息管理系统
java·ssm