概述
ListView
绝对是我们开发中的高频的控件。目前很多人建议使用ReclyeView
替换ListView
。RecycleView
更加灵活,支持丰富的布局(横向列表,竖线列表,瀑布流,网格等),RecycleView.Adaper
比BaseAdapter
做了更好的封装,但是有不少基础功能需要自己实现,如分割线,点击等等。故在实现简单列表页时我们还是优先选择ListView
。通常需要支持下拉刷新,重新加载第一页数据;滑动到底部或点击加载更多时,加载下一页的数据;在没有更多数据时,还可提醒用户。github应该有很多酷炫项目支持上述功能,但是跟自己预期的或多或少有些出入。
- 只支持下拉刷新
- 封装太复杂,支持太多,很多或许用不到
分析
下拉刷新 和 加载更多 是两个功能,官方v4库的SwipeRefreshLayout
已经支持下拉刷新功能,只需在ListView
外嵌SwipeRefreshLayout
即可。那剩下的问题就是如何实现加载更多功能。我们先思考加载更多触发的时机是什么?
- 滚动到底部,自动触发或者点击加载更多或者上拉刷新
- 若加载显示的数据未铺满一屏幕,未出现滚动时,第一种情况就触发不了。咋办?这时列表底部应该有一个点击加载跟多控件。
上拉刷新实现比较麻烦,且交互体验不佳。我更加推崇滚动到底部自动加载,但是某些场景(如加载耗很多流量)可能不是很适合。可以优化为前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;
/**
* 加载更多状态
*/
public static enum LoadMoreStatus {
/**
* 点击加载更多
*/
CLICK_TO_LOAD,
/**
* 正在加载
*/
LOADING,
/**
* 没有更多内容了
*/
LOADED_ALL
}
/**
* 加载监听器
*/
public static interface OnLoadListener {
/**
* 下来刷新或者加载更多时触发该回调
*
* @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:到达底部 2:底部当前可以加载更多 3:顶部不在刷新中状态
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("没有更多内容了");
} else if (mLoadMoreStatus == LoadMoreStatus.LOADING) {
mLoadMoreView.setText("正在加载...");
} else {
mLoadMoreView.setText("点击加载更多");
}
}
}
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
,内嵌一个ListView
-
ListView
添加一个footerView,做为加载更多控件mLoadMoreView
-
mLoadMoreView
有三种状态点击加载更多,正在加载和没有更多内容 -
finishLoad(boolean loadAll)
完成数据加载,关闭加载状态时调用,其中 loadAll表示是否加载完所有数据。 -
OnLoadListener
接口,下来刷新或者加载更多时触发该回调,其中的方法是
isRefresh true为下拉刷新 false为加载更多
public void onLoad(boolean isRefresh);
还有一些实现细节不再明说,有兴趣的同学可以运行一下demo