Android View之事件分發

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()會收到消息,並將事件發送給InputEventReceiverdispatchInputEvent()方法,而後繼續往下傳,流程是這樣的:

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按下時的這兩個方法,由於

ACTION_DOWN是事件的開始,因此
cancelAndClearTouchTargets這個方法是進行初始化,而resetTouchState是重置觸摸狀態,全新開始。

接下來看:

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)方法來讓父佈局攔截事件,交給父佈局處理。

相關文章
相關標籤/搜索