關於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。這樣就清楚了,看代碼:

在PullToRefreshView中:

    @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;
                }

橫向滑動距離大於縱向時,無須攔截這次滑動事件。其實,就是這麼簡單,但前提是你必須明確瞭解Android Touch事件的傳遞機制,期間各個方法執行的順序及意義。

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

有時候,我們不想去修改引入的第三方控件,或者說是無法修改時。就必須考慮從當前從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事件(即滑動行爲)。


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

發佈了47 篇原創文章 · 獲贊 23 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章