解決SwipeRefreshLayout和ViewPager滑動衝突的三種方案

一篇文章讀懂android事件消費、事件分發、事件攔截
Android 源碼分析事件分發機制、事件消費、事件攔截
解決SwipeRefreshLayout和ViewPager滑動衝突的三種方案

在SwipeRefreshLayout的內部包一個ViewPager,這樣左右滑動ViewPager的時候,頂部老是會彈出刷新按鈕,滑動很不靈敏。


瞭解事件分發機制和事件攔截機制的都知道解決滑動衝突無非兩種方法:外部攔截法和內部攔截法,現在我們運用這兩種方法,解決下這個問題。

注意:如果對事件分發機制和事件攔截機制不瞭解的可以看我的上兩篇文章《Android 源碼分析事件分發機制、事件消費、事件攔截》《一篇文章讀懂android事件消費、事件分發、事件攔截》

1、外部攔截法

 @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        // 外部攔截法
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int) event.getX();
                mLastY = (int) event.getY();

                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    return false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        return super.onInterceptTouchEvent(event);
    }
 }

外部攔截法,顧名思義,就是在外部父view裏攔截,我們直接重寫SwipeRefreshLayout的onInterceptTouchEvent方法,在ACTION_MOVE的時候,判斷如果是水平滑動的話,不攔截事件,把事件交由子View也就是ViewPager處理就ok了。這個方法很簡單,想必大家都可以想到。我們這篇文章重點是接下來的內部攔截法,通過內部攔截法教會大家真正學會去處理任何滑動衝突。

1、內部攔截法


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("內部View-CustomVPInner", "dispatchTouchEvent: Down");
                startX = ev.getX();
                startY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("內部View-CustomVPInner", "dispatchTouchEvent: Move");
                x = ev.getX();
                y = ev.getY();
                deltaX = Math.abs(x - startX);
                deltaY = Math.abs(y - startY);
                if (deltaX < deltaY) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }

        boolean a = super.dispatchTouchEvent(ev);
        Log.e("內部View-CustomVPInner", "dispatchTouchEvent: a = "+a);
        return a;
    }

我們直接重寫ViewPager的dispatchTouchEvent,在Down事件的時候,請求SwipeRefreshLayout不要攔截,只有在ACTION_MOVE事件的時候,並且判斷是垂直滑動的話,才請求SwipeRefreshLayout攔截。當然,還要記得重寫父view也就是SwipeRefreshLayout的onInterceptTouchEvent,並且在Down的時候返回false,因爲在Down的時候,是一定會去走onInterceptTouchEvent(ev);方法的。

 if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }

因爲在源碼裏是down事件的時候會執行resetTouchState();重置mGroupFlags標誌,導致一定會執行 intercepted = onInterceptTouchEvent(ev);這條語句,所以,在內部攔截法的時候,記得在外部父view裏重寫onInterceptTouchEvent,並且在Down的時候返回false。

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        Log.e("外部view-CustomSRL2", "onInterceptTouchEvent: "+ev.getAction());

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }

現在我們運行代碼,卻發現還是連ViewPager都滑不動了。小夥伴們有沒有覺得很奇怪呢,在很多情況下,這種方法是可以解決滑動衝突的。爲什麼在SwipeRefreshLayout裏卻不行呢?現在我們有兩個思路:1.子view也就是ViewPager的dispatchTouchEvent裏返回了false。2.父view還是攔截了事件,也就是getParent().requestDisallowInterceptTouchEvent(true);的方法失效了。

所以我們在SwipeRefreshLayout和ViewPager的dispatchTouchEvent裏打印下日誌看看

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        Log.e("外部view-CustomSRL2", "onInterceptTouchEvent: "+ev.getAction());

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("內部View-CustomVPInner", "dispatchTouchEvent: Down");
                startX = ev.getX();
                startY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("內部View-CustomVPInner", "dispatchTouchEvent: Move");
                x = ev.getX();
                y = ev.getY();
                deltaX = Math.abs(x - startX);
                deltaY = Math.abs(y - startY);
                if (deltaX < deltaY) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }

        boolean a = super.dispatchTouchEvent(ev);
        Log.e("內部View-CustomVPInner", "dispatchTouchEvent: a = "+a);
        return a;
    }
2020-04-14 19:53:38.105 31242-31242/com.enjoy.srl_vp E/外部view-CustomSRL2: onInterceptTouchEvent: 0
2020-04-14 19:53:38.106 31242-31242/com.enjoy.srl_vp E/內部View-CustomVPInner: dispatchTouchEvent: Down
2020-04-14 19:53:38.107 31242-31242/com.enjoy.srl_vp E/內部View-CustomVPInner: dispatchTouchEvent: a = true
2020-04-14 19:53:38.227 31242-31242/com.enjoy.srl_vp E/外部view-CustomSRL2: onInterceptTouchEvent: 2
2020-04-14 19:53:38.227 31242-31242/com.enjoy.srl_vp E/內部View-CustomVPInner: dispatchTouchEvent: a = true

我們左滑,發現子view也就是ViewPager的dispatchTouchEvent裏是返回true的,也就是接收到了事件,但是並沒有走到ACTION_MOVE事件,外部SwipeRefreshLayout的onInterceptTouchEvent方法裏打印出了兩次,綜上日誌,我們分析,確實是getParent().requestDisallowInterceptTouchEvent(true);方法失效了,父view還是攔截了事件,所以我們進入requestDisallowInterceptTouchEvent方法看看,因爲我們是在重寫SwipeRefreshLayout所以這裏的getParent就是SwipeRefreshLayout,

  public void requestDisallowInterceptTouchEvent(boolean b) {
        if ((VERSION.SDK_INT >= 21 || !(this.mTarget instanceof AbsListView)) && (this.mTarget == null || ViewCompat.isNestedScrollingEnabled(this.mTarget))) {
            super.requestDisallowInterceptTouchEvent(b);
        }

    }

發現SwipeRefreshLayout裏確實重寫了requestDisallowInterceptTouchEvent方法,並且加了判斷,requestDisallowInterceptTouchEvent方法失效,也就是沒有調用到super.requestDisallowInterceptTouchEvent(b);SwipeRefreshLayout繼承自ViewGroup,也就是沒有調用到ViewGroup的requestDisallowInterceptTouchEvent,所以應該是前面的if判斷沒通過,我們運行的虛擬機是大於21的,這個VERSION.SDK_INT >= 21是滿足的,因爲SwipeRefreshLayout裏包含了一個ViewPager ,所以SwipeRefreshLayout裏有子view,也就是this.mTarget是不等於null的,

 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

根據之前我們的日誌,viewpager是響應了事件返回true的所以進入if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {這個判斷,裏面的這句 newTouchTarget = addTouchTarget(child, idBitsToAssign);就是給mTarget賦值的,所以mTarget是不爲null的,所以現在我們只有看看能不能改變ViewCompat.isNestedScrollingEnabled(this.mTarget)的值,讓他返回true,這樣就會走super.requestDisallowInterceptTouchEvent(b)了(現在ViewCompat.isNestedScrollingEnabled(this.mTarget)是返回false的),所以我們進入ViewCompat.isNestedScrollingEnabled(this.mTarget)這個方法看看

   public static void setNestedScrollingEnabled(@NonNull View view, boolean enabled) {
        if (VERSION.SDK_INT >= 21) {
            view.setNestedScrollingEnabled(enabled);
        } else if (view instanceof NestedScrollingChild) {
            ((NestedScrollingChild)view).setNestedScrollingEnabled(enabled);
        }

    }

我們發現是一個static方法,所以我們是可以通過ViewCompat類直接調用到的,所以我們在getParent().requestDisallowInterceptTouchEvent(true);前面調用 ViewCompat.setNestedScrollingEnabled(this,true);跑一下程序看看,結果我們發現是可以的,所以內部攔截法我們要在ViewPager的dispatchTouchEvent方法裏這樣寫

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("wdy", "dispatchTouchEvent: Down");
                startX = ev.getX();
                startY = ev.getY();
                ViewCompat.setNestedScrollingEnabled(this,true);
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("wdy", "dispatchTouchEvent: Move");
                x = ev.getX();
                y = ev.getY();
                deltaX = Math.abs(x - startX);
                deltaY = Math.abs(y - startY);
                if (deltaX < deltaY) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }

        boolean a = super.dispatchTouchEvent(ev);
        Log.e("wdy", "dispatchTouchEvent: a = "+a);

        return a;
    }

現在我們就實現了外部攔截和內部攔截兩種方法解決了SwipeRefreshLayout和ViewPager滑動衝突了。

最後介紹一種方法:就是直接通過反射的方式直接更改ViewGroup裏的requestDisallowInterceptTouchEvent方法裏的mGroupFlags值,


    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

看過viewgroup的源碼的肯定都知道requestDisallowInterceptTouchEvent其實實際上就是通過改變mGroupFlags標誌位來決定是否攔截事件的。所以我們可以重寫requestDisallowInterceptTouchEvent,並且通過反射去改變mGroupFlags的值,使在viewgroup事件分發攔截的時候,mGroupFlags標記位滿足我們的條件,反射應該屬於基礎知識了,我也不多說,直接給出代碼,主要是位操作

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        Class clazz = ViewGroup.class;
        // FLAG_DISALLOW_INTERCEPT = 0x80000;
        //     1000 0000 0000 0000 0000       0x80000
        //10 1100 0100 0000  0101  0011     2900051
        //10 0010 0100 0100  0101  0011     2245715


        try {
            Field mGroupFlagsField =  clazz.getDeclaredField("mGroupFlags");
            mGroupFlagsField.setAccessible(true);
            int c = (int) mGroupFlagsField.get(this);
            Log.e("wdy", "dispatchTouchEvent: c " + c);
            if (b) {
                //2900051&FLAG_DISALLOW_INTERCEPT =true
                mGroupFlagsField.set(this, 2900051);
            } else {
                 //2245715&FLAG_DISALLOW_INTERCEPT =fasle
                mGroupFlagsField.set(this, 2245715);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

//        super.requestDisallowInterceptTouchEvent(b);
    }
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
       if (!disallowIntercept) {
              intercepted = onInterceptTouchEvent(ev);
              ev.setAction(action); // restore action in case it was changed
        } else {
             intercepted = false;
        }

到此,我們一共給出了三種解決SwipeRefreshLayout和ViewPager滑動衝突的方法,當然最方便的肯定是外部攔截法,我們在這裏講另外兩種方法,其實就是給大家提供另外兩種思路,並且帶大家一步步去解決了滑動衝突的問題,相信大家認真看這篇文章肯定收穫滿滿,以後再遇到滑動衝突,肯定毫無畏懼了,說白了就是內部攔截和外部攔截兩種方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章