徹底理解View事件體系!

我的CSDN博客同步發(fā)布:徹底理解View事件體系!

轉(zhuǎn)載請注明出處:【huachao1001的簡書:http://www.itdecent.cn/users/0a7e42698e4b/latest_articles】

View的事件體系整體上理解還是比較簡單的,但是卻有很多細(xì)節(jié)。這些細(xì)節(jié)很容易忘記,本文的目標(biāo)是理解性的記憶,爭取做到看完不忘。最近在復(fù)習(xí),希望本文能對你也有所幫助。如果你已經(jīng)對View事件體系有一定的了解,那么查漏補(bǔ)缺,看看你是不是已經(jīng)掌握了以下內(nèi)容呢?

1 View事件相關(guān)基礎(chǔ)

在正式接觸View事件體系之前,先看看相關(guān)基礎(chǔ)部分。

1.1 View的坐標(biāo)及寬高

在Android系統(tǒng)中,一個子View在ViewGroup中顯示的區(qū)域由top、right、bottom、left四個屬性確定。它們分別確定四條邊,如下圖所示:

子View所在區(qū)域

這四個參數(shù)我們可以通過如下方法得到:

//假設(shè)v是個View實(shí)例
//View v=···;
int top = v.getTop();
int right = v.getRight();
int bottom = v.getBottom();
int left = v.getLeft();

拿到這四個參數(shù)后,我們也可以計(jì)算出寬高:

int width = right-left;
int height = bottom-top;

我們知道,在Android3.0(api 11)之前,是不能用屬性動畫的,只能用補(bǔ)間動畫,而補(bǔ)間動畫所做的動畫效果只是將View的顯示轉(zhuǎn)為圖片,然后再針對這個圖片做透明度、平移、旋轉(zhuǎn)、縮放等效果。這帶來的問題是,View所在的區(qū)域并沒有發(fā)生變化,變化的只是個“幻影”而已。也就是說,在Android 3.0之前,要想將View區(qū)域發(fā)生變化,就得改變top、left、right、bottom。如果我們想讓View的動畫是實(shí)際的位置發(fā)生變化,并且要兼容3.0之前的軟件,該怎么辦呢?為了解決這個問題,從3.0開始,加了幾個新的參數(shù):x,y,translationX,translationY。

x = left + translationX;
y = top + translationY;

這樣,如果我們想要移動View,只需改變translationXtranslationY就可以了,top和left不會發(fā)生變化。也可以使用屬性動畫去改變translationXtranslationY。

1.2 手勢識別

(1)VelocityTracker 速度追蹤

我們知道,很多ViewGroup中,假設(shè)手指滑動的距離相同,但是滑動速度不同,那么滑動速度越快,ViewGroup中內(nèi)容滾動的距離越遠(yuǎn)。那么如何識別用戶滑動的速度呢?當(dāng)然了,你可以在onTouchEvent中不斷的監(jiān)聽計(jì)算。但是那樣的代碼太臃腫了,而且容易算錯。好在Android系統(tǒng)內(nèi)置了速度追蹤類VelocityTracker。有了它,媽媽再也不用擔(dān)心如何計(jì)算速度追蹤。先看看怎么用:

//event一般是通過onTouchEvent函數(shù)傳遞的MotionEvent對象
VelocityTracker vt=VelocityTracker.obtain();
vt.addMovement(event);

VelocityTracker.obtain();這句可以看出,這里是使用了享元模式,對享元模式不太熟悉的童鞋請參考我的另一篇文章《從Android代碼中來記憶23種設(shè)計(jì)模式》 。那么如何獲取當(dāng)前的移動速度呢?

vt.computeCurrentVelocity(1000);
int xv=(int) vt.getXVelocity();
int yv=(int) vt.getYVelocity();

在調(diào)用獲取x和y方向的速度之前,先要調(diào)用computeCurrentVelocity函數(shù),用于設(shè)定計(jì)算速度的時間間隔。很顯然,速度的計(jì)算為(終端位置-起始位置)/間隔時間。

既然是享元模式,那肯定是需要回收的啦~我們看看如何回收VelocityTracker對象:

vt.clear();
vt.recycle();

(2)GestureDetector手勢檢測

同樣,我們有時還需要檢測用戶的:單擊、滑動、長按、雙擊等動作。懶得自己去計(jì)算時間來識別,直接用系統(tǒng)的GestureDector來監(jiān)聽這些事件,GestureDector的使用也非常簡單:

GestureDetector.OnGestureListener listener=new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        //手指出品按下的瞬間
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        //手指觸摸屏幕,并且尚未松開或拖動。與onDown的區(qū)別是,onShowPress強(qiáng)調(diào)沒用松開和沒有拖動
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        //手指離開屏幕(單擊)
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //手指按下并拖動,當(dāng)前正在拖動
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        //手指長按事件
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        //手指快速滑動
        return false;
    }
};
GestureDetector mGestureDetector = new GestureDetector(this,listener);

//防止長按后無法拖動的問題
mGestureDetector.setIsLongpressEnabled(false);

既然要讓GestureDetector來識別各種動作事件,那么就得讓GestureDetector來接管事件管理,即在onTouchEvent里面只寫入如下代碼:

return mGestureDetector.onTouchEvent(event);

我們看到,OnGestureListener 監(jiān)聽器包含了各種事件的監(jiān)聽。除了OnGestureListener以外,還有OnDoubleTapListener它主要是處理雙擊相關(guān)的事件,可以通過setOnDoubleTapListener將該監(jiān)聽器設(shè)置到GestureDetector中。

2 View事件分發(fā)機(jī)制

2.1 三個重要函數(shù)

前面做了基礎(chǔ)熱身之后,我們現(xiàn)在開始學(xué)習(xí)View的事件分發(fā)機(jī)制。View的事件分發(fā)主要是由3個函數(shù)決定:dispatchTouchEventonInterceptTouchEvent 以及 onTouchEvent。一個觸摸事件,如果事件坐標(biāo)處于ViewGroup所“管轄范圍”,首先調(diào)用的是該ViewGroupdispatchTouchEvent函數(shù),dispatchTouchEvent函數(shù)內(nèi)部調(diào)用onInterceptTouchEvent函數(shù),用于判斷是否攔截該事件,如果攔截,則調(diào)用ViewGrouponTouchEvent。否則調(diào)用子ViewdispatchTouchEvent函數(shù),可以參考如下圖:

事件分發(fā)過程

注意,上述圖中,只是描述事件從ViewGroup往下傳遞過程,沒有考慮子ViewonTouchEvent的返回值,即沒有考慮事件從子View往上回傳的過程。后面再介紹事件回傳的過程。ViewGroup是否攔截事件,是通過onTnterceptTouchEvent返回值來確定,當(dāng)返回true時,表示攔截該事件,那么該系列事件全部傳遞給ViewGrouponTouchEvent,如果返回false,則表示不攔截該系列事件,該系列事件全部交給子View來處理。為什么我們說是“該系列事件”,而不是說“該事件”呢?注意,View的事件體系中,從down->move->......->move->up。這一個過程為同一個事件系列,如果在onInterceptTouchEvent中返回false,那么所有的事件都不會再交給ViewGroup的的onTouchEvent

2.2 事件來源

我們知道,我們直接通過onTouchEvent里面的形參就可以拿到事件對象,可是事件對象時從哪里產(chǎn)生的?又是經(jīng)歷過哪些曲折的道路才到達(dá)目的地的?

首先,Activity拿到事件對象,Activity把事件對象傳遞給PhoneWindow,PhoneWindow再傳遞給DecorViewDecorView通過遍歷再傳遞到我們的ViewGroup。那么Activity又是從哪里得到事件對象的呢?這里面就涉及的比較底層了,感興趣的童鞋參考任玉剛的《 Android中MotionEvent的來源和ViewRootImpl 》這篇文章。

2.3 從onTouch、onClick、onTouchEvent優(yōu)先級開始

當(dāng)一個View處理觸摸事件時,如果同時設(shè)置了OnTouchListener(內(nèi)含onTouch抽象方法)、OnClickListener(內(nèi)含onClick抽象方法).那么到底哪個函數(shù)先執(zhí)行?我們做一個實(shí)驗(yàn),自定義一個View,重寫onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            Log.d("--> down ", "onTouchEvent");
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d("--> move ", "onTouchEvent");
            break;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("--> up ", "onTouchEvent");
            break;
        }

    }
    return true;
}

并在MainActivity設(shè)置OnTouchListener、OnClickListener

myView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
       switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("--> down", "onTouch");
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                Log.d("--> move", "onTouch");
                break;
            }
            case MotionEvent.ACTION_UP: {
                Log.d("--> up", "onTouch");
                break;
            }

        }
        return false;
    }
});

myView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("-->", "onClick");
    }
});

點(diǎn)擊后,打印的日志信息如下:

06-27 00:36:56.756 2407-2407/? D/--> down: onTouch
06-27 00:36:56.756 2407-2407/? D/--> down: onTouchEvent
06-27 00:36:56.848 2407-2407/? D/--> up: onTouch
06-27 00:36:56.849 2407-2407/? D/--> up: onTouchEvent

注意到,首先執(zhí)行的是onTouch然后再執(zhí)行onTouchEvent,由此可見,onTouchonTouchEvent優(yōu)先級高。代碼中,onTouch返回的是false,表示不消耗事件,因此,觸摸事件能順利的從onTouch傳遞到onTouchEvent,現(xiàn)在我們把onTouch返回值改為true,表示消耗觸摸事件,看看會打印什么日志:

06-27 00:42:09.783 2499-2499/? D/--> down: onTouch
06-27 00:42:09.863 2499-2499/? D/--> up: onTouch

正如我們所猜想的那樣,并沒有執(zhí)行onTouchEvent。我們看到,onClick并沒有執(zhí)行。這是為什么呢?仔細(xì)看看onTouchEvent的返回值,我們看到,onTouchEvent返回的是true,表示消耗觸摸事件,而此時onClick就沒執(zhí)行了。是不是可以猜想:onTouchEvent優(yōu)先級比onClick高。我們把onTouchEvent返回值改為false,看看日志信息(確保onTouch返回值也是false,否則onTouchEvent連觸摸事件都拿不到,更別談是否消耗觸摸事件的問題了):

06-27 00:48:22.214 2947-2947/? D/--> down: onTouch
06-27 00:48:22.214 2947-2947/? D/--> down: onTouchEvent

什么??。?!,為什么還是沒有執(zhí)行onClick?仔細(xì)觀察會發(fā)現(xiàn)連up事件也沒了~。為什么up事件沒有了呢?主要是,onTouchEvent返回false,表示對此系列的事件不處理(不消耗),那么該系列事件又會返回到ViewGrouponTouchEvent。后續(xù)的moveup事件也不會再交給子ViewonTouchEvent了。這個過程我們暫時先放一放,回到我們前面所說的,為什么onClick不執(zhí)行?注意!什么是點(diǎn)擊?其實(shí),點(diǎn)擊包含downup,因此我們需要判斷downup是否都是在當(dāng)前View區(qū)域內(nèi),我們當(dāng)然就沒辦法只根據(jù)一個事件來判斷是否需要執(zhí)行onClick。因此,onTouchEvent的返回值不能用于決定是否把事件傳遞給onClick。如果想把事件傳遞到onClick函數(shù),我們需要在onTouchEvent里做判斷,并顯式調(diào)用OnClickListener實(shí)例對象的onClick。當(dāng)然了,你可以不用自己寫,直接在你的onTouchEvent中的最后一句改為:

return super.onTouchEvent(event);

View在onTouchEvent函數(shù)中,根據(jù)觸摸事件判斷,顯式的調(diào)用了OnClickListener實(shí)例對象的onClick。調(diào)用過程封裝到performClick函數(shù)中,看看performClick源碼:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

因此可以得出結(jié)論,執(zhí)行的順序是:onTouch->onTouchEvent->onClick。當(dāng)onTouch返回false時,onTouchEvent才會執(zhí)行,當(dāng)onTouchEvent顯式調(diào)用onClick時,onClick才會執(zhí)行。

2.4 事件的回傳過程

我們知道,在ViewGroup中,事件是dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent。由onInterceptTouchEvent決定是否將事件傳遞給子View。如果傳遞給子View,但是子View并不想處理這個系列的事件(子View的onTouchEvent返回false),該怎么處理這個系列事件呢?難道就拋棄這個系列的觸摸事件不管了嗎?當(dāng)然不是!我們先看一段測試代碼:

自定義的ViewGroup,重新如下函數(shù):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    print(ev, "ViewGroup dispatchTouchEvent");
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    print(ev, "ViewGroup onInterceptTouchEvent");
    //不攔截,將事件往子View傳遞
    return false;
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    print(event, "ViewGroup onTouchEvent");
    return true;

}

為了減少重復(fù)代碼,我們定義了print函數(shù):

private void print(MotionEvent event, String msg) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            Log.d("--> down ", msg);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d("--> move ", msg);
            break;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("--> up ", msg);
            break;
        }

    }

}

自定義View,重寫如下函數(shù):

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    print(event, "childView dispatchTouchEvent");
    return super.dispatchTouchEvent(event);
}


@Override
public boolean onTouchEvent(MotionEvent event) {

    print(event, "childView onTouchEvent");
    //子View不處理該系列事件
    return false;
}

觸摸子View后,打印如下信息:

06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView onTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup onTouchEvent

看到,當(dāng)子ViewonTouchEvent返回的是false,那么該系列的事件會回到ViewGrouponTouchEvent。注意,down事件先到達(dá)子View的onTouchEvent,如果子View不消耗,則down事件及其后續(xù)的事件會傳到ViewGrouponTouchEvent。而ViewGrouponTouchEvent也是一樣,如果ViewGroup不處理該系列事件,又會繼續(xù)回傳到ViewGroup的父View的onTouchEvent。如下圖所示:

事件回傳

我們以上討論的點(diǎn)擊位置都是子View所處的區(qū)域,即如下如所示。

點(diǎn)擊區(qū)域

如果點(diǎn)擊不是子View所處的區(qū)域,事件的傳遞會是怎么樣的呢?我們看看日志信息:

06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup onTouchEvent

可以看到,子View并沒有調(diào)用任何函數(shù)。這很容易理解,因?yàn)閴焊透?code>View沒有半毛錢關(guān)系,要是點(diǎn)擊任意區(qū)域子View都會有事件傳遞過去那才奇怪呢!因此,可以看出,ViewGroup在傳遞觸摸事件時,會遍歷子View,判斷觸摸點(diǎn)是否在各個子View中,如果在,則觸發(fā)調(diào)用相關(guān)函數(shù)。如果點(diǎn)擊的位置沒有子View,那么不管onIntercepTouchEvent返回的是什么,ViewGroup的onTouchEvent都會執(zhí)行!

最后,有幾點(diǎn)必須要知道的:

  • 如果View只消耗down事件,而不消耗其他事件,那么其他事件不會回傳給ViewGroup,而是默默的消逝掉。我們知道,一旦消耗down時間,接下來的該系列所有的事件都會交給這個View,因此,如果不處理down以外的事件,這些事件就會被“遺棄”。
  • 如果ViewGroup決定攔截,那么這個系列事件都只能由它處理,并且onInterceptTouchEvent不會再被調(diào)用。
  • 某個View,在onTouchEvent中,如果針對最開始的down事件都返回false,那么接下來的事件系列都不會交給這個View。
  • ViewGroup默認(rèn)不攔截事件,即onInterceptTouchEvent默認(rèn)返回false。
  • ViewonTouchEvent默認(rèn)返回false,即不消耗事件。
  • View沒有onInterceptTouchEvent方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容