Android開發藝術(2)——View的事件體系

水瓶阿遠發表於2017-11-21

View的基礎

View的位置引數

表示位置的幾種引數

  • left/right/top/bottom表示view(view最初的狀態)距離ViewGroup的左上右下的距離
  • x/y 表示view(view的當前狀態,平移後狀態會變)的左上角的座標,也是相對於ViewGroup的
  • translationX/translationY,表示view(view的當前狀態,平移後狀態會變)相對於ViewGroup的偏移量
width = right - left;
x = left + translationX;複製程式碼

注意:View的平移其實只是改變了x/y和translationX/translationY,left/right/top/bottom還是不變。

x改變,如果left不變,translationX也會改變

MotionEvent和TouchSlop

  • MotionEvent

    手指觸控(Down、Move、Up等)之後,會返回一個MotionEvent物件,通過它,可以拿到x、y(表示當前手指位置相對於被作用的控制元件——View或者Activity的座標),rawX、rawY(表示當前手指位置相對於螢幕座標)

  • TouchSlop

    這是一個常量,根據裝置不同而不同,系統推薦我們利用它作為一個臨界點,如果手指滑動小於這個值,就認為沒有滑動,這樣可以提升使用者體驗,當然不用他來處理也無所謂(這一點可以在demo中體現,即使兩次返回的MotionEvent的座標差小於TouchSlop也是可以的)

## VelocityTracker、GestureDetector和Scroller
### VelocityTracker
> 可以稱其為速度追蹤器,就是獲取手指滑動的速度的。當然在手機上,指的是一定時間內,手指劃過的畫素
#### 使用:
1. 首先需要在onTouchEvent中呼叫它,這樣才能採集到手指在螢幕上的座標變化資訊
2. 獲取速度前需要先計算,傳入時間,表示需要計算的時間長度
3. 獲取2傳入的時長內滑過的畫素,即使速度,不過這個速度的定義跟以往不同,只是畫素長度而已
4. 使用完後要釋放
#### 程式碼:
java
//初始化
    VelocityTracker velocityTracker = VelocityTracker.obtain();
//新增MotionEvent(在onTouchEvent中)
    velocityTracker.addMovement(event);
//計算速度 
    velocityTracker.computeCurrentVelocity(30);
//獲取速度
    velocityTracker.getXVelocity();
//釋放
    velocityTracker.clear();
    velocityTracker.recycle();複製程式碼

GestureDetector

手勢監控,使用方式十分簡單

//1.建立GestureDetector,注意只能在Looper Thread中使用,看原始碼可知,內部使用了handler
gestureDetector = new GestureDetector(Context,GestureDetector.OnGestureListener);
gestureDetector.setIsLongpressEnabled(false);//是否開啟長按,如果開了,可以捕獲到長按事件,但是長按後不可以捕獲到滑動事件
//2.在view的onThouchEvent中讓它接管事件,然後根據他的返回值決定是否消費(return true)
boolean resume = gestureDetector.onTouchEvent(event);
return resume;
//3.在OnGestureListener介面的各種方法就會被呼叫複製程式碼

這個類只是輔助類,幫助我們更容易的捕獲到手勢(雙擊、長按等,當然也可以自己寫,也就是各種onTouchEvent中的處理)

Scroller

注意,Scroller實質是不斷的呼叫scrollTo方法,所以就要了解scrollTo方法

scrollTo移動的是View的內容,所以,Scroller會給View的內容增加滾動效果

使用

//1.建立Scroller
scroller = new Scroller(context);
//2.重寫View(需要滾動的東西所在的View,因為scrollTo滾動的是View的內容)的computeScroll方法 
public void computeScroll() {
    if (scroller.computeScrollOffset()) {//該次滾動是否執行完
    scrollTo(scroller.getCurrX(),scroller.getCurrY());
    //postInvalidate();//這裡書上寫錯了(後來有糾正),不需要呼叫他,因為scrollTo之後就會自動重繪了,原始碼中可以看出
    }
}
//3.呼叫Scroller的startScroller方法
scroller.startScroll(0,0,-10,-10,1000);
invalidate();//注意呼叫之後還需要呼叫這個方法,讓view重繪,具體原因之後Scroller原理的方法複製程式碼

View的滑動

View滑動的三種方法:

  • 通過View的scrollBy、scrollTo(內容滾動)
  • 通過動畫
    • 補間動畫(只是改變的影像)
    • 屬性動畫(3.0以上才行,3.0以下可以使用相容庫實現,但是本質還是補間動畫。sh屬性動畫改變的就是屬性了——translationX)
  • LayoutParams(得看你改變的是什麼left、x、translationX都可以,效果不同)

ScrollTo/ScrollBy

scrollBy內部呼叫的是scrollTo,scrollTo其實就是修改mScrollX、mScrollY的值,然後呼叫invalidateParentCaches實現View的內容的位置改變

mScrollX:View的左邊緣距離內容的左邊緣的距離,內容邊緣在右邊,mScrollX為負,反之為正(正好相反)

三種對比

  • scrollTo/scrollBy:操作簡單,適合對View的內容滑動
  • 動畫:操作簡單,可實現複雜動畫效果
  • layoutparams:操作複雜,可以滿足各種需求

View的事件分發機制

基於2.3的原始碼(先用的7.0、4.0的原始碼,有點難懂,2.3簡單點了些)

原始碼走起

ViewGroup

public boolean dispatchTouchEvent(MotionEvent ev) {
    //過濾一些“錯誤”的事件,直接return false,
    if (!onFilterTouchEventForSecurity(ev)) {
            return false;
    }

      //FLAG_DISALLOW_INTERCEPT(一個標記,表示是否需要攔截事件,
      //這個是由childView來設定,通過它可以使chilidView擁有控制parentView
      //是否攔截事件的權利。7.0的原始碼中,這個值會在ACTION_DOWN的時候重置,2.3
      //的原始碼中暫未找到。這個標記一般在onTouchEvent的ACTION_DOWN之後的事件
      //中設定,所以即使重置也無影響)
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //針對down做一些處理
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
                // mMotionTarget是事件作用的View,down的時候,應該還沒有它,
                // 這裡不為空,所以是特殊情況,直接給他置空
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
        }
        //childView不讓parentView攔截  或者 自己的onInterceptTouchEvent返回false
        if(禁止攔截 或 !onInterceptTouchEvent(ev)){
            //不攔截,那就找合適的childView(位置之類的滿足條件的),把事件分發給他們
            for(遍歷childView){
                if(childView可接受事件——處於事件所在的座標等條件滿足){
                    //分發給childView(childView可以為viewgroup也可以為View)
                    //如果是viewGroup,就和現在分析的這套程式碼相同,否則稍後分析
                    if (child.dispatchTouchEvent(ev))  {
                        //這個孩子處理了事件了(可能是他自己處理的,也可能是他的孩子處理的)
                        // Event handled, we have a target now.
                        //給mMotionTarget賦值,表示事件將作用於它(這個它其實是與其他的子View)
                        //做區分的,因為事件可能最終被它的幾重孫子消費,但一定是這個孩子的子孫
                        //不會是它的兄弟們
                        mMotionTarget = child;
                        //既然處理了,作為父親,也return true,跟上行註釋類似,他也告訴
                        //他的父親,他處理了事件了(其實這裡是他的孩子處理的)
                        return true;
                    }
                    //這個孩子(或者孩子的孩子)沒處理,那就分發給下一個孩子,只要有一個
                    //後代處理了,上面就會return true,當前我們所分析的這個View的任務就完成了
                }
            }
            //所有的孩子、孫子都沒有處理,自己來處理
        } 
    }

    //下面這兩行程式碼&看不太懂,反正就是改變了mGroupFlags,而mGroupFlags在前面獲取disallowIntercept
    //的時候用到過,再結合下面那個Note,再結合7.0原始碼中ACTION_DOWN後會重置FLAG_DISALLOW_INTERCEPT,
    //所以這裡的意思大致應該是:如果UP活著CANCEL,就設定mGroupFlags,這將導致下次DOWN後
    //FLAG_DISALLOW_INTERCEPT變為“flase”,功能類似於7.0的重置
    //突然想起《STL原始碼剖析》一書的扉頁,侯捷先生只寫了一句話:
    //“原始碼之前,了無祕密”。
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        // Note, we have already copied the previous state to our local
        // variable, so this takes effect on the next event
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    //能走到這裡有以下幾種情況:
    //1.是DOWN且target==null(是DOWN且child不處理)
    //2.不是DOWN且target==null(自己處理)
    //3.不是DOWN,target!=null(DOWN已經由child處理,這個事件不是DOWN,孩子、自己都可以處理)

    //如果target==null,說明沒有childView要處理事件,交由自己處理
    //如果target!=null,說明這個不是ACTION_DOWN且DOWN已經被child處理,這是一個非DOWN
    final View target = mMotionTarget;
    if(target == null){//沒有childView處理
        //。。。
        //呼叫父類的dispatchTouchEvent(也就是這個ViewGroup當做普通View處理,自己處理事件)
        return super.dispatchTouchEvent(ev);
    }


    //能走到這裡,根據前面那三條,不是DOWN,而且target不為空,也就是DOWN已經被孩子處理
    //這裡的第一個引數,子View呼叫requestDisallowIntercept一般就是為了在這裡出效果
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {//要攔截了
    //這個if一般是這樣的:當子View上正在觸發非DOWN(比如MOVE)事件,然後把該View,也就是這個
    //子View的父親的disallowIntercept改變了,就走到這裡了,父親就開始攔截,然後就像下面那樣,
    //給孩子分發一個CANCEL的事件,然後把mMotionTarget置為空,並且return true,當下個事件來到
    //的時候,在上面那一步,target已經等於null了,這樣這個ViewGroup就會自己去處理事件了,然後就
    //走到onTouchEvent了,所以在這裡if裡面不用考慮onTouchEvent
        ev.setAction(MotionEvent.ACTION_CANCEL);
        if (!target.dispatchTouchEvent(ev)) {
            //這裡是空的
            //這裡主要作用是:我要攔截了,那麼我就給我的孩子分發一個事件,ACTION是ACTION_CANCEL
        }
        //攔截了,以後不會讓孩子去處理了,把mMotionTarget清空 
        mMotionTarget = null;
        return true;
    }

    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    //把事件分發給孩子(如果return false,就會呼叫它父親的這一行,一直往上,都是return false,
    //最後就到了Activity了,下一段程式碼分析)
    return target.dispatchTouchEvent(ev);    
}複製程式碼

前面說了,如果dispatchTouchEvent返回了false,會一直往上return,直到被Activity消費,上程式碼

Activity:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //window的superDispatchTouchEvent 如果return false,就往下走,呼叫了activity的onTouchEvent
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
PhoneWindow:
public boolean superDispatchTouchEvent(MotionEvent event) {
    //調了mDecor.superDispatchTouchEvent(event)
    //其實就是ViewGroup的dispatchTouchEvent,迴歸到上一個程式碼分析了
    return mDecor.superDispatchTouchEvent(event);
}
綜上,如果dispatchTouchEvent返回false,最終就會走到activity的onTouchEvent中複製程式碼

接下來分析View的dispatchTouchEvent

View

//看著很簡單
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }
    //有了onTouchListener,就執行onTouch,沒有,或者return false,才會走onTouchEvent
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    //其實onClick是在onTouchEvent中的,所以onTouch->onTouchEvent->onClick/onLongClick
    return onTouchEvent(event);
}複製程式碼

大家都知道事件分發機制搞清楚三個方法就ok了

  • dispatchTouchEvent

    • view 搞定
    • viewGroup 搞定
  • onInterceptTouchEvent

    • view 沒有
    • viewGroup 在dispatchTouchEvent原始碼中分析過了
  • onTouchEvent

    接下來繼續分析最後一個方法

onTouchEvent

//這個類的處理只存在於View中,用來處理事件(ViewGroup一般會把事件分發給孩子,讓孩子來處理,如果自己處理
//就通過super class來處理,也就是View,所以還是它)
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    //如果view是DISABLED,那麼這個View不可以響應事件,即不可以對事件作出迴應,但是他是會
    //消耗事件的:1.CLICKABLE 2.LONG_CLICKABLE,這裡直接根據這兩個條件return,不論true or false
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn not respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    //委託給另一個View去處理?沒用過
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //對事件處理,其實就是click和longclick(當然我們可以重寫這個方法,但是對於view,系統
    //只幫忙處理這兩個,scrollview等都是重寫的)
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                //手指抬起,各種校驗,最後決定呼叫點選、長按方法
                break;
            case MotionEvent.ACTION_DOWN:
                //按下
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                mPrivateFlags |= PREPRESSED;
                mHasPerformedLongPress = false;
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                break;
            case MotionEvent.ACTION_CANCEL:
                //事件取消,就會remveXXCallBack(),還會
                //refreshDrawableState();
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                //手指移出去(View的可點選範圍),就會remveXXCallBack(),還會
                //refreshDrawableState();
                //...
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                        (y < 0 - slop) || (y >= getHeight() + slop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        // Need to switch from pressed to not pressed
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }
    //不是DISABLE,沒有被委託的View處理,且不可點選(click 和 longclick),直接return false
    return false;
}複製程式碼

事件攔截

外部攔截法

在父容器的onInterceptTouchEvent中控制,一般return false,需要攔截的時候return true,一旦return true,子View就無法再接收到事件,所以在ACTION_DOWN中最好不要return true,而是在ACTION_MOVE中根據情況返回。在ACTION_UP中return false,因為它如果return true,子View就沒法接收UP事件,click就會失效

內部攔截法

子View中通過requestDisallowInterceptTouchEvent來控制父View是否攔截,這個方法呼叫之後,父View只是“是否允許攔截”,還要看父View的onInterceptTouchEvent,所以為了讓子View可以通過這個方法去控制父View的攔截,父View應該讓onInterceptTouchEvent攔截非DOWN的事件。為什麼不能攔截DOWN?因為攔截之後,事件就傳不到子View了,內部攔截法也就無意義了

綜上所述:內部攔截法稍微麻煩些(不過有時候不得不這麼做),需要修改父View和子View的程式碼,所以一般採用外部攔截法

相關文章