上一篇博文打造自己的图片加载缓存库(Picasso OR Glide)发表之后,非常荣幸得到了博客专家拭心的肯定,并被转载到了他的公众号“安卓进化论”,同时也得到了小组同事们的转载,在这也非常感谢他们。其实回过头来看看,实际上自己还是有很多不足和可以改进的地方。故人告诉我们,吾日三省吾身,只有在不断的踩坑中不断的总结,才能提升自己。就好比悟空在被敌人打得半死不活之后,不断地激发自己的潜能,才能进化成超级赛亚人1代、2代、3代……
扯远了……回到今天的主题。在上一篇文章里面,我们使用了建造者模式和策略模式对第三方的图片加载缓存库进行了封装,形成了自己的一套图片加载框架。今天我们接着上次的话题——打造自己的一套架构,给大家带来下拉刷新库的打造过程。由于篇幅问题和为了描述得更加清晰,这次的打造过程我们会分3篇文章给大家介绍。
下拉刷新库对比
古人云,站在巨人的肩膀上才能看得更远,所以我们需要先挑选一个功能强大、扩展性好的库,才能为我们后面的工作铺好路子。跟上次一样,这里也推荐一篇目前安卓下拉刷新库的对比文章。
Repo | 自定义顶部视图 | 支持的内容布局 |
---|---|---|
Android-PullToRefresh | 不支持,只能改代码。 由于仅支持其中实现的 LoadingLayout 作为顶视图, 改代码实现自定义工作量较大。 | 任意视图,内置: GridView , ListView , HorizontalScrollView , ScrollView , WebView |
android-Ultra-Pull-To-Refresh | 任意视图。通过继承 PtrUIHandler 并调用 PtrFrameLayout.addPtrUIHandler() 得到最大支持。 | 任意视图 |
android-pulltorefresh | 不支持,只能改代码。 代码仅一个 ListView ,耦合度太高,改动工作量较大。 | 无法扩展,自身为 ListView |
Phoenix | 不支持,此控件特点就是顶部视图及动画。 | 任意视图,只显示最后一个嵌套的子视图。 |
FlyRefresh | 不支持,此控件特点就是顶部视图及动画。 | 任意视图 |
SwipeRefreshLayout | 不支持,固定为Material风格 | 任意视图 |
上面列出的基本上是GitHub里面比较流行的安卓下拉刷新库,当然点开链接看看最后一次提交的时间,确实有些尴尬,但优秀的代码总是经得起时间考验的!
里面有我们比较熟悉的Android-PullToRefresh,在下拉刷新刚流行起来的那段时间里,那可是此诧风云的。内置了常用的ListView和ScrollView的下拉和上拉加载,顶部、底部加载视图也是当时最常用的“下拉立即刷新”、“松开马上加载”带“菊花”动画之类的,用起来杠杠的。
相对来说Ultra-Pull-To-Refresh(下面简称:Ultra-PTR)是后起之秀,可它扩展性却更加强大,加载的内容布局因为用的是View,所以支持任意视图,而顶部、底部视图则通过PtrUIHandler接口实现,能提供非常好的定制性。短短的1000来行的关键代码,就实现了一个功能强大的下拉加载库,自然也受到了后来很多开发者的喜欢。不过唯有他不支持上拉加载更多这点,可能会让有些人放弃对它的选用。
这里就不一一对其他的库进行说明了,详细可以参考上面的链接。
好了,又到了我们思考的时间。选用哪一个库,首先考虑我们要实现的目的。我们希望这个库能支持越多的加载内容View越好,我们希望应对射鸡师能非常方便的自定义刷新头部和底部,我们希望它既能下拉刷新也能上拉加载……
综上所述,我们选用Ultra-PTR,毕竟这也是个靠star吃饭的年代,除了早已不更新的Android-PullToRefresh,就数Ultra-PTR的扩展性最好(Star最多)了。而至于上拉加载更多的问题,网上也提供了很多的解决方案,这对于充分实践拿来主义的我们来说都不是问题。
设计
引入Ultra-PTR我们就能非常容易为我们的界面实现下拉刷新,他的用法十分简单,只需要在布局文件里配置,为我们需要下拉刷新的View外面套一层PtrFrameLayout即可,下面是关键代码。
<in.srain.cube.views.ptr.PtrFrameLayout
xmlns:ptr="http://schemas.android.com/apk/res-auto"
android:id="@+id/ptr_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
ptr:ptr_duration_to_close_either="1000"
ptr:ptr_keep_header_when_refresh="true"
ptr:ptr_pull_to_fresh="false"
ptr:ptr_resistance="1.7">
<android.support.v7.widget.RecyclerView
android:id="@+id/myRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</in.srain.cube.views.ptr.PtrFrameLayout>
上面涉及ptr的api这里就不作解释,点击上面的GitHub链接,大家可以去深入了解。
知道了Ultra-PTR的用法,我们就开始进行封装设计。调用第三方库,我们首要原则就是低耦合接入,一个可以保持我们项目代码的架构清晰,第二可以为以后切换这个库对原项目做最少的修改。因此我们考虑用一个基类封装Ultra-PTR,这个基类将提供下拉刷新(Ultra-PTR)的功能,并提供一个方法设置滚动内容View,以及一些对下拉刷新的定制方法。下面是我们设计的完整结构类图。
PullToRefreshBaseView
下拉刷新抽象基类,封装Ultra-PTR,继承于LinearLayout,主要提供2个抽象方法onInitMainView()和onInitContent(),其中onInitMainView()用于初始化与Ultra-PTR同层级的View,可以是加载失败图、加载进度框之类的View,onInitContent()则初始化Ultra-PTR的mContenView,由子类实例化内容View并返回。PullToRefreshRecyclerView
继承PullToRefreshBaseView,内部实现了RecyclerView的嵌入,目前只支持LinearLayoutManager布局方式,主要用于列表模式。实现接口OnPullBothListener,提供下拉刷新、上拉加载的功能。PullToRefreshScrollView
继承PullToRefreshBaseView,内部实现了ScollView的嵌入。这里并没有实现OnPullBothListener,或者OnPullRefreshListener(仅下拉刷新)接口,而交由外部client调用时实现,主要是考虑给外部更自由的定制。和RecyclerView不同,因为RecyclerView内部封装了数据自动装置和刷新的逻辑,所以就得在内部处理刷新事件,继而需要实现OnPullBothListener接口了。MyPullToRefreshListView
继承PullToRefreshRecyclerView,加入了网络请求异常情感图的View,和一些业务耦合的数据加载、刷新,主要是针对我们自己项目定制的。接口
OnPullBothListener:提供下拉、上拉刷新方法
OnPullRefreshListener:仅提供下拉刷新方法
OnLastPageHintListener:提供上拉时提示最后一页的回调
OnPullListActionListener:提供数据加载、item点击、item初始化、刷新完成的回调方法,主要用于封装统一的adapter,使界面只需要关系接口请求和界面布局。
在这一篇里面,我们主要介绍PullToRefreshBaseView、PullToRefreshRecyclerView和PullToRefreshScrollView的封装。其中PullToRefreshRecyclerView中RecyclerView的实现会在下一篇介绍,剩下的上层模块调用则会在第三篇中介绍。
难点攻克
1、Ultra-PTR只支持xml静态布局,无法在PullToRefreshBaseView中单独解耦
在写Demo的时候发现Ultra-PTR仅支持在xml静态布局,要调用只能像上面代码中那样写死在xml中,这样就导致我们没法单独在基类PullToRefreshBaseView中初始化Ultra-PTR的实例,然后动态地通过onInitContent()方法在子类中初始化mContent了。回归根本,这个问题还是得从Ultra-PTR的源码入手。
@Override
protected void onFinishInflate() {
final int childCount = getChildCount();
if (childCount > 3) {
throw new IllegalStateException("PtrFrameLayout only can host 3 elements");
} else if (childCount == 3) {
......
} else if (childCount == 2) { // ignore the footer by default
......
} else if (childCount == 1) {
mContent = getChildAt(0);
} else {
TextView errorView = new TextView(getContext());
......
mContent = errorView;
addView(mContent);
}
if (mHeaderView != null) {
mHeaderView.bringToFront();
}
if (mFooterView != null) {
mFooterView.bringToFront();
}
super.onFinishInflate();
}
以上是PtrFrameLayout初始化mContent的关键代码,可以看到它是重写了onFinishInflate()方法,难怪它只能在xml中静态初始化了。onFinishInflate()方法就是在View中所有的子控件被映射成xml后触发的。所以我们如果要做到动态布局,则需要我们在addView完mContent之后,手动调用一下onFinishInflate()内的代码。我们对源码稍作修改。
@Override
protected void onFinishInflate() {
doFinishInflate();
super.onFinishInflate();
}
public void doFinishInflate() {
......
}
提供了doFinishInflate()给PullToRefreshBaseView在获得onInitContent()返回的View之后调用,这样就不必在xml中静态初始化了。
2、无奈的上拉加载更多
Ultra-PTR不支持上拉加载更多是众所周知的,作者认为下拉刷新和上拉加载是无关的两个功能,而且上拉更多的是与mContent有关,因此应该由具体的调用环境自己实现。
这个说法也不无道理,从设计的角度分开实现可能更加灵活,利于扩展。所以包括作者在后来提供的方案,以及其他的一些例子都是针对RecyclerView、ListView的上拉加载方案,而对于RecyclerView的上拉实现,基本上都是判断滚动到最后一个adapter,然后显示“加载更多…”,然后自动加载更多的item。但结合到我们项目本身,所需要的是有回弹的“加载更多…”,因此这些方案明显不适用。
在GitHub上面搜了一下,发现一个从Ultra-PTR fork过去的库,兼容了Load-More的情况,而且也是从PtrFrameLayout本身实现的,可以兼容所有mContent的上拉加载,看过源码和测试几次之后并未发现重点的bug和性能问题,就决定先拿来主义。
3、RecyclerView的Item点击事件偶尔丢失,导致点击无反应
这个问题可以等集成完之后自己测试一下,回头再来改完试试。
在集成到项目中实际运行起来之后,我们小组的同事测出了PullToRefreshRecyclerView中的item点击事件偶尔会丢失,需要多点几次才能触发的bug。
我先是到Issues里面看了下,发现了有人也遇到了类似的问题。而单单使用RecyclerView是不会复现这个bug的,开始我怀疑是不是换了Ultra-PTR-Load-More引入的这个bug,我重新换回Ultra-PTR原本的库也有同样的问题,所以可以断定是Ultra-PTR库本身存在的bug。
一般来说这种点击事件的丢失都是由于滑动和点击事件冲突导致的,如果没有做好滑动事件的触发判断,点击事件就很容易被滑动事件消耗掉。
我们又回到Ultra-PTR的源码中,点击事件主要是在dispatchTouchEvent()里面处理,对比了一下历史版本,发现作者其实已经对这个问题做了一些的修改,但他只处理了水平滑动的阙值判断。
public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) {
......
final ViewConfiguration conf = ViewConfiguration.get(getContext());
mTouchSlop = conf.getScaledTouchSlop();
mPagingTouchSlop = conf.getScaledTouchSlop() * 2;
}
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
......
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
......
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
......
}
}
其中ViewConfiguration.getScaledTouchSlop() 就是系统判断手指移动触发控件滑动的距离。这就简单,我们再处理一下offsetY的滚动判断就可以了,为了保险起见,我们还加上了ACTION_DOWN 到 ACTION_MOVE 之间的时间判断,如果时间非常短的话,我们就认为是点击事件,交由子View处理。
public static final int TimeInterval = 100;
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
if (!isEnabled() || mContent == null || mHeaderView == null) {
return dispatchTouchEventSupper(e);
}
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
downTime = System.currentTimeMillis();
......
case MotionEvent.ACTION_MOVE:
......
long moveInterval = System.currentTimeMillis() - downTime;
if (Math.abs(offsetY) < mTouchSlop && moveInterval < TimeInterval) {
return dispatchTouchEventSupper(e);
}
......
}
}
再重新运行一下,问题解决。
关键类实现
1、PullToRefreshBaseView.java
下拉刷新抽象基类,封装Ultra-PTR。
public abstract class PullToRefreshBaseView extends LinearLayout {
private final float HEADER_REFRESH_POSITION = 55f; //刷新Header停留距离
private float REAL_HEADER_REFRESH_POSITION;
{
float scale = getResources().getDisplayMetrics().density;
REAL_HEADER_REFRESH_POSITION = HEADER_REFRESH_POSITION * scale + 0.5f;
}
//下拉、上拉刷新控件(Ultra-Pull-To-Refresh)
private PtrFrameLayout ptrLayout;
private View mContent;
private boolean hadGetWindowFocus = false;
private int currentHeaderHeight = 0;
public PullToRefreshBaseView(Context context) {
super(context);
onInitMainView();
mContent = onInitContent();
initView();
}
public PullToRefreshBaseView(Context context, AttributeSet attrs) {
super(context, attrs);
onInitMainView();
mContent = onInitContent();
initView();
}
/**
* 初始化 PullToRefresh 同层级的View(如:异常提示View)
* +++++++++++++++++++++++++++++++++++++++++
* + 必须写在该方法内 +
* +++++++++++++++++++++++++++++++++++++++++
*/
public abstract void onInitMainView();
/**
* 初始化 PullToRefresh 的 Context
*/
public abstract View onInitContent();
private void initView() {
if (mContent == null) {
throw new NullPointerException("Content is null : onInitContent() must called and return valid View");
}
setOrientation(VERTICAL);
ptrLayout = new PtrFrameLayout(getContext());
ptrLayout.setDurationToCloseHeader(500);
ptrLayout.setDurationToCloseFooter(500);
ptrLayout.setKeepHeaderWhenRefresh(true);
ptrLayout.setPullToRefresh(false);
ptrLayout.setResistance(1.7f);
ptrLayout.disableWhenHorizontalMove(true);
ptrLayout.addView(mContent);
setMode(Mode.BOTH);
//布局
super.addView(ptrLayout, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
//初始化PtrFrameLayout的HeaderView、mContent、FooterView
ptrLayout.doFinishInflate();
if (ptrLayout != null) {
ptrLayout.setPtrHandler(new PtrDefaultHandler2() {
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
if (mOnPullRefreshListener != null) {
mOnPullRefreshListener.onRefresh();
}
if (mOnPullBothListener != null) {
mOnPullBothListener.onPullDownToRefresh();
}
}
@Override
public void onLoadMoreBegin(PtrFrameLayout frame) {
if (mOnPullBothListener != null) {
mOnPullBothListener.onPullUpToRefresh();
}
}
});
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if(mContent != null && mContent instanceof ViewGroup && !(child instanceof PtrFrameLayout)) {
((ViewGroup) mContent).addView(child, index, params);
}else {
super.addView(child, index, params);
}
}
/**
* 显示下拉的顶部图片
*/
public void showLoadingHeaderImg() {
if (ptrLayout != null && ptrLayout.getHeaderView() != null) {
if (ptrLayout.getHeaderView() instanceof CommonPtrDefaultHeader) {
((CommonPtrDefaultHeader) ptrLayout.getHeaderView()).showIvTDPtrLoadingHeader();
}
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hadGetWindowFocus && ptrLayout != null) {
hadGetWindowFocus = true;
currentHeaderHeight = ptrLayout.getHeaderHeight();
ptrLayout.setRatioOfHeaderHeightToRefresh(REAL_HEADER_REFRESH_POSITION / currentHeaderHeight);
ptrLayout.setOffsetToKeepHeaderWhileLoading((int) REAL_HEADER_REFRESH_POSITION);
}
}
/**
* 设置默认头部
*/
public void setDefaultLoadingHeaderView() {
CommonPtrDefaultHeader myPtrDefaultHeader = new CommonPtrDefaultHeader(getContext());
myPtrDefaultHeader.setheaderHeightUpdateListener(new CommonPtrDefaultHeader.HeaderHeightUpdateListener() {
@Override
public void headerHeightUpdate() {
if (ptrLayout != null && ptrLayout.getHeaderHeight() != currentHeaderHeight) {
currentHeaderHeight = ptrLayout.getHeaderHeight();
ptrLayout.setRatioOfHeaderHeightToRefresh(REAL_HEADER_REFRESH_POSITION / currentHeaderHeight);
ptrLayout.setOffsetToKeepHeaderWhileLoading((int) REAL_HEADER_REFRESH_POSITION);
}
}
});
setLoadingHeaderView(myPtrDefaultHeader);
}
/**
* 设置默认底部
*/
public void setDefaultLoadingFooterView() {
setLoadingFooterView(new CommonPtrDefaultFooter(getContext()));
}
/**
* 设置头部HeaderView
*/
public void setLoadingHeaderView(PtrUIHandler ptrUIHandler) {
if (ptrUIHandler != null) {
if (ptrUIHandler instanceof View) {
if (ptrLayout != null) {
ptrLayout.setHeaderView((View) ptrUIHandler);
ptrLayout.addPtrUIHandler(ptrUIHandler);
}
} else {
throw new UnsupportedOperationException("ptrUIHandler is not a View so can't setHeaderView");
}
}
}
/**
* 设置底部FooterView
*/
public void setLoadingFooterView(PtrUIHandler ptrUIHandler) {
if (ptrUIHandler != null) {
if (ptrUIHandler instanceof View) {
if (ptrLayout != null) {
ptrLayout.setFooterView((View) ptrUIHandler);
ptrLayout.addPtrUIHandler(ptrUIHandler);
}
} else {
throw new UnsupportedOperationException("ptrUIHandler is not a View so can't setFooterView");
}
}
}
/**
* 自动刷新
*/
public void autoRefresh() {
if (ptrLayout != null) {
ptrLayout.autoRefresh();
}
}
/**
* 刷新完成,回弹
*/
public void onRefreshComplete() {
if (ptrLayout != null) {
ptrLayout.refreshComplete();
}
}
private OnPullRefreshListener mOnPullRefreshListener;
private OnPullBothListener mOnPullBothListener;
public void setOnRefreshListener(OnPullRefreshListener onPullRefreshListener) {
mOnPullRefreshListener = onPullRefreshListener;
}
public void setOnRefreshListener(OnPullBothListener onPullBothListener) {
mOnPullBothListener = onPullBothListener;
}
/**
* 仅下拉刷新接口
*/
public interface OnPullRefreshListener {
void onRefresh();
}
/**
* 下拉、上拉刷新接口
*/
public interface OnPullBothListener {
void onPullDownToRefresh();
void onPullUpToRefresh();
}
/**
* 上拉到最后一页的提示回调
*/
public interface OnLastPageHintListener {
void onLastPageHint();
}
/**
* 自定义封装Mode转第三方库Mode
*
* @param mode PullToRefreshBaseView.Mode
* <p>
* 默认:Mode.BOTH
*/
public void setMode(byte mode) {
switch (mode) {
case Mode.REFRESH:
setMode(PtrFrameLayout.Mode.REFRESH);
break;
case Mode.LOAD_MORE:
setMode(PtrFrameLayout.Mode.LOAD_MORE);
break;
case Mode.BOTH:
setMode(PtrFrameLayout.Mode.BOTH);
break;
default:
setMode(PtrFrameLayout.Mode.NONE);
break;
}
}
private void setMode(PtrFrameLayout.Mode mode) {
if (ptrLayout != null) {
ptrLayout.setMode(mode);
}
}
/**
* @return ptrLayout内部可滑动子控件是否滑动到顶部
*/
public boolean checkContentViewScrollTop() {
return ptrLayout != null && ptrLayout.checkContentViewScrollTop();
}
/**
* @return ptrLayout内部可滑动子控件是否滑动到底部
*/
public boolean checkContentViewScrollBottom() {
return ptrLayout != null && ptrLayout.checkContentViewScrollBottom();
}
private float eventX, eventY;
/**
* 处理滑动事件冲突
*
* @param event
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (ptrLayout != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
eventX = event.getX();
eventY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(eventX - event.getX()) > Math.abs(eventY - event.getY())) {
// 横向滑动事件时 滑动事件交予父容器处理
getParent().requestDisallowInterceptTouchEvent(false);
} else if (ptrLayout.getMode() == PtrFrameLayout.Mode.LOAD_MORE && eventY < event.getY() && checkContentViewScrollTop()) {
// 当加载模式为加载更多模式下 可滑动子布局滑动到顶部时 滑动事件交予父容器处理
getParent().requestDisallowInterceptTouchEvent(false);
} else if (ptrLayout.getMode() == PtrFrameLayout.Mode.REFRESH && eventY > event.getY() && checkContentViewScrollBottom()) {
// 当加载模式为下拉刷新模式下 可滑动子布局滑动到底部时 滑动事件交予父容器处理
getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
eventX = event.getX();
eventY = event.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
}
return super.dispatchTouchEvent(event);
}
/**
* 刷新类型
*/
public class Mode {
/**
* 不能刷新
*/
public static final byte NONE = 1;
/**
* 下拉
*/
public static final byte REFRESH = 2;
/**
* 上拉
*/
public static final byte LOAD_MORE = 3;
/**
* 下拉、上拉
*/
public static final byte BOTH = 4;
}
}
其中CommonPtrDefaultHeader和CommonPtrDefaultFooter分别是实现PtrUIHandler接口的HeaderView和FooterView。
这里稍微提一下onInitMainView()这个方法,注释提到初始化PTR的同级View必须写到该方法内,也就是类似要加网络加载失败View到LinearLayout上,必须写到这个方法里面来。这是为什么呢?我们往下面看,PullToRefreshBaseView重写了addView方法,其目的是为了能兼容类似PullToRefreshScrollView的使用,需要在其内部静态或者动态增加元素。
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if(mContent != null && mContent instanceof ViewGroup && !(child instanceof PtrFrameLayout)) {
((ViewGroup) mContent).addView(child, index, params);
}else {
super.addView(child, index, params);
}
}
从代码中可以看出,如果在mContent初始化完成之后,addView的子View都会加到mContent中的,所以onInitMainView()我们必须在onInitContent()前调用,因此要为LinearLayout增加子View,则必须在onInitMainView()中初始化了。
2、PullToRefreshRecyclerView.java
继承PullToRefreshBaseView,内部实现了RecyclerView的嵌入。鉴于这里面涉及比较多了RecyclerView的使用和统一封装,具体会在下一篇博文中详细讲解,这里只贴出关键的实现代码。
public abstract class PullToRefreshRecyclerView<T> extends PullToRefreshBaseView implements PullToRefreshBaseView.OnPullBothListener {
private final String TIPS_LOAD_DATA = "加载中…";
private RecyclerView mRecyclerView;
private LinearLayoutManager mLinearLayoutManager;
private PTRRecyclerViewDecoration myDecoration;
private List<View> mHeaderViewList;
private List<View> mFooterViewList;
private CommonBaseAdapter<T> commonBaseAdapter;
private OnPullListActionListener<T> mOnPullListActionListener;
private OnLastPageHintListener mOnLastPageHintListener;
public List<T> mList = new ArrayList<>();
public int mTotalCount;
public int mPageIndex;
@Override
public View onInitContent() {
mRecyclerView = new RecyclerView(getContext());
mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1));
return mRecyclerView;
}
public PullToRefreshRecyclerView(Context context, OnPullListActionListener<T> mPullListActionListener) {
super(context);
mOnPullListActionListener = mPullListActionListener;
initView();
}
public PullToRefreshRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public PullToRefreshRecyclerView(Context context) {
super(context);
initView();
}
private void initView() {
setDefaultLoadingHeaderView();
setDefaultLoadingFooterView();
setOnRefreshListener(this);
mLinearLayoutManager = new LinearLayoutManager(getContext());
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.setHasFixedSize(true); //确定每个item高度相同,提高性能
mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext()));
}
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
@Override
public void onPullDownToRefresh() {
loadRefreshData(false);
}
@Override
public void onPullUpToRefresh() {
if (mPageIndex <= mTotalCount) {
loadMoreData(false);
} else {
if (mOnLastPageHintListener != null) {
mOnLastPageHintListener.onLastPageHint();
}
onRefreshComplete();
if (mOnPullListActionListener != null) {
mOnPullListActionListener.onRefreshComplete();
}
}
}
/**
* 下拉刷新加载数据
*/
public void loadRefreshData(boolean isShowTops) {
String tips = isShowTops ? TIPS_LOAD_DATA : "";
mPageIndex = 1;
if (mOnPullListActionListener != null) {
mOnPullListActionListener.loadData(getId(), mPageIndex, tips);
}
}
/**
* 上拉刷新加载更多数据
*/
public void loadMoreData(boolean isShowTops) {
String tips = isShowTops ? TIPS_LOAD_DATA : "";
if (mOnPullListActionListener != null) {
mOnPullListActionListener.loadData(getId(), mPageIndex, tips);
}
}
public void setDivider(int padding, int divider) {
if (padding > 0 && divider >= 0) {
Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null;
myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding));
mRecyclerView.addItemDecoration(myDecoration);
}
}
public void addHeaderView(View headerView) {
if (mHeaderViewList == null){
mHeaderViewList = new ArrayList<>();
}
mHeaderViewList.add(headerView);
if(myDecoration != null) {
myDecoration.isHadHeader = true;
}
}
public void addFooterView(View footerView) {
if (mFooterViewList == null){
mFooterViewList = new ArrayList<>();
}
mFooterViewList.add(footerView);
if(myDecoration != null) {
myDecoration.isHadFooter = true;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) {
if (adapter instanceof HeaderAndFooterWrapper) {
((HeaderAndFooterWrapper) adapter).addFooterView(footerView);
adapter.notifyDataSetChanged();
}else {
HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
headerAndFooterWrapper.addFooterView(footerView);
mRecyclerView.setAdapter(headerAndFooterWrapper);
}
}
}
public void removeFooterView(View footerView) {
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) {
((HeaderAndFooterWrapper) adapter).removeFooterView(footerView);
if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) {
mFooterViewList.remove(footerView);
}
}
}
public void scrollToTop() {
mLinearLayoutManager.scrollToPositionWithOffset(0, 0);
}
public RecyclerView.Adapter getAdapter() {
return mRecyclerView.getAdapter();
}
public void setPageIndex(int page) {
mPageIndex = page;
}
public List<T> getList(){
return mList;
}
public void notifyDataSetChanged(){
if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {
mRecyclerView.getAdapter().notifyDataSetChanged();
}
}
/**
* 显示数据
*/
public void showAllData(List<T> list, int itemLayoutId) {
if (commonBaseAdapter == null) {
commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId);
mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter));
} else {
getAdapter().notifyDataSetChanged();
}
}
/**
* 包装adapter,增加HeaderView,FooterView
*/
private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) {
if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) {
HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
//增加HeaderView
if (mHeaderViewList != null && mHeaderViewList.size() != 0) {
headerAndFooterWrapper.addHeaderView(mHeaderViewList);
}
//增加FooterView
if (mFooterViewList != null && mFooterViewList.size() != 0) {
headerAndFooterWrapper.addFooterView(mFooterViewList);
}
return headerAndFooterWrapper;
}
return adapter;
}
/**
* 封装RecyclerView.Adapter,兼容ViewHolder
*/
private class MyListAdapter extends CommonBaseAdapter<T> {
public MyListAdapter(Context context, List<T> mData, int itemLayoutId) {
super(context, mData, itemLayoutId);
}
@Override
protected void onItemClick(View itemView, int position) {
if (position >= 0 && mList.size() > 0) {
T item = mList.get(position);
if (mOnPullListActionListener != null && item != null) {
int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0;
mOnPullListActionListener.clickItem(getId(), item, position + numHeaderView);
}
}
}
@Override
protected void convert(ViewHolder holder, T item, List<T> list, int position) {
if (mOnPullListActionListener != null && item != null) {
mOnPullListActionListener.createListItem(getId(), holder, item, list, position);
}
}
}
public void setOnPullListActionListener(OnPullListActionListener mPullListActionListener) {
mOnPullListActionListener = mPullListActionListener;
}
public void setOnLastPageHint(OnLastPageHintListener mLastPageHintListener) {
mOnLastPageHintListener = mLastPageHintListener;
}
}
3、PullToRefreshScrollView.java
继承PullToRefreshBaseView,内部实现了ScollView的嵌入。这部分代码比较简单,主要就是在onInitContent()里面实例化了有一个ScrollView,然后返回给PullToRefreshBaseView加入到Ultra-PTR的mContent中。由此也可以看出,我们封装完PullToRefreshBaseView之后,要实现一些View的下拉刷新功能跟原本用Ultra-PTR一样都是非常简单的。
public class PullToRefreshScrollView extends PullToRefreshBaseView {
private ScrollView mScrollView;
@Override
public void onInitMainView() {
}
@Override
public View onInitContent() {
mScrollView = new ScrollView(getContext());
mScrollView.setLayoutParams(new ScrollView.LayoutParams(-1, -1));
return mScrollView;
}
public PullToRefreshScrollView(Context context) {
super(context);
initView();
}
public PullToRefreshScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
mScrollView.setVerticalFadingEdgeEnabled(false);
mScrollView.setVerticalScrollBarEnabled(false);
setDefaultLoadingHeaderView();
setMode(Mode.REFRESH);
}
public void fullScroll(int direction) {
mScrollView.fullScroll(direction);
}
}
xml布局中调用
<com.commonlib.pulltorefresh.PullToRefreshScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="@android:color/transparent"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal">
......
......
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="1px"
android:paddingLeft="@dimen/dp_14"
android:paddingRight="@dimen/dp_14"
android:background="@drawable/dividing_padding_line"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout>
</com.commonlib.pulltorefresh.PullToRefreshScrollView>
结语
以上就给大家介绍了下拉刷新库的设计和封装过程,核心的部分还是在于PullToRefreshBaseView,通过Ultra-PTR-Load-More实现了下拉刷新、上拉加载的功能,提供onInitContent()抽象方法,给子类实现加载的内容View。同时我们继承PullToRefreshBaseView实现了PullToRefreshRecyclerView和PullToRefreshScrollView,可以直接使用PullToRefreshRecyclerView就能实现RecyclerView列表模式和ScrollView的下拉和上拉刷新,用起来非常简单,完全可以不用关心RecyclerView如何调用。到底如果使用,我们会在后面的两篇文章中继续为大家介绍。
回顾一下,我们主要用到的是Ultra-PTR这个第三方库,但真正调用起来,我们还需自己实现布局、刷新事件的处理、数据的刷新等等问题。而我们封装成PullToRefreshBaseView,这些事情统统就为我们解决了,我们仅仅需要调用loadRefreshData(),loadMoreData()和showAllData()这3个方法。而且从我们第三方库替换、更新的角度来说,我们也只需要关心PullToRefreshBaseView一个类的修改,外部完全可以不用做大改动,甚至不用改动。就好比我们替换成SwipeRefreshLayout也是可以做到的,当然这个就是后话了。