SwipeRefreshLayout和ViewPager滑动冲突的原理分析及解决方案

事先声明:本文只是对大神作品的整理,以及对事件分发的学习

 相信SwipeRefreshLayout现在用的特别广泛了吧,但是SwipeRefreshLayout和viewpager貌似存在很多冲突问题
SwipeRefreshLayout出来已经很久了,我也使用很久了,但是之前并没有太在意和viewpager的冲突问题
测试发现两个问题:
1. SwipeRefreshLayout会吃掉ViewPager的滑动事件。
2. SwipeRefreshLayout需要套在ScrollView和ListView上的时候才表现的比较友好,在其他ViewGroup上有点问题,不知道为什么,有兴趣的可以去看下源码。


经测试发现如果是往左下或右下滑动的时候,事件就会被SwipeRefreshLayout吃掉。但是平移滑动或者往右上左上滑动就没问题。

目前网上流传的解决方式:

1、监听ViewPager的OnTouch事件,滑动的时候禁用swipeRefreshLayout
viewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mSwipeRefreshLayout.setEnabled(false);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeRefreshLayout.setEnabled(true);
                break;
        }
        return false;
    }
});
2、继承ViewPager,请求父控件不要拦截ViewPager事件
public class CustomViewPager extends ViewPager {
    public CustomViewPager(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean in = super.onInterceptTouchEvent(ev);
        if (in) {
            getParent().requestDisallowInterceptTouchEvent(true);
            this.requestDisallowInterceptTouchEvent(true);
        }
        return false;
    }
}

这两种方法都会导致一个问题, 在ViewPager无法刷新。 

第一种方式,偶尔能滑动,偶尔滑不动。为什么会这样,继续往下看,带你分析源码。
第二种方式,连偶尔都不要想,不管在真机还是模拟器,都无法刷新了,这里就不演示了。具体原因请看我的另一篇博客,看懂以后妈妈再也不用担心你的事件分发了。
Android的事件分发源码分析,告别事件冲突 

因为事件是先从上层往下层传递的,既然ViewPager的事件被吃掉了,那么肯定是在SwipeRefreshLayout中被消费了。 
我们去看看SwipeRefreshLayout的源码。 
1. 先看dispatch方法,发现重写此方法。 
2. 然后看onIntercept方法,发现是在这里拦截了。那么onTouchEvent方法就不用看了。下面我们就来分析一下onInterceptTouchEvent方法的源码。
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
// 遍历所有child,第一个child就是target(除了刷新的那个圈)。        // 这就是为啥SwiperefreshLayout只能有一个child的原因
        ensureTarget();


        final int action = MotionEventCompat.getActionMasked(ev);

 // 这个也无视吧, mReturningToStart一直都是false的,源码中并没有赋值        // 估计原本用于判断是否正在刷新中,后来用了其他方式判断。(猜测)
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }


        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }


        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
// 一个记录是否正在进行拖拽的标记,初始化false。
                mIsBeingDragged = false;
  // 获取按下的Y轴位置 
                final float initialDownY = getMotionEventY(ev, mActivePointerId);
                if (initialDownY == -1) {
                    return false;
                }
                mInitialDownY = initialDownY;
                break;


            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

 // 获取当前的Y轴位置   
                final float y = getMotionEventY(ev, mActivePointerId);
                if (y == -1) {
                    return false;
                }
// 获取手指在Y轴的滑动距离
                final float yDiff = y - mInitialDownY;
// 如果滑动距离大于mTouchSlop(不同手机的值不同,一般为8px)并且当前不是在拖拽中 
                if (yDiff > mTouchSlop && !mIsBeingDragged) {
                    mInitialMotionY = mInitialDownY + mTouchSlop;
// 设置当前拖拽标记为true  
                    mIsBeingDragged = true;
                    mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
                }
                break;


            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;


            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
 //当手指抬起的时候设置拖拽标记为false; 
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

// 如果是拖拽中,拦截事件,否则不拦截。
        return mIsBeingDragged;
    }
冲突的原因就是手指在Y轴滑动距离大于mTouchSlop  ,此时swipeRefreshLayout拦截了viewpager的触摸事件,导致没有继续往下分发(事件分发机制是从上往下的,即由外层往里)如果我Y轴滑动距离没有大于这个mTouchSlop,mIsBeingDragged为false,事件就不拦截了,会继续往下分发,那么ViewPager就响应到了move事件,然而第一种解决方案将SwipeRefreshLayout设置成Disable了,这就是为什么往下滑动为什么总是不能将小球拉下来的原因。如果Y轴滑动距离大于这个mTouchSlop,那么事件就拦拦截了自己处理,小球就可以被拉下来了。这也是偶尔能将小球拉下来的原因。

什么时候Y轴滑动距离会大于mTouchSlop而不被ViewPager响应到事件呢?
要知道两次Touch之间也是有个很短的响应时间的,只要在这个时间内,Y轴滑动距离大于mTouchSlop就可以了,这时候事件就被拦截了,ViewPager没机会响应到move事件,从而不会禁用掉SwipeRefreshLayout

解决方法:

重写SwipeRefreshLayout的onIntercept方法。

思路如下

1. 因为下拉刷新,只有纵向滑动的时候才有效,那么我们就判断此时是纵向滑动还是横向滑动就可以了。 
2. 纵向滑动就拦截事件,横向滑动不拦截。 
3. 怎么判断是纵向滑动还是横向滑动,只要判断Y轴的移动距离大于X轴的移动距离那么就判定为纵向滑动就行了。
重写的SwipeRefreshLayout如下:
package com.pai.mai.view;

import android.content.Context;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * Created by AItsuki on 2016/1/20.
 */
public class IndexSwipeRefreshLayout extends SwipeRefreshLayout {
    private float startY;
    private float startX;
    // 记录viewPager是否拖拽的标记
    private boolean mIsVpDragger;

    public IndexSwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 记录手指按下的位置
                startY = ev.getY();
                startX = ev.getX();
                // 初始化标记
                mIsVpDragger = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // 如果viewpager正在拖拽中,那么不拦截它的事件,直接return false;
                if (mIsVpDragger) {
                    return false;
                }
                // 获取当前手指位置
                float endY = ev.getY();
                float endX = ev.getX();
                float distanceX = Math.abs(endX - startX);
                float distanceY = Math.abs(endY - startY);
                // 如果X轴位移大于Y轴位移,那么将事件交给viewPager处理。
                if (distanceX >= distanceY) {
                    mIsVpDragger = true;
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:                // 初始化标记
                mIsVpDragger = false;
                break;
        }
        // 如果是Y轴位移大于X轴,事件交给swipeRefreshLayout处理。
        return super.onInterceptTouchEvent(ev);
    }
}
Ok,解决冲突
原作地址点击打开链接




 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值