android 事件分發(fā)機制
1.android 點擊事件
當用戶觸摸屏幕時(View 或 ViewGroup派生的控件),將產(chǎn)生點擊事件(Touch事件)
一共包含四種事件類型
事件類型 具體動作
MotionEvent.ACTION_DOWN 按下View(所有事件的開始)
MotionEvent.ACTION_UP 抬起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結(jié)束事件(非人為原因)
一般情況下,事件列都是以DOWN事件開始铁追、UP事件結(jié)束,中間有無數(shù)的MOVE事件,當一個點擊事件(MotionEvent )產(chǎn)生后,
系統(tǒng)需把這個事件傳遞給一個具體的 View 去處理
2.事件的傳遞
事件傳遞順序: activity ——>ViewGroup——>View
事件分發(fā)過程由哪些方法協(xié)作完成等限?
dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
dispatchTouchEvent():分發(fā)(傳遞)事件,當點擊事件可以傳遞時芬膝,這個方法就會被調(diào)用
onTouchEvent():處理點擊事件,在dispatchTouchEvent()內(nèi)部調(diào)用
onInterceptTouchEvent():判斷是否攔截某個事件,在ViewGroup的dispatchTouchEvent()內(nèi)部調(diào)用
下面我們逐個來分析各事件的傳遞
2.1.Activity的事件傳遞
當一個點擊事件發(fā)生時望门,事件最先傳到Activity的dispatchTouchEvent()進行事件分發(fā),源碼入下:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析2:getWindow().superDispatchTouchEvent(ev)
說明:
a. getWindow() = 獲取Window類的對象
b. Window類是抽象類,其唯一實現(xiàn)類 = PhoneWindow類锰霜;即此處的Window類對象 = PhoneWindow類對象
c. Window類的superDispatchTouchEvent() = 1個抽象方法筹误,由子類PhoneWindow類實現(xiàn)
DecorView類是PhoneWindow類的一個內(nèi)部類
b. DecorView繼承自FrameLayout,是所有界面的父類癣缅,F(xiàn)rameLayout是ViewGroup的子類厨剪,故DecorView的間接父類 = ViewGroup
這就實現(xiàn)了事件從activity 傳遞到viewGroup,我們看看源碼:(截取部分代碼)
2.2.ViewGroup事件分發(fā)
我們看源碼中核心分發(fā)方法dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 重點分析1:ViewGroup每次事件分發(fā)時,都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件
// 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false)友存,可通過調(diào)用requestDisallowInterceptTouchEvent()修改
// 判斷值2: onInterceptTouchEvent(ev)
// a. 若在onInterceptTouchEvent()中返回false(即不攔截事件)祷膳,intercepted值為true,從而進入到條件判斷的內(nèi)部
// b. 若在onInterceptTouchEvent()中返回true(即攔截事件)屡立,intercepted值為false直晨,從而跳出了這個條件判斷
// c. 關(guān)于onInterceptTouchEvent() ->>分析1
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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//重點分析2:只有當canceled 、intercepted都為false 時,事件傳遞才能進行下去抡秆,接下來遍歷所有子view
找到被觸摸或者點擊的子view
if (!canceled && !intercepted) {
...
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);
//重點分析3:將事件繼續(xù)傳遞到子view 中奕巍,
// 若該控件可點擊,那么點擊時儒士,dispatchTouchEvent的返回值必定是true,因此會導致條件判斷成立
alreadyDispatchedToNewTouchTarget 賦值為true,并且直接跳出遍歷
// 于是給ViewGroup的dispatchTouchEvent()直接返回了true檩坚,即直接跳出
// 即把ViewGroup的點擊事件攔截掉
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;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//沒有找到目標view,則viewGroup自己處理該事件着撩,調(diào)用onTouch——>onTouchEvent()
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;
}
}
...
return handled;
}
2.3.View的事件分發(fā)過程
接下來我們看view的分發(fā)
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
/**重點分析1:*/
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
重點分析1:
條件1:mOnTouchListener != null,說明:mOnTouchListener變量在View.setOnTouchListener()方法里賦值匾委,也就是說
如果有設置view 的觸摸事件回調(diào)拖叙,那么,view 會優(yōu)先執(zhí)行onTouch方法
條件2:(mViewFlags & ENABLED_MASK) == ENABLED赂乐,該條件是判斷當前點擊的控件是否enable薯鳍,由于很多View默認enable,
故該條件恒定為true
條件3:mOnTouchListener.onTouch(this, event),說明:即 回調(diào)控件注冊Touch事件時的onTouch()挨措;需手動復寫設置,
以button 為例挖滤,
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
若在onTouch()返回true,就會讓上述三個條件全部成立浅役,從而使得View.dispatchTouchEvent()直接返回true斩松,事件分發(fā)結(jié)束
若在onTouch()返回false,就會使得上述三個條件不全部成立觉既,從而使得View.dispatchTouchEvent()中跳出If惧盹,執(zhí)行onTouchEvent(event)
繼續(xù)來看onTouchEvent()事件處理
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//若該控件可點擊,則進入switch判斷中
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//若當前的事件是抬起View(主要分析)
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
// 若該控件可點擊瞪讼,就一定返回true
return true;
}
// 若該控件不可點擊钧椰,就一定返回false
return false;
}
當view 可點擊切且手勢為抬手,進過一些判斷符欠,會進入執(zhí)行performClick()方法嫡霞,這里會判斷是否有設置點擊事件setOnclickListener(),
如果有背亥,執(zhí)行onClick(),并返回true
分析到這里秒际,我們可以發(fā)現(xiàn),view的 onTouch()的執(zhí)行 先于 onClick()
總結(jié):
1.dispatchTouchEvent() 逐上至下調(diào)用 Activity——>viewGroup——>View
2.onTouchEvent() 逐下至上級返回 view——>ViewGroup——>Activity
3.View的滑動沖突
滑動沖突有三種解決方式
- 外部攔截法
- 內(nèi)部攔截法
3.1 外部攔截法
點擊事件都先進過父容器的攔截處理狡汉,如果父容器需要此事件娄徊,就攔截,不需要則不攔截盾戴,外部攔截方法需要重寫父容器的onInterceptTouchEvent方法寄锐,在內(nèi)部做相應的攔截即可,類似這樣的偽代碼:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE:{
if(父容器需要當前點擊事件)
{
intercepted = true;
}else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
3.2 內(nèi)部攔截法
內(nèi)部攔截法指父容器不攔截任何事件,所有事件都傳給子元素橄仆,如果子元素需要則消耗掉剩膘,不需要則交還給父容器處理,盆顾,需配合requestDisallowInterceptTouchEvent()方法才能正常工作怠褐,我們需要重寫子元素的
dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x-mLastX;
int deltaY = y-mLastY;
if(父容器需要當前點擊事件)
{
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP:{
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要處理外,父元素也需要默認攔截除了ACTION_DOWN以外的其他事件您宪,這樣當子元素調(diào)用
parent.requestDisallowInterceptTouchEvent(false)方法時奈懒,父元素才能繼續(xù)攔截所需的事件。
父元素修改如下:
public Boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
renturn false;
}else {
return true;
}
}