下拉刷新和上拉加载应该是当前手机应用中最普遍的一个操作了Android本身提供了一个下拉刷新库,在support-v4包中的SwipeRefreshLayout
。但是这个库支持的效果比较单一,只能实现列表不动,刷新头部下拉滑出的效果。并且也没能提供上拉加载的功能。在项目初期的时候,也是因为调研不足,选择了比较经典的PullToRefresh库。然而这个库已经停止更新了,对新的控件(如RecyclerView
)并不能够支持。同时,在使用过程中也出现了很多诟病(无法显示分隔线等)。在经历了之前的框架重构之后,我决定替换掉它。当然,项目已经开发近一年,替换掉一个常用控件的重构工作并不轻松。因此,选择需要相对谨慎。
在经过一段时间的搜索之后,我发现只有UltraPullToRefresh能够满足我的需求:能够适配所有的View
。这个库是由国内大牛廖祜秋
开源出来的,通过接口的形式支持了所有View的适配,以及自定义下拉刷新头部的功能。之前,我就已经阅读过了关于UltraPullToRefresh
的分析,也根据学习的结果开发出了自己的一个动画效果库PseudoMaterialHeader。但是,UltraPullToRefresh却很”傲娇”的表示不支持上拉加载更多,因为他认为这两个功能不属于同一个层次的功能。相应的,在其Readme中也给出了实现上拉加载的一个解决方案,即使用cubeSDK中的对应View。然而,这个解决方案只是通过给ListView和GridView添加Footer来实现,效果和扩展性都不好,另外也额外增加了一层布局。这样的效果显然是不能令人满意的,因此,我决定自己拓展一个带上拉加载的库。
项目地址:https://github.com/captainbupt/android-Ultra-Pull-To-Refresh-With-Load-More
这个库是从UltraPullToRefresh
库fork过来的,已经发起Pull Request,不过原作者只是回信了,并没有Merge进去。因此,想要使用该库,就必须下载下来,手动导入到IDE中去。
下面,对修改过程进行一下讲解。
PtrFrameLayout
是一个核心类,它本质上是一个ViewGroup
,目的是包含需要下拉刷新的View,Header和Footer。作为所有控件的Parent View,它能够接收到所有屏幕触碰事件,并选择是否下发,这也是其实现下拉刷新的保障。下面,我就来具体说一下下关键的函数,以及相关的改动。
PtrIndicator
是在PtrFrameLayout
用来记录Header相关属性的工具类,包含了高度,当前偏移位置,阻抗等等必要信息。一开始我本来打算是给Footer也单独定义一个PtrIndicator
的。但是后来发现,这样做,就需要把PtrFrameLayout
所有关于PtrIndicator
的操作都重复一遍,显得十分的冗余。另外,我也注意到,Header和Footer是不可能同时出现的(不可能同时下拉刷新和上拉加载)。因此,PtrFrameLayout
中的PtrIndicator
可以直接复用,只需要再添加一个布尔变量标记为Header或者Footer,需要进行不同操作的时候再进行判断即可。
private boolean isHeader = true;
public boolean isHeader() {
return isHeader;
}
public void setIsHeader(boolean isHeader) {
this.isHeader = isHeader;
}
当布局文件加载完成后,这个方法会被调用。原有库中做的操作是判断是否存在Header和Content View,并作相应赋值。在此,我也就简单拓展了一下。即当存在3个子View的时候,分别检测它们分别属于哪个部分(Header,Content,Footer)。这个部分并没有进行过调试,因为一般情况下,都是只包含一个Content View作为展示的。
// not specify header or content or footer
if (mContent == null || mHeaderView == null || mFooterView == null) {
final View child1 = getChildAt(0);
final View child2 = getChildAt(1);
final View child3 = getChildAt(2);
// all are not specified
if (mContent == null && mHeaderView == null && mFooterView == null) {
mHeaderView = child1;
mContent = child2;
mFooterView = child3;
}
// only some are specified
else {
ArrayList view = new ArrayList(3) {{
add(child1);
add(child2);
add(child3);
}};
if (mHeaderView != null) {
view.remove(mHeaderView);
}
if (mContent != null) {
view.remove(mContent);
}
if (mFooterView != null) {
view.remove(mFooterView);
}
if (mHeaderView == null && view.size() > 0) {
mHeaderView = view.get(0);
view.remove(0);
}
if (mContent == null && view.size() > 0) {
mContent = view.get(0);
view.remove(0);
}
if (mFooterView == null && view.size() > 0) {
mFooterView = view.get(0);
view.remove(0);
}
}
}
在这个类中,就可以分别获取到Header和Footer的高度了。
if (mHeaderView != null) {
measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mPtrIndicator.setHeaderHeight(mHeaderHeight);
}
if (mFooterView != null) {
measureChildWithMargins(mFooterView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams();
mFooterHeight = mFooterView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mPtrIndicator.setFooterHeight(mFooterHeight);
}
这个类是用来指定各个布局的边界的。正如前面所说,PtrIndicator
中包含了当前的偏移量,即Header或者Footer展示出来的高度。根据这个值,我们就需要相应的将Header/Footer和Content进行上移或者下移,从而使得Header和Footer可以被显示。
整个过程中,视图范围是固定的,即整个控件的高宽是一定的。超出的部分就会被自动隐藏掉。例如,当Header的上边框为负值时,Header应当是隐藏或者部分显示的。对于Footer也是同样的道理。根据偏移量的不断变化,不断的设定这几个部分的上下边框,就可以达到上拉和下拉的效果。
if (mHeaderView != null) {
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
final int left = paddingLeft + lp.leftMargin;
final int top = paddingTop + lp.topMargin + offsetHeaderY - mHeaderHeight;
final int right = left + mHeaderView.getMeasuredWidth();
final int bottom = top + mHeaderView.getMeasuredHeight();
mHeaderView.layout(left, top, right, bottom);
}
if (mContent != null) {
MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
int left;
int top;
int right;
int bottom;
if (mPtrIndicator.isHeader()) {
left = paddingLeft + lp.leftMargin;
top = paddingTop + lp.topMargin + (isPinContent() ? 0 : offsetHeaderY);
right = left + mContent.getMeasuredWidth();
bottom = top + mContent.getMeasuredHeight();
} else {
left = paddingLeft + lp.leftMargin;
top = paddingTop + lp.topMargin - (isPinContent() ? 0 : offsetFooterY);
right = left + mContent.getMeasuredWidth();
bottom = top + mContent.getMeasuredHeight();
}
contentBottom = bottom;
mContent.layout(left, top, right, bottom);
}
if (mFooterView != null) {
MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams();
final int left = paddingLeft + lp.leftMargin;
final int top = paddingTop + lp.topMargin + contentBottom - (isPinContent() ? offsetFooterY : 0);
final int right = left + mFooterView.getMeasuredWidth();
final int bottom = top + mFooterView.getMeasuredHeight();
mFooterView.layout(left, top, right, bottom);
}
这个就是用来处理触碰事件的关键类了。在UltraPullToRefresh
中,其中心思想就是只处理不拦截。即,所有的事件都会通过调用super.dispatchTouchEvent(e)
,最终被传递到子类中去,这就可以保证子View的事件不会被覆盖。而当上拉或者下拉事件开始处理的时候,那么就会向子View发送Cancel事件,使得上拉和下拉不会被其他事件打断。
当接收到一个ACTION_DOWN
事件的时候,会将该坐标记录到PtrIndicator
中。当ACTION_MOVE
时,就可以根据这些信息判断出移动距离和方向。接着,判断Header和Footer是否已经显示,或者判断是否可以显示Header和Footer(mPtrHandler.checkCanDoRefresh()是开发人员可以定义的结果)。获取到这些判断信息后,就可以决定下一步的工作了:
PtrIndicator
的偏移量,并作相关判断(是否达到刷新高度等)。如果不需要显示,则直接将事件传递给子View即可。如果Header或者Footer已经显示,那么就只需要根据滑动距离更新PtrIndicator
的偏移量即可。因为这个时候,必然是处于上拉或者下拉的过程中,不能够让子类去进行处理,也不应当产生其他的事件。
boolean moveDown = offsetY > 0;
boolean moveUp = !moveDown;
boolean canMoveUp = mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the header is showing
boolean canMoveDown = mFooterView != null && !mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the footer is showing
boolean canHeaderMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView) && (mMode.ordinal() & 1) > 0;
boolean canFooterMoveUp = mPtrHandler != null && mFooterView != null // The footer view could be null, so need double check
&& mPtrHandler instanceof PtrHandler2 && ((PtrHandler2) mPtrHandler).checkCanDoLoadMore(this, mContent, mFooterView) && (mMode.ordinal() & 2) > 0;
if (DEBUG) {
PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s canHeaderMoveDown: %s canFooterMoveUp: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown, canHeaderMoveDown, canFooterMoveUp);
}
// if either the header and footer are not showing
if (!canMoveUp && !canMoveDown) {
// disable move when header not reach top
if (moveDown && !canHeaderMoveDown) {
return dispatchTouchEventSupper(e);
}
if (moveUp && !canFooterMoveUp) {
return dispatchTouchEventSupper(e);
}
// should show up header
if (moveDown) {
moveHeaderPos(offsetY);
return true;
}
// should show up footer
if (moveUp) {
moveFooterPos(offsetY);
return true;
}
}
// if header is showing, then no need to move footer
if (canMoveUp) {
moveHeaderPos(offsetY);
return true;
}
// if footer is showing, then no need to move header
if (canMoveDown) {
moveFooterPos(offsetY);
return true;
}
这几个关键函数修改完成后,其他函数的修改基本只需要根据PtrIndicator
中的isHeader
来进行不同的处理即可(基本上只是将offset反转的操作)。
原有的PtrHandler
中,只包含了下拉刷新所需要的方法。为了保证向下兼容性,我定义了PtrHandler2
并继承了PtrHandler
。这样,就可以保证使用这个库的时候不会和之前的部分产生冲突。同样的,作为默认的PtrDefaultHandler
,我也提供了PtrDefaultHandler2
来实现默认的判断方法。
public static boolean canChildScrollDown(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
return absListView.getChildCount() > 0
&& (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1
|| absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
} else if (view instanceof ScrollView) {
ScrollView scrollView = (ScrollView) view;
if (scrollView.getChildCount() == 0) {
return false;
} else {
return scrollView.getScrollY() < scrollView.getChildAt(0).getHeight() - scrollView.getHeight();
}
} else {
return false;
}
} else {
return view.canScrollVertically(1);
}
}
为了提供更加多样化的操作,我提供了Mode属性。通过设置Mode属性,开发人员就可以随时开启或者禁用PtrFrameLayout
的上拉或者下拉功能。
public enum Mode {
NONE, REFRESH, LOAD_MORE, BOTH
}
这个修改是因为项目需要,临时赶工而成,很多地方都没有完善。例如:没能将Header和Footer完全分开,因此,Header和Footer的所有属性必然保持一致。不能满足更加多样化的需求。另外,在使用过程中,我也在不断的发现BUG,并进行修复。
修改完之后UltraPullToRefreshWithLoadMore
,我已经正式使用到项目中,目前表现良好。另外,这也是我第一次fork并发起PullRequest的git操作,尽管目前还没能被成功Merge进去,还是给了我不少信心。这次经历之后,我也会慢慢开始对开源库进行自己的修改,来帮助开源社区的发展。
PS: 目前已经Merge成功的一个开源库:CircleProgressView