Android view的事件分發機制是開發中的一個重點,所以掌握它的真正意義是很是必要的。android
一.什麼是view的事件分發機制呢?設計模式
將點擊事件傳遞到具體某個view處理的整個過程,就叫作事件分發。app
二.爲何要有事件分發呢?ide
由於Android的view是樹形結構的,view可能會重疊,當點擊的地方多的時候,爲了解決點擊事件傳遞給誰的時候,就要用到事件分發了,所以也能夠看出,事件分發採用的是責任鏈的設計模式。oop
三.事件分發的傳遞對象是哪些?佈局
主要是Activity->ViewGroup->View的傳遞過程。ui
四.事件分發中的三個重要方法?this
1.public boolean dispatchTouchEvent(MotionEvent ev) spa
事件傳遞方法,把事件依照順序往下傳遞。設計
2.public boolean onInterceptTouchEvent(MotionEvent ev)
事件攔截方法,該方法是在事件分發的 dispatchTouchEvent 方法內部進行調用。是用來判斷在觸摸事件傳遞過程當中,是否攔截某個 事件。若是方法true表示將時間攔截,交給當前view的onTouchEvent進行處理,若是返回false不攔截,繼續往下傳遞。
3.public boolean onTouchEvent(MotionEvent ev)
事件響應方法,這是用來處理具體事件的,在dispatchTouchEvent中調用。
五.瞭解了事件分發的一些基本概念後,就開始根據事件傳遞對象的流程來一步步分析事件的分發過程:
1.Activiy事件分發過程:
首先按下一個頁面的按鈕後,事件第一個傳遞的對象就是Activity,
所以先分析事件是怎麼到達Activity又是怎麼往下傳遞的?
寫個touch事件來看下調用棧:
咱們知道Android採用handler的消息機制,消息存在messagequeue消息隊列中,同過loope輪詢的方式來調用消息,咱們從調用棧中的最下面的紅框中看到,當咱們按下按鈕時,nativePollOnce()會收到消息,並將事件發送給InputEventReceiver的dispatchInputEvent()
方法,而後繼續往下傳,流程是這樣的:
nativePollOnce(收到消息)->InputEventReceiver(的dispatchInputEvent)-> ViewRootImpl(view的根節點)的WindowInputEventReceiver
->Activity的dispatchTouchEvent。
到達Activity,接下來分析,事件由activity到達view的過程:
這個過程咱們從調用棧中就能夠很清楚的看出來,流程是這樣的:
Activity(dispatchTouchEvent)-> PhoneWindow(superDispatchTouchEvent) -> DecorView(superDispatchTouchEvent) -> ViewGroup(dispatchTouchEvent)
爲何中間會通過PhoneWindow和DecorView呢?
由於Activity中持有一個Window對象,Window中包含一個PhoneWindow實例,PhoneWindow中又持有一個DecorView對象。
2.ViewGroup事件分發過程:
咱們經過一個例子來看下事件傳遞過程,如下是自定義的一個 ViewGroup:
public class CustomLinearLayout extends LinearLayout {
private final String TAG = "CustomLinearLayout";
public CustomLinearLayout(Context context) {
super(context);
}
public CustomLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onTouchEvent ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"dispatchTouchEvent ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}
}
使用這個自定義 ViewGroup:
<com.example.linwenbing.demo.CustomLinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="50dp"
android:text="點擊"
/>
</com.example.linwenbing.demo.CustomLinearLayout>
點擊button:
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouch ACTION_DOWN");
break;
}
return false;
}
});
看下log的打印:
根據log的打印咱們知道,當咱們點擊button時首先調用ViewGroup的調用流程是:
ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->子view的dispatchTouchEvent
如今咱們在ViewGroup中對事件進行攔截,即在onInterceptTouchEvent方法中返回true:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return true;
這時看下log的打印:
調用流程:
ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->ViewGroup的onTouchEvent
說明攔截事件後,事件就不往子view中傳遞了,就在ViewGroup的onTouchEvent進行處理。這就很好解釋了三個方法的做用:
dispatchTouchEvent:首先調用的方法,對事件進行分發
onInterceptTouchEvent:攔截事件,若是攔截返回true,並執行改VierGroup的onTouchEvent,若是返回false則傳給下一個view.
onTouchEvent:對事件的處理
搞懂了流程接下來咱們來看下源碼,這裏主要看部分重要的代碼:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
首先咱們看dispatchTouchEvent方法中,view按下時的這兩個方法,由於
接下來看:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags &FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
這裏判斷是否攔截事件,這裏intercepted = onInterceptTouchEvent(ev);用來敷值,onInterceptTouchEvent默認返回false,不攔截的,因此咱們能夠重寫onInterceptTouchEvent來對事件進行攔截.
if (actionMasked == MotionEvent.
ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child =getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
這一大段代碼主要就是當ViewGroup不攔截事件時,把事件分發到子view,循環遍歷完全部的子view後,若是點擊事件都沒有被消耗,ViewGroup就會本身處理點擊事件,以下代碼:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
3.View事件分發過程:
這裏咱們說的view就是最後一層的子view了,因此並不存在攔截不往下分發的方法,這裏咱們主要分析onTouch中處理事件的分發過程:
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"onClick");
}
});
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"onTouch ACTION_UP");
break;
}
return false;
}
});
上面的代碼咱們看一下log的執行(這裏點擊按鈕的時候有稍微移動下):
咱們看到執行流程是:
ACTION_DOWN->ACTION_MOVE>ACTION_UP>onClick
能夠看到ACTION_DOWN最開始執行onClick最後執行,若是咱們在onTouch中返回true會發現onClick就不執行了。
這裏源碼不作具體分析,看源碼的時候主要發現MotionEvent中有個MotionEvent.ACTION_CANCEL須要注意一下:
case MotionEvent.
ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
MotionEvent.ACTION_CANCEL官方分析:
當你的手指(或者其它)移動屏幕的時候會觸發這個事件,好比當你的手指在屏幕上拖動一個listView或者一個ScrollView而不是去按上面的按鈕時會觸發這個事件。
一個解決衝突的重要方法使用說明:
requestDisallowInterceptTouchEvent方法:
requestDisallowInterceptTouchEvent方法用於影響父元素的事件攔截策略,requestDisallowInterceptTouchEvent(true),表示不容許父元素攔截事件,這樣事件就會傳遞給子View。通常這個方法子View用的多,能夠用來處理滑動衝突問題。
如:
(1)在子View的dispatchTouchEvent方法中,對於ACTION_DOWN事件,經過調用requestDisallowInterceptTouchEvent(true)默認不容許父佈局攔截事件,這樣後續事件都交給子View處理。
(2)在子View的dispatchTouchEvent方法中,對於ACTION_MOVE事件,默認是子View處理,在須要父佈局處理時,調用requestDisallowInterceptTouchEvent(false)方法來讓父佈局攔截事件,交給父佈局處理。