開發(fā)筆記之打造通用下拉刷新(重難點篇)

開發(fā)筆記之打造通用下拉刷新(介紹篇)
開發(fā)筆記之打造通用下拉刷新(細節(jié)篇)
開發(fā)筆記之打造通用下拉刷新(重難點篇)

本篇不講總體實現(xiàn)思路和每一個細節(jié),只講一下實現(xiàn)過程中遇到的幾個關鍵問題,雖然叫重難點,但也并不是什么高級復雜的技術,只是一些實現(xiàn)過程中遇到的問題并且通過不斷思考和嘗試之后才得到的結果。

一、在哪里實現(xiàn)事件處理邏輯

我們知道,安卓給我們提供了三個事件處理的方法:onTouchEvent、onInterceptTouchEvent、dispatchTouchEvent。一般我們自定義控件的時候,事件的攔截有兩種思路(來自《android 開發(fā)藝術探索》):

  • 外部攔截法,點擊事件先經過父容器判斷,如果父容器需要此事件就攔截,否不攔截,這種方法比較符合點擊事件的分發(fā)機制。此方法需要重寫父容器的onInterceptTouchEvent方法,攔截事件后在onTouchEvent方法中寫控件邏輯.
  • 內部攔截法,父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就消耗掉,否則就交給父容器去處理,這種方法和android中的事件分發(fā)機制不一致,需要配合requestDisallwInterceptTouchEvent方法才能正常工作,需要子元素重寫dispatchTouchEvent方法。

現(xiàn)在來考慮ptrLayout的實現(xiàn),內部攔截法對子元素有特定的要求,需要上下配合,不符合我們的要求,因為ptrLayout不知道也不應該知道子view的具體實現(xiàn)。再看外部攔截法,在onInterceptTouchEvent方法中攔截事件,同樣不符合ptrLayout的要求,原因如下:

  1. 我們在acton_down的時候是無法判斷事件是否要攔截或消費的,需要通過action_move計算方向才能決定,但是如果沒有view消費down,那就不會有后續(xù)的move,那就無從談攔截的事了。
  2. 父容器一旦攔截了某個move事件,從此以后子view再也無法收到任何事件了,無法實現(xiàn)我在細節(jié)篇提到的下拉與子view滑動的銜接

那么在ptrLayout是如何控制事件呢?——所有的邏輯都在dispatchTouchEvent中實現(xiàn)。ptrLayout重寫了dispatchTouchEvent方法,在這個方法中,如果希望子元素接收到事件,則調用 super.dispatchTouchEvent(),否則不調用。這樣一來就可以靈活地控制事件的傳遞。另外,為了防止沒有view消費down事件從而無法接收后續(xù)的move事件,任何時候都要在dispatchTouchEvent方法的down事件中返回true。

二、子view按下效果的取消

在上一節(jié)提到過,手指按下的時候無法判斷是否要攔截事件,所以action_down是一定會傳到子view,如果子view是listview或者button等控件,就會出現(xiàn)被按下的效果(比如顏色加深、5.0的ripple波紋),如果隨后的move事件被ptrLayout攔截了,那么子view的按下效果是不會消失的,會一直顯示著被按住狀態(tài)。如圖

不處理按下效果.gif

那么為什么我們平時用onInterceptTouchEvent攔截事件的時候不會有這樣的問題呢?原因就是如果父容器攔截了事件,那么其子view就會收到一個action_cancel事件,從而讓子view知道事件被攔截了,取消當前的效果。既然知道了原因,那么解決辦法自然就出來了,那就是在我們攔截的時候,自己手動向下發(fā)一個acion_cancel事件,主要的代碼是這樣的:

public boolean dispatchTouchEvent(MotionEvent ev)
{
    switch (ev.getAction())
    {
        ...
        case ACTION_MOVE:
            if(需要攔截并通知下層取消事件)
            {
               //構造一個cancel事件,其他參數(shù)與當前事件一致
                MotionEvent cancelEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, ev.getX(), ev.getY(), ev.getMetaState());
                super.dispatchTouchEvent(cancelEvent);
             }
    }
    ...
}

這樣一來,子view的問題就可以解決了。當然,我們應該控制cancel事件只向下發(fā)一次,就是剛開始攔截的那一次,之后就不用再發(fā)了,以免無謂的方法調用浪費運算資源。

三、下拉與子view滑動的銜接

這個點已經提到過兩次了(具體效果看細節(jié)篇),前面的dispatchTouchEvent的重寫就是為了實現(xiàn)這一特性。首先我們應該要清楚,ptrLayout什么時候該攔截事件,什么時候該傳遞事件,然后用第一節(jié)說的方法去攔截和傳遞就可以了。關于判斷攔截條件這一點,涉及到的條件判斷太多了,這里不一一細說,有興趣可以看具體的代碼,里面有各種注釋,代碼比文字更容易明白。這里只說一種情況,又是需要手動發(fā)事件才能解決的。文字描述比較乏力,看圖

開始.png
滑動.png
隱藏.png

子view在Y1處收到down事件到在Y2處收到move事件。從上一節(jié)我們知道,子view在收到down事件后會馬上收到一個cancel事件,然后再收到move事件,也就是等于子view直接收到move事件的,這時子view可能有兩種處理方法:

  1. 直接忽略這些Move事件,因為沒有收到down事件,無法判斷滑動的距離。這是大部分滑動控件的做法。忽略move事件,就意味著整個滑動沒有銜接起來。
  2. 處理move事件,按上一次的down事件的位置來計算滑動的距離,但是由于Y2到Y1是有一定的距離的,子view會在一瞬間滑動Y1-Y2的距離,雖然滑動銜接起來了,還是畫面沒有銜接起來。

這里解決辦法的思路和第二節(jié)一樣,既然缺一個action_down事件,那么我們就手動發(fā)一個就可以了,發(fā)的時機就是手指剛到達Y2的時候。代碼大概是這樣的:

public boolean dispatchTouchEvent(MotionEvent ev)
{
    switch (ev.getAction())
    {
        ...
        case ACTION_MOVE:
            if(不再攔截move事件)
            {
               if(還沒發(fā)送down事件)
               {
                   //構造一個down事件,其他參數(shù)與當前事件一致
                    MotionEvent downEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_Down, ev.getX(), ev.getY(), ev.getMetaState());
                    return super.dispatchTouchEvent(downEvent);
               }
               }else//已經發(fā)送過down事件了,直接向下傳遞move事件
               {
                    return super.dispatchTouchEvent(ev);
               } 
           }
    }
    ...
}

四、橫向滑動處理

當我們的子view存在橫向滑動的時候,比如viewPager,我們就需要考慮橫向滑動的處理了。首先,并不是任何時候子view都可以橫向滑動的,所以設置一個mHasHorizontalChild變量,當這個功能開啟的時候才會去考慮橫向的處理。
  我們怎么判斷滑動是橫向的還是縱向的呢?這是一個簡單的幾何數(shù)學題,無非就是計算滑動前后兩個點的連線的斜率,k = ΔY / ΔX, 我們可以簡單地認為當k大于1時為縱向,小于1時為橫向(當然這個1可以是其他值,可以根據(jù)橫縱向的靈敏度來設置)。但是,考慮到實現(xiàn)的使用體驗,當我們手指開始橫向滑動時就不太可能想要下拉刷新了,意思是,當手指橫向滑動了一段距離,手指突然向下移動,也不應該去觸發(fā)下拉的邏輯,不然子view向右滑動到一半然后下拉,頭部顯示出來就會變得十分奇怪,反之亦然。

橫向處理.gif

這里只是一個細節(jié)問題,實現(xiàn)的方法很簡單,開始滑動時判斷當前的滑動方向,就記住當前的判斷結果,以后就按照這個結果來處理事件,直到下一次的滑動,這里就不再多講。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容