概述
ListView
絕對(duì)是我們開發(fā)中的高頻的控件。目前很多人建議使用ReclyeView
替換ListView
。RecycleView
更加靈活,支持豐富的布局(橫向列表,豎線列表,瀑布流,網(wǎng)格等),RecycleView.Adaper
比BaseAdapter
做了更好的封裝,但是有不少基礎(chǔ)功能需要自己實(shí)現(xiàn),如分割線,點(diǎn)擊等等。故在實(shí)現(xiàn)簡單列表頁時(shí)我們還是優(yōu)先選擇ListView
。通常需要支持下拉刷新,重新加載第一頁數(shù)據(jù);滑動(dòng)到底部或點(diǎn)擊加載更多時(shí),加載下一頁的數(shù)據(jù);在沒有更多數(shù)據(jù)時(shí),還可提醒用戶。github應(yīng)該有很多酷炫項(xiàng)目支持上述功能,但是跟自己預(yù)期的或多或少有些出入。
- 只支持下拉刷新
- 封裝太復(fù)雜,支持太多,很多或許用不到
分析
下拉刷新 和 加載更多 是兩個(gè)功能,官方v4庫的SwipeRefreshLayout
已經(jīng)支持下拉刷新功能,只需在ListView
外嵌SwipeRefreshLayout
即可。那剩下的問題就是如何實(shí)現(xiàn)加載更多功能。我們先思考加載更多觸發(fā)的時(shí)機(jī)是什么?
- 滾動(dòng)到底部,自動(dòng)觸發(fā)或者點(diǎn)擊加載更多或者上拉刷新
- 若加載顯示的數(shù)據(jù)未鋪滿一屏幕,未出現(xiàn)滾動(dòng)時(shí),第一種情況就觸發(fā)不了。咋辦?這時(shí)列表底部應(yīng)該有一個(gè)點(diǎn)擊加載跟多控件。
上拉刷新實(shí)現(xiàn)比較麻煩,且交互體驗(yàn)不佳。我更加推崇滾動(dòng)到底部自動(dòng)加載,但是某些場景(如加載耗很多流量)可能不是很適合。可以優(yōu)化為前N頁自動(dòng)加載,之后只能點(diǎn)擊加載更多控件來加載下一頁數(shù)據(jù)。數(shù)據(jù)集通常是有限的,加載到最后一頁時(shí)應(yīng)該提示用戶,并且不再可觸發(fā)加載下一頁。
實(shí)現(xiàn)
直接上代碼:
public class SimpleListView extends SwipeRefreshLayout {
private ListView mListView;
private LoadMoreStatus mLoadMoreStatus = LoadMoreStatus.CLICK_TO_LOAD;
private OnLoadListener mOnLoadListener;
private TextView mLoadMoreView;
private AbsListView.OnScrollListener mOnScrollListener;
private View mEmptyView;
private ListAdapter mAdapter;
/**
* 加載更多狀態(tài)
*/
public static enum LoadMoreStatus {
/**
* 點(diǎn)擊加載更多
*/
CLICK_TO_LOAD,
/**
* 正在加載
*/
LOADING,
/**
* 沒有更多內(nèi)容了
*/
LOADED_ALL
}
/**
* 加載監(jiān)聽器
*/
public static interface OnLoadListener {
/**
* 下來刷新或者加載更多時(shí)觸發(fā)該回調(diào)
*
* @param isRefresh true為下拉刷新 false為加載更多
*/
public void onLoad(boolean isRefresh);
}
public SimpleListView(Context context) {
super(context);
init(context, null);
}
public SimpleListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mListView = new ListView(context, attrs);
addView(mListView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
private boolean mIsEnd = false;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChanged(view, scrollState);
}
if (scrollState == SCROLL_STATE_IDLE) {
//1:到達(dá)底部 2:底部當(dāng)前可以加載更多 3:頂部不在刷新中狀態(tài)
if (mIsEnd && mLoadMoreStatus == LoadMoreStatus.CLICK_TO_LOAD && !isRefreshing()) {
setLoadMoreStatus(LoadMoreStatus.LOADING);
if (mLoadMoreStatus != null) {
mOnLoadListener.onLoad(false);
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (firstVisibleItem + visibleItemCount >= totalItemCount - 1) {
mIsEnd = true;
} else {
mIsEnd = false;
}
}
});
super.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
if (mLoadMoreStatus != LoadMoreStatus.LOADING) {
if (mOnLoadListener != null) {
mOnLoadListener.onLoad(true);
}
} else {
SimpleListView.super.setRefreshing(false);
}
}
});
}
public void addHeaderView(View view) {
mListView.addHeaderView(view);
}
public void addHeaderView(View v, Object data, boolean isSelectable) {
mListView.addHeaderView(v, data, isSelectable);
}
public void addFooterView(View view) {
mListView.addFooterView(view);
}
public void addFooterView(View v, Object data, boolean isSelectable) {
mListView.addFooterView(v, data, isSelectable);
}
public void setOnScrollListener(AbsListView.OnScrollListener listener) {
mOnScrollListener = listener;
}
public void setOnItemClickListener(AdapterView.OnItemClickListener listener){
mListView.setOnItemClickListener(listener);
}
public void setEmptyView(View emptyView) {
if (emptyView != null) {
mEmptyView = emptyView;
if (mAdapter != null && mAdapter.getCount() > 0) {
mEmptyView.setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.VISIBLE);
}
// mListView.setEmptyView(emptyView);
}
}
@Override
@Deprecated
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
}
@Override
@Deprecated
public void setRefreshing(boolean refreshing) {
}
public void setAdapter(final ListAdapter adapter) {
if (adapter == null) {
return;
}
mAdapter = adapter;
if (mLoadMoreView == null) {
mLoadMoreView = new TextView(getContext());
mLoadMoreView.setTextColor(0xff333333);
mLoadMoreView.setTextSize(14);
mLoadMoreView.setGravity(Gravity.CENTER);
int count = adapter.getCount();
mLoadMoreView.setVisibility(count == 0 ? View.GONE : View.VISIBLE);
if (mEmptyView != null) {
mEmptyView.setVisibility(count == 0 ? View.VISIBLE : View.GONE);
}
mLoadMoreView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mLoadMoreStatus == LoadMoreStatus.CLICK_TO_LOAD && !isRefreshing()) {
setLoadMoreStatus(LoadMoreStatus.LOADING);
if (mLoadMoreStatus != null) {
mOnLoadListener.onLoad(false);
}
}
}
});
mLoadMoreView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, getResources().getDimensionPixelOffset(R.dimen.dp10) * 4));
mListView.addFooterView(mLoadMoreView);
}
mListView.setAdapter(adapter);
adapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
int count = adapter.getCount();
mLoadMoreView.setVisibility(count == 0 ? View.GONE : View.VISIBLE);
if (mEmptyView != null) {
mEmptyView.setVisibility(count == 0 ? View.VISIBLE : View.GONE);
}
}
});
}
private void setLoadMoreStatus(LoadMoreStatus status) {
mLoadMoreStatus = status;
if (mLoadMoreView != null) {
if (mLoadMoreStatus == LoadMoreStatus.LOADED_ALL) {
mLoadMoreView.setText("沒有更多內(nèi)容了");
} else if (mLoadMoreStatus == LoadMoreStatus.LOADING) {
mLoadMoreView.setText("正在加載...");
} else {
mLoadMoreView.setText("點(diǎn)擊加載更多");
}
}
}
public void setOnLoadListener(OnLoadListener listener) {
mOnLoadListener = listener;
}
public void finishLoad(boolean loadAll) {
super.setRefreshing(false);
setLoadMoreStatus(loadAll ? LoadMoreStatus.LOADED_ALL : LoadMoreStatus.CLICK_TO_LOAD);
}
}
基本思路如下:
- 繼承
SwipeRefreshLayout
,內(nèi)嵌一個(gè)ListView
-
ListView
添加一個(gè)footerView,做為加載更多控件mLoadMoreView
-
mLoadMoreView
有三種狀態(tài)點(diǎn)擊加載更多,正在加載和沒有更多內(nèi)容 -
finishLoad(boolean loadAll)
完成數(shù)據(jù)加載,關(guān)閉加載狀態(tài)時(shí)調(diào)用,其中 loadAll表示是否加載完所有數(shù)據(jù)。 -
OnLoadListener
接口,下來刷新或者加載更多時(shí)觸發(fā)該回調(diào),其中的方法是
isRefresh true為下拉刷新 false為加載更多
public void onLoad(boolean isRefresh);
還有一些實(shí)現(xiàn)細(xì)節(jié)不再明說,有興趣的同學(xué)可以運(yùn)行一下demo