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