手把手教你實現Android RecyclerView上拉加載功能

心靈雞湯:知之者不如好之者,好之者不如樂之者。javascript

摘要

一直在用到RecyclerView時都會微微一顫,由於一直都沒去了解怎麼實現上拉加載,受夠了每次去Github找開源引入,由於感受就爲了一個上拉加載功能而去引入一大堆你不知道有多少BUG的代碼,不只增長了項目的冗餘程度,並且出現BUG的時候,你卻發現很難去改,正由於這樣,我就下定決心去了解如何來實現RecyclerView的上拉加載功能,相信你們和我有過一樣的狀況,可是我相信,只要你給本身幾分鐘看完這篇文章,你就會發現實現一個上拉加載是很是的簡單。java

什麼是上拉加載

上拉加載和下拉刷新相對應,在Android API LEVEL 19(即4.4)以後,Google官方推出了SwipeRefreshLayout和RecyclerView的共同使用,爲咱們提供了更加便捷的列表下拉刷新功能,可是,並無給咱們提供上拉加載功能,可是在RecyclerView強大的可擴展之下,Github上面有了不少開源項目實現了上拉加載功能,即咱們不會一次性將全部數據加載到列表中,當用戶滑動到底部時,再向服務器請求數據,再填充數據到列表中,這樣不只能夠有更好的人機交互,同時在減小了服務器的壓力的同時也對客戶端的性能有了更好的提高。本篇文章主要經過介紹實現如下簡單的上拉加載功能,同窗們能夠在掌握了最基本的實現功能以後,再經過擴展和優化,甚至能夠封裝成比較通用的代碼,開源到Github上面。android

Demo

實現思路

1、XML的實現

佈局很簡單,只有一個SwipeRefreshLayout包裹了一個RecyclerView,相信用過RecyclerView的都很容易看懂。以下爲activity_main.xml:git

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refreshLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"/> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout>複製代碼

而後,咱們RecyclerView的Item佈局也是很是簡單,只有一個TextView。以下爲item.xml:github

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="120dp" android:background="@android:color/holo_blue_dark" android:gravity="center" android:textSize="30sp" android:textColor="#ffffff" android:text="11" android:layout_marginBottom="1dp"/> </LinearLayout>複製代碼

看到咱們效果圖都知道,在咱們上拉時,還有一個提示的條目,我定義爲 footview.xml:緩存

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView android:id="@+id/tips" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="30dp" android:textSize="15sp" android:layout_marginBottom="1dp"/> </LinearLayout>複製代碼

2、初始化SwipeRefreshLayout

在準備好了佈局文件以後,咱們就把目光轉到Activity中去,首先咱們須要初始化SwipeRefreshLayout,初始化也是很簡單,這裏省去了findView操做,因此就只有設置轉動的顏色,還有設置刷新的監聽事件:服務器

private void initRefreshLayout() {
    refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
            android.R.color.holo_orange_light, android.R.color.holo_green_light);
    refreshLayout.setOnRefreshListener(this);
}

@Override
public void onRefresh() {
    // 設置可見
    refreshLayout.setRefreshing(true);
    // 重置adapter的數據源爲空
    adapter.resetDatas();
    // 獲取第第0條到第PAGE_COUNT(值爲10)條的數據
    updateRecyclerView(0, PAGE_COUNT);
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            // 模擬網絡加載時間,設置不可見
            refreshLayout.setRefreshing(false);
        }
    }, 1000);
}複製代碼

3、定義RecyclerView的Adapter

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<String> datas; // 數據源
    private Context context;    // 上下文Context

    private int normalType = 0;     // 第一種ViewType,正常的item
    private int footType = 1;       // 第二種ViewType,底部的提示View

    private boolean hasMore = true;   // 變量,是否有更多數據
    private boolean fadeTips = false; // 變量,是否隱藏了底部的提示

    private Handler mHandler = new Handler(Looper.getMainLooper()); //獲取主線程的Handler

    public MyAdapter(List<String> datas, Context context, boolean hasMore) {
        // 初始化變量
        this.datas = datas;
        this.context = context;
        this.hasMore = hasMore;
    }

    // 獲取條目數量,之因此要加1是由於增長了一條footView
    @Override
    public int getItemCount() {
        return datas.size() + 1;
    }

    // 自定義方法,獲取列表中數據源的最後一個位置,比getItemCount少1,由於不計上footView
    public int getRealLastPosition() {
        return datas.size();
    }


    // 根據條目位置返回ViewType,以供onCreateViewHolder方法內獲取不一樣的Holder
    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return footType;
        } else {
            return normalType;
        }
    }

    // 正常item的ViewHolder,用以緩存findView操做
    class NormalHolder extends RecyclerView.ViewHolder {
        private TextView textView;

        public NormalHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv);
        }
    }

    // // 底部footView的ViewHolder,用以緩存findView操做
    class FootHolder extends RecyclerView.ViewHolder {
        private TextView tips;

        public FootHolder(View itemView) {
            super(itemView);
            tips = (TextView) itemView.findViewById(R.id.tips);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 根據返回的ViewType,綁定不一樣的佈局文件,這裏只有兩種
        if (viewType == normalType) {
            return new NormalHolder(LayoutInflater.from(context).inflate(R.layout.item, null));
        } else {
            return new FootHolder(LayoutInflater.from(context).inflate(R.layout.footview, null));
        }
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
        // 若是是正常的imte,直接設置TextView的值
        if (holder instanceof NormalHolder) {
            ((NormalHolder) holder).textView.setText(datas.get(position));
        } else {
            // 之因此要設置可見,是由於我在沒有更多數據時會隱藏了這個footView
            ((FootHolder) holder).tips.setVisibility(View.VISIBLE);
            // 只有獲取數據爲空時,hasMore爲false,因此當咱們拉到底部時基本都會首先顯示「正在加載更多...」
            if (hasMore == true) {
                // 不隱藏footView提示
                fadeTips = false;
                if (datas.size() > 0) {
                    // 若是查詢數據發現增長以後,就顯示正在加載更多
                    ((FootHolder) holder).tips.setText("正在加載更多...");
                }
            } else {
                if (datas.size() > 0) {
                    // 若是查詢數據發現並無增長時,就顯示沒有更多數據了
                    ((FootHolder) holder).tips.setText("沒有更多數據了");

                    // 而後經過延時加載模擬網絡請求的時間,在500ms後執行
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // 隱藏提示條
                            ((FootHolder) holder).tips.setVisibility(View.GONE);
                            // 將fadeTips設置true
                            fadeTips = true;
                            // hasMore設爲true是爲了讓再次拉到底時,會先顯示正在加載更多
                            hasMore = true;
                        }
                    }, 500);
                }
            }
        }
    }

    // 暴露接口,改變fadeTips的方法
    public boolean isFadeTips() {
        return fadeTips;
    }

    // 暴露接口,下拉刷新時,經過暴露方法將數據源置爲空
    public void resetDatas() {
        datas = new ArrayList<>();
    }

    // 暴露接口,更新數據源,並修改hasMore的值,若是有增長數據,hasMore爲true,不然爲false
    public void updateList(List<String> newDatas, boolean hasMore) {
        // 在原有的數據之上增長新數據
        if (newDatas != null) {
            datas.addAll(newDatas);
        }
        this.hasMore = hasMore;
        notifyDataSetChanged();
    }

}複製代碼

4、初始化RecyclerView

private void initRecyclerView() {
    // 初始化RecyclerView的Adapter
    // 第一個參數爲數據,上拉加載的原理就是分頁,因此我設置常量PAGE_COUNT=10,即每次加載10個數據
    // 第二個參數爲Context
    // 第三個參數爲hasMore,是否有新數據
    adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
    mLayoutManager = new GridLayoutManager(this, 1);
    recyclerView.setLayoutManager(mLayoutManager);
    recyclerView.setAdapter(adapter);
    recyclerView.setItemAnimator(new DefaultItemAnimator());

    // 實現上拉加載重要步驟,設置滑動監聽器,RecyclerView自帶的ScrollListener
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            // 在newState爲滑到底部時
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                // 若是沒有隱藏footView,那麼最後一個條目的位置就比咱們的getItemCount少1,本身能夠算一下
                if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // 而後調用updateRecyclerview方法更新RecyclerView
                            updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
                        }
                    }, 500);
                }

                // 若是隱藏了提示條,咱們又上拉加載時,那麼最後一個條目就要比getItemCount要少2
                if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // 而後調用updateRecyclerview方法更新RecyclerView
                            updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
                        }
                    }, 500);
                }
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            // 在滑動完成後,拿到最後一個可見的item的位置
            lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
        }
    });
}

// 上拉加載時調用的更新RecyclerView的方法
private void updateRecyclerView(int fromIndex, int toIndex) {
    // 獲取從fromIndex到toIndex的數據
    List<String> newDatas = getDatas(fromIndex, toIndex);
    if (newDatas.size() > 0) {
        // 而後傳給Adapter,並設置hasMore爲true
        adapter.updateList(newDatas, true);
    } else {
        adapter.updateList(null, false);
    }
}複製代碼

因此,Activity的完整代碼以下:網絡

public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
    private SwipeRefreshLayout refreshLayout;
    private RecyclerView recyclerView;
    private List<String> list;

    private int lastVisibleItem = 0;
    private final int PAGE_COUNT = 10;
    private GridLayoutManager mLayoutManager;
    private MyAdapter adapter;
    private Handler mHandler = new Handler(Looper.getMainLooper());

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

        initData();
        findView();
        initRefreshLayout();
        initRecyclerView();
    }

    private void initData() {
        list = new ArrayList<>();
        for (int i = 1; i <= 40; i++) {
            list.add("條目" + i);
        }
    }


    private void findView() {
        refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);

    }

    private void initRefreshLayout() {
        refreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,
                android.R.color.holo_orange_light, android.R.color.holo_green_light);
        refreshLayout.setOnRefreshListener(this);
    }

    private void initRecyclerView() {
        adapter = new MyAdapter(getDatas(0, PAGE_COUNT), this, getDatas(0, PAGE_COUNT).size() > 0 ? true : false);
        mLayoutManager = new GridLayoutManager(this, 1);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setAdapter(adapter);
        recyclerView.setItemAnimator(new DefaultItemAnimator());

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (adapter.isFadeTips() == false && lastVisibleItem + 1 == adapter.getItemCount()) {
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
                            }
                        }, 500);
                    }

                    if (adapter.isFadeTips() == true && lastVisibleItem + 2 == adapter.getItemCount()) {
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                updateRecyclerView(adapter.getRealLastPosition(), adapter.getRealLastPosition() + PAGE_COUNT);
                            }
                        }, 500);
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
            }
        });
    }

    private List<String> getDatas(final int firstIndex, final int lastIndex) {
        List<String> resList = new ArrayList<>();
        for (int i = firstIndex; i < lastIndex; i++) {
            if (i < list.size()) {
                resList.add(list.get(i));
            }
        }
        return resList;
    }

    private void updateRecyclerView(int fromIndex, int toIndex) {
        List<String> newDatas = getDatas(fromIndex, toIndex);
        if (newDatas.size() > 0) {
            adapter.updateList(newDatas, true);
        } else {
            adapter.updateList(null, false);
        }
    }

    @Override
    public void onRefresh() {
        refreshLayout.setRefreshing(true);
        adapter.resetDatas();
        updateRecyclerView(0, PAGE_COUNT);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshLayout.setRefreshing(false);
            }
        }, 1000);
    }
}複製代碼

後話

以上代碼我是考慮到了更多的邊界條件,因此在代碼上會稍微多了一點,可是也不影響觀看。你們也能夠經過改變數據源的數量和PAGE_COUNT等來測試,每一個人在具體使用上都會有不一樣的要求,因此基本代碼我擺了出來,衆口難調,更多的細節須要你們來優化,例如footView能夠設置一個動畫條,下拉刷新用其餘樣式替換原生的樣式等,我想,這些對於學習完這篇文章的你來講,都會是簡單的問題了。ide

Demo下載

Github下載:PullToLoadData-RecyclerViewoop

CSDN資源:PullToLoadData-RecyclerView

相關文章
相關標籤/搜索