Android技能——上拉加载更多

编写程序时经常会使用到下拉刷新和上拉加载更多。下拉刷新很简单,使用Google的SwipRefreshLayout就能非常方便地实现。而上拉加载更多则稍微复杂一些。

效果

上拉加载更多.GIF

思路:

1.实现上拉加载更多需要使用到RecyclerView.OnScrollerListener这个类。

当执行mRecyclerView.addOnScrollListener(mOnScrollListener)注册监听后,在OnScrollListener的onScrolled()回调中就可以监听到当前mRecyclerView的滑动事件。在监听到滑动事件时判断当前是否需要执行加载更多,如果需要加载更多,就通过抽象方法onLoadMore()将加载更多的事件从mOnScrollListener中传递出去

2.编写Adapter。
重写onAttachedToRecyclerView()方法,初始化OnScrollListener并调用mRecyclerView.addOnScrollListener(mOnScrollListener)来注册滑动事件监听,同时实现onLoadMore()方法,通过Adapter中的接口进一步将onLoadMore()事件由Adapter传递到Activity中

3.此时已经能够在Activity中来具体实现onLoadMore()了,这里执行具体的加载更多操作。

4.数据加载完毕时更新数据。

虽然整个思路看起来简单明了,但实际实现过程中还是有些麻烦的,比如以下两个问题。

1.什么时候下拉加载更多

2.如何去正确的更新UI

实现:

1.自定义一个继承自OnScrollListener的RecyclerOnScrollerListener。
    public abstract class RecyclerOnScrollerListener extends RecyclerView.OnScrollListener {
    private int currentPage = 1;
    //是否正在加载
    private boolean isLoading = false;
    //是否能够加载更多
    private boolean isCanLoadMore = true;
    private RecyclerView mRecyclerView;

    public RecyclerOnScrollerListener(RecyclerView recyclerView) {
        super();
        this.mRecyclerView = recyclerView;
    }

    @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);
        //能够加载更多
        if (isCanLoadMore) {
            //已滑动到最底部
            if (isSlideToBottom()) {
                currentPage++;
                onLoadMore(currentPage);
                isLoading = true;
            }
        }
    }

    /*
    * 是否滑动到最底部
    *
    * !isLoading, 不在加载过程中
    * lastVisibleItemPosition == totalItemCount - 1, 当前可见的最后一个item是列表的最后一个
    * */
    private boolean isSlideToBottom() {
        if (mRecyclerView == null) {
            return false;
        }

        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager instanceof LinearLayoutManager) {
            int visibleItemCount = layoutManager.getChildCount();
            int totalItemCount = layoutManager.getItemCount();
            int lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            return !isLoading && visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1;
        }
        return false;
    }

    //抽象方法,用来传递加载更多事件
    public abstract void onLoadMore(int currentPage);

    public void setCanLoadMore(boolean load) {
        isCanLoadMore = load;
    }

    public void setLoading(boolean load) {
        isLoading = load;
    }

}

本类中加入了两个变量,isLoading和isCanLoadMore,用来控制加载更多事件。
这里通过isSlideToBottom()方法来判断是否滑动到最底部,结合isCanLoadMore和isSlideToBottom共同判断是否加载更多,即当能够加载更多且滑动到最底部时执行加载更多。这就解决了问题1。

2.编写Adapter,这里使用和郭神同样的Fruit为例。
public class FruitAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private List<Fruit> mFruitList;
    private Context mContext;
    private RecyclerOnScrollerListener mOnScrollListener;
//    private RecyclerView.AdapterDataObserver mAdapterDataObserver;

    private static final int VIEW_TYPE_CONTENT = 0;
    private static final int VIEW_TYPE_FOOTER = 1;
    private boolean isCanLoadMore = true;

    private Animation rotateAnimation;

    private static final int PER_PAGE = 10;

    public FruitAdapter(List<Fruit> fruitList) {
        this.mFruitList = fruitList;
    }


    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (mContext == null) {
            mContext = parent.getContext();
        }
        if (rotateAnimation == null) {
            rotateAnimation = AnimationUtils.loadAnimation(mContext, R.anim.loading);
            rotateAnimation.setInterpolator(new LinearInterpolator());
        }
        if (viewType == VIEW_TYPE_CONTENT) {
            return new ContentViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_fruit, parent, false));
        } else {

            return new FooterViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_footer, parent, false));
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder.getItemViewType() == VIEW_TYPE_CONTENT) {
            Fruit fruit = mFruitList.get(position);
            //这里必须强制转换
            //如果外层的判断条件改为if(holder instance ContentViewHolder),这里输入holder后会自动转换
            ((ContentViewHolder) holder).tvName.setText(fruit.getName());
            ((ContentViewHolder) holder).ivImage.setImageResource(fruit.getImageId());
        } else {
            Log.d("mytest", "isCanLoadMore: " + isCanLoadMore);
            if (isCanLoadMore) {
                ((FooterViewHolder) holder).showLoading();
            } else {
                ((FooterViewHolder) holder).showTextOnly("无更多数据");
            }
        }

    }

    /*
    * 本例中所有数据加载完毕后还是保留footerView并显示已加载完全,所以footerView一直存在。
    * */
    @Override
    public int getItemCount() {
//        return isCanLoadMore ? mFruitList.size() + 1 : mFruitList.size();
        return mFruitList.size() + 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return VIEW_TYPE_FOOTER;
        }
        return VIEW_TYPE_CONTENT;
    }

    //ContentView,水果们
    class ContentViewHolder extends RecyclerView.ViewHolder {
        TextView tvName = itemView.findViewById(R.id.tv_name);
        ImageView ivImage = itemView.findViewById(R.id.iv_image);

        public ContentViewHolder(View itemView) {
            super(itemView);
        }
    }

    //底部的FooterView
    class FooterViewHolder extends RecyclerView.ViewHolder {

        ImageView ivLoading = itemView.findViewById(R.id.iv_loading);
        TextView tvLoading = itemView.findViewById(R.id.tv_loading);

        public FooterViewHolder(View itemView) {
            super(itemView);
        }

        void showTextOnly(String s) {
            Log.d("mytest", "showTextOnly: " + s);
            ivLoading.setVisibility(View.INVISIBLE);
            tvLoading.setText(s);
        }

        void showLoading() {
            Log.i("mytest", "show loading");
            ivLoading.setImageResource(R.mipmap.ic_launcher);
            tvLoading.setText("正在加载...");
            if (ivLoading != null) {
                ivLoading.startAnimation(rotateAnimation);
            }
        }

    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        mOnScrollListener = new RecyclerOnScrollerListener(recyclerView) {
            @Override
            public void onLoadMore(int currentPage) {
                Log.i("loadingtest", "currentPage: " + currentPage);
                mOnLoadMoreListener.onLoadMore(currentPage);
            }
        };
        recyclerView.addOnScrollListener(mOnScrollListener);
//        mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
//            @Override
//            public void onChanged() {
//                super.onChanged();
//            }
//        };
        //初始化的时候如果未填满一页,那么肯定就没有更多数据了
        if (mFruitList.size() < PER_PAGE) {
            setCanLoadMore(false);
        } else {
            setCanLoadMore(true);
        }
    }


    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
        super.onDetachedFromRecyclerView(recyclerView);
        if (mOnScrollListener != null) {
            recyclerView.removeOnScrollListener(mOnScrollListener);
        }
//        if (mAdapterDataObserver != null) {
//            unregisterAdapterDataObserver(mAdapterDataObserver);
//        }
        mOnScrollListener = null;
    }

    public void setData(List<Fruit> list) {
        mFruitList = list;
        notifyDataSetChanged();
    }

    /*
    * 数据加载完毕时执行setCanLoadMore(),此时isLoading都置为false
    * */
    public void setCanLoadMore(boolean isCanLoadMore) {
        this.isCanLoadMore = isCanLoadMore;
        mOnScrollListener.setCanLoadMore(isCanLoadMore);
        mOnScrollListener.setLoading(false);
    }


    public interface OnLoadMoreListener {
        void onLoadMore(int currentPage);
    }

    private OnLoadMoreListener mOnLoadMoreListener;

    public void setOnLoadMoreListener(OnLoadMoreListener listener) {
        this.mOnLoadMoreListener = listener;
    }

}

Adapter的编写主要有以下几点:
1.因为有footerView提示加载更多,所以这里有两个自定义的ViewHolder,ContentViewHolder和FooterViewHolder,分别用来存放recyclerView的水果们,和底部的FooterView。
2.与一般的RecyclerView相比,这里主要是多了onAttachedToRecyclerView()和onDetachedFromRecyclerView()两个重写方法。
在onAttachedToRecyclerView()进行思路的第2步,在onDetachedFromRecyclerView()注销监听。
3.setCanLoadMore()一般在加载完数据时候调用,重置isCanLoadMore的状态。

3.编写MainActivity
public class MainActivity extends AppCompatActivity {

    private RecyclerView mRvFruits;
    private List<Fruit> mFruitList = new ArrayList<>();
    private FruitAdapter mAdapter;
    private static final int PER_PAGE = 10;

    private int mCurrentPage = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();

    }

    private void init() {
        initFruits();
        mAdapter = new FruitAdapter(mFruitList);
        mRvFruits = findViewById(R.id.rv_fruits);
        mRvFruits.setLayoutManager(new LinearLayoutManager(this));
        mRvFruits.setAdapter(mAdapter);
        mAdapter.setOnLoadMoreListener(new FruitAdapter.OnLoadMoreListener() {
            @Override
            public void onLoadMore(int currentPage) {
                mCurrentPage = currentPage;
                loadMoreTest();
            }
        });
    }

    private void initFruits() {
        Log.i("mytest", "initFruits");
        Fruit mango = new Fruit(getRandomLenthName("Mango"), R.drawable.mango);
        mFruitList.add(mango);
        Fruit orange = new Fruit(getRandomLenthName("Orange"), R.drawable.orange);
        mFruitList.add(orange);
        Fruit pear = new Fruit(getRandomLenthName("Pear"), R.drawable.pear);
        mFruitList.add(pear);
        Fruit pineapple = new Fruit(getRandomLenthName("Pineapple"), R.drawable.pineapple);
        mFruitList.add(pineapple);
        Fruit strawberry = new Fruit(getRandomLenthName("Strawberry"), R.drawable.strawberry);
        mFruitList.add(strawberry);
        Fruit watermelon = new Fruit(getRandomLenthName("Watermelon"), R.drawable.watermelon);
        mFruitList.add(watermelon);
        Fruit apple = new Fruit(getRandomLenthName("Apple"), R.drawable.apple);
        mFruitList.add(apple);
        Fruit banana = new Fruit(getRandomLenthName("Banana"), R.drawable.banana);
        mFruitList.add(banana);
        Fruit cherry = new Fruit(getRandomLenthName("Cherry"), R.drawable.cherry);
        mFruitList.add(cherry);
        Fruit grape = new Fruit(getRandomLenthName("Grape"), R.drawable.grape);
        mFruitList.add(grape);
    }

    //模拟加载网络数据
    private void loadMoreTest() {
        Observable.timer(2000, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        //第一个条件是有没有获取到新数据,第二个条件是新数据是否填满了一整页
                        //这里假如3页之后没有更多数据了,这里的条件根据实际网络请求的返回情况来确定。
                        Log.d("loadingtest", "main current page: " + mCurrentPage);
                        if (mCurrentPage < 4) {
                            List<Fruit> loadMoreList = new ArrayList<>();
                            Fruit pear = new Fruit(getRandomLenthName("Pear"), R.drawable.pear);
                            loadMoreList.add(pear);
                            Fruit pineapple = new Fruit(getRandomLenthName("Pineapple"), R.drawable.pineapple);
                            loadMoreList.add(pineapple);
                            Fruit strawberry = new Fruit(getRandomLenthName("Strawberry"), R.drawable.strawberry);
                            loadMoreList.add(strawberry);
                            Fruit pineapple2 = new Fruit(getRandomLenthName("Pineapple"), R.drawable.pineapple);
                            loadMoreList.add(pineapple2);
                            Fruit strawberry2 = new Fruit(getRandomLenthName("Strawberry"), R.drawable.strawberry);
                            loadMoreList.add(strawberry2);
                            Fruit watermelon = new Fruit(getRandomLenthName("Watermelon"), R.drawable.watermelon);
                            loadMoreList.add(watermelon);
                            Fruit apple = new Fruit(getRandomLenthName("Apple"), R.drawable.apple);
                            loadMoreList.add(apple);
                            Fruit banana = new Fruit(getRandomLenthName("Banana"), R.drawable.banana);
                            loadMoreList.add(banana);
                            Fruit cherry = new Fruit(getRandomLenthName("Cherry"), R.drawable.cherry);
                            loadMoreList.add(cherry);
                            Fruit grape = new Fruit(getRandomLenthName("Grape"), R.drawable.grape);
                            loadMoreList.add(grape);
                            mFruitList.addAll(loadMoreList);

                            //如果未填满一整页,那么肯定没有更多数据了
                            Log.d("loadingtest", "size1: " + mFruitList.size() + "  -  size2: " + mCurrentPage * PER_PAGE);
                            if (mFruitList.size() == mCurrentPage * PER_PAGE) {
                                mAdapter.setCanLoadMore(true);
                            } else {
                                mAdapter.setCanLoadMore(false);
                            }
                            mAdapter.setData(mFruitList);

                        } else {
                            //网络数据已加载完
                            mAdapter.setCanLoadMore(false);
                            mAdapter.notifyDataSetChanged();
                        }
                    }
                });

    }

    private String getRandomLenthName(String name) {
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(name);
        }
        return builder.toString();
    }

}

在init()中初始化mRvFruits, 并设定加载更多的操作是loadMoreTest()。在loadMoreTest()中先判断了有没有获取到新数据,然后再判断新数据是否填满了一页。
1.如果没有新数据就说明数据已加载完毕。setCanLoadMore(false),并更新适配器。
2.如果有新数据,但是新数据没有填满一页,也说明数据已经加载完毕。设置setCanLoadMore(false),更新数据。
3.如果有新数据并且新数据填满了一页,说明可能还有更多的数据,可以加载更多,setCanLoadMore(true),更新数据。


以上就是Android上拉加载更多的思路和实现方法,希望能够帮助到你。Demo上传到了我的github,需要的朋友们可以去这里下载:
Kanade —— 上拉加载更多Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348