1. 程式人生 > >複製AItsuki的Android:SwipeRefreshLayout和ViewPager滑動衝突的原因和正確的解決方式

複製AItsuki的Android:SwipeRefreshLayout和ViewPager滑動衝突的原因和正確的解決方式

原文連結:http://blog.csdn.net/u010386612/article/details/50548977

用第一種就解決了問題

BUG修復

2016.01.21 用幾部真機測試,發現有些手機,手指沒有滑動,move也一直執行。這回導致我們的判斷出現一些問題。現在已經修復,加入了TouchSlop判斷。

一、前言

急著解決問題的直接看博文的最後面吧,或者點這裡跳轉過去,正確的解決方式就在那。

雖然SwipeRefreshLayout出來已經很久了,但是知道今天我才第一次使用。 
然後發現兩個問題: 
1. SwipeRefreshLayout會吃掉ViewPager的滑動事件。 
2. SwipeRefreshLayout需要套在ScrollView和ListView上的時候才表現的比較友好,在其他ViewGroup上有點問題,不知道為什麼,到時候去看下原始碼。

今天我只說第一個問題: 
很明顯如果是往左下或右下滑動的時候,事件就會被SwipeRefreshLayout吃掉。但是平移滑動或者往右上左上滑動就沒問題。 
這裡寫圖片描述

二、目前網上流傳的解決方式

我網上找解決方法的時候,發現無非都是兩種方式。 
1、監聽ViewPager的OnTouch事件,滑動的時候禁用swipeRefreshLayout

mViewPager.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; } })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這兩種方法都會導致一個問題, 在ViewPager無法重新整理。 
就像這樣: 
第一種方式,偶爾能滑動,偶爾滑不動。為什麼會這樣,繼續往下看,帶你分析原始碼。 
這裡寫圖片描述

第二種方式,連偶爾都不要想,不管在真機還是模擬器,都無法重新整理了,這裡就不演示了。具體原因請看我的另一篇部落格,看懂以後媽媽再也不用擔心你的事件分發了。 
Android的事件分發原始碼分析,告別事件衝突 




因為事件是先從上層往下層傳遞的,既然ViewPager的事件被吃掉了,那麼肯定是在SwipeRefreshLayout中被消費了。 
我們去看看SwipeRefreshLayout的原始碼。 
1. 先看dispatch方法,發現重寫此方法。 
2. 然後看onIntercept方法,發現是在這裡攔截了。那麼onTouchEvent方法就不用看了。下面我們就來分析一下onInterceptTouchEvent方法的原始碼。

三、SwipeRefreshLayout的onInterceptTouchEvent原始碼分析。

有目的性的分析,我們只需要分析和事件衝突相關的原始碼,所以只註釋的關鍵部分。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 確保有SwipeRefreshLayout有Target
        // 遍歷所有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) {
            // 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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

看不懂的可以再看幾遍,主要是mIsBeingDragged這個引數的值是否為true。

四、使用第一種方式,偶爾能拉下小球的原因

1、那麼我們來分析下,為什麼使用第一種方式的時候,偶爾將小球給拉下來。 
首先看這裡

// 獲取手指在Y軸的滑動距離
                final float yDiff = y - mInitialDownY;
                // 如果滑動距離大於mTouchSlop(不同手機的值不同,一般為8px)
                // 並且當前不是在拖拽中
                if (yDiff > mTouchSlop && !mIsBeingDragged) {
                    mInitialMotionY = mInitialDownY + mTouchSlop;
                    // 設定當前拖拽標記為true
                    mIsBeingDragged = true;
                    mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
                }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

當滑動距離大於mTouchSlop的時候才攔截事件。 
也就是說

  1. 如果我Y軸滑動距離沒有大於這個mTouchSlop,mIsBeingDragged為false,事件就不攔截了,會繼續往下分發,那麼ViewPager就響應到了move事件,並且將SwipeRefreshLayout設定成Disable了。這就是為什麼往下滑動為什麼總是不能將小球拉下來的原因。
  2. 如果Y軸滑動距離大於這個mTouchSlop,那麼事件就攔攔截了自己處理,小球就可以被拉下來了。這也是偶爾能將小球拉下來的原因。

什麼時候Y軸滑動距離會大於mTouchSlop而不被ViewPager響應到事件呢。 
要知道兩次Touch之間也是有個很短的響應時間的,只要在這個時間內,Y軸滑動距離大於mTouchSlop就可以了,這時候事件就被攔截了,ViewPager沒機會響應到move事件,從而不會禁用掉SwipeRefreshLayout。

我們來測試一下,超級快速的往下滑動。 
可以看到,慢慢滑動的時候,小球無法拉下來,如果快速下拉,小球就出來了。 
這也是因為在模擬器上比較卡的原因,如果在真機上,要更快一些才可以。 
這裡寫圖片描述

五、解決方式

寫了一大堆有的沒的才到了重點,彆著急,我覺得看完上面內容會對以後解決相關問題會有幫助,百度谷歌也不是所有問題都能搜的出來。

重寫SwipeRefreshLayout的onIntercept方法就可以很簡單的解決了。 
思路: 
1. 因為下拉重新整理,只有縱向滑動的時候才有效,那麼我們就判斷此時是縱向滑動還是橫向滑動就可以了。 
2. 縱向滑動就攔截事件,橫向滑動不攔截。 
3. 怎麼判斷是縱向滑動還是橫向滑動,只要判斷Y軸的移動距離大於X軸的移動距離那麼就判定為縱向滑動就行了。

以下就是重寫後的SwipeRefreshLayout,直接複製到專案就可以使用了。

/**
 * Created by AItsuki on 2016/1/20.
 */
public class VpSwipeRefreshLayout extends SwipeRefreshLayout {

    private float startY;
    private float startX;
    // 記錄viewPager是否拖拽的標記
    private boolean mIsVpDragger;
    private final int mTouchSlop;

    public VpSwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @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 > mTouchSlop && 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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

六、後話 
不知道說點什麼好了,部落格有點冷清,是我寫的不夠好麼,還是太難懂_(:з」∠)_