我已經寫了個Demo上傳到GitHub上了。大家可以看看。BaseLoadAdapter
大家好,又是新的一期項目需求討論,這期的需求是關于分頁加載。我本來先是網上看RecycleView的分頁加載的方式,但是看到很多文章都是幫你封裝好,然后讓你拿來直接用,一是直接拿別人封裝的東西自己還是不理解,二是如果要加定制化的東西,改別人的代碼畢竟不方便,或者你就用了一個功能,別人封裝好的可能包含很多功能,就多余了。所以我主要還是來分析,分頁加載到底是怎么樣一步步來實現,而不是說封裝好來讓大家使用。
什么是分頁加載,通俗的說就是,比如你在微信朋友圈,可能今天一共有100個別人發在朋友圈的狀態:
有二種方式加載方式:
- 后臺是直接把100個別人發的狀態一次性給你了,然后你在列表上層顯示100個朋友圈狀態,然后上下滑動查看。
- 可能后臺先給你10個朋友圈狀態,然后當你拉到底的時候,顯示<加載中>,然后再去像后臺請求后面10條朋友圈狀態,然后再滑到底部,再去加載10個新的數據。一直到最后100個數據都加載完了。就在底部顯示<沒有更多數據>了。用戶也就知道今天朋友圈狀態已經看完了。
優缺點:
第一種加載開發起來方便,簡單。可以直接下滑看全部狀態,不需要看幾條,等它加載更多后,再看幾條,再等著加載再去看。但是如果你只看了前面5個朋友圈狀態,卻把100條的數據都發給你,一個是流量問題,一個是加載的速度問題。畢竟數據變多了,而且萬一有好幾千條數據怎么辦。
第二種開發起來麻煩,要設置多種狀態。比如滑到底了要去再去獲取信息,然后顯示<加載中>,如果還有數據就加入,沒有數據再去顯示<沒用更多數據>。然后假如獲取失敗,還要顯示<加載失敗>。但是彌補了上述的第一種方法的缺點
所以第一種更適合用于條數固定,或者條數不多的情況下。開發方便。比如微信的聯系人列表。一般都是直接全部層顯,不會說我先顯示幾個聯系人,然后下拉再加載再去加載剩下的聯系人。第二種更適合數據會不停的變多的情況,比如你的某個軟件有個交易查詢功能,查詢你的交易記錄,雖然剛開始你的列表上的數據比較少,但是隨著時間的推移,你的數據也會越來越多。所以就更適合第二種方式。
好了我們開始我們的正題,也正是項目中遇到的具體需求。
后臺接口:
現在是一個交易記錄列表,后臺給我的接口是這樣的:第一次給我10個數據,我這邊就先顯示10個,然后上拉到底的時候,把最后一個數據的orderid(也就是訂單id)給他,他再根據這個id,加載接下來這個訂單后面的10個數據給我。
(以前還有一種接口是這樣的。比如第一次要數據的時候給我10條,然后同時給我一個頁數的字段,告訴我如果是一頁10條的話,一共有幾頁,然后我后面再去加載數據的時候就傳頁數即可。)
(以下為了方便。我都假設每次后臺最多傳遞給我4個數據。)
第一步:
第一次調用接口拿數據,分二種情況:
- 第一次給我就沒有4條數據,比如就給我3條,那就說明肯定沒有其他數據了。這時候你就算拉到最下面,也不需要顯示什么加載更多的顯示。(別問我為啥。因為如果還有更多,最少也要給你4條)
- 如果給了你4條,這時候你滑到底部就要顯示<加載中>。因為有可能說明后面還有數據。
那我們怎么樣才能滑到下面的時候能看到<加載中>這個呢,其實很簡單,把這個<加載中>也作為RecycleView的列表中的一項即可。
如下圖所示:
這樣是不是當你滑到最下面的時候一定能看到<加載中>這一項了。
所以在第一次訪問的時候,我們的RecycleView的adapter中返回列表的個數要進行判斷。如果是小于4條(就是跟后臺約定好的條數),那adapter中item的個數直接返回就是實際的條數,比如返回三條,那我們列表就只要顯示3條即可。如果是返回了4條,那么我們這時候adapter中item的個數就返回4+1 條了。(4條數據外加一個<加載中>這一項)。
第二步:
我們既然我們知道我們需要有<加載中>這一項,那我們就肯定知道這個<加載中>跟我們上面的具體的一項項數據的布局肯定不一樣。比如我上面實際開發中,上面的數據布局是交易記錄。那我們就來看怎么實現這個RecycleView的列表中如何層顯不同布局。
我們自定義一個BaseLoadAdapter繼承RecycleView.Adapter。然后覆寫public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
方法。
這里面有個viewType,我們可以根據不同的viewType來返回不同的ViewHolder即可。那這個viewType又是怎么來的。就是復寫
public int getItemViewType(int position)
方法,不同的position
的item,返回特定的viewType即可。
所以我們這里就是:
public class BaseLoadAdapter<T> extends RecyclerView.Adapter {
public List<T> list;
public static final int TYPE_OTHER = 1;
public static final int TYPE_BOTTOM = 2;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (TYPE_BOTTOM == viewType) {
//返回我們的那個加載中的布局Viewholder
return new NewBottomViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_footer_new, parent, false));
} else {
//返回我們的交易記錄的布局Viewholder
return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transferexam_info, parent, false));
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_BOTTOM) {
//對相應的onBindViewHolder進行處理
LinearLayout container = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).container;
final ProgressBar pb = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).pb;
final TextView content = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).content;
.................
.................
.................
} else {
//對具體的交易記錄的itemView進行相應的控件進行處理。
TransferExamItemBean bean = ((TransferExamItemBean) list.get(position));
holder.itemView.setTag(bean);
((ExamRefreshAdapter.MyViewHolder) holder).name.setText(bean.getToCompanyName());
((ExamRefreshAdapter.MyViewHolder) holder).date.setText(bean.getCreateDate());
((ExamRefreshAdapter.MyViewHolder) holder).money.setText(bean.getAmount()+"");
}
}
@Override
public int getItemCount() {
return list.size() < 4 ? list.size() : list.size() + 1;
}
@Override
public int getItemViewType(int position) {
if (!list.isEmpty() && list.size() < position ) {
return TYPE_OTHER;
} else {
return TYPE_BOTTOM;
}
}
}
第三步:
好了,現在我們已經可以滑到下面的時候能看到<加載中>這一項了。因為我們看到<加載中>的時候要繼續去向后臺訪問獲取數據,說明當滑到底部看到這個<加載中>的時候我們就要去調用相應的后臺接口去獲取接下來的交易記錄數據。那問題就變成了:我們怎么知道我們已經滑到了底部并且已經出現了<加載中>這一項,然后進行網絡接口調用。
自定義繼承RecyclerView.OnScrollListener
,復寫public void onScrolled(RecyclerView recyclerView, int dx, int dy)
方法,我們就可以監聽RecycleView的滑動了。
public class LoadMoreScrollListener etends RecyclerView.OnScrollListener {
private RecyclerView mRecyclerView;
public LoadMoreScrollListener(RecyclerView recyclerView) {
this.mRecyclerView = recyclerView;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
BaseLoadMoreAdapter adapter = (BaseLoadMoreAdapter) mRecyclerView.getAdapter();
if (null == manager) {
throw new RuntimeException("you should call setLayoutManager() first!!");
}
if (manager instanceof LinearLayoutManager) {
int lastCompletelyVisibleItemPosition = ((LinearLayoutManager) manager).findLastCompletelyVisibleItemPosition();
if (adapter.getItemCount() > 4 &&
lastCompletelyVisibleItemPosition == adapter.getItemCount() - 1 &&
adapter.isHasMore()) {
adapter.isLoadingMore();
}
}
}
}
說明:
- adapter.getItemCount():獲取adapter一共有多少項。
- findLastCompletelyVisibleItemPosition():由字面意思就可以看懂,返回最后一個完全可見的item項的position值。因為position是從0開始的,所以當findLastCompletelyVisibleItemPosition()返回的是adapter.getItemCount() - 1的時候,就說明已經可以看到最后一項了。
- adapter.isHasMore():這個方法是我們自己在adapter中自定義的方法,返回一個boolean值,比如我們再次調用后臺接口獲取數據的時候,后臺給我們返回的數據已經為空了。那我們就知道我們后面已經無法加載更多數據了。這時候把這個boolean值設為false,這樣在監聽滑動的時候就算滑到最底下也不需要去再次調用接口。
- adapter.isLoadingMore():這個方法也是我們自己在adapter中自定義的方法,去調用后臺接口。獲取數據等后續操作。
然后進行監聽即可recyclerView.addOnScrollListener(new LoadMoreScrollListener(recyclerView));
第四步:
底部這個<加載中>item在以后會有二種狀態,一種是<加載失敗>選項,一種是后臺給的數據為空后的<沒有更多>選項。
而我們第一次滑到底部的時候,總是先顯示<加載中>。
因為這個最后一個選項會有三種狀態顯示情況。(即:<加載中>,<加載失敗>,<加載更多>)所以定義一個變量。用來記錄最后一項當前的狀態。
public int loadState;
int STATE_LOADING = 1;
int STATE_LASTED = 2;
int STATE_ERROR = 3;
因為我們在滑到底部的時候去調用我們自己定義在adapter中的自定義方法isLoadingMore(),這個方法里面是什么內容呢:
public final void isLoadingMore() {
if (loadState == STATE_LOADING) {
return;
}
loadState = STATE_LOADING;
notifyItemRangeChanged(getItemRealCount(), 1);
}
沒錯,我們就是默認先讓當前最后一項的狀態先變為STATE_LOADING,然后去刷新最后一項的內容,notifyItemRangeChanged(int positionStart, int itemCount)
方法,從字面意思就能看出通知某個范圍內的數據發生改變了。從posistionStart開始的itemCount個數據發生變化。我們因為是最后一項,它的position是list.size(),然后個數是一個,所以是notifyItemRangeChanged(getItemRealCount(), 1);
然后在通知最后一項發生變化后我們的onBindViewHolder就會再次被調用,這時候我們就要根據相應的不同STATE狀況下對這個最后一項的布局進行相應的處理:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_BOTTOM) {
//對相應的onBindViewHolder進行處理
LinearLayout container = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).container;
final ProgressBar pb = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).pb;
final TextView content = ((BaseLoadMoreAdapter.NewBottomViewHolder) holder).content;
//根據不同state來進行相應處理
switch (loadState) {
//1.當state是STATE_LOADING,
//那我們就知道要把最后一項的字變為“加載中”
//并且要讓我寫在布局中的滾動條進行顯示(一般在加載中才會有滾動條的顯示)
//這時候調用我們的自定義方法loadMoreListener.onLoadMore();方法,這個方法是用來訪問后臺接口,然后去獲取數據的。
case AdapterLoader.STATE_LOADING:
content.setText("加載中");
container.setOnClickListener(null);
pb.setVisibility(View.VISIBLE);
if (loadMoreListener != null) {
loadMoreListener.onLoadMore();
}
break;
//2.當state是STATE_LASTED的時候
//最后一項的字變為“沒有更多了”
//我們的加載進度條也可以隱藏了
case AdapterLoader.STATE_LASTED:
pb.setVisibility(View.GONE);
container.setOnClickListener(null);
content.setText("--- 沒有更多了 ---");
//大家還記不記得我們在監聽滑動的時候,我們有個adapter.isHasMore()變量作為控制,
//當我們的狀態已經變為了STATE_LASTED了。那我們也不需要再監聽是否滑到了最底部了。因為已經加載全部了。
adapter.setHasMore(false);
break;
//3.當state是STATE_ERROR的時候
//最后一項的字變為“加載更多失敗點擊重試”
//我們的加載進度條也可以隱藏了
//這里會跟其他二個狀態不同的地方,那就是當加載失敗的時候,我們可以通過點擊這項,再去重新加載。
//所以就要在最后一項中添加一個點擊事件。所以在其他二個狀態下,要重新設置setOnClickListener(null),來取消這個重新加載的點擊事件。
case AdapterLoader.STATE_ERROR:
pb.setVisibility(View.GONE);
content.setText("--- 加載更多失敗點擊重試 ---");
container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (loadMoreListener != null) {
loadMoreListener.onLoadMore();
}
content.setText("加載中");
pb.setVisibility(View.VISIBLE);
}
});
break;
default:
break;
}
} else {
//對具體的交易記錄的itemView進行相應的控件進行處理。
TransferExamItemBean bean = ((TransferExamItemBean) list.get(position));
holder.itemView.setTag(bean);
((ExamRefreshAdapter.MyViewHolder) holder).name.setText(bean.getToCompanyName());
((ExamRefreshAdapter.MyViewHolder) holder).date.setText(bean.getCreateDate());
((ExamRefreshAdapter.MyViewHolder) holder).money.setText(bean.getAmount()+"");
}
}
好了,所以現在的情況是,滑到底部,然后通知去刷新底部的item,因為剛開始默認是STATE_LOADING,所以在刷新創建這底部這項的時候,就會按照我們寫的判斷。出現加載框,文件顯示“加載中”,然后會運行我們寫的向后臺獲取數據的接口。然后我們只要在訪問后臺接口,根據返回的情況,適當的更改底部item的狀態,然后再去刷新底部item,就可以了。
第五步:
我們滑到了底部,調用了我們的獲取數據的接口代碼,這時候我們要分三種情況來處理:
- 如果后臺給我們的是四個數據,那說明有可能后面還會有數據,那我們這時候拿到四條數據后,只需要在最后一項前面插入,這樣的話,最后一項的狀態也不需要改變。
這時候我們把新加載的四條數據插在<加載中>的前面,然后我們對于最后一項不需要做處理,這樣當我們往下滑的時候。又會重新跑一遍上面的邏輯。(也就是再次看到最后一項,調用notifyItemRangeChanged方法,然后根據狀態去刷新最后一項,然后因為我們沒改變過狀態,還是STATE_LOADING,所以又再去向后臺拿數據。)
我們在adapter中定義方法:
public final void appendList(List<T> data) {
int positionStart = list.size();
list.addAll(data);
int itemCount = list.size() - positionStart;
if (positionStart == 0) {
notifyDataSetChanged();
} else {
notifyItemRangeInserted(positionStart + 1, itemCount);
}
}
假設我們已經拿到了后臺給我們的list數據,這時候我們判斷下這個list數據個數是不是等于4,如果等于4,我們就調用adapter.appendList(list)即可
2.如果后臺給你的數據是小于四個的,這時我們要設置我們的adapter中最后一項的狀態為STATE_LASTED,然后也要調用adapter.appendList(list);
3.查看后臺返回的json中的code值是不是200(比如code== 200說明獲取數據成功),我們獲取到的數據時候,就對code做判斷。如果不是200,那我們就把adapter中的狀態變為STATE_ERROR。然后再調用notifyItemRangeChanged去刷新一下最后一項即可。這樣最后一項就變成了<加載失敗>,并且具有了點擊重新加載的功能。
注意,比如我們已經滑到最下面了。這時候去調用我們后臺的接口了。這時候,最好前面用一個boolean值去做判斷。比如下面這個方法是我的訪問后臺接口方法:
public void onLoadMore() {
if (isRun) {
return;
}
isRun = true;
presenter.getTransferExamList("zjzt", lastOrderID);
}
防止重復滑到下面去調用多次后臺接口,當后臺接口返回數據后,再設置isRun = false即可。
我先大致寫到這里。后面再貼上完整的代碼,我主要先寫的還是對分頁加載來進行分析。thanks。哪里不對,請指教。