Android滑動衝突解決方法(一)

敘述

滑動衝突可以說是日常開發中比較常見的一類問題,也是比較讓人頭疼的一類問題,尤其是在使用第三方框架的時候,兩個原本完美的控件,組合在一起之後,忽然發現整個世界都不好了。

關於滑動衝突

滑動衝突分類###

滑動衝突,總的來說就是兩類。

  1. 同方向滑動衝突
    比如ScrollView嵌套ListView,或者是ScrollView嵌套自己

  2. 不同方向滑動衝突
    比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,這種情況其實很典型。現在大部分應用最外層都是ViewPager+Fragment 的底部切換(比如微信)結構,這種時候,就很容易出現滑動衝突。不過ViewPager裏面無論是嵌套ListView還是ScrollView,滑動衝突是沒有的,畢竟是官方的東西,可能已經考慮到了這些,所以比較完善。

複雜一點的滑動衝突,基本上就是這兩個衝突結合的結果。

滑動衝突解決思路###

滑動衝突,就其本質來說,兩個不同方向(或者是同方向)的View,其中有一個是占主導地位的,每次總是搶着去處理外界的滑動行爲,這樣就導致一種很彆扭的用戶體驗,明明只是橫向的滑動了一下,縱向的列表卻在垂直方向發生了動作。就是說,這個占主導地位的View,每一次都身不由己的攔截了這個滑動的動作,因此,要解決滑動衝突,就是得明確告訴這個占主導地位的View,什麼時候你該攔截,什麼時候你不應該攔截,應該由下一層的View去處理這個滑動動作。

這裏不明白的同學,可以去了解一下Android Touch事件的分發機制,這也是解決滑動衝突的核心知識。

第二種滑動衝突,解決起來是比較簡單的。這裏就結合例子說一下。

滑動衝突

這裏,說一下背景情況。之前做下拉刷新、上拉加載更多時一直使用的是PullToRefreshView這個控件,因爲很方便,不用導入三方工程。在其內部可以放置ListView,GridView及ScrollView,非常方便,用起來可謂是屢試不爽。但是直到有一天,因項目需要,在ListView頂部加了一個輪播圖控件BannerView(這個可以參考之前寫的一篇學習筆記)。結果發現輪播圖滑動的時候,和縱向的下拉刷新組件衝突了。

如之前所說,解決滑動衝突的關鍵,就是明確告知接收到Touch的View,是否需要攔截此次事件。

解決方法

解決方案1,從外部攔截機制考慮###

這裏,相當於是PullToRefreshView嵌套了ViewPager,那麼每次優先接收到Touch事件的必然是PullToRefreshView。因爲正常情況下,父控件會優先接收到touch事件。這樣就清楚了,看代碼:

在PullToRefreshView的onInterceptTouchEvent方法中:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        int y = (int) e.getRawY();
        int x = (int) e.getRawX();
        boolean resume = false;
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 發生down事件時,記錄y座標
                mLastMotionY = y;
                mLastMotionX = x;
                resume = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // deltaY > 0 是向下運動,< 0是向上運動
                int deltaY = y - mLastMotionY;
                int deleaX = x - mLastMotionX;

                if (Math.abs(deleaX) > Math.abs(deltaY)) {
                    resume = false;
                } else {
                //當前正處於滑動
                    if (isRefreshViewScroll(deltaY)) {
                        resume = true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return resume;
    }

這裏最關鍵的代碼就是這行

if (Math.abs(deleaX) > Math.abs(deltaY)) {
                    resume = false;
                }

橫向滑動距離大於縱向時,無須攔截這次滑動事件,滑動事件會傳遞到下一層的view,也就是這裏的輪播圖控件,這樣橫向滑動輪播圖的時候,PullToRefreshView就不會有下拉的動作了。其實,就是這麼簡單,但前提是你必須明確瞭解Android Touch事件的傳遞機制,期間各個方法執行的順序及意義。

ps: 關於上文中提到的isRefreshViewScroll 方法代碼(這個方法其實是PullToRefreshView這個控件自帶的一個方法)

/** * 是否應該到了父View,即PullToRefreshView滑動 
* 
* @param deltaY , deltaY > 0 是向下運動,< 0是向上運動
 * @return
 */
private boolean isRefreshViewScroll(int deltaY) {
        if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
            return false;
        }
        // 對於ListView和GridView
        if (mAdapterView != null) {
            // 子view(ListView or GridView)滑動到最頂端
            if (deltaY > 0) {
                View child = mAdapterView.getChildAt(0);
                if (child == null) {
                    // 如果mAdapterView中沒有數據,不攔截
                    return false;
                }
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && child.getTop() == 0) {
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }
                int top = child.getTop();
                int padding = mAdapterView.getPaddingTop();
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && Math.abs(top - padding) <= 8) {// 這裏之前用3可以判斷,但現在不行,還沒找到原因
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }
            } else if (deltaY < 0) {
                View lastChild = mAdapterView.getChildAt(mAdapterView
                        .getChildCount() - 1);
                if (lastChild == null) {
                    // 如果mAdapterView中沒有數據,不攔截
                    return false;
                }
                // 最後一個子view的Bottom小於父View的高度說明mAdapterView的數據沒有填滿父view,
                // 等於父View的高度說明mAdapterView已經滑動到最後
                if (lastChild.getBottom() <= getHeight()
                        && mAdapterView.getLastVisiblePosition() == mAdapterView
                        .getCount() - 1) {
                    mPullState = PULL_UP_STATE;
                    return true;
                }
            }
        }
        // 對於ScrollView
        if (mScrollView != null) {
            // 子scroll view滑動到最頂端
            View child = mScrollView.getChildAt(0);
            if (deltaY > 0 && mScrollView.getScrollY() == 0) {
                mPullState = PULL_DOWN_STATE;
                return true;
            } else if (deltaY < 0
                    && child.getMeasuredHeight() <= getHeight()
                    + mScrollView.getScrollY()) {
                mPullState = PULL_UP_STATE;
                return true;
            }
        }
        return false;

解決方案2,從內容逆向思維分析###

有時候,我們不想去修改或者是無法修改最先接收到Touch事件的View 時,比如這裏我不想去修改PullToRefreshView的代碼。就必須考慮從當前從Touch傳遞事件中最後的那個View逆向考慮。首先,由Android中View的Touch事件傳遞機制,我們知道Touch事件,首先必然由最外層View接收到,並很有可能被它攔截,如果無法更改這個最外層View,那麼是不是就沒轍了呢?其實不然,Android這麼高大上的系統必然考慮到了這個問題,好了廢話不說,先看代碼

    private BannerView carouselView;
    private Context mContext;

    private PullToRefreshView refreshView;
    

    refreshView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                return false;
            }
        });

        carouselView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                int x = (int) event.getRawX();
                int y = (int) event.getRawY();

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        lastX = x;
                        lastY = y;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int deltaY = y - lastY;
                        int deltaX = x - lastX;
                        if (Math.abs(deltaX) < Math.abs(deltaY)) {
                            carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                        } else {
                            carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                        }
                    default:
                        break;
                }
                return false;
            }
        });

首先說一下這個方法

public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)

Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.

Parameters
disallowIntercept
True if the child does not want the parent to intercept touch events.

API裏的意思很明確,子View如果不希望其父View攔截Touch事件時,可調用此方法。當disallowIntercept這個參數爲true時,父View將不攔截。

PS:這個方法的命名和其參數的使用邏輯,讓我想到了一句很有意思的話,敵人的敵人就是朋友,真不知道Google的大神們是怎麼想的,非要搞一個反邏輯。

好了,言歸正傳。這裏攔截直接也很明確,在carouselView的onTouch方法中每次進入就設定父View不攔截此次事件,然後在MOTION_MOVE時候,根據滑動的距離判斷再決定是父View是否有權利攔截Touch事件(即滑動行爲)。

關鍵的處理邏輯就是這裏:

if (Math.abs(deltaX) < Math.abs(deltaY)) {
                            carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                        } else {
                            carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                        }

這個結合上面對這個方法的解釋,應該很好理解了,就不多做闡述了。


好了,這裏可以看到,解決這種滑動衝突的方法很簡單,最根本的還是得充分了解Touch事件的傳遞機制,只有這樣,才能明白該在哪裏做什麼事情。
當然,橫豎滑動的衝突很好理解,但同一方向的滑動衝突情況就有點複雜了,下次再說。

鏈接:https://www.jianshu.com/p/8bc0765dffc9
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章