《Android藝術開發探索》學習筆記之View的事件體系(滑動衝突)

鋸齒流沙發表於2017-12-26

常見的滑動衝突場景

1)外部滑動方向和內部滑動方向不一致:主要是將ViewPager和Fragment配合使用所組成的頁面滑動效果,主流應用幾乎都是用這個效果。在這效果中,可以通過左右滑動來切換頁面,而每個頁面內部往往又是一個ListView。本來這種情況下是有滑動衝突的,但是ViewPager內部處理了這種滑動衝突,因此採用ViewPager時我們不需要關注這個問題,如果我們採用的不是ViewPager,而是採用ScrollView等,那就必須手動處理滑動衝突了。否則造成的後果就是內外兩層只有一層能夠滑動,這是因為兩者之間的滑動事件有衝突。除了這種典型的還有其他情況存在,比如外部上下滑動、內部左右滑動等,都屬於這類滑動衝突。

2)外部滑動方向和內部滑動方向一致:當內外兩層都在同一個方向上可以滑動的時候,就會存在邏輯問題。因為當手指開始滑動的時候,系統無法知道使用者到底是想那一層滑動,所以手指滑動的時候就會出現問題,要麼只有一層可以滑動,要麼就是內外兩層都滑動的很卡頓。在實際開發中,這種場景主要是指同時能夠上下滑動或者內外兩層同時能左右滑動。

3)上面兩種情況的巢狀:具體的說就是,外部有一個SlideMenu效果,然後內部有一個ViewPager,ViewPager的每一個頁面中又是一個ListView。雖然這種場景比較複雜,但是它是幾個單一的滑動衝突的疊加,因此需要分別處理內層和中層,中層和外層之間的滑動衝突即可,而具體的處理方法和場景1、場景2相同的。

滑動衝突的處理規則

場景1處理規則:當使用者左右滑動時,需要讓外部的View攔截點選事件,當使用者上下滑動時,需要讓內部View攔截點選事件。這時候我們就可以根據它們的特徵來解決滑動衝突,具體來說是:根據滑動是水平滑動還是豎直滑動來判斷到底由誰攔截事件。例如,根據滑動過程中兩點之間的座標就可以得出到底是水平滑動還是豎直滑動。如何根據座標得到滑動方向?比較簡單,很多方法參考,比如根據滑動路徑和水平方向所形成的夾角,也可以根據水平方向和垂直方向的距離差來判斷,某些特殊時候還可以依據水平和垂直方向的速度差來判斷。這裡我們採用水平和垂直方向的距離差來判斷,比如豎直方向滑動的距離大就判斷為豎直滑動,否則判斷為水平滑動,根據這個規則進行下一步的解決方法制定。

場景2處理:場景2比較特殊,它無法根據滑動的角度、距離差以及速度差來做判斷,但是這個時候一般都能在業務上找到突破點,比如業務上有規定:當處於某種狀態時需要外部View響應使用者的滑動,而處於另外一種狀態時則需要內部View來響應View的滑動,根據業務上的需求,我們也能得出響應的處理規則,有了處理規則同樣可以進行下一步處理。

場景3處理:場景3的滑動規則更加複雜,和場景2一樣,只能從業務上找到突破點,從業務需求得出相應的處理規則。

滑動衝突的解決方法

1)外部攔截法:是指點選事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突問題,這種方法比較適合點選事件的分發機制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截處理即可。 虛擬碼如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int)ev.getX();
        int y = (int)ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:{
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                if (父容器需要當前的點選事件){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP:{
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
複製程式碼

針對不同的滑動衝突,只需要修改父容器需要當前點選事件這個條件即可,其他均不需要做修改並且也不能修改。在onInterceptTouchEvent方法中,ACTION_DOWN這個事件,不容器必須返回false,即不能攔截ACTION_DOWN事件,因為一旦父容器攔截了ACTION_DOWN,那麼後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞給子元素;其次是ACTION_MOVE事件需要來決定是否攔截,如果父容器需要攔截就返回true,否則返回false。最後ACTION_UP事件,這裡必須返回false,因為ACTION_UP事件本身沒有太大意義,假設父容器在ACTION_UP時返回true,就會導致子元素無法接收到ACTION_UP事件,這個時候子元素的onClick事件就無法觸發,但是父容器比較特殊,一旦它開始攔截任何一個事件,那麼後續的事件都會交給它處理,而ACTION_UP作為最後一個事件,也必定傳遞給父容器,即使父容器的onInterceptTouchEvent方法ACTION_UP時返回false。

2)內部攔截法:是指父容器不攔截任何事件,所有事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器進行處理,這種方法和Android中的事件分發機制不一樣,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來較外部攔截法稍微顯複雜。 需要重寫子元素的dispatchTouchEvent方法。 虛擬碼:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int)ev.getX();
        int y = (int)ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:{
                parent.requestDisallowInterceptTouchEvent(true);//不允許攔截
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此類點選事件){
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP:{
                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
複製程式碼

這就是內部攔截法的典型程式碼,當面對不同的滑動策略時只需要修改裡面的條件即可,其他不需要做改動而且也不能有改動。除了子元素需要處理以外,父元素也要預設攔截除了ACTION_DOWN以外的其他事件,這樣當子元素呼叫parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需要的事件。

為什麼父容器不能攔截ACTION_DOWN事件呢?因為ACTION_DOWN事件並不受FLAG_DISALLOW_INTERCEPT這個標記位的控制,所以一旦父容器攔截了ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去,這樣內部攔截就無法起作用。 父容器所做的修改如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN){
            return false;
        }else{
            return true;
        }
    }
複製程式碼

相關文章