android-Ultra-Pull-To-Refresh/SwipeRefreshLayout嵌套ViewPager/ScrollView滑动冲突解决

 每次必不可少的前戏又来了。发文时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滑动的时候禁用了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与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)来灵活控制是否需要拦截。

收尾福利

问题描述:SwipeRefreshLayoutandroid-Ultra-Pull-To-Refresh嵌套ScrollViewListViewGrdiViewRecyclerView时,页面往上滑再往下滑时还没滑到顶部就触发了下拉刷新。

SwipeRefreshLayout嵌套上下滑动View时滑动冲突

  解决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) 。 
  但是我们每次嵌套都需要对的子View的滑动做判断,所以我们动态的指定 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);
        }
    }
}

和android-Ultra-Pull-To-Refresh嵌套上下滑动View时滑动冲突

  其实PtrFramLayout的刷新接口PtrHandler提供了一个方法checkCanDoRefresh(...)来检查是否允许刷新,我们只需要在这里做判断返回truefalse就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) {
        // 做刷新的操作
        ...
    }
};

你可能感兴趣的:(android疑难)