思路
实现LoadMore功能主要是从Adapter入手,通过在onAttachedToRecyclerView方法里给RecyclerView添加滑动监听来实现。一种思路是实现一个LoadMoreAdapter基类让其他的Adapter继承来获得load more的能力,但是这样的实现方式感觉有点侵入原有的Adapter代码,而且不够灵活,所以本文采用代理原有Adapter的方式来实现。
实现FooterView
第一步先来实现FooterView,我们定义一个FooterView的接口
public interface IFooterView {
public void changState(LoadingState state);
}
其中LoadingState是load more的状态,有如下几种
public enum LoadingState {
LOADING_STATE_NO_MORE, // 没有更多
LOADING_STATE_LOADING, // 加载中
LOADING_STATE_ERROR, // 加载错误
LOADING_STATE_NORMAL // 不显示footer
}
然后我们实现一个默认的FooterView
public class DefaultFooterView extends RelativeLayout implements IFooterView {
private ProgressBar mLoadingView;
private TextView mNoMoreView;
private TextView mErrorView;
private LoadMoreAdapter.LoadingState mState = LoadMoreAdapter.LoadingState.LOADING_STATE_NORMAL;
public DefaultFooterView(Context context) {
this(context, null);
}
public DefaultFooterView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DefaultFooterView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
initView();
}
private void initView() {
inflate(getContext(), R.layout.default_footer_layout, this);
mLoadingView = (ProgressBar) findViewById(R.id.loading_view);
mNoMoreView = (TextView) findViewById(R.id.no_more_view);
mErrorView = (TextView) findViewById(R.id.error_view);
}
@Override
public void changState(LoadMoreAdapter.LoadingState state) {
mState = state;
mLoadingView.setVisibility(mState == LoadMoreAdapter.LoadingState.LOADING_STATE_LOADING? VISIBLE: GONE);
mNoMoreView.setVisibility(mState == LoadMoreAdapter.LoadingState.LOADING_STATE_NO_MORE? VISIBLE: GONE);
mErrorView.setVisibility(mState == LoadMoreAdapter.LoadingState.LOADING_STATE_ERROR? VISIBLE: GONE);
}
}
具体的布局就不展示了,各位自由发挥。如上所示,FooterView对外提供changeState方法来改变布局为对应的状态。
实现LoadMoreAdapter
LoadMoreAdapter用来代理一个真正的Adapter,将相关的系统回调传递给Adapter并实现load more的功能,而在外部RecyclerView使用LoadMoreAdapter而不是被代理的Adapter。
代理Adapter
创建LoadMoreAdapter需要传入一个Adapter,LoadMoreAdapter保存这个Adapter并在相关的系统回调方法里触发Adapter相应的回调,如下所示方法
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(hasStableIds);
mAdapter.setHasStableIds(hasStableIds);
}
@Override
public long getItemId(int position) {
if (position > mAdapter.getItemCount() - 1) {
return super.getItemId(position);
}
return mAdapter.getItemId(position);
}
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
if (holder instanceof FooterViewHolder) {
super.onViewRecycled(holder);
return;
}
mAdapter.onViewRecycled(holder);
}
@Override
public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
if (holder instanceof FooterViewHolder) {
return super.onFailedToRecycleView(holder);
}
return mAdapter.onFailedToRecycleView(holder);
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
if (holder instanceof FooterViewHolder) {
super.onViewAttachedToWindow(holder);
return;
}
mAdapter.onViewAttachedToWindow(holder);
}
@Override
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
if (holder instanceof FooterViewHolder) {
super.onViewDetachedFromWindow(holder);
}
mAdapter.onViewDetachedFromWindow(holder);
}
@Override
public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
super.registerAdapterDataObserver(observer);
mAdapter.registerAdapterDataObserver(observer);
}
@Override
public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
super.unregisterAdapterDataObserver(observer);
mAdapter.unregisterAdapterDataObserver(observer);
}
其中mAdapter.registerAdapterDataObserver(observer);方法必须要写,因为只有这样才会在调用Adapter的notify方法的时候通知到RecyclerView来刷新数据。
实现LoadMore
LoadMoreAdapter内部保存一个当前的state,通过这个state来判断当前是否应该显示footer,主要是通过getItemCount()、getItemViewType()、onCreateViewHolder()、onBindViewHolder()四个方法来实现
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_FOOT) {
IFooterView footView = (View) new DefaultFooterView(mContext);
return new FooterViewHolder(footView);
}
return mAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof FooterViewHolder) {
FooterViewHolder viewHolder = (FooterViewHolder) holder;
viewHolder.bind(mState);
return;
}
mAdapter.onBindViewHolder(holder, position);
}
@Override
public int getItemCount() {
return mAdapter.getItemCount() + (mState == LoadingState.LOADING_STATE_NORMAL? 0: 1);
}
@Override
public int getItemViewType(int position) {
if (position == mAdapter.getItemCount()) {
return TYPE_FOOT;
}
return mAdapter.getItemViewType(position);
}
且对外提供一个方法来改变当前的状态
public void updateLoadMoreState(LoadingState state) {
if (!checkState(state)) {
return;
}
mState = state;
notifyDataSetChanged();
}
当然真正的LoadMore逻辑是在onAttachedToRecyclerView的时候给RecyclerView添加滑动监听来实现的,代码如下
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mAdapter.onAttachedToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
mRecyclerView.addOnScrollListener(mScrollerListener);
}
private void initScrollListener() {
mScrollerListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
boolean showLoadMore = false;
showLoadMore = !recyclerView.canScrollVertically(1) && dy > 0;
if (showLoadMore) {
if (mLoadMoreListener != null && mState != LoadingState.LOADING_STATE_LOADING) {
mState = LoadingState.LOADING_STATE_LOADING;
notifyDataSetChanged();
mLoadMoreListener.onLoadMore();
}
}
}
};
}
public interface OnLoadMoreListener {
public void onLoadMore();
}
在RecyclerView要向上滑动但不能向上滑的时候触发LoadMore(当前不处于loading的时候),后面就要求外部在加载结束后调用LoadMoreAdapter的updateLoadMoreState方法来更新状态。
实现过程中的坑
原来的思路是itemCount写死mAdapter.getItemCount + 1,然后在不现实footer的时候让FooterView内部的View都gone,然后设置高度为0,看起来就相当于没有FooterView。但是RecyclerView貌似会对布局中元素都是gone或者空布局做一些优化,导致canScrollVertically始终返回true,所以后来采用了更改itemCount的方法。