如何应对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;

欢迎三连

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

相关推荐
腥臭腐朽的日子熠熠生辉43 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
Harrison_zhu44 分钟前
Ubuntu18.04 编译 Android7.1代码报错
android
ejinxian44 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之1 小时前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen2 小时前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
松韬2 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存