非浸入式实现RecyclerView添加加载更多功能


开源的下拉刷新控件非常多,但是很少有上拉加载,可能是因为上拉加载需求比较简单并且没有下拉刷新使用的频繁吧。我非常需要一个上拉加载的功能,因为公司的框架中上拉加载简直不忍直视,没有加载中提示,更别提什么没有更多数据和加载失败的提示了,用户根本不知道自己上拉加载是否成功了。

实现上拉加载的思路:监听滚动状态和数据观察者,判断是否到达最底部或临界点,执行加载下一页操作。

目前发现有这么几种实现方案:

  1. 自定义 RecyclerView 包装原来的 Adapter ,添加对应的 Footer Item
  2. 自定义 ViewGroup,包裹 RecyclerView ,滚动最底部或临界点展示加载中 View
  3. 帮助类实现对 RecyclerView Adapter 的包装,添加对应的 Foot Item

说说各自的优缺点:

第一种使用简单、方便,一般也集成了下拉刷新,缺点是不灵活、较重,如果只使用刷新,同样得引入全部的功能。

第二种相对第一种来说耦合性比较低,但对布局文件同样没有解除耦合。

第三种耦合性最低,也易于自定义,但是使用上没第一种简单。

个人比较看好第三种实现方案,因为第三种解耦性和扩展性都比较强。下面说说具体实现。

大体思路

  1. 监听 RecyclerView 滚动事件,在滚动事件的回调中检测是否到列表的最底部,并执行相应的回调,如 loadMore()
  2. 获取 RecyclerView 的 Adapter,对其进行包装,以扩展出可自定义的 FootView
  3. 为 RecyclerView 设置 AdapterDataObserver, 调用 Adapter 数据集改变的通知方法,并通过回调判断 FootView 的状态,显示对一个的 FoorView
  4. 对 GridLayoutManager 进行特别处理

具体实现

思路优先,思考和代码辅助。

只贴出核心方法名,及代码结构,不涉及大量代码,需要看源码请到 Github

BKLoadMore

构建一个 BKLoadMore 类,来对 RecyclerView 进行功能扩展。

BKLoadMore 的构造方法肯定就得包含 RecyclerView,Callbacks,自定义的 FootView,包括 LoadingItem 和 NoMoreDataItem,下个版本需要增加加载错误的 Item。

像这样:

private BKLoadMore(RecyclerView recyclerView,
                   Callbacks callbacks,
                   LoadingItem loadingListItemCreator,
                   NoMoreDataItem noMoreDataItem) {

        this.recyclerView = recyclerView;
        this.callbacks = callbacks;

增加滚动监听,检测是否滚动到了底部。

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                checkEndOffset();
            }
        });
private void checkEndOffset() {
    // ...
    if ((totalItemCount - visibleItemCount) <= (firstVisibleItemPosition)
            || totalItemCount == 0) {
        // 滚动到底部,掉用 Callbacks 的 oLoadMore() 方法。
        if (!callbacks.isLastPage() && !callbacks.isLoading()) {
            callbacks.onLoadMore();
        }
    }
}

包装 RecyclerView 原有的 Adapter

// 包装 Adapter
RecyclerView.Adapter adapter = recyclerView.getAdapter();
wrapperAdapter = new WrapperAdapter(adapter, loadingListItemCreator, noMoreDataItem);
RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
    @Override
    public void onChanged() {
        wrapperAdapter.notifyDataSetChanged();
        // 实现显示不同的 item
        onAdapterDataChanged();
    }
    // 省略其他回调方法
};
// 设置数据观察者
adapter.registerAdapterDataObserver(dataObserver);
// 重新设置包装后的 Adapter
recyclerView.setAdapter(wrapperAdapter);
private void onAdapterDataChanged() {
    // 根据 Callbacks 回调的状态显示不同的 item
    wrapperAdapter.displayLoadingRow(!callbacks.isLastPage());
    wrapperAdapter.displayNoMoreDataRow(callbacks.isLastPage());
    checkEndOffset();
}

WapperAdapter

WapperAdapter 是 包装后的 Adapter,需要判断当前是否是正在加载的行或没有更多数据行,以返回对应的 ViewHolder,否则返回原 Adapter 的 Viewolder。

class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 返回不同的 ViewHolder
        if (viewType == VIEW_TYPE_LOADING) {
            return loadingListItem.onCreateViewHolder(parent, viewType);
        } else if (viewType == VIEW_TYPE_NO_DATA) {
            return noMoreDataItem.onCreateViewHolder(parent, viewType);
        }
        return wrappedAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public int getItemViewType(int position) {
        // 返回当前 item 类型
        int viewType = wrappedAdapter.getItemViewType(position);
        if (isLoadingRow(position)) {
            viewType = VIEW_TYPE_LOADING;
        } else if (isNoMoreDataRow(position)) {
            viewType = VIEW_TYPE_NO_DATA;
        }
        return viewType;
    }

    // 其他需要重写的方法省略

    /**
     * 显示加载中行
     */
    void displayLoadingRow(boolean displayLoadingRow) {
        this.displayLoadingRow = displayLoadingRow;
    }

    /**
     * 显示没有更多数据行
     */
    void displayNoMoreDataRow(boolean displayNoMoreDataRow) {
        this.displayNoMoreDataRow = displayNoMoreDataRow;
    }

}

最后来看看 LoadingItem 和 NoMoreDataItem 接口,上面的 WrapperAdapter 用到的。

LoadingItem.java:

public interface LoadingItem {

    RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

    void onBindViewHolder(RecyclerView.ViewHolder holder, int position);

    // 提供一个默认实现
    LoadingItem DEFAULT = new LoadingItem() {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.loading_row, parent, false);
            return new RecyclerView.ViewHolder(view) {
            };
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            // 默认实现没有绑定数据
        }
    };
}

到这里,功能都已实现,我们再使用 Builder 模式,向外界提供方法。

BKLoadMore.java:

public static Builder with(RecyclerView recyclerView) {
        return new Builder(recyclerView);
    }

public static class Builder {

    private final RecyclerView recyclerView;

    private Builder(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
    }

    // 省略 seter 方法

    public void callBack(Callbacks callbacks) {
        if (recyclerView.getAdapter() == null) {
            throw new IllegalStateException("Adapter needs to be set!");
        }
        if (recyclerView.getLayoutManager() == null) {
            throw new IllegalStateException("LayoutManager needs to be set on the RecyclerView");
        }

        if (loadingItem == null) {
            loadingItem = LoadingItem.DEFAULT;
        }

        if (noMoreDataItem == null) {
            noMoreDataItem = NoMoreDataItem.DEFAULT;
        }

        if (itemSpanLookup == null) {
            itemSpanLookup = new DefaultItemSpanLookup(recyclerView.getLayoutManager());
        }

        new BKLoadMore(recyclerView,
                callbacks,
                loadingItem,
                noMoreDataItem);
        }
    }

评论