Android事件分發(fā)詳解
1.事件傳遞的流程是從外到內(nèi),即事件總是由父元素分發(fā)給子元素:Activity->ViewGroup-View,但是通過requestDisallowInterceptTouchEvent可以使子view要求父view不攔截事件
2.當一個事件發(fā)生時,首先由Activity接收,然后activity會交由他的window處理,window的處理方式即是調(diào)用頂級容器DecorView(即setContentView所設(shè)置的View的父容器,是一個FramLayout)的dispatchTouchEvent方法,事件就進入了一個ViewGroup,開始按照事件分發(fā)機制去分發(fā)事件,若DecorView不處理,則activity會調(diào)用自身的onTouchEvent()方法消費事件
3.三個最主要方法:
-
public boolean dispatchTouchEvent(MotionEvent ev)
此方法是事件進入某個view和ViewGroup的入口,返回true表示在此層或者下層已經(jīng)消費了事件,返回false則表示此層即下層都沒處理
public boolean onTouchEvent(MotionEvent event)
此方法是view和ViewGroup自身事件的處理方法,返回true表示自己處理事件,返回false表示自己不處理
- public boolean onInterceptTouchEvent(MotionEvent ev)
此方法是ViewGroup攔截事件的方法,返回true表示攔截事件不再向下層傳遞事件,返回false表示不攔截
4.整個ViewGroup的事件分發(fā)機制大致可以用下面的偽代碼來表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if(onInterceptTouchEvent(ev)){
handled = super.dispatchTouchEvent(ev);
}else{
if(child.dispatchTouchEvent(ev)){
handled = true;
}else{
handled = super.dispatchTouchEvent(ev);
}
}
return handled;
}
ViewGroup的dispatchTouchEvent方法的功能可分為兩部分,即事件下發(fā)部分和自身事件處理部分,ViewGroup是繼承于View,所以它的自身事件就是view的dispatchTouchEvent方法().
由兩種情況來決定走什么功能:
1.是否攔截,攔截,則直接調(diào)用super.dispatchTouchEvent()方法處理身為一個view的特性處理自身事件。不攔截則調(diào)用child.dispatchTouchEvent()下發(fā)事件 2.所有被下發(fā)事件的子view的dispatchTouchEvent()在ACTION_DOWN就返回false,表示下面無view處理,則再調(diào)用super.dispatchTouchEvent()處理自身事件。這就完成了這一輪的事件分發(fā),其他層同理,是一個遞歸的過程
3、一個事件從手指按下屏幕的那一刻到離開屏幕,中間還可能有多次滑動,這個過程以一個ACTION_DOWN開始,中間可能有0及以上個ACTION_MOVE,然后以一個ACTION_UP結(jié)束。這是一個事件序列。
4.事件的分發(fā)是一層一層分發(fā)下來的,一旦某個ViewGrou成功攔截了事件序列中的某個事件,那么此事件序列之后的所有事件都會默認被攔截(只要后續(xù)事件能傳遞到它這里),不會再去調(diào)用onInterceptTouchEvent詢問是否攔截,當然也不會再向下分發(fā),
5.如果一個事件傳遞到某個View或ViewGroup,一旦在ACTION_DOWN返回了false,那么此事件序列后續(xù)的事件都不會傳遞到這里了,并且父View的onTouchEvent方法會被調(diào)用即事件會再次向上傳遞,若一旦開始消費某個事件,那么即便在后續(xù)事件選擇不消費了(比如此View的onTouchEvent方法在ACTION_DOWN返回true,但是ACTION_MOVE返回false),后續(xù)事件也會持續(xù)的傳到這里,并且父View的onTouchEvent方法不會被調(diào)用即事件不會再次向上傳遞,這些后續(xù)被返回false的事件最終會直接傳遞給Activity處理
6.View.dispatchTouchEvent()關(guān)鍵代碼,onTouchListener的優(yōu)先級要高于onTouchEvent(),onClickListener在onTouchEvent()里面調(diào)用。:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
onTouchEvent方法里如果clickable和longClickable有一個為真,就會調(diào)用
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;
}
7.Enable屬性不影響默認的系統(tǒng)控件的onTouchEvent的返回值,只要view的clickable或longClickAble有一個為true,默認的onTouchEvent方法便會返回true,否則返回false(例如,當TextView的onTouchEvent默認返回false,因為它默認就是不可點擊的,在設(shè)置了clickable為true后,它的onTouchEvent就返回true了)
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
8.以ViewGroup為例,當事件傳進來時,dispatchTouchEvent中的代碼:
// Check for interception.
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;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
這里的條件判斷的決定是否直接將攔截標志位設(shè)為true,不再調(diào)用onInterceptTouchEvent方法去判斷。滿足條件為這個事件是ACTION_DOWN事件,或者mFirstTouchTarget不為空。ACTION_DOWN是一個新事件序列的開始肯定不能直接將攔截標志位設(shè)為true,而是要經(jīng)過onInterceptTouchEvent去決定,mFirstTouchTarget是只要下面有子view消耗了事件就會賦值,所以不為空就是下面有子view處理了事件,而如果是后續(xù)事件但是mFirstTouchTarget為空說明子view在ACTION_DOWN事件就返回了false不消耗事件,那么后續(xù)的事件也就無需再傳遞給下面了,直接將攔截標志為設(shè)為true,而不再通過onInterceptTouchEvent去決定,這里需結(jié)合4和5仔細體會。
若進入了條件為真的的代碼塊,這個disallowIntercept一般是在子view中被用parent.requestDisallowInterceptTouchEvent后結(jié)果為true,就不再走onInterceptTouchEvent判斷了,直接使攔截標志位位true,使得本parentView不攔截事件。雖然事件的分發(fā)是從外到里,但是這是特殊的子view干預父view的事件分發(fā)的辦法,另外在MotionEvent.ACTION_DOWN事件到來時,disallowIntercept標志會在時被重置,mFirstTouchTarget會被清除,因為這是新事件序列的開始,所以這也更好理解為什么在MotionEvent.ACTION_DOWN時不能直接攔截了
// 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();
}
如果不攔截,就會遍歷子view,通過是否在播放動畫和事件是否落在它的范圍內(nèi)來獲得合適的 View,如果存在就為mFirstTouchTarget賦值,注意這里能找到的合適子view可能不止一個,因此如果第一個合適的view不處理事件,那么會繼續(xù)為下面合適的繼續(xù)賦值,以此類推
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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 = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
接著檢驗mFirstTouchTarget是否為空,為空的可能有兩種,一是沒有合適的view,二是view不處理事件:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
可以看到,不管是否為空都調(diào)用了dispatchTransformedTouchEvent()這個方法,看看它的關(guān)鍵代碼
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
看到這里就能明白了,關(guān)鍵在于第三個參數(shù),mFirstTouchTarget為空則將child賦值空,就會調(diào)用了父view的dispatchTouchEvent方法,事件再次向上傳遞,否則直接調(diào)用了子view的dispatchTouchEvent方法,事件進入下一輪
最后事件分發(fā)大致圖:
