Android的View事件傳遞及傳遞問題 事件傳遞機制

來源:互聯網
上載者:User

Android的View 事件傳遞

1、基礎知識

(1) 所有 Touch 事件都被封裝成了 MotionEvent 對象,包括 Touch 的位置、時間、記錄以及第幾個手指(多指觸摸)等。

(2) 事件類型分為 ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每個事件都是以 ACTION_DOWN 開始 ACTION_UP 結束。

(3) 對事件的處理包括三類,分別為傳遞——dispatchTouchEvent()函數、攔截——onInterceptTouchEvent()函數、消費——onTouchEvent()函數和 OnTouchListener


2、傳遞流程

(1) 事件從 Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的 View(ViewGroup)開始一直往下(子 View)傳遞。子 View 可以通過 onTouchEvent()對事件進行處理。

(2) 事件由父 View(ViewGroup)傳遞給子 View,ViewGroup 可以通過 onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。

(3) 如果事件從上往下傳遞過程中一直沒有被停止,且最底層子 View 沒有消費事件,事件會反嚮往上傳遞,這時父 View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最後會到 Activity 的 onTouchEvent()函數。

(4) 如果 View 沒有對 ACTION_DOWN 進行消費,之後的其他事件不會傳遞過來。

(5) OnTouchListener 優先於 onTouchEvent()對事件進行消費。
上面的消費即表示相應函數傳回值為 true。


附上兩張原文中流程圖:
(1) View 不處理事件流程圖



(2) View 處理事件流程圖


3、最後說幾句

Android Touch事件
假設布局層次為
Layout0
Layout1
Layout2
Layout3

如果誰都沒有去interceptTouch,同時誰都沒有處理onTouch事件。
那麼Layout0->intercept Layout1->intercept Layout2->intercept Layout3->intercept
Layout3->onTouch Layout2->onTouch Layout1->onTouch Layout0->onTouch
由於誰都沒有消費ACTION_DOWN事件,後續的MOVE,UP事件將不會傳進來。

如果Layout2 intercept了,但是不消費onTouch
那麼Layout0->intercept Layout1->intercept Layout2->intercept
Layout2->onTouch Layout1->onTouch Layout0->onTouch
後續事件不會傳入

如果Layout2 intercept了,同時消費了。
那麼 0->intercept 1->intercept 2->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch

如果Layout2 intercept了,不消費,Layout1消費了。
那麼0->intercept 1->intercept 2->intercept
2->onTouch 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch

總結一下。規律就是
如果當前Layout intercept了,那麼子View和子ViewGroup都沒有機會去獲得Touch事件了。如果當前Layout並不消費事件的話,這個事件會一直向上冒泡,直到某個父Layout的onTouchEvent消費了這個事件。如果沒有任何一個父Layout消費這個事件,那麼後續的事件都不會被接受。
如果在冒泡過程中有某個Layout消費了這個事件。那麼這個Layout的所有父Layout的intercept仍然會被調用。但是當前Layout的intercept不會再被調用了。直接調用onTouch事件。

另外,對於底層的View來說,有一種方法可以阻止父層的View截獲touch事件,就是調用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底層View收到touch的action後調用這個方法那麼父層View就不會再調用onInterceptTouchEvent了,也無法截獲以後的action。在實踐過程中發現ListView在滾動的時候會調用這個方法。使得action不能被攔截。


android上view touch事件的傳遞問題

項目中要實現拖拽功能,即在scrollview裡面放的imageview長按後,有影子被拖走的感覺。實現思路基本是這樣的:
1、在布局檔案裡把imageview放到scrollview中
2、為imageview註冊touch監聽
3、重寫scrollview的onTouchEvent函數。
4、建立由一張imgeview產生的popupwindow
5、通過touch的move更新所建立的popupwindow的位置。實現imageview上的圖片被拖走的感覺。
開始時,由imageview響應touch事件,但隨著手移出imageview,touch事件就不一定會還是由imageview接收了!那touch事件該傳給誰呢?imageview上的touch事件是從哪步溜走了呢?經過嘗試發現是經過imageview的cancel事件之後,後面的touch事件都交給了其父類操作,這裡就是scrollview。touch事件從imageview消失後直接將後續的move事件和up事件交由了scrollview。我們要實現拖走imageview上圖片的效果就可以通過建立popupwindow的方式來實現,所建立的popupwindow只有一張圖片組成。通過更新popupwindow顯示的位置就可以實現拖拽imageview上圖片的效果了。


Android的事件傳遞機制

Android的事件傳遞機制分為按鍵事件和觸摸事件,而這裡的事件指的是touchevent,即觸摸事件。

一個touchevent一般是由多個motionevent(有DOWN,UP,MOVE,CANCEL四種)構成,合理的分配這些motionevent到達指定的控制項,這些控制項才能夠接收到相應的touchevent,然後做出處理。關於motionevent請參考我轉的另一篇博文。

 
一.相關類和方法

1.與觸摸事件有關係的類是view,viewgroup,activity。

1)這裡view我們表示的是那些繼承自view不能再容納其他控制項的類,比如textview,imageview。其中下面兩個方法是三者都有的,且與touchevent相關的。

2)這裡的viewgroup表示的是那些繼承自viewgroup的類,它們的共同點是可以繼續包含view。比如各種layout以及上面說到的噁心的listview。

3)這裡的activity表示的就是那些繼承自activity的類。

所以下面沒有特殊描述,均用一個類代表它們整個群體。
 

2.與觸摸事件有關係的方法是dispatchTouchEvent,onTouchEvent以及onInterceptTouchEvent,

public boolean dispatchTouchEvent(MotionEvent event) - 用於事件分發,三個類都有該方法

public boolean onTouchEvent(MotionEvent event) - 用於事件消費,三個類都有該方法

public boolean onInterceptTouchEvent(MotionEvent ev) - 用於攔截事件,只有viewgroup有該方法

這三個方法在三個類中的用途是一樣的,但是詳細的處理過程卻不同。這我們將在下一部分去說明。

 
二.motionevent的dispatchTouchEvent流程

1.Activity部分

對於正常的理解來說,應該是activity拿到某一個motionevent,然後開始事件分發,所以我們來看看activity的dispatchTouchEvent源碼

public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}



 
1)Activity處理事件

對於一個手機程式來說,最先拿到手勢的我覺得應該就是window了,所以首先用superDispatchTouchEvent分配motionevent到activity的內部控制項,superDispatchTouchEvent實際做的事情就是FrameLayout.dispatchTouchEvent(處理流程如同下文的ViewGroup.dispatchTouchEvent),它將會去尋找有沒有view可以處理該事件。如果activity沒有內部控制項或者內部控制項無法處理該motionevent時,superDispatchTouchEvent返回false,然後接收該motionevent的就是activity本身了,我們可以在Activity的onTouchEvent方法裡做詳細的處理。

2)Activity不處理事件

剛才1)中說到,Activity沒有內部控制項或者內部控制項無法處理該motionevent時superDispatchTouchEvent會返回false,但是如果有內部控制項切可以處理該motionevent時,將返回true,這時Activity的dispatchTouchEvent也會返回true告知系統我有一個控制項接收了motionevent。

 
2.ViewGroup部分

superDispatchTouchEvent將事件進行分發,首先接到的當然是該Activity的Layout控制項,它繼承自ViewGroup。當它接收到了之後顯然也要先考慮事件的分發。我們來看看ViewGroup的dispatchTouchEvent代碼

public boolean dispatchTouchEvent(MotionEvent ev) {final int action = ev.getAction();final float xf = ev.getX();final float yf = ev.getY();final float scrolledXFloat = xf + mScrollX;final float scrolledYFloat = yf + mScrollY;final Rect frame = mTempRect;boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (action == MotionEvent.ACTION_DOWN) {if (mMotionTarget != null) {// 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;}// If we're disallowing intercept or if we're allowing and we didn't// interceptif (disallowIntercept || !onInterceptTouchEvent(ev)) {// reset this event's action (just to protect ourselves)ev.setAction(MotionEvent.ACTION_DOWN);// We know we want to dispatch the event down, find a child// who can handle it, start with the front-most child.final int scrolledXInt = (int) scrolledXFloat;final int scrolledYInt = (int) scrolledYFloat;final View[] children = mChildren;final int count = mChildrenCount;for (int i = count - 1; i >= 0; i--) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE|| child.getAnimation() != null) {child.getHitRect(frame);if (frame.contains(scrolledXInt, scrolledYInt)) {// offset the event to the view's coordinate systemfinal float xc = scrolledXFloat - child.mLeft;final float yc = scrolledYFloat - child.mTop;ev.setLocation(xc, yc);child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;if (child.dispatchTouchEvent(ev)) {// Event handled, we have a target now.mMotionTarget = child;return true;}// The event didn't get handled, try the next view.// Don't reset the event's location, it's not// necessary here.}}}}}boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||(action == MotionEvent.ACTION_CANCEL);if (isUpOrCancel) {// Note, we've already copied the previous state to our local// variable, so this takes effect on the next eventmGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// The event wasn't an ACTION_DOWN, dispatch it to our target if// we have one.final View target = mMotionTarget;if (target == null) {// We don't have a target, this means we're handling the// event as a regular view.ev.setLocation(xf, yf);if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {ev.setAction(MotionEvent.ACTION_CANCEL);mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;}return super.dispatchTouchEvent(ev);}// if have a target, see if we're allowed to and want to intercept its// eventsif (!disallowIntercept && onInterceptTouchEvent(ev)) {final float xc = scrolledXFloat - (float) target.mLeft;final float yc = scrolledYFloat - (float) target.mTop;mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;ev.setAction(MotionEvent.ACTION_CANCEL);ev.setLocation(xc, yc);if (!target.dispatchTouchEvent(ev)) {// target didn't handle ACTION_CANCEL. not much we can do// but they should have.}// clear the targetmMotionTarget = null;// Don't dispatch this event to our own view, because we already// saw it when intercepting; we just want to give the following// event to the normal onTouchEvent().return true;}if (isUpOrCancel) {mMotionTarget = null;}// finally offset the event to the target's coordinate system and// dispatch the event.final float xc = scrolledXFloat - (float) target.mLeft;final float yc = scrolledYFloat - (float) target.mTop;ev.setLocation(xc, yc);if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {ev.setAction(MotionEvent.ACTION_CANCEL);target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;mMotionTarget = null;}return target.dispatchTouchEvent(ev);}



1)ViewGroup被當成普通的View

整體代碼的意思是說有一個motionevent傳遞過來了,ViewGroup首先看自己的內部View能不能處理或者說哪個View能夠處理,判斷的方法是看看該motionevent是不是DOWN,如果是則看看View是否可見,如果可見再看看焦點是不是在View的內部,如果在其內部,那麼我們說找到了一個View可能處理該motionevent,將其命名為target(We know we want to dispatch the event down, find a child who can handle it, start with the front-most child.),如果說沒有一個View可以處理(We don't have a target, this means we're handling the event as a regular view.),此時ViewGroup將被當做普通的View來處理這個motionevent,那麼將調用 super.dispatchTouchEvent(ev)方法,這裡就是View的dispatchTouchEvent了。return語句返回的就是super.dispatchTouchEvent(ev)

2)ViewGroup內部有View可以處理

如果說ViewGroup內部有View可以處理,假設為target,那麼將調用target.dispatchTouchEvent方法。return語句返回target.dispatchTouchEvent(ev)

3)onInterceptTouchEvent

這裡有一個非常特殊的方法,就是onInterceptTouchEvent了,它可以讓ViewGroup對motionevent進行攔截,意思就是我們發現某個target可以獲得DOWN的焦點,但是ViewGroup不想讓它內部的View處理事件,則進行攔截,此時dispatchTouchEvent返回true。


3.View部分

View部分在相對就簡單一些了,在上面的target.dispatchTouchEvent之後,motionevent被傳遞到了View的dispatchTouchEvent中,看到這裡應該也就明白了motionevent也是類似的從Activity傳遞到ViewGroup中的,在superDispatchTouchEvent裡Framelayout.dispatchTouchEvent找到了某個View(實際是某個Layout的ViewGroup),調用了它的dispatchTouchEvent。

看View原始碼中的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event)) {return true;}return onTouchEvent(event);}



因為View表示沒有任何其他內部的控制項了,所以它只有兩種選擇,這裡首先將使用我們開發人員定義的OnTouchListener進行處理,如果可以處理,那麼將返回true,如果不能處理將使用預設的onTouchEvent來處理。
 
三.motionevent的onTouchEvent流程

最底層的View的dispatchTouchEvent會調用onTouchListener來進行處理motionevent,或者使用onTouchEvent來處理motionevent,不論哪種都預設會返回true。所以這時ViewGroup的dispatchTouchEvent傳回值為true,所以Activity的dispatchTouchEvent的傳回值是true。

如果我們沒定義自己的onTouchListener,並且重寫了onTouchEvent,返回一個false,那麼ViewGroup的dispatchTouchEvent返回為false,Activity將會調用它的onTouchEvent方法。

 
四.後續的motionevent

如果motionevent為DOWN的時候View沒有處理,即在它的dispatchTouchEvent內返回了false,那麼該View的容器ViewGroup不會再調用該View的dispatchTouchEvent了,即它將無法接收到後續的MOVE,UP。只有DOWN的時候被View處理了(在dispatchTouchEvent返回true),後續的MOVE,UP才會傳遞到該View。

 
五.ListView的問題

想給ListView實現左右滑動翻頁的功能,正常是想著使用ViewFillper,但是不用ViewFlipper動態改變adapter的內容再重新整理ListView的話應該如何?呢。有下面的想法

1.給ListView定義一個手勢對象gestureDector,重寫它的onTouchEvent,在裡面使用return gestureDector.onTouchEvent。gestureDector的手勢監聽器預設在onDown的時候返回false,所以一個DOWN的motionevent傳過來,onTouchEvent返回false,根據上面的說法,dispatchTouchEvent也將返回false,後續motionevent將不會再傳遞到ListView,失敗。

2.重寫手勢監聽器的onDown傳回值為true,這時可以實現左右翻頁,但是ListView本身的onItemClickListener將沒辦法正常工作。失敗。

3.重寫dispatchTouchEvent,調用super.dispatchTouchEvent,然後始終返回true。重寫onTouchEvent,首先調用gestureDector.onTouchEvent,如果返回為false,說明gestureDector.onTouchEvent沒有處理該事件,我們的左右滑動也沒有觸發,那麼return super.onTouchEvent處理,包括它的onItemClickListener等等都可以正常運行。不管super.onTouchEvent返回何值,因為dispatchTouchEvent返回了true,所以後續的動作都會傳來。如果返回為true,說明gestureDector.onTouchEvent處理了左右滑動事件(前提是在手勢監聽器裡面fling動作返回了true),此時return true。成功。

4.重寫dispatchTouchEvent,一開始就調用gestureDector.onTouchEvent,然後同樣的處理方式,最終保證能夠return true。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.