每次必不可少的前戏又来了。发文时Android-PullToRefresh
这个框架已经停止维护3年了,很多人在关心我们现在用什么框架好,这里给大家推荐两个。一个是可爱可亲起可恨的Google官方v4包自带的SwipeRefreshLayout
,一个是liaohuqiu同学在Github上发表的android-Ultra-Pull-To-Refresh
,我强烈推荐使用后者,后者扩展性好,而且是国人自己开发的框架,交互设计也符合Google官方的操作规范。
android-Ultra-Pull-To-Refresh托管地址:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/
英文版官网:http://android-ultra-ptr.liaohuqiu.net/
中文版官网:http://android-ultra-ptr.liaohuqiu.net/cn
力荐android-Ultra-Pull-To-Refresh
的原因是它特别易于扩展,而且api丰富简单,作者也是咱中国人,但是根本原因还是这个框架确实特别好用。而且SwipeRefreshLayout
有的功能和动画它都有。
现在引出我们的问题,我们好多人都使用SwipeRefreshLayout
,如果不需要修改Header和动画的话,应该可以满足大多数需求了。上面也说了SwipeRefreshLayout
有的动画和功能android-Ultra-Pull-To-Refresh
都有,但是我们现在的情况是,不论使用这两者其中的哪个框架和ViewPager
互相嵌套的时候(比如作为Banner),二者与ViewPager
都会发生滑动冲突。
先来解决个人认为次要且相对简单的SwipeRefreshLayout
的冲突问题,只需要在ViewPager
滑动的时候禁用了SwipeRefreshLayout
就可以了,代码如下:
ViewPager mViewPager;
SwipeRefreshLayout mRefreshLayout;
...
mViewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:// 经测试,ViewPager的DOWN事件不会被分发下来
case MotionEvent.ACTION_MOVE:
mRefreshLayout.setEnabled(false);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mRefreshLayout.setEnabled(true);
break;
}
return false;
}
});
做个简单的解释,当我们手指在ViewPager上按下或者移动的时候让SwipeRefreshLayout不可用,当我们手指抬起或者滑动到ViewPager之外的时候让SwipeRefreshLayout可用。
此方法同样适用于android-Ultra-Pull-To-Refresh(达哥做了测试后发现不是很准),但是是不是有点麻烦了,每次都需要这么处理一下,或者要继承系统的View做个小的封装。因此推荐看官使用android-Ultra-Pull-To-Refresh,我们接下来解决android-Ultra-Pull-To-Refresh与ViewPager嵌套时的滑动冲突。
当我们不亦乐乎的在项目中使用android-Ultra-Pull-To-Refresh时,当我们用android-Ultra-Pull-To-Refresh和ViewPager互相嵌套的时候猛然发现,二者居然有滑动冲突,我们在框架主页的issues
中也能看到这个问题,但是一直没有被解决,心中上万只草泥马奔腾而过,这时候达哥我就要出来来拯救世界了,看我们如何来解决这个问题。
我们在android-Ultra-Pull-To-Refresh的源码托管ReadMe的末尾发现work with ViewPager: disableWhenHorizontalMove(),翻译过来就是和ViewPager一起使用,但是我们调用了这个方法后发现然并卵,那么问题在哪里呢?
达哥阅读了android-Ultra-Pull-To-Refresh的PtrFramLayout源码后发现修改了这个bug,这里直接提供修改方法,我们打开PtrFramLayout.Java这个类,找到308行代码:
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
把上述代码的
if
判断的
Math.abs(offsetX) > mPagingTouchSlop
这一句去掉就可以了,完整代码如下:
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
原因是,我们既然要禁用横向滑动的拦截,那么判断操作为横向并且要禁用横向拦截时给mPreventForHorizontal
赋值为true
即可,并不需要判断滑动距离。
同时PtrFramLayout
的第113、114行代码就无用了,可以注释了,第54行mPagingTouchSlop
成员变量也无用,可以注释了。
使用的同学请注意还是需要调用PtrFrameLayout.disableWhenHorizontalMove(true)
来灵活控制是否需要拦截。
问题描述:SwipeRefreshLayout
和android-Ultra-Pull-To-Refresh
嵌套ScrollView
、ListView
、GrdiView
、RecyclerView
时,页面往上滑再往下滑时还没滑到顶部就触发了下拉刷新。
解决SwipeRefreshLayout
冲突方案,我们来看看SwipeRefreshLayout
源码发现一个方法canChildScrollUp()
,意思是子View可以向上滚动吗,当返回true的时候SwipeRefreshLayout
就不能被下拉刷新了,细细分析了代码之后发现这里只对子View做了判断,而我们实际开发中往往会在SwipeRefreshLayout
中再嵌套一个ViewGroup,再在这个ViewGroup中放一个ScrollView,这时候我们说的问题就出现了。
canChildScrollUp()
方法源码
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
所以我们需要重写
SwipeRefreshLayout
的
canChildScrollUp()
方法,完整代码如下:
public class MySwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout {
private boolean canChildScrollUp;
public MySwipeRefreshLayout(Context context) {
super(context);
}
public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setCanChildScrollUp(boolean canChildScrollUp) {
this.canChildScrollUp = canChildScrollUp;
}
@Override
public boolean canChildScrollUp() {
return canChildScrollUp;
}
}
当我们监听到
SwipeRefreshLayout
中可以上下滑动的View向上滚动了就调用
swipeRefreshLayout.setChildScrollUp(true)
,当我们监听到
SwipeRefreshLayout
中可以上下滑动的View向下滚动并且已经到顶部了,就调用
swipeRefreshLayout.setChildScrollUp(false)
。
SwipeRefreshLayout
中的contentView,并且使用
SwipeRefreshLayout
的
canChildScrollU()
方法中的代码来判断,简直完美啊:
public class MySwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout {
private View contentView;
public MySwipeRefreshLayout(Context context) {
super(context);
}
public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setContentView(View contentView) {
this.contentView = contentView;
}
@Override
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (contentView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) contentView;
return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(contentView, -1) || contentView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(contentView, -1);
}
}
}
其实PtrFramLayout的刷新接口PtrHandler提供了一个方法checkCanDoRefresh(...)
来检查是否允许刷新,我们只需要在这里做判断返回true
和false
就OK了。
然而我们的作者liaohuqiu同学也是想的非常周到,提供了一个PtrDefaultHandler
类,实现了和SwipeRefreshLayout
同样的判断,so看到这里的同学肯定知道怎么改了吧,我们先来看看PtrDefaultHandler
源码:
public abstract class PtrDefaultHandler implements PtrHandler {
public static boolean canChildScrollUp(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return view.getScrollY() > 0;
}
} else {
return view.canScrollVertically(-1);
}
}
/**
* Default implement for check can perform pull to refresh
*
* @param frame
* @param content
* @param header
* @return
*/
public static boolean checkContentCanBePulledDown(PtrFrameLayout frame, View content, View header) {
return !canChildScrollUp(content);
}
@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
return checkContentCanBePulledDown(frame, content, header);
}
}
所以我们在使用刷新接口的时候使用
PtrDefaultHandler
就好了,重写
checkCanDoRefresh()
方法:
private ScrollView scrollView;
...
private PtrDefaultHandler defaultHandler = new PtrDefaultHandler() {
@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
return !canChildScrollUp(scrollView);
}
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
// 做刷新的操作
...
}
};