MultiItem

Project Url: free46000/MultiItem
Introduction: 一个优雅的实现多类型的 RecyclerView 类库 支持 DataBinding Form 表单录入
More: Author   ReportBugs   
Tags:
recyclerview-databinding-form-input-animation-

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提供两种方式,直接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"));

充满宽度详见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);
}

感谢

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

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools