实现 RecyclerView 上拉加载及自动加载

之前在《一步步打造自己的通用上拉加载布局》(如果没有看过,建议先看下这一篇)写到如何实现一个通用的上拉加载布局,本文将基于此进行扩展,实现 RecyclerView 的上拉加载及自动加载。

在之前的《一步步打造自己的通用上拉加载布局》已经提到,下拉刷新是将获取到的数据替换掉原有的数据,而上拉加载则是将获取到的数据插入到原来数据的末尾与底部提示加载的 View(如FooterView)之间,它们的展现方式的不同,使得上拉出来用于提示加载的 View,对于列表类视图如 ListViewRecyclerView 而言,适合作为它们的 FooterView 而不适合封装到我们的上拉布局中。因此,我们需要继承 LoadMoreLayout 并实现对于 RecyclerView 的扩展。

准备工作

由于在 LoadMoreLayout 中已经把上拉的逻辑都封装好,因此这里主要是对 RecyclerViewFooterView 及自动加载的封装。在这里,对于带 FooterView 的 Adapter,我是通过包装原有的 Adapter 来实现,这样在使用的时候就不需要把原来的 Adapter 改成我们的上拉加载中的 Adapter,以减少对代码的侵入。这个 Adapter 代码如下。这个类代码可不必细看,它是上拉加载的副产品,不是主要逻辑,于这里贴出只是交底。

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.recycleradapter;

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-03 0.1
 */
public abstract class HiRecyclerAdapter extends RecyclerView.Adapter {
    /**
     * 由于RecyclerView的灵活性,可能会出现不同的Adapter的嵌套,而因此可能导致viewType重复。
     * 因此在这里尝试为每个Adapter定义一个区间,而避免它们的重复问题。
     */
    public static final int TYPE_SECTION = 2 << 10;

    /**
     * 从2<<20开始,留足够的type值给content使用。
     */
    private static final int HEADER_OFFSET = TYPE_SECTION << 10;
    private static final int FOOTER_OFFSET = HEADER_OFFSET + TYPE_SECTION;

    private int mHeaderCount;
    private int mContentCount;
    private int mFooterCount;

    private RecyclerView.Adapter mWrapperAdapter;
    private RecyclerView.AdapterDataObserver mDataObserver;

    public HiRecyclerAdapter(RecyclerView.Adapter wrapperAdapter) {
        mWrapperAdapter = wrapperAdapter;
        mDataObserver = new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount) {
                notifyItemRangeChanged(positionStart + mHeaderCount, itemCount);
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
                notifyItemRangeChanged(positionStart + mHeaderCount, itemCount, payload);
            }

            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                notifyItemRangeInserted(positionStart + mHeaderCount, itemCount);
            }

            @Override
            public void onItemRangeRemoved(int positionStart, int itemCount) {
                notifyItemRangeRemoved(positionStart + mHeaderCount, itemCount);
            }

            @Override
            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
                notifyItemMoved(fromPosition + mHeaderCount, toPosition + mHeaderCount);
            }
        };
        mWrapperAdapter.registerAdapterDataObserver(mDataObserver);
    }

    public RecyclerView.Adapter getWrapperAdapter() {
        return mWrapperAdapter;
    }

    public int getContentPosition(int position) {
        return position - mHeaderCount;
    }

    @Override
    public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType >= HEADER_OFFSET && viewType < HEADER_OFFSET + TYPE_SECTION) {
            return onCreateHeaderHolder(parent, viewType - HEADER_OFFSET);
        } else if (viewType >= FOOTER_OFFSET && viewType < FOOTER_OFFSET + TYPE_SECTION) {
            return onCreateFooterHolder(parent, viewType - FOOTER_OFFSET);
        } else {
            return mWrapperAdapter.onCreateViewHolder(parent, viewType);
        }
    }

    /**
     * Called when RecyclerView needs a new header {@link RecyclerView.ViewHolder} of the given type to
     * represent an item.
     *
     * @param parent   The ViewGroup into which the new View will be added after it is bound to
     *                 an adapter position.
     * @param viewType The view type of the new View.
     * @return A new ViewHolder that holds a View of the given view type.
     */
    protected abstract RecyclerView.ViewHolder onCreateHeaderHolder(ViewGroup parent, int viewType);

    /**
     * Called when RecyclerView needs a new footer {@link RecyclerView.ViewHolder} of the given type to
     * represent an item.
     *
     * @param parent   The ViewGroup into which the new View will be added after it is bound to
     *                 an adapter position.
     * @param viewType The view type of the new View.
     * @return A new ViewHolder that holds a View of the given view type.
     */
    protected abstract RecyclerView.ViewHolder onCreateFooterHolder(ViewGroup parent, int viewType);

    @Override
    public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position < mHeaderCount) {
            onBindHeaderHolder(holder, position);
        } else if (position < mHeaderCount + mContentCount) {
            mWrapperAdapter.onBindViewHolder(holder, position - mHeaderCount);
        } else {
            onBindFooterHolder(holder, position - mHeaderCount - mContentCount);
        }
    }

    /**
     * Called by RecyclerView to display the header at the specified position. This method should
     * update the contents of the {@link RecyclerView.ViewHolder#itemView} to reflect the header
     * item at the given position.
     *
     * @param holder   The ViewHolder which should be updated to represent the contents of the
     *                 item at the given position in the data set.
     * @param position The position of the item within the adapter's data set.
     */
    protected abstract void onBindHeaderHolder(RecyclerView.ViewHolder holder, int position);

    /**
     * Called by RecyclerView to display the footer at the specified position. This method should
     * update the contents of the {@link RecyclerView.ViewHolder#itemView} to reflect the footer
     * item at the given position.
     *
     * @param holder   The ViewHolder which should be updated to represent the contents of the
     *                 item at the given position in the data set.
     * @param position The position of the item within the adapter's data set.
     */
    protected abstract void onBindFooterHolder(RecyclerView.ViewHolder holder, int position);

    public int getHeaderCount() {
        return 0;
    }

    public final int getContentCount() {
        return mWrapperAdapter.getItemCount();
    }

    public int getFooterCount() {
        return 0;
    }

    @Override
    public final int getItemCount() {
        mHeaderCount = getHeaderCount();
        mContentCount = mWrapperAdapter.getItemCount();
        mFooterCount = getFooterCount();
        return mHeaderCount + mContentCount + mFooterCount;
    }

    @Override
    public final int getItemViewType(int position) {
        if (position < mHeaderCount) {
            return validateViewType(getHeaderViewType(position)) + HEADER_OFFSET;
        } else if (position < mHeaderCount + mContentCount) {
            return validateViewType(mWrapperAdapter.getItemViewType(position - mHeaderCount));
        } else {
            return validateViewType(getFooterViewType(position - mHeaderCount - mContentCount)) + FOOTER_OFFSET;
        }
    }

    private int validateViewType(int viewType) {
        if (viewType < 0 || viewType >= TYPE_SECTION) {
            throw new IllegalStateException("viewType must be between 0 and " + TYPE_SECTION);
        }
        return viewType;
    }

    protected int getHeaderViewType(int position) {
        return 0;
    }

    protected int getFooterViewType(int position) {
        return 0;
    }

    public void notifyFooterInsert(int positionStart) {
        notifyItemInserted(mHeaderCount + mContentCount + positionStart);
    }

    public void notifyFooterRemoved(int positionStart) {
        notifyItemRemoved(mHeaderCount + mContentCount + positionStart);
    }
}

接下来,继承自这个 Adapter,实现对 FooterView 添加,代码如下:

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.hiloadmore.recyclerview;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;

import com.githang.recycleradapter.HiRecyclerAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-03 0.1
 */
public class FooterRecyclerAdapter extends HiRecyclerAdapter {

    private List<View> mFooterViews = new ArrayList<>();

    public FooterRecyclerAdapter(RecyclerView.Adapter wrapperAdapter) {
        super(wrapperAdapter);
    }

    @Override
    protected RecyclerView.ViewHolder onCreateHeaderHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    protected RecyclerView.ViewHolder onCreateFooterHolder(ViewGroup parent, int viewType) {
        return new FooterViewHolder(mFooterViews.get(viewType));
    }

    @Override
    protected void onBindHeaderHolder(RecyclerView.ViewHolder holder, int position) {
    }

    @Override
    protected void onBindFooterHolder(RecyclerView.ViewHolder holder, int position) {
    }

    @Override
    protected int getFooterViewType(int position) {
        return position;
    }

    @Override
    public int getFooterCount() {
        return mFooterViews.size();
    }

    void addFooterView(View view) {
        if (!mFooterViews.contains(view)) {
            mFooterViews.add(view);
            notifyFooterInsert(mFooterViews.indexOf(view));
        }
    }

    void removeFooterView(View view) {
        if (mFooterViews.contains(view)) {
            int index = mFooterViews.indexOf(view);
            mFooterViews.remove(view);
            notifyFooterRemoved(index);
        }
    }

    static FooterRecyclerAdapter wrapper(RecyclerView.Adapter adapter) {
        if (adapter instanceof FooterRecyclerAdapter) {
            return (FooterRecyclerAdapter) adapter;
        } else {
            return new FooterRecyclerAdapter(adapter);
        }
    }

    static class FooterViewHolder extends RecyclerView.ViewHolder {

        FooterViewHolder(View itemView) {
            super(itemView);
            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
            if (lp == null) {
                lp = new RecyclerView.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                itemView.setLayoutParams(lp);
            }
        }
    }
}

然后,实现一个默认的 FooterView。它的布局文件很简单,就是一个 TextView,如下:

<?xml version="1.0" encoding="utf-8"?>
<TextView
    android:id="@+id/footer_text"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:gravity="center"
    android:textColor="#666"
    android:textSize="12sp"/>

它需要实现 LoadMoreUIHandler 接口,Java 代码如下:

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.hiloadmore.recyclerview;

import android.content.Context;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.TextView;

import com.githang.hiloadmore.LoadMoreUIHandler;
import com.githang.hiloadmore.R;

/**
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-03 0.1
 */
public class RecyclerFooterView extends FrameLayout implements LoadMoreUIHandler {
    private TextView mText;

    public RecyclerFooterView(@NonNull Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.hlm_footer_load_more, this, true);
        mText = findViewById(R.id.footer_text);
        mText.setText(R.string.hlm_pull_to_load_more);
    }

    @Override
    public void onPrepare() {
        mText.setText(R.string.hlm_pull_to_load_more);
    }

    @Override
    public void onBegin() {
        mText.setText(R.string.hlm_tip_loading_more);
    }

    @Override
    public void onComplete(boolean hasMore) {
        mText.setText(hasMore ? R.string.hlm_pull_to_load_more : R.string.hlm_tip_load_finish);
    }

    @Override
    public void onPositionChange(int offsetY, int offsetToLoadMore) {
        if (Math.abs(offsetY) > offsetToLoadMore) {
            mText.setText(R.string.hlm_release_to_load_more);
        } else {
            mText.setText(R.string.hlm_pull_to_load_more);
        }
    }
}

上拉加载的 FooterView 扩展

前面这些,都只是对 FooterView 显示的准备工作。最后,才是对 LoadMoreLayout 实现支持 RecyclerView 的扩展。如果你不需要这个FooterView,那么直接使用 LoadMoreLayout 也是可以的。当然,这里的扩展过程也很简单,继承 LoadMoreLayout,然后重写 protected void onFinishInflate() 方法,限定子 View 只能是 RecyclerView,再定义一个设置 FooterView 的方法,如此即可,代码如下:

/*
 * Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
 */
package com.githang.hiloadmore.recyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

import com.githang.hiloadmore.LoadMoreLayout;

/**
 * RecyclerView的上拉加载以及自动加载实现。
 *
 * @author Geek_Soledad (msdx.android@qq.com)
 * @since 2017-05-03 0.1
 */
public class LoadMoreRecyclerViewContainer extends LoadMoreLayout {
    private View mFooterView;

    public LoadMoreRecyclerViewContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View content = getContentView();
        if (!(content instanceof RecyclerView)) {
            throw new IllegalStateException("LoadMoreRecyclerViewContainer only support RecyclerView");
        }
    }

    public void setFooterView(final View footer) {
        final RecyclerView recyclerView = (RecyclerView) getContentView();
        final FooterRecyclerAdapter adapter = FooterRecyclerAdapter.wrapper(recyclerView.getAdapter());
        if (mFooterView != null && mFooterView != footer) {
            adapter.removeFooterView(mFooterView);
        }
        adapter.addFooterView(footer);
        mFooterView = footer;
        recyclerView.setAdapter(adapter);
        footer.post(new Runnable() {
            @Override
            public void run() {
                int height = footer.getHeight();
                if (height > 0) {
                    setOffsetYToLoadMore(height);
                }
            }
        });
    }
}

这就完成了上拉加载的扩展。

实现自动加载

接下来实现自动加载,这个也很简单,思路就是监听 RecyclerView 的滚动,如果到达底部,则主动触发上拉加载。这里只需要定义一个表示是否需要自动加载的成员变量,添加其 setter 方法,然后修改上面重写的 protected void onFinishInflate() 方法,给 RecyclerView 加一个监听器即可,如下:


    private View mFooterView;

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View content = getContentView();
        if (content instanceof RecyclerView) {
            ((RecyclerView) content).addOnScrollListener(new RecyclerView.OnScrollListener() {

                private LinearLayoutManager mLinearLayoutManager;

                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (!mAutoLoadMore || !hasMore()) {
                        return;
                    }
                    if (mLinearLayoutManager == null) {
                        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                        if (layoutManager instanceof LinearLayoutManager) {
                            mLinearLayoutManager = (LinearLayoutManager) layoutManager;
                        }
                    }
                    if (mLinearLayoutManager == null) {
                        return;
                    }

                    if ((mLinearLayoutManager.getItemCount() - recyclerView.getChildCount())
                            <= mLinearLayoutManager.findFirstVisibleItemPosition()) {
                        triggerToLoadMore();
                    }
                }
            });
        } else {
            throw new IllegalStateException("LoadMoreRecyclerViewContainer only support RecyclerView");
        }
    }

    public void setAutoLoadMore(boolean autoLoadMore) {
        mAutoLoadMore = autoLoadMore;
    }

这样就扩展完成了。

使用示例

以上代码已提交至 Github 上的 hi-loadmore 项目,并且已发布至 bintray。使用方式如下:

首先添加 Gradle 依赖:

    compile 'com.githang:hi-loadmore:0.1.1'
    // OR
    implementation 'com.githang:hi-loadmore:0.1.1'

然后在布局中使用 LoadMoreRecyclerViewContainer作为 RecyclerView 的父布局。接下来在 Java 代码中添加配置:


        RecyclerFooterView footerView = new RecyclerFooterView(this);
        mLoadMoreLayout.setFooterView(footerView);
        mLoadMoreLayout.setLoadMoreUIHandler(footerView);
        mLoadMoreLayout.setHasMore(true);

然后设置触发了上拉加载后的处理:

        mLoadMoreLayout.setLoadMoreHandler(new LoadMoreHandler() {
            @Override
            public void onLoadMore() {
                // 在这里请求数据
            }
        });

最后看下运行结果:
实现 RecyclerView 上拉加载及自动加载

项目地址:https://github.com/msdx/hi-loadmore

上一篇:开发者社区也能用语雀编辑器啦


下一篇:【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 注入工具的 main 函数分析 )