MultiType源码解析

一.前言

         首先我想说一下我为什么要写这篇文章,还在上家公司的时候我就一直想熟悉关于RecycleView多Item类型框架的原理,然后想熟悉后自己也能整一个出来。但是由于一直写需求,然后就慢慢拖了下来。现在刚加入了一家新的公司,我在熟悉代码的过程中发现这个项目的多种类型的界面使用的都是ScollView+ListView的嵌套使用。这样这些法有明显的问题就是需要重写一个不可滑动的ListView,然后将其嵌套在ScrollView中,这样就避免了滑动冲突的问题了。但是这样ListView的缓存优势就没了,需要将Adapter中的item全部绘制出来,对内存的影响是不可忽视。今天这里就介绍的是复杂布局情况下使用RecycleView去解决问题。

二.常规写法

      熟悉使用RecycleView的人的知道,我们可以通过重写方法getItemViewType(),根据这个方法返回的position拿到当前的Item对象,然后判断当前Position是哪种ViewType,返回一个固定Int值的ViewType类型,然后在OnCreatViewHolder方法中根据ViewType去inflater不同的布局,每一个种类型的Layout都对应着一个ViewHolder这个自己对应着去新建一个。最后一步就是赋值操作在onBindViewHolder通过参数Holder去给不同的ViewType设置值,具体代码实现请接着看:

public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final int ITEM_TYPE_HEADER = 1;
    private final int ITEM_TYPE_CONTENT = 2;
    private final int ITEM_TYPE_FOOTER = 3;
    public ArrayList<data> mList;

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
        if (viewType == ITEM_TYPE_HEADER) {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_v1, parent, false);
            return new HeaderViewHolder(view);
        } else if (viewType == ITEM_TYPE_CONTENT) {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_v2, parent, false);
            return new ContentViewHolder(view);
        } else {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_v3, parent, false);
            return new FooterViewHolder(view);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof FooterViewHolder) {
            FooterViewHolder viewHolder = (FooterViewHolder) holder;

        } else if (holder instanceof ContentViewHolder) {
            ContentViewHolder viewHolder = (ContentViewHolder) holder;
            
        } else if (holder instanceof HeaderViewHolder) {
            HeaderViewHolder viewHolder = (HeaderViewHolder) holder;
            
        }
    }


    class FooterViewHolder extends RecyclerView.ViewHolder {
        public ImageView mView;

        public FooterViewHolder(View itemView) {
            super(itemView);
            mView = (ImageView) itemView.findViewById(R.id.view3);
        }
    }

    class ContentViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;

        public ContentViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.view2);
        }
    }

    class HeaderViewHolder extends RecyclerView.ViewHolder {
        public Button mButton;

        public HeaderViewHolder(View itemView) {
            super(itemView);
            mButton = (Button) itemView.findViewById(R.id.view3);

        }
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return ITEM_TYPE_HEADER;
        } else if (position == 1) {
            return ITEM_TYPE_CONTENT;
        } else {
            return ITEM_TYPE_FOOTER;
        }
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }
}

    通过上面的代码我们也知道了如何去写简单多类型的条目了。但是这样不够的,虽然实现原理是这样,用过的人都知道这样的写法遇到一个复杂的页面,比如电商的首页这样的写的话,这个MultiTypeAdapter估计得写个几千行,好这样也可以写出来,但是现在需求改变了,首页增加了个比如广告栏的需求,那这个时候再去改怎么办,看见这么多代码都在一起,估计自己也要疯掉了。那么下面今天的主角出现了MultiType出现了。网上还有很多类似的封装库,而我为什么偏偏独爱他呢?

1.Issues解决的快,你会发现提的问题,作者会很快给解决了。

2.作者对代码的追求深深的吸引了我。

因为本篇主要是讲原理的,所以不谈使用了,MultiType的使用作者drakeet写的已经足够详细了,点击查看使用,看过使用的人我相信你一定也会想着看下这源码到底是怎么写出来的? 那么下面就由我来介绍源码的实现:

四.源码分析

 

1.设计思想

    本段是引用作者使用文档里面的部分drakeet,点击查看使用

MultiType 设计伊始,我给它定了几个原则:

  • 要简单,便于他人阅读代码

    因此我极力避免将它复杂化,避免加入许多不相干的内容。我想写人人可读的代码,使用简单精巧的方式,去实现复杂的需求。过多不相干、没必要的代码,将会使项目变得令人晕头转向,难以阅读,遇到需要定制、解决问题的时候,无从下手。

  • 要灵活,便于拓展和适应各种需求

    很多人会得意地告诉我,他们把 MultiType 源码精简成三四个类,甚至一个类,以为代码越少就是越好,这我不能赞同。MultiType 考虑得更远,这是一个提供给大众使用的类库,过度的精简只会使得大幅失去灵活性。它或许不是使用起来最简单的,但很可能是使用起来最灵活的,均衡性最好的。 在我看来,”直观”、”灵活”优先级大于”简单”。因此,MultiType 以接口或抽象进行连接,这意味着它的角色、组件都可以被替换,或者被拓展和继承。如果你觉得它使用起来还不够简单,完全可以通过继承封装出更具体符合你使用需求的方法。它已经暴露了足够丰富、周到的接口以供拓展,我们不应该直接去修改源码,这会导致一旦后续发现你的精简版满足不了你的需求时,已经没有回头路了。

  • 要直观,使用起来能令项目代码更清晰可读,一目了然

    MultiType 提供的 ItemViewBinder 沿袭了 RecyclerView Adapter 的接口命名,使用起来更加舒适,符合习惯。另外,MultiType 很多地方放弃使用反射而是让用户显式指明一些关系,如:MultiTypeAdapter#register 方法,需要传递一个数据模型 class 和 ItemViewBinder 对象,虽然有很多方法可以把它精简成单一参数方法,但我们认为显式声明数据模型类与对应关系,更加直观。

   之所以引用作者这段话,我是觉得写的太好了,也是吸引我看完了作者源码的主要原因,下面开始介绍源码的相关逻辑。

1.MultiTypeAdapter

      MultiTypeAdapter继承RecycleView.Adapter,这个类和我们平时写普通的Adapter差不多下面我们看具体的方法:


  public MultiTypeAdapter() {
    this(Collections.emptyList());
  }
   /**
    *默认会初始换一个MultiTypePool();
    */
  public MultiTypeAdapter(@NonNull List<?> items) {
    this(items, new MultiTypePool());
  }
   /**
    *初始换一个MultiTypePool()同时,给了一个默认集合的长度;
    */
  public MultiTypeAdapter(@NonNull List<?> items, int initialCapacity) {
    this(items, new MultiTypePool(initialCapacity));
  }
   /**
    *这里说明的是我们自己可以自定义一个TypePooL.
    */
  public MultiTypeAdapter(@NonNull List<?> items, @NonNull TypePool pool) {
    checkNotNull(items);
    checkNotNull(pool);
    this.items = items;
    this.typePool = pool;
  }

我们一般使用的时候都会使用默认的MutiTypePool,这个类主要的作用就是记录我们的TypePool,分别有classes(对应着数据Data.class),binders(当前的ItemViewBinder),linkers(实现一个类型对应多个 ItemViewBinder的时候使用),三种,在MultiTypeAdapter创建的时候,这三个集合都已经创建好了。MultiTypePool的构造方法如下:

  /**
   * Constructs a MultiTypePool with default lists.
   */
  public MultiTypePool() {
    this.classes = new ArrayList<>();
    this.binders = new ArrayList<>();
    this.linkers = new ArrayList<>();
  }

下面接着说MultiTypeAdapter类中的register方法,这个方法使用时就是传递Data.class,与之对应的ItemViewBinder具体的写法是这样:

    adapter = new MultiTypeAdapter();
    adapter.register(Category.class, new CategoryItemViewBinder());

    adapter.register(Post.class, new PostViewBinder());
    adapter.register(PostList.class, new HorizontalPostsViewBinder());
 public <T> void register(@NonNull Class<? extends T> clazz, @NonNull ItemViewBinder<T, ?> binder) {
    checkNotNull(clazz);
    checkNotNull(binder);
    checkAndRemoveAllTypesIfNeeded(clazz);
    register(clazz, binder, new DefaultLinker<T>());
  }


  <T> void register(
      @NonNull Class<? extends T> clazz,
      @NonNull ItemViewBinder<T, ?> binder,
      @NonNull Linker<T> linker) {
//这里调用了typePool的register方法,目的将当前新注册的Data.class 加入到集合之中
    typePool.register(clazz, binder, linker);
    binder.adapter = this;
  }

    TypePool是一个接口,我们看他的具体实现类都干了什么,他的实现类是MultiType,我们看他的实现方法都干了些什么

 @Override
  public <T> void register(
      @NonNull Class<? extends T> clazz,
      @NonNull ItemViewBinder<T, ?> binder,
      @NonNull Linker<T> linker) {
    checkNotNull(clazz);
    checkNotNull(binder);
    checkNotNull(linker);
    classes.add(clazz);
    binders.add(binder);
    linkers.add(linker);
  }

从上面代码我们看到的就是把当前的Data.class,和ItemViewBinder加入到集合里面,集合是ArrayList一个有序的集合。以上就是我们使用的时候要做的两种操作在源码中所走的路程。下面我们就看MultiType是如何做到分发条目类型的。上面我们都知道了,我们想知道当前条目是是什么类型,首先我们是根据getItemViewType()方法更具position去决定当前是哪一种类型,所以我们看下MultiTypeAdapter方法中getItemViewType()做了什么:

  @Override
  public final int getItemViewType(int position) {
    Object item = items.get(position);
    return indexInTypesOf(position, item);
  }

在使用过程中我们需要将一个条目加入items.add(Category);所以在取得时候,我们拿到了当前的Category.class对象,然后我们接着看 indexInTypesOf(position, item)方法;

  /**
   *这个方法主要就是返回当前条目的类型,条目类型是通过Int值来控制
   *Linker是一对多的时候使用,我们这里就不做解释,我们默认使用一对一的,所以 
   *linker.index(position, item)的值就会一直是0
   **/

int indexInTypesOf(int position, @NonNull Object item) throws BinderNotFoundException {
   

 int index = typePool.firstIndexOf(item.getClass());
    if (index != -1) {
      @SuppressWarnings("unchecked")
      Linker<Object> linker = (Linker<Object>) typePool.getLinker(index);
      return index + linker.index(position, item);
    }
    throw new BinderNotFoundException(item.getClass());
  }
 @Override
  public int firstIndexOf(@NonNull final Class<?> clazz) {
    checkNotNull(clazz);
    int index = classes.indexOf(clazz);
    if (index != -1) {
      return index;
    }
    for (int i = 0; i < classes.size(); i++) {
      if (classes.get(i).isAssignableFrom(clazz)) {
        return i;
      }
    }
    return -1;
  }

通过firstIndexOf方法我们也看出来了,就是去我们的存的集合里面找到当前位置的clazz对象对应的index,下面的for循环同样是处理一对多的,我这里没有解释是因为我觉得我们使用一对一的模式就可以解决所有的问题了。

现在我们确定了有多少条目类型,那么现在就该我们根据类型去适配不同的布局了吧,同样是在MultiTypeAdapter方法中,我们要查看onCreateViewHolder方法,应为是在这个方法里面处理了当前view的inflater工作。

  @Override
  public final ViewHolder onCreateViewHolder(ViewGroup parent, int indexViewType) {
    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    //这个地方就是关键,通过indexViewType我们去typePool里面找到当前type对应的ItemViewBinde
    ItemViewBinder<?, ?> binder = typePool.getItemViewBinder(indexViewType);
    return binder.onCreateViewHolder(inflater, parent);
  }

最后一行调用了ItemViewBinder的onCreateViewHolder方法,这个方法在ItemViewBinder里面是一个抽象方法,所以需要我们自己来重写,这个时候就相当于我们向框架提供了一个ViewHolde和ItemVIew;其实还有个typePool.getItemViewBinder(indexViewType)方法,这个就是通过当前的类型,去找到我们之前存的里面里面对应的ItemViewBinder,我就不做详细解释了。

下面我们就看最后一个重要方法onBindViewHolder()这个是数据绑定的方法,就是对当前控件进行赋值的操作。

  @Override @SuppressWarnings("unchecked")
  public final void onBindViewHolder(ViewHolder holder, int position, @NonNull List<Object> payloads) {
    Object item = items.get(position);
    ItemViewBinder binder = typePool.getItemViewBinder(holder.getItemViewType());
    binder.onBindViewHolder(holder, item, payloads);
  }

这个方法很好理解了,上面都说过了,一个是找到对应position的clazz,传递给我们自己使用,typePool.getItemViewBinder(holder.getItemViewType());这个和onCreate方法是一致的,找到与之对应的binder对象,调用我们继承后的类,然后在进行赋值。

写我觉得看到这里了基本上能够理解框架的作用了,还有一些最主要的两个类吧MultiTypeAdapter,和MultiTypePool,这两个类的方法都需要理解清楚了。还有就是Linker这个接口我没有说,因为不去使用这个我也同样能实现一样的功能,有兴趣的自己可以去查看源码。

 

五.结语

     本人能力有限,如果有哪位道友发现我的错误,很感谢能够给我指出来。

 

    原文作者:Fd你好
    原文地址: https://blog.csdn.net/fdadala/article/details/82049915
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞