# MultiItem **Repository Path**: junhu11/MultiItem ## Basic Information - **Project Name**: MultiItem - **Description**: 一个优雅的实现多类型的RecyclerView类库 支持DataBinding Form表单录入 跨多个RecyclerView拖动 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-12 - **Last Updated**: 2021-11-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 前言 `RecyclerView`是一个大家常用的列表控件,在列表中不免会出现多种类型的布局,这时`adapter`中多种类型的判断就会充满着`switch`的坏味道,可怕的是需求变更,增加或修改新的类型时,所有的改动都在`adapter`中进行,没有一个良好的扩展性。 `MutliItem`主要就是解决这些问题,提供了多类型和`ViewHolder`创建绑定的管理器,自动进行`item type`的计算,这样`Adapter`通过依赖倒置与列表中的多类型解耦,还提高了扩展性。有以下特点: - 直接使用业务中的实体类为`RecyclerView Adapter`设置数据源,不需要做任何封装 - `RecyclerView Adapter`零编码,解放了复杂的`Adapter`类 - 支持`DataBinding`,让你清爽的编写列表代码 - 支持Form表单录入,懒加载易复用,支持`DataBinding`、隐藏域、输入内容验证及是否变化 - 支持`Header` `Footer` 和 下拉刷新 加载更多 - 支持`Item`滑动动画 - 支持空白、错误等状态页的展示 ## 系列文章 - **用法与详解(详见下文)** - **[MultiItem进阶 实现Head Foot和加载更多](https://juejin.im/post/58da77ed1b69e6006bc7fffa)** - **[MultiItem进阶 使用DataBinding让RecyclerView代码更简洁清爽](https://juejin.im/post/58ea5e88b123db1ad0636295)** - **[MultiItem进阶 用RecyclerView实现Form表单](https://juejin.im/post/58ea5e88b123db1ad0636295)** - **[MultiItem进阶 用RecyclerView实现错误和空白状态页 附加详情实践](https://juejin.im/post/58fe298d8d6d810058a0f2a5)** - **[仿任务面板 跨RecyclerView的Item拖动 支持缩小后拖动](https://juejin.im/post/58e37dae0ce46300583b4ab0)** ![chat](https://github.com/free46000/cloud/raw/master/multiitem/chat.png) ![headfoot](https://user-gold-cdn.xitu.io/2017/3/29/1846a3cd1a81b9b0bb516402d1cee6aa.png ) ![fullspan](https://user-gold-cdn.xitu.io/2017/3/29/59b465977da7c11b455a9998143e5e2a.png ) ![loadmore](https://user-gold-cdn.xitu.io/2017/3/29/7b50786340f253d2f8e5f7966cfd7fc1.png ) ![数据绑定效果](https://user-gold-cdn.xitu.io/2017/4/8/909aaf58179acc6abb55d79ae188f09e.png) ![Form表单效果](https://github.com/free46000/cloud/raw/master/multiitem/form.png) ![Form表单提交](https://github.com/free46000/cloud/raw/master/multiitem/form_submit.png) ![详情页效果](https://github.com/free46000/cloud/raw/master/multiitem/user_info.png) ![空白错误页](https://github.com/free46000/cloud/raw/master/multiitem/empty_error.gif) ![动画效果](https://github.com/free46000/cloud/raw/master/multiitem/anim.gif) ![缩放后跨Recycler拖动](https://github.com/free46000/cloud/raw/master/multiitem/panel_drag_scale.gif) ## 下一步要做什么 - 思考动画分割线等一些功能封装 ## 用法 #### 添加依赖 - 配置gradle: 在`Project root`的`build.gradle`中添加: ``` allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` 在`Module`中添加: ``` dependencies { compile 'com.github.free46000:MultiItem:0.9.7' } ``` - 或者你也可以直接克隆源码 #### 多种类型列表用法 这里由于单一和多种类型写法上没有差别,所以就不单独贴出单一类型的列表代码了。 注册多种类型`ViewHolderManager`,并为`adapter`设置多种类型数据源: ``` java //初始化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 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`类,其他管理类相似,下面贴出本类全部代码,是不是非常清晰: ``` java public class ImageViewManager extends BaseViewHolderManager { @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(聊天界面) 这是一种特殊的需求,需要在运行时通过数据源中的某个属性,判断加载的布局,典型的就是聊天功能,相同消息数据对应左右两种气泡视图,在此处贴出注册时的关键代码,其他和多种类型列表类似: ``` java //初始化adapter BaseItemAdapter adapter = new BaseItemAdapter(); //为XXBean数据源注册XXManager管理类组合 adapter.register(MessageBean.class, new ViewHolderManagerGroup(new SendMessageManager(), new ReceiveMessageManager()) { @Override public int getViewHolderManagerIndex(MessageBean itemData) { //根据message判断是否本人发送并返回对应ViewHolderManager的index值 return itemData.getSender().equals(uid) ? 0 : 1; } }); recyclerView.setAdapter(adapter); ``` #### 设置点击监听 点击监听: ``` java adapter.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(BaseViewHolder viewHolder) { //通过viewHolder获取需要的数据 toastUser(String.format("你点击了第%s位置的数据:%s", viewHolder.getItemPosition() , viewHolder.getItemData())); } }); ``` 长按监听: ``` java 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`方式: ``` java //为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` ``` java //此处为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`后在调用开启方法: ``` java //开启加载更多视图 adapter.enableLoadMore(new LoadMoreHolderManager(this::loadData)); //加载完成 isLoadAll:是否全部数据 adapter.setLoadCompleted(boolean isLoadAll); //加载失败 adapter.setLoadFailed(); ``` 通过开启方法我们可以看出依赖于`LoadMoreHolderManager`,主要是处理不同状态下加载更多界面的变化,下面贴出代码,更多实现细节请参阅`LoadMoreManager`: ``` java /** * 加载更多视图管理类 */ 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); } } ``` #### 开启滑动动画 ``` java //开启动画,并取消动画只在第一次加载时展示 adapter.enableAnimation(baseAnimation, false); ``` 下面写下动画相关方法,库中已经默认实现了部分`BaseAnimation`,可以通过Demo看到具体效果 ``` java /** * 启动加载动画 * * @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`中的命名规则,在使用中减少一些认知成本: ``` java /** * 创建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`集合进行统一注册管理。下面贴出关键代码: ``` java 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 List`和`viewHolderManager List`两组集合对类型进行管理,并对`Adapter`提供注册和对应关系查找等方法的支持,这里并没有把这个地方设计灵活,如果有一些变化还是希望可以在`ViewHolderManager`做出适配。 - 数据源一对一`viewHolderManager`时比较简单,关键代码: ``` java /** * 通过数据源`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`时,关键代码: ``` java /** * 通过group获取一组ViewHolderManager循环注册,并生成不同的className作为标识
* 其他类似{@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); } ``` ## 感谢 在编写中感谢以下开源项目提供了很多思路 - https://github.com/wasabeef/recyclerview-animators - https://github.com/drakeet/MultiType