RecyclerView 的封装

2,702 阅读12分钟

RecyclerView 是 Android 开发中常用的一个列表组件, 是一个更高级的, 更灵活的 ListView,其使用起来比较复杂, 但是非常灵活, 为了方便我们的使用, 我们可以对 RecyclerViewAdapter 以及 ViewHolder 进行封装, 以方便日后开发过程中的使用.

封装来自于 Mooc 课程学习 慕课网

RecyclerView 的基本使用

使用一个 RecyclerView 需要以下几个组件:

  • RecyclerView: RecyclerView 组件作为整个列表的容器, 用于盛放列表内容.
  • LayoutManager: 布局管理器, 用于管理 RecyclerView 中元素的排列方式, 使用不同的布局管理器可以实现不同的布局效果, 例如线性布局, 瀑布流等, Android SDK 提供了一些默认的布局管理器, 例如 LinearLayoutManager, GridLayoutManager 等, 我们也可以自定义 LayoutManager.
  • ViewHolder: 每一个 ViewHolder 管理一个 View, ViewHolder 由 RecyclerView.Adapter 管理, 每个 RecyclerView 并不会为每一个 View 生成一个 ViewHolder, 屏幕上需要显示多少 View, 会生成比这个稍微多一点的 ViewHolder. ViewHolder 与 View 之间有一个绑定的概念, 当列表滑动时, 划出屏幕的 View 会解绑定, 将新划入的数据绑定到 ViewHolder 上.
  • RecyclerView.Adapter: Adapter 完成创建 ViewHolder, 将 ViewHolder 与数据绑定的操作.

我们使用 RecyclerView 时, 最关键的就是继承 RecyclerView.Adapter, 实现自己的 Adapter. Adapter 需要完成的任务包括:

  • 获取数据 item 的总大小 (getItemCount())
  • 根据不同的数据坐标 (position), 获取不同的 viewType, 即一个 RecyclerView 可以包含不同的展现形式.
  • 根据不同的 viewType, 在需要时创建 ViewHolder (实现 onCreateViewHolder() 方法)
  • 在需要时绑定 ViewHolder 与具体的数据 (onBindView())

一个 ViewHolder 包含一个具体的 View, 我们应该继承 RecyclerView.ViewHolder 来实现我们自己的 ViewHolder.

RecyclerView 形象描述

上面是一个非常抽象的概念的描述, 下面我尝试使用一种形象的比喻来介绍一下 RecyclerView 的相关概念, 以便理解.

我们可以假设这样一个场景, 我们有一个仓库, 里面存放着各种各样的货物, 同时, 我们有一个橱窗, 可以同时展示一定数量的货物. 查看货物的人可以在橱窗上通过某种方式进行操控, 来切换橱窗所展示的货物.

为了实现这个功能, 橱窗后面有一排传送带 (ViewHolder), 传送带上面每一个位置上放这一个待展示的货物, 当用户对橱窗进行操作时, 传送带转动, 依次切换所展示的货物.

当然, 我们仓库中的货物数量通常是远大于橱窗所能展示的货物的数量的, 为了实现上述的功能, 我们传送带的长度并不需要很长, 只需要比橱窗所能展示的货物数量稍微长一点就可以, 形象一点, 我们可以想象一下, 传送带是一个环形的, 就像是机场的行李运送传送带一样.

为了让用户在橱窗进行切换操作时, 货物的展示是按一定顺序变化的, 我们需要一个管理员 (RecyclerView.Adapter) 来不停的装卸货物, 使得外面的用户在橱窗上看, 就好像后面有一个无限长的传送带在传送货物一样.

上面就是根据我对 RecyclerView 的理解, 构建的一个形象的场景描述, 下面我们来分析一下 RecyclerView 对应的各个操作.

ViewHolder

ViewHolder 对应的就是传送带上一个个的盛放货物的模块, 在创建 ViewHolder 时, 我们就确定了这个 ViewHolder 会盛放什么类型的货物, 因此每个 ViewHolder 在创建的时候, 就需要传入一个具体的 itemView.

这里这个概念可以更加细分下去, 例如, 我们所要展示的数据是一件漂亮的衣服 (data), 我们的传送带 (ViewHolder) 上面还要盛放一个用于挂衣服的衣架 (itemView), 我们在创建 ViewHolder 的时候, 并不知道要挂的是哪一件衣服 (并不知道具体的数据, 是可以复用的), 但是我们知道上面挂的一定是一件衣服.

ViewHolder 装入数据的过程, 就是往这个衣架上挂上具体的衣服的过程, 对应的是 Adapter 的 onBindViewHolder() 的过程.

onCreateViewHolder()

一开始, 我们传送带还是空的, 当我们需要展示数据的时候, 我们的仓库管理员 (Adapter), 会根据所要展示数据的类型 (类型通过 getItemViewType() 获取), 生成对应的 itemView (如果是一件衣服, 就生成一个衣架, 如果是一朵花, 就生成一个花瓶, 等等), 但是这时, 这个 itemView 还没有任何数据 (衣架上还没挂衣服, 花瓶里还没插上花), 同时, 在传送带上加上一截, 放上这个 itemView.

onBindViewHolder()

当传送带快转到橱窗的时候, 我们需要给传送带上面放上合适的数据了, 管理员 (Adapter) 会时刻关注当前传送带转动的位置, 以确定下一个 ViewHolder 上应该放编号为多少的数据. 例如, 管理员确定了下一个传送带上应该放编号为 88 的数据, 通过 getItemViewType(int position), 其中 position = 88, 就可以知道这是一朵玫瑰花, 那么, 管理员会看一下, 还有没有空闲的花瓶 (ViewHolder), 如果没有, 那就创建一个, 如果有的话, 就把原来花瓶里的花拿出来, 放入新的花. 这其实就是一个将 ViewHolder 与具体的数据绑定的过程, 实现了 ViewHolder 的复用.

其他操作

Adapter, 也就是仓库管理员还应该完成对仓库里面货物的管理, 比如, 添加新的货物, 移除已有的货物, 清空货物等等操作, 在完成这些操作的过程中, 还要保证橱窗里面显示的货物是正确的.

这其实就对应的是, 我们对数据集合进行的增删改等操作, 在完成操作之后, 我们应该调用 notify_ 这一系列的正确的函数, 进行数据正确显示的检查, 以保证我们在改变了数据集合之后, 仍然显示的是正确的数据.

BaseRecyclerAdapter 的封装

完整代码点击此处.

继承和泛型

public abstract class BaseRecyclerAdapter<Data>
        extends RecyclerView.Adapter<BaseRecyclerAdapter.BaseViewHolder<Data>>
        implements View.OnClickListener, View.OnLongClickListener, AdapterCallback<Data> {
...
}

RecyclerAdapter 是继承自 RecyclerView.Adapter 的, 我们取名为 BaseRecyclerAdapter.

BaseRecyclerAdapter 指定了一个泛型 <Data>, 用于指定 RecyclerAdapter 中数据的类型. 所继承的 RecyclerView.Adapter 需要指定一个泛型, 是 ViewHolder 的具体类型, 这里我们的 ViewHolderRecyclerAdapter 内部类的形式创建, 因此指定为 <BaseRecyclerAdapter.BaseViewHolder<Data>>

这里 BaseRecyclerAdapter 还实现了两个接口, 用于处理点击事件, 这部分会在后面进行介绍.

除此之外, BaseRecyclerAdapter 还实现了 AdapterCallback, 这是一个我们定义的 ViewHolder 用于处理数据更新的接口.

/**
 * @author Dcr
 */
public interface AdapterCallback<Data> {
    /**
     * 更新数据回调
     * @param data 更新的数据
     * @param holder BaseViewHolder
     */
    void update(Data data, BaseRecyclerAdapter.BaseViewHolder<Data> holder);
}

成员变量

private final List<Data> mDataList;
private AdapterListener<Data> mListener;

BaseRecyclerAdapter 有两个私有成员:

mDataList 是一个 List 类型, 表示的是列表的数据, 也就是我们上面讲解中所指定的货仓, 表示所有待展示的数据.

另一个成员变量 mListener 是我们构造的用于处理点击事件的 Listener, 会在后面介绍.

构造方法

BaseRecyclerAdapter 的两个私有成员对应着三个构造方法.

public BaseRecyclerAdapter() {
    // 空数据
    mDataList = new ArrayList<>();
}

public BaseRecyclerAdapter(@NonNull List<Data> dataList) {
    this(dataList, null);
}

public BaseRecyclerAdapter(@NonNull List<Data> dataList, AdapterListener<Data> listener) {
    mDataList = Objects.requireNonNull(dataList);
    mListener = listener;
}

这里构造方法中, 我用到了 Objects.requireNonNull() 方法和 @NonNull 注解, 意味着, 如果使用了带参数的构造方法, 就不能传入空列表, 如果没有数据, 就使用空构造方法, 通过这样的限制, 让用户在使用构造方法时, 明确的指定, 避免一不小心传入空指针的错误.

BaseViewHolder

从上面的分析, 我们知道, ViewHolder 是承载 View 的重要角色, 我们需要继承 RecyclerView.ViewHolder 来实现我们的 ViewHolder.

/**
 * ViewHolder
 *
 * @param <Data> 泛型类型
 */
public static abstract class BaseViewHolder<Data> extends RecyclerView.ViewHolder {

    private Unbinder mUnbinder;
    private AdapterCallback<Data> callback;

    protected Data mData;


    public BaseViewHolder(@NonNull View itemView) {
        super(itemView);
    }

    /**
     * 用于绑定数据的触发
     *
     * @param data 绑定的数据
     */
    void bind(Data data) {
        mData = data;
        onBind(mData);
    }

    /**
     * 当触发绑定数据的时候的回调函数, 子类必须实现
     *
     * @param data 绑定的数据
     */
    protected abstract void onBind(Data data);

    /**
     * Holder 对 Data 进行更新操作
     *
     * @param data Data 数据
     */
    public void updateData(Data data) {
        if (callback != null) {
            // 调用更新回调
            callback.update(data, this);
        }
    }
}

成员变量

private Unbinder mUnbinder;
private AdapterCallback<Data> callback;

protected Data mData;

我们的 ViewHolder 有三个成员变量, 其中, mUnbinderButterKnife 用于解绑定的对象. callback 用于更新数据的回调.

相应的, 我们的 ViewHolder 也提供了对外的方法, 来进行数据的更新操作

public void updateData(Data data) {
    if (callback != null) {
        // 调用更新回调
        callback.update(data, this);
    }
}

构造方法

public BaseViewHolder(@NonNull View itemView) {
            super(itemView);
        }

这里, 父类必须包含有一个 View 参数的构造方法, 因此这里实现了一个带参的构造方法. 一个 ViewHolder 会承载一个 View, 就是从这个构造方法所传入的.

bind()

我们前面说了, ViewHolder 需要和具体的数据进行绑定, 在我们的封装里, 提供一个 bind() 方法, 用于将 View 和数据进行绑定.

/**
 * 用于绑定数据的触发
 *
 * @param data 绑定的数据
 */
void bind(Data data) {
    mData = data;
    onBind(mData);
}

首先, 我们 ViewHolder 持有一个数据的引用, 绑定数据时进行设置. 其次, 我们提供了一个 onBind() 的接口, 由子类进行实现, 完成数据绑定时关于 View 的操作, 比如, View 中图片的更新, 文字的更新等等.

实现继承自 RecyclerView.Adapter 的方法

从上面的分析, 我们知道, RecyclerAdapter 需要实现几个非常关键的方法, 来完成好他的仓库展示 "管理员" 的角色:

  • getItemViewType()
  • onCreateViewHolder()
  • onBindViewHolder()
  • getItemCount()

getItemViewType()

我们这里对 getItemViewType() 进行了进一步封装, 提供了一个新的接口:

/**
 * 返回 View 类型
 *
 * @param position item 的坐标
 * @return 返回的其实是布局资源文件 id
 */
@Override
public int getItemViewType(int position) {
    // 调用接口, 来获取布局资源 id
    return getItemViewType(position, mDataList.get(position));
}

/**
 * 子类接口, 子类必须实现, 返回布局资源文件 id
 *
 * @param position 坐标
 * @param data     当前的数据
 * @return xml 文件资源 id, 用于创建 ViewHolder
 */
@LayoutRes
protected abstract int getItemViewType(int position, Data data);

这里, itemViewType 指定的是 View 的类型, 为了简便, 这里的封装做了约定, 约定我们的 view type 就是 xml 布局文件的 id.

onCreateViewHolder()

从前面的分析, 我们知道, 这一步是我们创建 ViewHolder 的方法.

/**
 * 创建一个 BaseViewHolder, 可以根据不同的 ViewType, 创建不同的 BaseViewHolder
 * 这里进行封装时, 为了简化, 约定 viewType 就是 xml 布局的资源 id
 *
 * @param parent   RecyclerView
 * @param viewType 所要生成的 View 类型
 * @return 返回一个 BaseViewHolder
 */
@NonNull
@Override
public BaseViewHolder<Data> onCreateViewHolder(@NonNull ViewGroup parent, @LayoutRes int viewType) {
    // 获取 LayoutInflater, Context 传入 parent.getContext()
    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    // 生成 View, layout id 为 viewType
    View root = inflater.inflate(viewType, parent, false);
    // 调用子类接口生成, 完成用户的 onCreatedViewHolder 操作, 创建 ViewHolder
    BaseViewHolder<Data> holder = onCreateViewHolder(root, viewType);

    // 将 view 与 BaseViewHolder 进行绑定
    root.setTag(R.id.tag_recycler_holder, holder);

    // 绑定 ButterKnife
    holder.mUnbinder = ButterKnife.bind(holder, root);
    // 绑定 callback
    holder.callback = this;

    // 设置点击事件监听
    root.setOnClickListener(this);
    root.setOnLongClickListener(this);

    return holder;
}

/**
 * 用于生成 BaseViewHolder, 子类必须实现
 *
 * @param root     parent, 就是 RecyclerView
 * @param viewType 这里指定 viewType 为资源 id
 * @return 返回创建的 BaseViewHolder
 */
protected abstract BaseViewHolder<Data> onCreateViewHolder(View root, @LayoutRes int viewType);

我们的 ViewHolder 需要持有一个 View, 因此我们通过 LayoutInflater 生成一个 View, 根据前面的约定, View 的布局文件来自于 viewType.

然后, 我们调用我们所构建的接口 onCreateViewHolder() 来生成 ViewHolder.

之后, 我们通过 Tag, 将 view 与 ViewHolder 进行绑定, 使得我们可以通过 view.getTag() 来获取到对应的 ViewHolder.

然后是进行 ButterKnife 的绑定.

然后绑定 holder 的 callback 为当前 Adapter, 使得我们只需要继承 BaseRecyclerAdapter, 实现 callback 对应的方法即可.

进而我们设置了 view 的点击事件, 对于点击事件, 首先是 View 获取到点击事件, 然后传给 Adapter, 由 Adapter 对点击事件进行处理.

/**
 * 覆写 onClick, 将 item 的 onClick 事件, 通过 AdapterListener 接口, 传给子类
 *
 * @param view
 */
@Override
public void onClick(View view) {
    // 这是前面 tag 的作用, 用于通过 view 获取 ViewHolder
    BaseViewHolder viewHolder = (BaseViewHolder) view.getTag(R.id.tag_recycler_holder);
    if (this.mListener != null) {
        int pos = viewHolder.getAdapterPosition();
        mListener.onItemClick(viewHolder, mDataList.get(pos));
    }
}

/**
 * 覆写 onLongClick, 将 item 的 onLongClick 事件, 通过 AdapterListener 接口, 传给子类
 *
 * @param view
 */
@Override
public boolean onLongClick(View view) {
    BaseViewHolder viewHolder = (BaseViewHolder) view.getTag(R.id.tag_recycler_holder);
    if (this.mListener != null) {
        int pos = viewHolder.getAdapterPosition();
        mListener.onItemLongClick(viewHolder, mDataList.get(pos));
        return true;
    }
    return false;
}

增删改

对于一个列表, Adapter 是其管理员, 我们应该通过 Adapter 来管理我们数据的变化, 因此我们可以封装增删改的方法.

/**
 * 添加数据
 *
 * @param data 所添加的元素
 */
public void add(Data data) {
    mDataList.add(data);
    // 通知数据更新
    notifyItemInserted(mDataList.size() - 1);
}

/**
 * 插入多个数据, 并更新
 *
 * @param dataList 所插入的数据
 */
public void add(Data... dataList) {
    if (Objects.requireNonNull(dataList, DATA_LIST_ERROR_MESSAGE).length > 0) {
        int start = mDataList.size();
        Collections.addAll(mDataList, dataList);
        // 通知更新, 参数为起始位置和插入个数
        notifyItemRangeChanged(start, dataList.length);
    }
}

/**
 * 插入多个数据, 并更新
 *
 * @param dataList 所插入的数据
 */
public void add(Collection<Data> dataList) {
    if (Objects.requireNonNull(dataList, DATA_LIST_ERROR_MESSAGE).size() > 0) {
        int start = mDataList.size();
        mDataList.addAll(dataList);
        notifyItemRangeChanged(start, dataList.size());
    }
}

/**
 * 清空
 */
public void clear() {
    mDataList.clear();
    notifyDataSetChanged();
}

/**
 * 将原有集合替换为新的集合
 *
 * @param dataList 所替换的集合
 */
public void replace(Collection<Data> dataList) {
    mDataList.clear();
    mDataList.addAll(Objects.requireNonNull(dataList, DATA_LIST_ERROR_MESSAGE));
    notifyDataSetChanged();
}

这里就不进行详细介绍了, 需要注意的就是, 我们更新完数据后, 需要调用对应的 notify 方法, 通知 Adapter 对应的数据已经更新了, 来及时的更新我们的界面显示.

至此, 对 RecyclerView.Adapter 的封装介绍完毕, 后续如果有更新, 会及时的更新本文, 如果有任何的问题, 欢迎随时指出, 我会及时回复.