第一部分 android按鍵事件處理流程 keyevent
規則如下:
1.View的各種KeyEvent.Callback接口早於Activity的對應接口被調用;
2.整個處理環節中只要有一處表明處理掉了,則處理結束,不在往下傳遞;
3.各種Callback接口的處理優先級低於監聽器,也就是說各種onXXXListener的方法優先被調用。
舉例:當控件沒有獲取焦點時,只有activity中的onKeyDown()可以獲取物理鍵的點擊事件。當自定義的控件獲取焦點時,事件的獲取順序是: 和控件綁定的監聽器首先獲取事件,然後是自定義控件代碼中覆蓋的onKeyDown()獲取事件,最後是activity中的onKeyDown()獲取點擊事件。第二部分 View的事件分發
button的onClick事件與onTouch事件
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
Log1順序:
onTouch execute, action 0
onTouch execute, action 1
onClick execute
可以看到,onTouch是優先於onClick執行的,並且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。因此view的事件傳遞的順序是先經過onTouch,再傳遞到onClick。
但是如果onTouch事件返回true時,
Log2 順序是:
onTouch execute, action 0
onTouch execute, action 1
也就是說,該按鍵事件由onTouch消費了,不往下傳遞了.
那麼此時疑問出現,這個分發事件的順序由誰控制,先後順序是什麼?爲什麼先執行onTouch事件?爲什麼不傳遞給onClick事件?
答:任何一個控件都會調用自己的的dispatchTouchEvent方法來分發事件和處理事件的.
舉例:當我們去點擊按鈕的時候,就會去調用Button類裏的dispatchTouchEvent方法,可是你會發現Button類裏並沒有這個方法,那麼就到它的父類TextView裏去找一找,你會發現TextView裏也沒有這個方法,繼續在TextView的父類View裏找一找,View中是有這個方法的.dispatchTouchEvent源碼如下:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
由源碼中,可知,第一綁定了mOnTouchListener監聽器,第二view對象是enable的,第三onTouch方法裏返回true時,也就是說mOnTouchListener.onTouch(this, event)監聽onTouch回調方法返回爲true時,dispatchTouchEvent就不會再繼續執行了,直接返回true值,既不會調用onTouchEvent(event)方法.
結合Log1與Log2可知,onTouch事件返回true時,onClick事件不會被執行,說明onClick方法是在onTouchEvent(event)方法中調用的.onTouchEvent(event)源碼如下:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
// 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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
其中 performClick()方法的源碼爲:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
如果控件是可點擊的,那麼就會進入到switch判斷中去,若是擡起手指的事件,則會進入到MotionEvent.ACTION_UP這個case當中。在經過種種判斷之後,會執行到performClick()方法,從performClick()源碼中可知,只要mOnClickListener不是null,就會執行當前控件的onClick方法.因此,onClick方法是在onTouchEvent(event)中調用的結論正確.
另外touch事件的層級傳遞。我們都知道如果給一個控件註冊了touch事件,每次點擊它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這裏需要注意,如果你在執行ACTION_DOWN的時候返回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,纔會觸發後一個action。
容易混淆的地方:在onTouch事件裏面返回了false與在執行ACTION_DOWN的時候返回了false,是不一樣的情況,前者是返回了false,導致dispatchTouchEvent中前面部分條件不成立,就一定會進入到onTouchEvent方法中,而onTouchEvent方法源碼中不管當前的case是那一條,都會最終返回一個true,所以會有當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,纔會觸發後一個action的結論.而後者是,進入到onTouchEvent方法中,對switch的某一條case執行完了,且返回了一個false,導致後面一系列其它的action就不會再得到執行了.
如果是不可點擊的view時,註冊touch事件,且返回false時
imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
Log3爲:onTouch execute, action 0;
Log3的結果分析:在ACTION_DOWN執行完後,後面的一系列action都不會得到執行了。因爲ImageView和按鈕不同,它是默認不可點擊的,在onTouchEvent方法中, if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) 條件不成立,無法進入到if的內部,直接跳到返回了false,也就導致後面其它的action都無法執行了。與button按鈕在ACTION_DOWN的時候返回了false,是一樣的效果.
結論:
1. onTouch和onTouchEvent有什麼區別,又該如何使用?
從源碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中調用的,onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。
2.另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能爲空,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實現。
3.如果想要使用ImageView,可以有兩種改法。第一,在ImageView的onTouch方法裏返回true,這樣可以保證ACTION_DOWN之後的其它action都能得到執行,才能實現圖片滾動的效果。第二,在佈局文件裏面給ImageView增加一個android:clickable=”true”的屬性,這樣ImageView變成可點擊的之後,即使在onTouch裏返回了false,ACTION_DOWN之後的其它action也是可以得到執行的。
- 第三部分 ViewGroup的事件分發
ViewGroup就是一組View的集合,它包含很多的子View和子VewGroup,是android中所有佈局的父類或間接父類,像LinearLayout、RelativeLayout等都是繼承自ViewGroup的。但ViewGroup實際上也是一個View,只不過比起View,它多了可以包含子View和定義佈局參數的功能。
demo代碼如下,其中,button1,button2是myLayout中的子控件,myLayout是繼承LinearLayout佈局的.
myLayout.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "myLayout on touch");
return false;
}
});
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "You clicked button1");
}
});
button2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "You clicked button2");
}
});
分別點擊一下Button1、Button2和空白區域,Log4輸出順序是:
You clicked button1
You clicked button2
myLayout on touch
當myLayout類中重寫了如下方法,並返回true值,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
此時,分別點擊一下Button1、Button2和空白區域,Log5輸出順序是:
myLayout on touch
myLayout on touch
myLayout on touch
Android中touch事件的傳遞,絕對是先傳遞到ViewGroup,再傳遞到View的。當點擊了某個控件,首先會去調用該控件所在佈局的dispatchTouchEvent方法,然後在佈局的dispatchTouchEvent方法中找到被點擊的相應控件,再去調用該控件的dispatchTouchEvent方法。如果我們點擊了MyLayout中的按鈕,會先去調用MyLayout的dispatchTouchEvent方法,可是你會發現MyLayout中並沒有這個方法。那就再到它的父類LinearLayout中找一找,發現也沒有這個方法。繼續再找LinearLayout的父類ViewGroup,你終於在ViewGroup中看到了這個方法,按鈕的dispatchTouchEvent方法就是在這裏調用的.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) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
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)) {
final 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)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
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 (!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)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
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);
}
ViewGroup的dispatchTouchEvent中的 if (disallowIntercept || !onInterceptTouchEvent(ev)) 條件判斷可知,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個爲true,就會進入到這個條件判斷中。disallowIntercept指是否禁用掉事件攔截的功能,默認false,也可通過用requestDisallowInterceptTouchEvent方法對這個值進行修改。當disallowIntercept爲false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的內部,第二個值是對onInterceptTouchEvent方法的返回值取反!也就是說如果我們在onInterceptTouchEvent方法中返回false,就會讓第二個值爲true,從而進入到條件判斷的內部,如果我們在onInterceptTouchEvent方法中返回true,就會讓第二個值爲false,從而跳出了這個條件判斷。
Log4分析:而if (disallowIntercept || !onInterceptTouchEvent(ev)) 條件判斷的內部通過一個for循環,遍歷了當前ViewGroup下的所有子View,然後判斷當前遍歷的View是不是正在點擊的View,如果是的話就會調用了該View的dispatchTouchEvent,之後的流程就和 第二部分是一樣的了。執行按鈕點擊事件或者onTouch事件.
調用子View的dispatchTouchEvent後是有返回值的。如果一個控件是可點擊的,那麼點擊該控件時,dispatchTouchEvent的返回值必定是true。也就是說ViewGroup的dispatchTouchEvent方法直接返回了true。導致後面的代碼無法執行到了,印證了Log4打印的結果,按鈕的點擊事件得到執行,return true ,不再執行super.dispatchTouchEvent(ev), 就會把MyLayout的touch事件攔截掉。
Log5分析:由於剛剛在MyLayout中重寫了onInterceptTouchEvent方法,且返回true,導致所有按鈕的點擊事件都被屏蔽了,那我們就完全有理由相信,按鈕點擊事件的處理就是在條件判斷的內部進行的!
如果空白區域呢?這種情況就一定不會判斷 if (disallowIntercept || !onInterceptTouchEvent(ev)) 進入內部執行了,而是繼續執行後面的代碼,後面代碼中的判斷條件if (target == null)成立的話,就會進入到該條件判斷內部,這裏一般情況下target都會是null,因此會調用super.dispatchTouchEvent(ev)。這句代碼會調用到哪裏呢?當然是View中的dispatchTouchEvent方法了,因爲ViewGroup的父類就是View。之後的處理邏輯又和前面所說的是一樣的了,也因此MyLayout中註冊的onTouch方法會得到執行。
總結:對整個ViewGroup的事件分發流程的分析最後再來簡單梳理一下吧。
- Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。
- 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,默認返回false。
- 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。