ViewDragHelper的點擊事件處理

  • 2020 年 4 月 25 日
  • 筆記

  在上一篇ViewDragHelper的介紹後,已經完成了自定義控制項SwipeLayout的滑動,這一篇,我們來處理它的點擊事件。之前提到過,它有兩個子view,最開始顯示的是surfaceLayout,隱藏在右邊的是bottomLayout。當你給surfaceLayout設置點擊事件時,你會發現,surface確實可以點擊,但向左滑動卻什麼反應都沒有,還是在響應點擊事件。原因是什麼呢,通過對事件分發的認識,可以得出結論:由於給surfaceLayout設置了點擊事件,導致這個子view的變成可點擊的了,於是你手指的down事件從SwipeLayout向子view分發時,SwipeLayout並沒有攔截這個down,而surfaceLayout作為ViewGroup,默認也不攔截,但是surface已經沒有子view在給它分發了,它攔不攔截,這個down都得歸它處理,由於surface設置了點擊事件,所以它能消費了這個down事件。這麼一來,SwipeLayout 如果不在攔截方法里做些處理,後面紛至而來的move事件,都會傳遞surface,那麼SwipeLayout的onTouchEvent就怎麼都不會調用,肯定也就滑不起來了。那為什麼surface沒設置點擊事件時,swipeLayout可以滑動呢,這是因為surface不設置點擊事件,那它就不會消耗down事件的,那麼這個down事件會以冒泡的形式,再向swipeLayout傳遞,而swipeLayout的onTouchEvent始終返回true,所以swipe消耗了down,之後的move便會一直傳遞給它。

  照這樣看來,得down者,得move!但我們的點擊事件是必須要設置的,這樣就導致,surface一定會先於父view消耗這個down,好,down給surface沒關係,但move事件父View表示我要搶過來。要不然因為你的點擊導致我都滑不起來,我還有毛用。由於我們把SwipeLayout的攔截事件方法全部交給了viewDragHelper的shouldInterceptTouEvent ()這個方法,那麼看看它什麼情況下會攔截事件。

 

  一頓switch判斷後,最終會判斷mDragState是不是等於拖動。那好說,我們就看這個變數在哪裡被賦值。經過一番搜索,可以找到以下幾個調用關係:

  

——————————————————————————————————————————————————-

——————————————————————————————————————————————————————

  

————————————————————————————————————————————————–

從下往上看的話,你會發現在viewDragHelper的攔截方法中,當你一步步走完tryCaptureViewForDrag時,mDragState會被賦值等於STATE_DRAGGING。根據前面提過,子view是一定要消耗down的,接下來的move,從外往內傳遞,而且子view也沒有設置不允許父view攔截的標誌,那麼父view的攔截方法就會一直調用,也就是shouldInterceptTouchEvent會一直調用。上面action =move是一直能進來的,那tryCaptureViewForDrag為啥不走呢,原因很明了了:就是前面的pastSlop一直等於false嘛!從pastSlop 的賦值情況來看,它的結果跟checkTouchSlop有直接的關係:接著看圖:

  在上一篇部落格中,我們重寫了mCallback的幾個回調,但是並沒有重寫getViewHorizontalDragRange這個回調,看來有的回調不重寫就會留坑。默認這個方法返回0,所以checkTouSlop也就返回false,pastSlope返回false,後面的方法不執行,move動作始終不被swipeLayout攔截,也就滑不起來。那我重寫這個回調,讓它返回值大於0,是不是就行了呢,當然可以!當checkHorzontal為true,系統會判斷你手指從點擊到滑動的這段距離是否超過系統認為的最小距離,如果超過,系統認為你是在滑動,那麼後面發送是連鎖反應導致swipeLayout攔截下move事和up事件,並在processTouchEvent里處理,這個方法根據不同的action調用callback,例如move會回調水平移動,以及位置改變,up會回調onViewReleased。下面是我重寫的getViewHorizontalDragRange這個回調,

@Override
        public int getViewHorizontalDragRange(@NonNull View child) {
            return helper.getTouchSlop();
        }

 

helper.getTouSlop的值就是上圖中的mTouchSlop。這個值默認等於8,它還與動畫的執行時間有關係,最好別設得太大。到這裡,在運行下程式,surface的點擊以及swipeLayout的滑動都可以了,但你滑著會發現還有一些問題,當swipelayout處於關閉狀態,你向右滑會觸發surface的點擊事件,為什麼呢,難道我右滑時,swipeLayout沒有攔截成功這個move嗎?當你這麼猜想時,你可以在swipeLayout的攔截方法里打個日誌,記錄下

shouldInterceptTouchEvent的返回結果,你會發現,你的猜想就是對的!看來,有些回調你就算實現了,坑還是會有的。我們在分析下攔截里的move到了幹了什麼好事:

就上面的4個圈,給我們挖的坑,第一個圈裡面,由於我們重寫了上面的那個回調,現在它的返回值是true,沒問題,第二個圈在前一篇部落格里,記不記得我們在這個回調里設置了邊界,當開始處於關閉狀態,你向右滑,我們做了限制,讓surface返回還是0.所以newLeft=0,第3個圈是剛重寫的,它的值等於大於0,沒問題,出問題的是,我們的newLeft = oldLeft,oldLeft是你點擊view的左上角頂點的橫坐標,它就是等於0的,所以,在這裡直接break了。你看

圈裡面提前break,跳出循環,導致tryCaptureViewForDrag 也不會執行,這樣move、up事件還是會下發給surface的。這個問題怎麼解決呢?其實很簡單,你只要強制把返回結果改為true,後面的事交給processTouchEvent去搞定就完了。因為它就是負責將你的手勢轉為相應的回調去操作子view,攔截只是個不讓你進入美麗花園的巨人,童話故事就是這麼說的。這是修改後的攔截程式碼

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = helper.shouldInterceptTouchEvent(ev);
        float curX = ev.getX();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = curX;
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(curX - startX) > helper.getTouchSlop()){
                    result = true;
                }
                break;
        }

        return result;
    }

 

第一行程式碼可以保證你把系統默認的攔截都走一遍,後面的就是判斷用戶滑動的距離超過了系統認定的最小滑動距離,我們就攔截下來。就這麼簡單的處理,你在向右滑,就還是被swipeLayout攔截,子view的點擊事件就不會生效,你單純去點擊,當然也不會進入我們的判斷,swipeLayout就正常的分發。至此,點擊事件的坑就踩到這裡了。看到這裡,你會發現,廢話那麼多,才加了10行左右的程式碼,看別人的部落格的話,別人直接告訴你,重寫某某回調,點擊事件就有了,那當然很省事,但這個探索的過程,我認為是很值得的,因為我經常閑的蛋疼。