下拉刷新是Android App开发中非常常用的功能, 网上也有很多开源的下拉刷新控件. CoreLibs中原先使用的handmark pulltorefresh, 现在选用的则是 Ultra-Pull-To-Refresh. 新的PTR框架有如下优点:
具体的使用方法请参考上面的链接, 这里就不再赘述了. 但是呢, Ultra ptr也不是没有缺点, 比如库中提供了Lollipop风格的下拉头部, 如果想要在每一个下拉控件中使用还需要加入不少代码. 然后每次使用下拉组件的时候需要一些相似的配置代码. 最主要的是Ultra ptr只支持下拉, 而不支持加载更多. 因此我们需要扩展一下这个库, 目标有两个:
默认头部变为Lollipop风格, 去掉重复代码, 使用更简洁
加入自动加载更多 - auto load more.
ptr的扩展类均位于com.corelibs.views.ptr下. 以下是包结构:
| ptr
| layout 扩展的布局
-PtrAutoLoadMoreLayout //自动加载更多布局
-PtrLollipopLayout //Lolipop头部风格布局
| loadmore
| adapter
-GridViewAdapter //GridView系列适配器
-ListViewAdapter //ListView系列适配器
-LoadMoreAdapter //自动加载更多的适配类
-RecyclerViewAdapter //RecyclerView系列适配器
| widget
-AutoLoadMoreGridView //自动加载更多的GridView
-AutoLoadMoreListView //自动加载更多的ListView
-AutoLoadMoreSwipeMenuListView //自动加载更多的带侧滑菜单的ListView
-AutoLoadMoreRecyclerView //自动加载更多的RecyclerView
-AutoLoadMoreHandler //自动加载更多的真正处理类
-AutoLoadMoreHook //PtrAutoLoadMoreLayout的child需实现此类以供PtrAutoLoadMoreLayout获取AutoLoadMoreHandler
-OnScrollListener //兼容AdapterView与RecyclerView的OnScrollListener
layout.PtrLollipopLayout
我们首要目标是去掉重复的配置代码, 使用起来更简洁. 首先看看一个简单的例子:
<com.corelibs.views.ptr.layout.PtrLollipopLayout
android:id="@+id/ptrLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="拉我拉我"/>
</com.corelibs.views.ptr.layout.PtrLollipopLayout>
Activity代码:
@Bind(R.id.ptrLayout) PtrLollipopLayout<TextView> ptrLayout;
ptrLayout.setRefreshCallback(new PtrLollipopLayout.RefreshCallback() {
@Override public void onRefreshing(PtrFrameLayout frame) {
ptrLayout.getPtrView().setText("我被刷了");
ptrLayout.complete();
}
});
效果如下:
用法非常简单, 只需使用PtrLollipopLayout包裹任意你想要刷新的控件即可. 在代码中, 如果在声明PtrLollipopLayout时加上泛型, 如PtrLollipopLayout<TextView>
, 就可以使用ptrLayout.getPtrView()
将PtrLollipopLayout内的TextView取出. 如果不加泛型, 则需要单独为TextView设置id, 并使用ButterKnife bind出来. 两种方式均可.
PtrLollipopLayout内部默认使用了Lollipop风格的下拉头部, 并且做了一些配置工作. 我们同样可以在代码中为PtrLollipopLayout做一些个性化的配置, 如通过setHeaderView(View header)设置自己的头部, 请注意, 自定义的头部必须实现PtrUIHandler接口. 如果出现PtrLollipopLayout解决不了的滑动冲突, 可以调用setPtrHandler(PtrHandler ptrHandler)
自行处理滑动.
以下是几个需要注意的点:
此控件仅支持下拉刷新, 如果需要自动加载, 请使用PtrAutoLoadMoreLayout
如果出现横向滑动冲突, 请设置disableWhenHorizontalMove(boolean)为true.
如果不想为child设置id并使用findViewById取出, 可以在声明PtrLollipopLayout的时候带上child类型的泛型, 然后就可以使用getPtrView()取出child. 如PtrLollipopLayout<ScrollView>.
layout.PtrAutoLoadMoreLayout
接下来是第二个目标 - 自动加载更多. 先看栗子:
<com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout
android:id="@+id/ptrLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.corelibs.views.ptr.loadmore.widget.AutoLoadMoreListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:listSelector="#00000000"
android:divider="#aaa"
android:dividerHeight="1dp"/>
</com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout>
ListView item布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#333"
android:gravity="center">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:textColor="#fff"
android:textSize="14sp"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"/>
</LinearLayout>
Activity代码:
@Bind(R.id.ptrLayout) PtrAutoLoadMoreLayout<AutoLoadMoreListView> ptrLayout;
private Handler handler = new Handler(); // 使用handler模拟网络请求
private int count = 0; // 模拟页数计数
protected void init(Bundle savedInstanceState) {
// 简单的适配器
final QuickAdapter<String> adapter = new QuickAdapter<String>(this, R.layout.item_test) {
@Override protected void convert(BaseAdapterHelper helper, String item) {
helper.setText(R.id.text, item);
}
};
ptrLayout.setLoadingBackgroundColor(0xff333333); // 设置自动加载视图的背景颜色
ptrLayout.getPtrView().setAdapter(adapter); // 为AutoLoadMoreListView设置Adapter
adapter.addAll(getData()); // 为adapter添加数据
// 设置刷新和加载回调
ptrLayout.setRefreshLoadCallback(new PtrAutoLoadMoreLayout.RefreshLoadCallback() {
@Override public void onRefreshing(PtrFrameLayout frame) {
adapter.replaceAll(getData()); // 替换adapter中的数据
ptrLayout.enableLoading(); // 重新启用自动加载
ptrLayout.complete(); // 刷新完成
count = 0; // 重置模拟计数
}
@Override public void onLoading(PtrFrameLayout frame) {
count++; // 模拟页数++
handler.postDelayed(new Runnable() { // 模拟网络加载延迟
@Override public void run() {
adapter.addAll(getData()); // 将数据加入adapter中.
ptrLayout.complete(); // 加载完成
if (count > 2)
ptrLayout.disableLoading(); // 禁用自动加载
}
}, 1500);
}
});
}
// 模拟数据
private List<String> getData() {
List<String> data = new ArrayList<>();
for (int i = 0; i < 25; i++)
data.add("呵呵呵呵" + (i + 1));
return data;
}
效果:
使用带自动加载的下拉刷新就要比单纯的下拉刷新复杂的多. 这种时候就不能使用PtrLollipopLayout而需要使用PtrAutoLoadMoreLayout. 一般情况下, 自动加载更多只会出现在有ListView/GridView的情况下. 因此PtrAutoLoadMoreLayout的子视图基本都是ListView/GridView, 或他们的派生类.
但是如果直接使用PtrAutoLoadMoreLayout加上ListView/GridView, 也是无法实现自动加载的, 如:
<com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout>
不仅没有效果, 还会报如下错误:
java.lang.IllegalStateException: PtrAutoLoadMoreLayout child should implement AutoLoadMoreHook
这是因为PtrAutoLoadMoreLayout只是一个外壳, 本身只带有下拉刷新的功能, 不带有自动加载的功能. PtrAutoLoadMoreLayout所有有关自动加载的api全部是代理至另外一个类 - AutoLoadMoreHandler. AutoLoadMoreHandler才是真正处理自动加载功能的类. PtrAutoLoadMoreLayout需要借助AutoLoadMoreHook来获取AutoLoadMoreHandler, 因此PtrAutoLoadMoreLayout的子控件必须实现AutoLoadMoreHook. AutoLoadMoreHook的定义:
/**
* {@link com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout}的child view需要实现此接口,
* 供PtrAutoLoadMoreLayout获取{@link AutoLoadMoreHandler}.
*/
public interface AutoLoadMoreHook {
/**
* {@link com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout}需通过此方法获取
* {@link AutoLoadMoreHandler}对象.
*/
AutoLoadMoreHandler getLoadMoreHandler();
}
现在PtrAutoLoadMoreLayout就可以通过getLoadMoreHandler来获取AutoLoadMoreHandler实现自动加载更多的功能了. 那么, 例子中的AutoLoadMoreListView又是什么鬼? AutoLoadMoreListView是CoreLibs中预定义好的一个控件, 它实现了AutoLoadMoreHook. 如果我们需要一个带自动加载的ListView, 就可以使用AutoLoadMoreListView. AutoLoadMoreListView全部代码:
public class AutoLoadMoreListView extends ListView implements AutoLoadMoreHook {
public AutoLoadMoreListView(Context context) {
super(context);
}
public AutoLoadMoreListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoLoadMoreListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public AutoLoadMoreHandler getLoadMoreHandler() {
return new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<ListView>(this));
}
}
AutoLoadMoreListView的代码非常简单, 除了三个继承自ListView需要实现的构造函数外, 就只有一个实现了AutoLoadMoreHook的getLoadMoreHandler方法. 该方法中也只是new了一个AutoLoadMoreHandler对象并返回而已.
如果我们有一个自定义的ListView, 实现了侧滑菜单, 名字叫SwipeMenuListView, 我们想要为SwipeMenuListView加上下拉和自动加载怎么办? 很简单, 定义一个新的继承自SwipeMenuListView, 并且实现了AutoLoadMoreHook的控件, 然后我们在PtrAutoLoadMoreLayout中包含该控件即可. 使用方法除了SwipeMenuListView自己的API外, 其他与默认的ListView完全一样. 代码如下:
public class AutoLoadMoreSwipeMenuListView extends SwipeMenuListView implements AutoLoadMoreHook {
public AutoLoadMoreSwipeMenuListView(Context context) {
super(context);
}
public AutoLoadMoreSwipeMenuListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoLoadMoreSwipeMenuListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public AutoLoadMoreHandler getLoadMoreHandler() {
return new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<SwipeMenuListView>(this));
}
}
接着看看AutoLoadMoreHandler的定义与构造函数:
public class AutoLoadMoreHandler<T extends LoadMoreAdapter> {
public AutoLoadMoreHandler(Context context, T wrapper);
}
AutoLoadMoreHandler的构造函数需要两个参数, 第一个context不用多说, 第二个LoadMoreAdapter又是何方神圣? LoadMoreAdapter是一个接口, 类似于适配器, 因此取名叫做Adapter:
/**
* 针对ListView/GridView等View的适配接口, 用于带自动加载更多的视图
* <BR/>
* Created by Ryan on 2016/1/21.
*/
public interface LoadMoreAdapter<T extends ViewGroup> {
/**
* 添加FooterView的适配
*/
void addFooterView(View v, Object data, boolean isSelectable);
/**
* 删除FooterView的适配
*/
boolean removeFooterView(View v);
/**
* 设置OnScrollListener的适配
*/
void setOnScrollListener(OnScrollListener<T> l);
/**
* 获取总行数的适配
*/
int getRowCount();
/**
* 获取被包装的View
*/
T getView();
}
可以看到LoadMoreAdapter的方法基本都是对ListView/GridView的一些方法的包装. LoadMoreAdapter的存在就是为了适配各种ListView/GridView. AutoLoadMoreHandler内部会在需要添加底部视图的时候去调用LoadMoreAdapter的addFooterView, 在需要计算何时要显示底部视图的时候调用getLastVisiblePosition, getRowCount等. 至于具体实现, 则交由各种实现类去处理. CoreLibs中目前定义好了两个实现类 - ListViewAdapter和GridViewAdapter. 两个类的代码差不多, 这里只贴ListViewAdapter的:
/**
* 针对ListView或继承自ListView的控件的适配类
* <BR/>
* Created by Ryan on 2016/1/21.
*/
public class ListViewAdapter<T extends ListView> implements LoadMoreAdapter<T> {
private T listView;
public ListViewAdapter(T listView) {
this.listView = listView;
}
@Override
public void addFooterView(View v, Object data, boolean isSelectable) {
listView.addFooterView(v, data, isSelectable);
}
@Override
public boolean removeFooterView(View v) {
return listView.removeFooterView(v);
}
@Override
public void setOnScrollListener(final OnScrollListener<T> l) {
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (l != null)
l.onScroll(listView, firstVisibleItem, visibleItemCount, totalItemCount);
}
});
}
@Override
public int getRowCount() {
return listView.getCount();
}
@Override
public T getView() {
return listView;
}
}
代码逻辑比较简单, 基本都是直接调用listView的方法而没有很多逻辑判断. 可以看到, AutoLoadMoreSwipeMenuListView中的getLoadMoreHandler的返回语句new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<SwipeMenuListView>(this))
也是使用的ListViewAdapter, 只不过泛型传递的是SwipeMenuListView. 这意味着, 只要是继承自ListView的控件都可以使用ListViewAdapter. GridView同理. 如果以后有了其他类型的控件可以创建一个新的LoadMoreAdapter的实现类.
接下来看看RecylerView的Adapter, 相较于ListView/GridView的会复杂一下:
/**
* 针对RecyclerView或继承自RecyclerView的控件的适配类
* <BR/>
* Created by Ryan on 2016/9/21.
*/
public class RecyclerViewAdapter<T extends RecyclerView>
implements LoadMoreAdapter<T> {
private T recyclerView;
public RecyclerViewAdapter(T recyclerView) {
this.recyclerView = recyclerView;
}
@Override
public void addFooterView(View v, Object data, boolean isSelectable) {
AbstractHeaderAndFooterWrapper adapter =
(AbstractHeaderAndFooterWrapper) recyclerView.getAdapter();
adapter.addFootView(v);
}
@Override
public boolean removeFooterView(View v) {
AbstractHeaderAndFooterWrapper adapter =
(AbstractHeaderAndFooterWrapper) recyclerView.getAdapter();
adapter.removeFootView(v);
return true;
}
@Override @SuppressWarnings("unchecked")
public void setOnScrollListener(final OnScrollListener<T> l) {
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int firstVisibleItem = 0, lastVisibleItem, visibleItemCount = 0;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
firstVisibleItem = manager.findFirstVisibleItemPosition();
lastVisibleItem = manager.findLastVisibleItemPosition();
visibleItemCount = lastVisibleItem - firstVisibleItem + 1;
}
if (layoutManager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) layoutManager;
firstVisibleItem = manager.findFirstVisibleItemPositions(null)[0];
lastVisibleItem = manager.findLastVisibleItemPositions(null)[1];
visibleItemCount = lastVisibleItem - firstVisibleItem + 1;
}
if (l != null)
l.onScroll((T) recyclerView, firstVisibleItem, visibleItemCount, recyclerView.getAdapter().getItemCount());
}
});
}
@Override
public int getRowCount() {
return recyclerView.getAdapter().getItemCount();
}
@Override
public T getView() {
return recyclerView;
}
}
由于RecyclerView与传统的AdapterView的OnScrollListener不一样, 所以在setOnScrollListener
代码中做了一些额外的适配工作. 如果需要使用自定义的LayoutManager, 建议继承自系统默认的三个 - LinearLayoutManager, GridLayoutManager以及StaggeredGridLayoutManager, 因为RecyclerViewAdapter中只适配了这几个的LayoutManager. 或者可以新建一个Adapter专门对自定义的LayoutManager做处理.
接着, 我们来看看具体实现自动加载更多功能的AutoLoadMoreHandler的关键代码:
public class AutoLoadMoreHandler<T extends LoadMoreAdapter> {
public static final int DEFAULT_WHEN_TO_LOADING = 1;
private T adapter;
private int whenToLoading = DEFAULT_WHEN_TO_LOADING; // 当滚动到倒数第几个条目时触发加载
private PtrAutoLoadMoreLayout.RefreshLoadCallback callback; //刷新与加载的回调
private PtrFrameLayout ptrFrameLayout;
public AutoLoadMoreHandler(Context context, T adapter) {
this.context = context;
this.adapter = adapter;
}
public void setup(PtrFrameLayout ptrFrameLayout) {
this.ptrFrameLayout = ptrFrameLayout;
init();
}
public void setRefreshLoadCallback(PtrAutoLoadMoreLayout.RefreshLoadCallback callback) {
this.callback = callback;
}
private void init() {
// 通过OnScrollListener来监听滑动, 判断何时触发加载操作
adapter.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 当前如果是DISABLED, REFRESHING以及FORCE_REFRESH状态, 或者处于下拉刷新的状态时,不再触发加载
if (state != State.DISABLED && state != State.REFRESHING
&& state != State.FORCE_REFRESH && !ptrFrameLayout.isRefreshing()) {
// 如果条目没有超过一个屏幕, 也不触发
if (visibleItemCount < totalItemCount) {
if (view.getCount() > 0)
if (aboutToLoad())
setLoadingStatus();
}
}
}
});
}
private boolean aboutToLoad() {
for (int i = 0; i < whenToLoading; i++) {
// 如果当前最后可见的条目等于总条目-whenToLoading-i, 则触发加载
if (adapter.getLastVisiblePosition() == (adapter.getRowCount() - whenToLoading - i))
return true;
}
return false;
}
private void setLoadingStatus() {
setLoadingState(State.REFRESHING);
}
private void setLoadingState(State state) {
// 如果当前处于DISABLED, 则状态不能被设置成FINISHED或者REFRESHING, 也就是说无法触发加载
if (this.state == State.DISABLED && (state == State.FINISHED || state == State.REFRESHING))
return;
this.state = state;
load();
}
private void load() {
switch (state) {
case ENABLED:
break;
case REFRESHING:
case FORCE_REFRESH:
showLoadingView();
break;
case DISABLED:
case FINISHED:
hideLoadingView();
break;
}
}
private synchronized void showLoadingView() {
if (!isLoading) {
loadingContent.setVisibility(View.VISIBLE);
progress.startAnimation();
isLoading = true;
if (callback != null)
callback.onLoading(ptrFrameLayout);
}
}
private synchronized void hideLoadingView() {
if (isLoading) {
loadingContent.setVisibility(View.GONE);
progress.stopAnimation();
isLoading = false;
}
}
public enum State {
/** 刷新结束 **/
FINISHED,
/** 刷新中 **/
REFRESHING,
/** 失效 **/
DISABLED,
/** 生效, 默认状态 **/
ENABLED,
/** 强制刷新 **/
FORCE_REFRESH
}
}
上述代码逻辑也不是很复杂, 相信配合注释不难理解. 需要注意的是, 在构造函数中, AutoLoadMoreHandler并没有做一些初始化操作, 仅仅是赋值. 初始化操作init是在setup方法中被调用的. 那么setup何时会被调用? 这需要看看PtrAutoLoadMoreLayout的部分代码:
public class PtrAutoLoadMoreLayout<T> extends PtrLollipopLayout<T> {
private AutoLoadMoreHandler loadMoreHandler;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
loadMoreHandler = setupHook().getLoadMoreHandler();
if (loadMoreHandler == null)
throw new IllegalStateException("AutoLoadMoreHandler should not be null");
loadMoreHandler.setup(this);
}
private AutoLoadMoreHook setupHook() {
if (mContent != null && mContent instanceof AutoLoadMoreHook) {
return (AutoLoadMoreHook) mContent;
} else {
throw new IllegalStateException("PtrAutoLoadMoreLayout child should implement AutoLoadMoreHook");
}
}
}
可以看到, 在PtrAutoLoadMoreLayout被从xml解析完成之后, 就会去调用setupHook()获取AutoLoadMoreHook, 并通过AutoLoadMoreHook的getLoadMoreHandler来获取AutoLoadMoreHandler对象. 然后调用setup. setupHook方法中的mContent就是被PtrAutoLoadMoreLayout包裹的子控件. 到此, 整个自动加载更多就解释的差不多了.
以下是使用PtrAutoLoadMoreLayout需要注意的几个地方:
如果只需下拉刷新功能, 请使用PtrLollipopLayout
此控件中的child view必须实现AutoLoadMoreHook
此控件是对AutoLoadMoreHandler功能的转发. AutoLoadMoreHook的getLoadMoreHandler()需要的就是AutoLoadMoreHandler.
刷新完成或加载完成后请调用complete(), 而不是refreshComplete()或loadingFinished().
总结
下面总结一下PtrLollipopLayout与PtrAutoLoadMoreLayout在使用上的相同点与区别:
只有下拉刷新功能时应该使用PtrLollipopLayout, 带有自动加载更多时应该使用PtrAutoLoadMoreLayout.
两者都应该使用complete()方法来结束刷新或者加载状态.
PtrLollipopLayout使用setRefreshCallback(RefreshCallback callback)来设置回调.
PtrAutoLoadMoreLayout使用setRefreshLoadCallback(RefreshLoadCallback callback)来设置回调.
RefreshLoadCallback继承自RefreshCallback, 比RefreshCallback多了onLoading方法.
RefreshCallback定义在PtrLollipopLayout内, RefreshLoadCallback定义在PtrAutoLoadMoreLayout内.
PtrLollipopLayout与PtrAutoLoadMoreLayout都只能包含一个子视图.
PtrLollipopLayout子视图可以是任意View, 但是PtrAutoLoadMoreLayout的子视图必须实现AutoLoadMoreHook接口.