魏子翔, 2017 年加入去哪儿网技术团队, 目前在平台事业部 / 大前端技术中心, 主要负责 Android 平台的基础框架和跨端混合应用方向研发. 关注前沿技术, 热爱分享, 拥抱开源.
前言
RecyclerView 是一个大家常用的列表控件, 在列表中不免会出现多种类型的布局, 这时 Adapter 中多种类型的判断就会充满着 Switch 的坏味道, 可怕的是需求变更, 增加或修改新的类型时, 所有的改动都在 Adapter 中进行, 没有一个良好的扩展性. 为了解决这些问题, 作者把相关代码整理并进行了开源, GitHub 地址: https://github.com/free46000/Multi Item, 在正常使用中做到了 Adapter 零编码, 解放了复杂的 Adapter 类, 本库提供了多类型和 ViewHolder 创建绑定的管理器, 这样 Adapter 通过依赖倒置与列表中的多类型解耦, 还提高了扩展性. 在本库中不同实体类可以直接当成数据源绑定到 Adapter 中, 你不用去担心 Item Type 的计算, 并且对每种类型的 ViewHolder 也做到了隔离. 下面首先来看下用法, 然后会有具体详解.
效果截图
用法
多种类型列表用法
这里由于单一和多种类型写法上没有差别, 所以就不单独贴出单一类型的列表代码了. 注册多种类型 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 管理类的子类 TextViewManager 类, 其他类相似, 下面贴出本类全部代码, 是不是非常清晰:
- 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;
- }
- }
至此本库的多种类型列表用法已经完成, 并没有修改或继承 RecyclerView Adapter 类, 完全使用默认实现 BaseItemAdapter 即可.
相同数据源对应多个 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"));
下拉刷新加载更多功能的用法
下拉刷新采用 SwipeRefreshLayout 这里就不在过多介绍, 开启和处理加载更多功能比较简单, 但是需要注意加载更多本质上是一个 Footer, 并且对添加顺序敏感, 所以需要先去 AddFoot 后在调用开启方法:
- // 开启加载更多视图
- adapter.enableLoadMore(new LoadMoreHolderManager(this::loadData));
- // 加载完成 isLoadAll: 是否全部数据
- adapter.setLoadCompleted(boolean isLoadAll);
- // 加载失败
- adapter.setLoadFailed();
通过开启方法我们可以看出依赖于 LoadMoreHolderManager, 主要是处理不同状态下加载更多界面的变化, 下面贴出代码, 更多实现细节请参阅 LoadMore Manager:
- /**
- * 加载更多视图管理类
- */
- 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)
详解
主要流程
为指定的数据源注册 ViewHolderManager 提供视图创建绑定等工作; 在列表创建的过程中通过数据源在 ItemTypeManager 找到对应 ViewHolder Manager; 按照需要创建与刷新视图并对视图做一些通用处理.
ViewHolder 管理
ViewHolder 管理源码类为 ViewHolder Manager, 使用者会首先注册数据源和本实例的对应关系, 由类型管理类提供统一管理. 提供了参数类, 会在 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 管理组合
组合管理即相同数据源对应多个 ViewHolder Manager, 源码类为 ViewHolderManager Group, 本实例需要一个 ViewHolder Manager 集合, 并增加通过数据源指定哪个 ViewHolderManager 的方法, 使用者同样会注册数据源和本实例的对应关系, 由类型管理类对本类中的 ViewHolder Manager 集合进行统一注册管理. 下面贴出关键代码:
- 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 时比较简单, 关键代码:
- /**
- * 通过数据源 `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);
- }
感谢
在编写中感谢以下开源项目提供了很多思路: https://github.com/wasabeef/recyclerview-animators https://github.com/drakeet/MultiType
来源: https://juejin.im/entry/5c06837151882508082597f9