Android開發View滑動衝突處理

最近在重構一個老項目,遇到ScrollView嵌套WebView的場景,因爲WebView加載的網頁並不是自適應,所以導致在滑動網頁的時候異常卡頓,很明顯是滑動衝突了,解決方式也很常規,針對滑動衝突這裏順便做筆記吧。

衝突場景

滑動衝突場景可簡單分爲兩種:

  • 外部和內部的滑動方向不一致
  • 外部和內部的滑動方向一致

示圖如下:
滑動衝突場景圖示
其他情況的滑動衝突都是在這兩種衝突的基礎上延伸出來的,或者以上兩種場景的嵌套,面對這種我們處理方式就是剝離成以上基礎的衝突場景,逐個處理。

處理規則

面對場景1,它的處理規則是:當用戶左右滑動時(父),需要讓外部的View攔截點擊事件;當用戶上下滑動時(子),需要讓內部View攔截點擊事件。其實說白了,我們的主要目標是確定目標view的滑動方向。

確定滑動方向,列舉以下三種方式參考:

  1. 根據垂直滑動和水平滑動的距離對比判斷,哪個大就是哪個方向滑動。
  2. 根據滑動路徑與水平方向的夾角,大於45度認爲是垂直滑動,否則是水平滑動。
  3. 根據垂直滑動和水平滑動的速度對比判斷,哪個大就是哪個方向滑動。

面對場景2,因爲滑動方向是一致的,所以我們只有根據業務來區分到底應該滑動哪一個view。核心點就是準確判斷滑動區域的位置。

解決方式

從上邊可以知道衝突的產生主要是不確定具體應該在哪一層滑動(內層、外層),上邊也說了相關的處理規則,接下就是我們按照規則主動把事件分發給相應的view。之前我寫過一篇《Android開發之onTouch事件的分發攔截消費機制探究學習》,這裏用到的知識點就是事件的攔截機制。

針對滑動衝突,這裏給出兩種解決滑動衝突的方式:外部攔截法和內部攔截法。

外部攔截法

外部攔截法,就是處理父View的滑動事件時,父View主動把事件攔截下來自行消化,其他情況繼續不攔截由子View消化處理。
外部攔截法

這裏需要注意只在父View的ACTION_MOVE事件中判斷是否進行攔截,其他事件不需要,因爲一旦攔截事件就無法傳遞到子View了。

內部攔截法

內部攔截法,默認子View處理滑動事件,當判斷某個位置或者某種情況下應該父View滑動時,主動告知父View開啓事件攔截,由父View消化處理。這裏用到一個parent.requestDisallowInterceptTouchEvent()方法。
內部攔截法

ViewPager處理滑動衝突分析

有同學可能會問,既然內外層都可以滑動,這樣很容易出現衝突,但是爲什麼Viewpager中不存在滑動衝突呢。其實官方已經爲我們處理了。

ViewPager也是一個ViewGroup,在ViewPager的initViewPager方法中生成Scroller對象,Scroller是Android內置的專門用於漸進式滑動的類,配合插值器可以產生立體的滑動感,既然ViewPager是一個容器並且可以滑動,那麼也就避免不了內嵌view滑動衝突這一遭。

ViewPager只關注水平方向的手指滑動,根據水平方向的手指滑動來切換頁面。在垂直方向上,ViewPager並不關心,因此,ViewPager很有必要解決一下滑動衝突,把豎直方向的滑動傳遞給子View來處理。

我們知道,ViewGroup是在onInterceptTouchEvent函數中決定是否攔截觸摸事件, 所以我們直接去查看ViewPager的onInterceptTouchEvent事件攔截,來分析ViewPager的滑動衝突處理方式:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    //1. 觸摸動作
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    //2. 時刻要注意觸摸是否已經結束
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        //3. Release the drag.
        if (DEBUG) Log.v(TAG, "Intercept done!");
        //4. 重置一些跟判斷是否攔截觸摸相關變量
        resetTouch();
        //5. 觸摸結束,無需攔截
        return false;
    }

    //6. 如果當前不是按下事件,我們就判斷一下,是否是在拖拽切換頁面
    if (action != MotionEvent.ACTION_DOWN) {
        //7. 如果當前是正在拽切換頁面,直接攔截掉事件,後面無需再做攔截判斷
        if (mIsBeingDragged) {
            if (DEBUG) Log.v(TAG, "Intercept returning true!");
            return true;
        }
        //8. 如果標記爲不允許拖拽切換頁面,我們就"放過"一切觸摸事件
        if (mIsUnableToDrag) {
            if (DEBUG) Log.v(TAG, "Intercept returning false!");
            return false;
        }
    }
    //9. 根據不同的動作進行處理
    switch (action) {
        //10. 如果是手指移動操作
        case MotionEvent.ACTION_MOVE: {

            //11. 代碼能執行到這裏,就說明mIsBeingDragged==false,否則的話,在第7個註釋處就已經執行結束了

            //12.使用觸摸點Id,主要是爲了處理多點觸摸
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                //13.如果當前的觸摸點id不是一個有效的Id,無需再做處理
                break;
            }
            //14.根據觸摸點的id來區分不同的手指,我們只需關注一個手指就好
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            //15.根據這個手指的序號,來獲取這個手指對應的x座標
            final float x = MotionEventCompat.getX(ev, pointerIndex);
            //16.在x軸方向上移動的距離
            final float dx = x - mLastMotionX;
            //17.x軸方向的移動距離絕對值
            final float xDiff = Math.abs(dx);
            //18.同理,參照16、17條註釋
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = Math.abs(y - mInitialMotionY);
            if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

            //19.判斷當前顯示的頁面是否可以滑動,如果可以滑動,則將該事件丟給當前顯示的頁面處理
            //isGutterDrag是判斷是否在兩個頁面之間的縫隙內移動
            //canScroll是判斷頁面是否可以滑動
            if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                    canScroll(this, false, (int) dx, (int) x, (int) y)) {
                mLastMotionX = x;
                mLastMotionY = y;
                //20.標記ViewPager不去攔截事件
                mIsUnableToDrag = true;
                return false;
            }
            //21.如果x移動距離大於最小距離,並且斜率小於0.5,表示在水平方向上的拖動
            if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                if (DEBUG) Log.v(TAG, "Starting drag!");
                //22.水平方向的移動,需要ViewPager去攔截
                mIsBeingDragged = true;
                //23.如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //24.設置滾動狀態
                setScrollState(SCROLL_STATE_DRAGGING);
                //25.保存當前位置
                mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                        mInitialMotionX - mTouchSlop;
                mLastMotionY = y;
                //26.啓用緩存
                setScrollingCacheEnabled(true);
            } else if (yDiff > mTouchSlop) {//27.否則的話,表示是豎直方向上的移動
                if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                //28.豎直方向上的移動則不去攔截觸摸事件
                mIsUnableToDrag = true;
            }
            if (mIsBeingDragged) {
                // 29.跟隨手指一起滑動
                if (performDrag(x)) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
            break;
        }
        //30.如果手指是按下操作
        case MotionEvent.ACTION_DOWN: {

            //31.記錄按下的點位置
            mLastMotionX = mInitialMotionX = ev.getX();
            mLastMotionY = mInitialMotionY = ev.getY();
            //32.第一個ACTION_DOWN事件對應的手指序號爲0
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            //33.重置允許拖拽切換頁面
            mIsUnableToDrag = false;
            //34.標記開始滾動
            mIsScrollStarted = true;
            //35.手動調用計算滑動的偏移量
            mScroller.computeScrollOffset();
            //36.如果當前滾動狀態爲正在將頁面放置到最終位置,
            //且當前位置距離最終位置足夠遠
            if (mScrollState == SCROLL_STATE_SETTLING &&
                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                //37. 如果此時用戶手指按下,則立馬暫停滑動
                mScroller.abortAnimation();
                mPopulatePending = false;
                populate();
                mIsBeingDragged = true;
                //38.如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //39.設置當前狀態爲正在拖拽
                setScrollState(SCROLL_STATE_DRAGGING);
            } else {
                //40.結束滾動
                completeScroll(false);
                mIsBeingDragged = false;
            }

            if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                    + " mIsBeingDragged=" + mIsBeingDragged
                    + "mIsUnableToDrag=" + mIsUnableToDrag);
            break;
        }

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

    //41.添加速度追蹤
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);


    //42.只有在當前是拖拽切換頁面時我們纔會去攔截事件
    return mIsBeingDragged;
}

從源碼上面看出,斜率小於0.5時,則要攔截,否則不攔截。越靠近y軸的直線,斜率越大,越靠近x軸直線斜率越小。因此,當手指滑動的傾斜度比0.5小時就去攔截事件,由ViewPager來響應切換頁面。
斜率圖示

參考

  • 《Android開發藝術探索》,微信讀書可以免費閱讀。
  • https://blog.csdn.net/huachao1001/article/details/51654692
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章