Recylerview 加載更多功能實現(分頁加載)

2017.3.20更新

更新說明:之前看評論裏的童鞋們給我反映問題,很感謝大家指出,針對出現的問題我進行了一些改進。同時將加載更多的這個功能從主體的adapter中分離出來,使用了裝飾者模式,降低了代碼的耦合,這樣便於維護和修改。

裝飾者模式是常用的Java設計模式之一,不熟悉的童鞋可以自行查閱資料,先了解了裝飾者模式看以下內容會容易懂一點~這個設計模式在Android裏也是廣泛應用,最典型的就是Context的應用了。

……………………….我是分割線…………………………………………

Recyclerview是 Listview 的升級版本,在項目中使用較爲廣泛,官方也推薦我們使用 Recyclerview 來代替 Listview,在此就不多說 Recyclerview 的優勢特點 balala了。。。

在實際項目中,列表通常是分頁的,請求服務器也只會一次請求若干條,按需加載,這樣比較節省流量,這樣就有了我們很常見的上拉加載更多的功能,具體的實現效果如下圖:

這裏寫圖片描述

該activity的佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="io.geek.myapplication.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>


</LinearLayout>

底部加載更多的view佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:gravity="center"
              android:layout_height="wrap_content">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:text="正在加載..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

底部提示到底的view佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:padding="8dp"
              android:gravity="center"
              android:layout_height="wrap_content">

    <TextView
        android:text="已經到底了"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

數據item的view:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="horizontal"
              android:gravity="center"
              android:padding="8dp"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <View
        android:background="@color/colorAccent"
        android:layout_width="50dp"
        android:layout_height="50dp"/>

    <TextView
        android:gravity="center"
        android:id="@+id/tv_content"
        tools:text="我是第一項"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

接下來就是代碼的編寫了,開頭就已經講到過我們在這要用到裝飾者模式,其實裝飾者模式並沒有多麼複雜,可以理解成給一個類裝飾點新功能,並且不會影響到這個類本來的功能。裝飾者模式寫法也很簡單,分爲以下幾步套路:

1. 裝飾者模式套路第一步:

創建基類(裝飾類和被裝飾類共同父類),首先寫一個抽象類BaseAdapter,繼承自RecyclerView.Adapter,這個Adapter中只封裝了與數據源相關的字段和方法:

/**
 * 與數據源相關的字段和方法封裝在父類中
 */

public abstract class BaseAdapter<T> extends RecyclerView.Adapter {
    protected List<T> dataSet = new ArrayList<>();


    public void updateData(List dataSet) {
        this.dataSet.clear();
        appendData(dataSet);
    }

    public void appendData(List dataSet) {
        if (dataSet != null && !dataSet.isEmpty()) {
            this.dataSet.addAll(dataSet);
            notifyDataSetChanged();
        }
    }

    public List<T> getDataSet() {
        return dataSet;
    }

    @Override
    public int getItemCount() {
        return dataSet.size();
    }
}

2. 裝飾者模式套路第二步

創建裝飾類,然後開始編寫加載更多的adapter的包裝類,這個類繼承自BaseAdpater,這個裝飾類中只負責處理加載更多的邏輯,代碼如下:

/**
 * 在這個裝飾器中,只做與加載更多相關操作。

public class LoadMoreAdapterWrapper extends BaseAdapter<String> {
    private BaseAdapter mAdapter;
    private static final int mPageSize = 10;
    private int mPagePosition = 0;
    private boolean hasMoreData = true;
    private OnLoad mOnLoad;

    public LoadMoreAdapterWrapper(BaseAdapter adapter, OnLoad onLoad) {
        mAdapter = adapter;
        mOnLoad = onLoad;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == R.layout.list_item_no_more) {
            View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
            return new NoMoreItemVH(view);
        } else if (viewType == R.layout.list_item_loading) {
            View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
            return new LoadingItemVH(view);
        } else {
            return mAdapter.onCreateViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof LoadingItemVH) {
            requestData(mPagePosition, mPageSize);
        } else if (holder instanceof NoMoreItemVH) {

        } else {
            mAdapter.onBindViewHolder(holder, position);
        }
    }

    private void requestData(int pagePosition, int pageSize) {

        //網絡請求,如果是異步請求,則在成功之後的回調中添加數據,並且調用notifyDataSetChanged方法,hasMoreData爲true
        //如果沒有數據了,則hasMoreData爲false,然後通知變化,更新recylerview

        if (mOnLoad != null) {
            mOnLoad.load(pagePosition, pageSize, new ILoadCallback() {
                @Override
                public void onSuccess() {
                    notifyDataSetChanged();
                    mPagePosition = (mPagePosition + 1) * mPageSize;
                    hasMoreData = true;
                }

                @Override
                public void onFailure() {
                    hasMoreData = false;
                }
            });
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            if (hasMoreData) {
                return R.layout.list_item_loading;
            } else {
                return R.layout.list_item_no_more;
            }
        } else {
            return mAdapter.getItemViewType(position);
        }
    }

    @Override
    public int getItemCount() {
        return mAdapter.getItemCount() + 1;
    }

    static class LoadingItemVH extends RecyclerView.ViewHolder {

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

    }

    static class NoMoreItemVH extends RecyclerView.ViewHolder {

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

}

public interface OnLoad {
    void load(int pagePosition, int pageSize, ILoadCallback callback);
}


public interface ILoadCallback {
    void onSuccess();

    void onFailure();
}

解析如下:
1. 該類中的傳入的mAdapter就是需要被裝飾的adapter,mOnLoad是自定義的一個接口,傳入之後可以回調處理加載更多數據。
2. 首先重寫getItemCount方法,因爲在列表中最後始終會有一個加載更多或者是到底提示的view,所以item的數目始終是數據源的數目多一個。
3. 重寫getItemType方法:首先判斷position是否是最後一個,如果不是的話,則直接返回被裝飾的mAdapter的getItemType;如果是的話判斷hasMoreData變量,是true則返回加載更多的佈局文件ID,反之返回到底的佈局文件ID。這裏的hasMoreData是我們自己定義的一個boolean字段,通過改變這個值就可以最後一個view應該是加載更多還是到底。
4. 重寫onCreateViewHolder方法,在這個方法中通過判斷viewType的類型返回不同的ViewHolder。
5. 重寫onBindViewHolder方法,在這個方法中,判斷holder的類型的不同做出不同的操作,如果holder是加載更多的holder,那麼表示加載更多的view正在展示,這時候就應該做出加載更多的操作,這個操作放在了requestData方法中。
6. 在requestData方法中,觸發了OnLoad接口中的load回調,傳入的參數有當前的頁碼加載數據一頁的大小,加載完成之後的回調接口,可以看到加載完成之後會進行不同的作,如果成功,則設置hasMoreData爲true,並且通知數據發生改變,更新列表,改變前頁碼;如果失敗的話,則把hasMoreData設置爲false。

3. 裝飾者模式套路第三步

創建被裝飾類,同樣這個類也需要繼承自BaseAdapter:


/**
 * 被裝飾類要和裝飾類繼承自同一父類
 */

public class MyAdapter extends BaseAdapter<String> {
    private Context mContext;

    public MyAdapter(Context context) {
        mContext = context;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(mContext).inflate(R.layout.list_item_mine, parent, false);
        return new MyViewHolder(v);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ((MyViewHolder) holder).bind(getDataSet().get(position));
    }


    static class MyViewHolder extends RecyclerView.ViewHolder {
        TextView mTextView;

        public MyViewHolder(View itemView) {
            super(itemView);

            mTextView = (TextView) itemView.findViewById(R.id.tv_content);
        }

        public void bind(CharSequence content) {
            mTextView.setText(content);
        }
    }

}

4. 裝飾者模式套路第四步

最後一步就是如何使用,這個adapter就是一個普通的沒有上拉加載功能的adapter,如果要給它加上這個功能,只需要這麼使用:


public class MainActivity extends AppCompatActivity {

    RecyclerView mRecyclerView;
    BaseAdapter mAdapter;
    int loadCount;

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

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler);

        //創建被裝飾者類實例
        final MyAdapter adapter = new MyAdapter(this);
        //創建裝飾者實例,並傳入被裝飾者和回調接口
        mAdapter = new LoadMoreAdapterWrapper(adapter, new OnLoad() {
            @Override
            public void load(int pagePosition, int pageSize, final ILoadCallback callback) {
                //此處模擬做網絡操作,2s延遲,將拉取的數據更新到adpter中
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        List<String> dataSet = new ArrayList();
                        for (int i = 0; i < 10; i++) {
                            dataSet.add("我是一條數據");
                        }
                        //數據的處理最終還是交給被裝飾的adapter來處理
                        adapter.appendData(dataSet);
                        callback.onSuccess();
                        //模擬加載到沒有更多數據的情況,觸發onFailure
                        if (loadCount++ == 3) {
                            callback.onFailure();
                        }
                    }
                }, 2000);
            }
        });
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
    }

}

這樣就完整實現了 Recylerview 的加載更多功能,使用裝飾者模式使得整個代碼結構更加清晰,在被裝飾的adapter中就不用處理太多邏輯,專心處理數據展示即可,易於維護。

其中比較繞的地方可能就是數據加載的回調接口,不過仔細理一下還是沒有多大問題,總體來說還是很簡單的,如果還是有不明白的,可能你對 Recylerview 的使用方法還不夠了解,可以自行學習。
有不當之處請大家批評指正,謝謝:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章