Awesome Open Source
Awesome Open Source

前言

RecyclerView是一个大家常用的列表控件,在列表中不免会出现多种类型的布局,这时adapter中多种类型的判断就会充满着switch的坏味道,可怕的是需求变更,增加或修改新的类型时,所有的改动都在adapter中进行,没有一个良好的扩展性。 MutliItem主要就是解决这些问题,提供了多类型和ViewHolder创建绑定的管理器,自动进行item type的计算,这样Adapter通过依赖倒置与列表中的多类型解耦,还提高了扩展性。有以下特点:

  • 直接使用业务中的实体类为RecyclerView Adapter设置数据源,不需要做任何封装
  • RecyclerView Adapter零编码,解放了复杂的Adapter
  • 支持DataBinding,让你清爽的编写列表代码
  • 支持Form表单录入,懒加载易复用,支持DataBinding、隐藏域、输入内容验证及是否变化
  • 支持Header Footer 和 下拉刷新 加载更多
  • 支持Item滑动动画
  • 支持空白、错误等状态页的展示

系列文章

chat headfoot fullspan loadmore 数据绑定效果 Form表单效果 Form表单提交 详情页效果 空白错误页 动画效果 缩放后跨Recycler拖动

下一步要做什么

  • 思考动画分割线等一些功能封装

用法

添加依赖

  • 配置gradle:

Project rootbuild.gradle中添加:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Module中添加:

dependencies {
    compile 'com.github.free46000:MultiItem:0.9.7'
}
  • 或者你也可以直接克隆源码

多种类型列表用法

这里由于单一和多种类型写法上没有差别,所以就不单独贴出单一类型的列表代码了。 注册多种类型ViewHolderManager,并为adapter设置多种类型数据源:

//初始化adapter
BaseItemAdapter adapter = new BaseItemAdapter();
//为TextBean数据源注册ViewHolderManager管理类
adapter.register(TextBean.class, new TextViewManager());
//为更多数据源注册ViewHolderManager管理类
adapter.register(ImageTextBean.class, new ImageAndTextManager());
adapter.register(ImageBean.class, new ImageViewManager());

//组装数据源list
List<Object> list = new ArrayList<>();
list.add(new TextBean("AAA"));
list.add(new ImageBean(R.drawable.img1));
list.add(new ImageTextBean(R.drawable.img2, "BBB" + i));

//为adapter注册数据源list
adapter.setDataItems(list);

recyclerView.setAdapter(adapter);

ViewHolder管理类的子类ImageViewManager类,其他管理类相似,下面贴出本类全部代码,是不是非常清晰:

public class ImageViewManager extends BaseViewHolderManager<ImageBean> {

    @Override
    public void onBindViewHolder(BaseViewHolder holder, ImageBean data) {
        //在指定viewHolder中获取控件为id的view
        ImageView imageView = getView(holder, R.id.image);
        imageView.setImageResource(data.getImg());
    }

    @Override
    protected int getItemLayoutId() {
        //返回item布局文件id
        return R.layout.item_image;
    }
}

相同数据源对应多个ViewHolder(聊天界面)

这是一种特殊的需求,需要在运行时通过数据源中的某个属性,判断加载的布局,典型的就是聊天功能,相同消息数据对应左右两种气泡视图,在此处贴出注册时的关键代码,其他和多种类型列表类似:

//初始化adapter
BaseItemAdapter adapter = new BaseItemAdapter();

//为XXBean数据源注册XXManager管理类组合
adapter.register(MessageBean.class, new ViewHolderManagerGroup<MessageBean>(new SendMessageManager(), new ReceiveMessageManager()) {
    @Override
    public int getViewHolderManagerIndex(MessageBean itemData) {
        //根据message判断是否本人发送并返回对应ViewHolderManager的index值
        return itemData.getSender().equals(uid) ? 0 : 1;
    }
});

recyclerView.setAdapter(adapter);

设置点击监听

点击监听:

adapter.setOnItemClickListener(new OnItemClickListener() {
    @Override
    public void onItemClick(BaseViewHolder viewHolder) {
        //通过viewHolder获取需要的数据
        toastUser(String.format("你点击了第%s位置的数据:%s", viewHolder.getItemPosition()
        , viewHolder.getItemData()));
    }
});

长按监听:

adapter.setOnItemLongClickListener(new OnItemLongClickListener() {
    @Override
    public void onItemLongClick(BaseViewHolder viewHolder) {
        //通过viewHolder获取需要的数据
        toastUser(String.format("你长按了第%s位置的数据:%s", viewHolder.getItemPosition()
                , viewHolder.getItemData()));
    }
});

为列表添加header和footer

添加header footer提供两种方式,直接addView或者addItem方式:

//为XXBean数据源注册XXManager管理类
adapter.register(TextBean.class, new TextViewManager());

//添加header
TextView headView = new TextView(this);
headView.setText("通过addHeadView增加的head1");
//方式一:方便实际业务使用
adapter.addHeadView(headView);
//方式二:这种方式和直接addDataItem添加数据源原理一样
adapter.addHeadItem(new TextBean("通过addHeadItem增加的head2"));

//添加footer,方式同添加header
TextView footView = new TextView(this);
footView.setText("通过addFootView增加的foot1");
adapter.addFootView(footView);
adapter.addFootItem(new TextBean("通过addFootItem增加的foot2"));

为表格添加充满宽度的Item(含header和footer)

充满宽度详见ViewHolderManager#isFullSpan返回true即可,适用于head foot或任意数据源Item

//此处为TextBean数据源注册FullSpanTextViewManager管理类
adapter.register(TextBean.class, new FullSpanTextViewManager());

//添加header或者footer
TextView headView = new TextView(this);
headView.setText("通过addHeadView增加的head1");
//使用HeadFootHolderManager已经实现isFullSpan方法,默认充满宽度
adapter.addHeadView(headView);

//添加普通Item,详见FullSpanTextViewManager,默认充满宽度
adapter.addDataItem(new TextBean("FullSpanTextViewManager充满宽度Item"));

下拉刷新加载更多功能的用法

下拉刷新采用SwipeRefreshLayout这里就不在过多介绍,开启和处理加载更多功能比较简单,但是需要注意加载更多本质上是一个footer,并且对添加顺序敏感,所以需要先去addFoot后在调用开启方法:

//开启加载更多视图
adapter.enableLoadMore(new LoadMoreHolderManager(this::loadData));

//加载完成 isLoadAll:是否全部数据
adapter.setLoadCompleted(boolean isLoadAll);

//加载失败
adapter.setLoadFailed();

通过开启方法我们可以看出依赖于LoadMoreHolderManager,主要是处理不同状态下加载更多界面的变化,下面贴出代码,更多实现细节请参阅LoadMoreManager

/**
 * 加载更多视图管理类
 */
public class LoadMoreHolderManager extends LoadMoreManager {

    public LoadMoreHolderManager(OnLoadMoreListener onLoadMoreListener, boolean isAutoLoadMore) {
        super(onLoadMoreListener, isAutoLoadMore);
    }

    @Override
    protected int getItemLayoutId() {
        return R.layout.item_load_more;
    }

    @Override
    protected void updateLoadInitView() {
        ((TextView) getView(loadMoreView, R.id.text)).setText("");
    }

    @Override
    protected void updateLoadingMoreView() {
        ((TextView) getView(loadMoreView, R.id.text)).setText(R.string.loading_more);
    }

    @Override
    protected void updateLoadCompletedView(boolean isLoadAll) {
        ((TextView) getView(loadMoreView, R.id.text))
                .setText(isLoadAll ? R.string.load_all : R.string.load_has_more);
    }

    @Override
    protected void updateLoadFailedView() {
        ((TextView) getView(loadMoreView, R.id.text)).setText(R.string.load_failed);
    }
}

开启滑动动画

//开启动画,并取消动画只在第一次加载时展示
adapter.enableAnimation(baseAnimation, false);

下面写下动画相关方法,库中已经默认实现了部分BaseAnimation,可以通过Demo看到具体效果

 /**
 * 启动加载动画
 *
 * @param animation    BaseAnimation
 * @param isShowAnimWhenFirstLoad boolean 是否只有在第一次展示的时候才使用动画
 */
public void enableAnimation(BaseAnimation animation, boolean isShowAnimWhenFirstLoad)

/**
 * 设置动画时长 默认400L
 */
public void setAnimDuration(long animDuration)

/**
 * 设置动画加速器 默认{@link LinearInterpolator}
 */
public void setInterpolator(@NonNull Interpolator interpolator)

至此本库的多种类型列表用法已经完成,并没有修改或继承RecyclerView Adapter类,完全使用默认实现BaseItemAdapter即可。

详解

主要流程

  • 为指定的数据源注册ViewHolderManager提供视图创建绑定等工作
  • 在列表创建的过程中通过数据源在ItemTypeManager找到对应的ViewHolderManager
  • 按照需要创建与刷新视图并对视图做一些通用处理

ViewHolder管理

ViewHolder管理源码类为ViewHolderManager,使用者会首先注册数据源和本实例的对应关系,由类型管理类提供统一管理。

  • 提供了参数类,会在adapter调用本类方法的时候传入并做出通用处理
  • 本类的设计使用泛型,是为了在后续回调方法中有更直观的类型体现,这也是强类型和泛型带来的好处,给人在编写代码的时候带来确定感
  • 本类为抽象类需要重写ViewHolder的创建与绑定方法,为了方便后续使用,写了一个简单的BaseViewHolderManager实现类,请读者根据业务自行决定是否需要使用更灵活的基类,这里贴出需要复写的两个方法,延续了Adapter中的命名规则,在使用中减少一些认知成本:
/**
 * 创建ViewHolder
 * {@link android.support.v7.widget.RecyclerView.Adapter#onCreateViewHolder}
 */
@NonNull
public abstract V onCreateViewHolder(@NonNull ViewGroup parent);

/**
 * 为ViewHolder绑定数据
 * {@link android.support.v7.widget.RecyclerView.Adapter#onBindViewHolder}
 *
 * @param t 数据源
 */
public abstract void onBindViewHolder(@NonNull V holder, @NonNull T t);

ViewHolder管理组合(相同数据源对应多个ViewHolderManager)

组合管理源码类为ViewHolderManagerGroup,本实例需要一个ViewHolderManager集合,并增加通过数据源指定哪个ViewHolderManager的方法,使用者同样会注册数据源和本实例的对应关系,由类型管理类对本类中的ViewHolderManager集合进行统一注册管理。下面贴出关键代码:

 private ViewHolderManager[] viewHolderManagers;

/**
 * @param viewHolderManagers 相同数据源对应的所有ViewHolderManager
 */
public ViewHolderManagerGroup(ViewHolderManager... viewHolderManagers) {
    if (viewHolderManagers == null || viewHolderManagers.length == 0) {
        throw new IllegalArgumentException("viewHolderManagers can not be null");
    }
    this.viewHolderManagers = viewHolderManagers;
}

/**
 * 根据item数据源中的属性判断应该返回的对应viewHolderManagers的index值
 *
 * @param itemData item数据源
 * @return index值应该是在viewHolderManagers数组有效范围内
 */
public abstract int getViewHolderManagerIndex(T itemData);

类型管理

类型管理源码类为ItemTypeManager,通过数据源className ListviewHolderManager List两组集合对类型进行管理,并对Adapter提供注册和对应关系查找等方法的支持,这里并没有把这个地方设计灵活,如果有一些变化还是希望可以在ViewHolderManager做出适配。

  • 数据源一对一viewHolderManager时比较简单,关键代码:
 /**
 * 通过数据源`className List`和`viewHolderManager List`两组集合对类型进行管理
 *
 * @param cls     数据源class
 * @param manager ViewHolderManager
 * @see com.freelib.multiitem.adapter.BaseItemAdapter#register(Class, ViewHolderManager)
 */
public void register(Class<?> cls, ViewHolderManager manager) {
    register(getClassName(cls), manager);
}
  • 数据源一对多viewHolderManager时,关键代码:
/**
 * 通过group获取一组ViewHolderManager循环注册,并生成不同的className作为标识<br>
 * 其他类似{@link #register(Class, ViewHolderManager)}
 *
 * @param cls   数据源class
 * @param group 多个ViewHolderManager的组合
 * @see com.freelib.multiitem.adapter.BaseItemAdapter#register(Class, ViewHolderManagerGroup)
 */
public void register(Class<?> cls, ViewHolderManagerGroup group) {
    ViewHolderManager[] managers = group.getViewHolderManagers();
    for (int i = 0, length = managers.length; i < length; i++) {
        register(getClassNameFromGroup(cls, group, managers[i]), managers[i]);
    }
    itemClassNameGroupMap.put(getClassName(cls), group);
}

感谢

在编写中感谢以下开源项目提供了很多思路


Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Java (709,010
Android (40,977
Animation (5,101
Form (2,179
Recyclerview (1,343
Databinding (768
Input (765
Adapter (601
Related Projects