美文网首页
ConcatAdapter源码解析

ConcatAdapter源码解析

作者: Magic旭 | 来源:发表于2021-06-10 18:50 被阅读0次

背景

  1. 前面一篇文章介绍了如何使用ConcatAdapter,本篇文章将围绕ConcatAdapter是如何做到合并多个Adapter。(超级缝合怪)

ConcatAdapter源码分析

  1. 分析新的类时候,特别是那个有add和remove的,应该先从这两个地方下手。我们先分析下如何添加和删除的吧。
ConcatAdapter如何添加和移除
    public boolean addAdapter(int index, @NonNull Adapter<? extends ViewHolder> adapter) {
        return mController.addAdapter(index, (Adapter<ViewHolder>) adapter);
    }

    public boolean removeAdapter(@NonNull Adapter<? extends ViewHolder> adapter) {
        return mController.removeAdapter((Adapter<ViewHolder>) adapter);
    }

看了上面代码,可以基本确定ConcatAdapter是一层壳,所有的逻辑操作基本都是在Controller里面完成的,所以我们着重看Controller(ConcatAdapterController)即可。


ConcatAdapterController是什么?

介绍介绍下:ConcatAdapterController是操作多个Adapter聚合到一个RecyclerView的控制类,基本信息包含:存储多个Adapter的数据结构(NestedAdapterWrapper)、查找不同Adapter的位置的数据结构(ViewTypeStorage)等。但这里主要拿这两个来分析大概就能让大家理解ConcatAdapter如何运作的。为了引出下面NestedAdapterWrapper的作用,我们先看一段add的代码。

boolean addAdapter(int index, Adapter<ViewHolder> adapter) {
        ……………………
        NestedAdapterWrapper existing = findWrapperFor(adapter);
        //查找NestedAdapterWrapper,虽然传递进来的是Adapter,但实际上会封装成NestedAdapterWrapper来进行核心逻辑交互
        if (existing != null) {
            return false;
        }
        NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this,
                mViewTypeStorage, mStableIdStorage.createStableIdLookup());
        mWrappers.add(index, wrapper);
        // notify attach for all recyclerview(你传递来的新的adapter要重新attach上已经和ConcatAdapter建立好连接的RecyclerView上)
        for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
            RecyclerView recyclerView = reference.get();
            if (recyclerView != null) {
                adapter.onAttachedToRecyclerView(recyclerView);
            }
        }
      …………………………
        return true;
    }

NestedAdapterWrapper作用
  1. 由上面的代码可以看出,每个通过新添加的adapter最后都会被包装成一个NestedAdapterWrapper。NestedAdapterWrapper可以说是整个ConcatAdapter的核心,现在说明下构造函数里面的参数作用。
  • adapter:持有adapter引用,后面的数据刷新都是通过Wrapper获取adapter去操作的。
  • callBack:这个参数就是整一套设计的精髓所在,该callBack是回调所有添加到ConcatAdapter里的Adapter在数据层面的回调。详情看下面的《监听不同Adapter数据层变化》
  • viewTypeStorage:adapter存储的容器,其中该设计从RecyclerView角度上来看是总体的Adapter,但从ConcatAdapterController角度来说是从总体里面找出每个Adapter的位置,所以说ViewTypeStorage是一个值得一讲的东西。详情看下面《ViewTypeStorage神奇设计》
  • stableIdLookup:这个参数就是提供查找不同adapter对于RecyclerView的位置的工具。多嘴提一句,工程师很友好的命名让我们能更好的理解其中作用。例如localToGlobal,含义就是普通adapter对于ConcatAdapter的位置信息。(🐂🍺)
  1. 简单讲解下Wrapper内部原理:在实例化的时候,创建RecyclerView.AdapterDataObserver,给当前的adapter注册一个数据变化的回调,因为创建Wrapper后会把adapter attach到RecyclerView上,所以回调会在RecyclerView数据源发送变化时候回调相应接口,然后接口会通过callBack回调回到Controller去处理。
private RecyclerView.AdapterDataObserver mAdapterObserver =
               …………………………………………………………
               @Override
                public void onItemRangeInserted(int positionStart, int itemCount) {
                    mCachedItemCount += itemCount;
                    mCallback.onItemRangeInserted(
                            NestedAdapterWrapper.this,
                            positionStart,
                            itemCount);
                    if (mCachedItemCount > 0
                            && adapter.getStateRestorationPolicy() == PREVENT_WHEN_EMPTY) {
                        mCallback.onStateRestorationPolicyChanged(NestedAdapterWrapper.this);
                    }
                }
                …………………………………………………………
            };

提示:Wrapper里面还有个mCachedItemCount私有属性,该属性记录当前adapter有多少个ViewHolder,在下面介绍《监听不同Adapter数据层变化》起着重要作用。


监听不同Adapter数据层变化

上面Wrapper都说了所有的adapter数据层面变化,都会抛到Controller是实现具体逻辑,所以回到ConcatAdapterController里面去查看某个adapter数据层面变化后,如何同步到整体的。我们下面只看数据插入来,其他的都类似,我们要懂的是核心设计思想。

//发送数据变化的adapter通过callBack回调回来的
@Override
    public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
            int positionStart, int itemCount) {
        //从Wrapper
        final int offset = countItemsBefore(nestedAdapterWrapper);
        mConcatAdapter.notifyItemRangeInserted(
                positionStart + offset,
                itemCount
        );
    }
//从当前Controller存储的Wrapper中查找当前发送数据变化的Wrapper(wrapper即adapter,只不过二次封装下)
//找到当前变化的adapter并且返回此adapter之前的holder个数
private int countItemsBefore(NestedAdapterWrapper wrapper) {
        int count = 0;
        for (NestedAdapterWrapper item : mWrappers) {
            if (item != wrapper) {
                count += item.getCachedItemCount();
            } else {
                break;
            }
        }
        return count;
    }

上述代码可以看出,通过countItemsBefore函数获取发送变化的adapter之前有多少个ViewHolder,这个获取ViewHolder数量正是上面提过的mCachedItemCount,每个Wrapper都可以通过接口返回mCachedItemCount数值。核心思想:当前变化adapter开始变化的index+获取排在adapter前面ViewHolder的数量,调用adapter接口,告知RecyclerView刷新哪部分区域的视图。(妙,真的妙)


ViewTypeStorage神奇设计

上面我们分析了ConcatAdapter里面子adapter数据源发送改变,是如何通知RecyclerView去刷新的了。那么我们还有最重要一步就是创建ViewHolder和给ViewHolder绑定数据的?这里就涉及到ViewTypeStorage的神奇地方,从名字都可以看出这个是不同类型的View存储结构了。

一、如何从多个Adapter中返回各自ViewType
下面findWrapperAndLocalPosition核心算法就是通过全局的position去算出对应子Adapter的position,虽然算法不难,但是设计简单合理,挺棒的。获取到对应的adapter和position后,通过不同策略生成与子Adapter的本地ViewType唯一映射关系的全局itemViewType,然后返回给RecyclerView。

提示:不同策略的查询与存储ViewType都是实现了ViewTypeLookup接口的,有兴趣可以自己去看下。这里的本地和全局可以理解成:本地ViewType是从子Adapter角度来看type类型,全局ViewType是从RecyclerView的角度来看type类型。

 //globalPosition就是对于RecyclerView来说整体的位置
public int getItemViewType(int globalPosition) {
        //拿到了全局position对应的adapter信息
        WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
        //根据子Adapter的viewType,递增生成唯一的globalType并且存储着对应的映射关系
        int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition);
        ……
        return itemViewType;
    }

//根据返回的整体position,算出整体position落到哪个adapter上面,计算出对于该adapter的哪个位置,封装一下进行返回
private WrapperAndLocalPosition findWrapperAndLocalPosition(
            int globalPosition
    ) {
        WrapperAndLocalPosition result;
        ……………………
        for (NestedAdapterWrapper wrapper : mWrappers) {
            if (wrapper.getCachedItemCount() > localPosition) {
                result.mWrapper = wrapper;
                result.mLocalPosition = localPosition;
                break;
            }
            localPosition -= wrapper.getCachedItemCount();
        }
       ……………………
        return result;
    }

二、如何创建ViewHolder

  1. 因为ConcatAdapter的createViewHolder直接个传递参数,所以我们直接看Controller的onCreateViewHolder。上面已经从ViewTypeLookup返回了全局唯一的ViewType,那么剩下就是要看下怎么从唯一的ViewType转换成真实的adapter的ViewType了。
  2. Controller的onCreateViewHolder方法实际上只是从存储Wrapper地方,通过全局的viewType找到Wrapper,拿到关联的子Adapter的信息而已。getWrapperForGlobalType 函数只是从mapping映射里直接获取了Wrapper对象而已。该对象的存储在getItemViewType函数调用时候就已经把映射关系存放进去了。
//Controller的onCreateViewHolder
public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) {
        NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType);
        return wrapper.onCreateViewHolder(parent, globalViewType);
    }

//IsolatedViewTypeStorage类的getWrapperForGlobalType方法,直接获取Wrapper对象
public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
            NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get(
                    globalViewType);
            ………………
            return wrapper;
        }
  1. 上面既然拿到了我们的Wrapper对象,那什么数据我们都有了,接下来我们看看怎样根据全局viewType,怎么回调到子Adapter的onCreateViewHolder吧。
//wrapper的onCreateViewHolder
ViewHolder onCreateViewHolder(
            ViewGroup parent,
            int globalViewType) {
        int localType = mViewTypeLookup.globalToLocal(globalViewType);
        return adapter.onCreateViewHolder(parent, localType);
    }

可以从上面看出,其实比较简单,因为LookUp在getItemViewType时候已经建立了全局与本地的映射关系了,所以直接从数据中通过key获取真实的子Adapter的viewType就可以了,然后通过adapter去回调到开发者的代码上。是不是设计上超给力的👍👍👍

三、如何绑定ViewHolder
绑定的代码实际和创建相差不大,这里留给读者自行去看,比较有自己的收获


总结

  1. 官方新推出的ConcatAdapter,也在RecyclerView源码层做了很多工作的,这部分可能讲起来没十天十夜都讲不完,所以这篇文章不会从RecyclerView内部去讲解。
  2. ConcatAdapter非常好用,特别是能将一个列表拆分成多个模块,这样从设计角度来说更为合理,从之前的数值区分样式,变成模块划分,模块划分完再细分成样式。真心不错👍
  3. 大家如果想接入使用的话,可以参加我的另外篇文章:https://www.jianshu.com/p/21a2b004fc96

相关文章

网友评论

      本文标题:ConcatAdapter源码解析

      本文链接:https://www.haomeiwen.com/subject/mqlyeltx.html