講講Android事件攔截機制
簡介
什麼是觸控事件?顧名思義,觸控事件就是捕獲觸控式螢幕幕後產生的事件。當點選一個按鈕時,通常會產生兩個或者三個事件——按鈕按下,這是事件一,如果滑動幾下,這是事件二,當手擡起,這是事件三。所以在Android中特意為觸控事件封裝了一個類MotionEvent
,如果重寫onTouchEvent()方法,就會發現該方法的引數就是這樣的一個MotionEvent
,在一般重寫觸控相關的方法中,引數一般都含有MotionEvent
,可見它的重要性。
那麼MotionEvent
到底是什麼東東呢,它包含了幾種型別。
- Action_Down:手指剛接觸螢幕
- Action_Move:手指在螢幕上移動
- Action_Up:手指從螢幕上鬆開的一瞬間
在正常情況下,一次手指觸控式螢幕幕的行為會觸發一系列點選事件,考慮如下幾種情況:
- 點選屏幕後離開鬆開,事件序列為Down->Up
- 點選螢幕滑動一會再鬆開,事件序列為Down->Move->......>Move->Up
那麼,在MotionEvent
裡面封裝了不少好東西,比如觸控點的座標,可以通過event.getX()方法和event.getRawX(),這兩者區別也很簡單,getX()返回的是相對於當前View左上角的x座標,getRawY()返回是相對於手機螢幕左上角的x座標,同理,y座標也是可以獲取的,getY()和getRawY()方法,MotionEvent
例子
如此看來,觸控事件還是簡單的,其實就是一個動作型別加座標而已。但是我們知道,Android的View結構是樹形結構,也就是說,View可以放在ViewGroup裡面,通過不同的組合來實現不同的樣式,那麼如果View放在ViewGroup裡面,這個ViewGroup又巢狀在另一個ViewGroup裡面,甚至還有可能繼續巢狀,一層層的疊加起來呢,我們先看一個例子,是通過一個按鈕點選的。
XML檔案
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:gravity="center" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/mylayout"> <Button android:id="@+id/my_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="click test"/> </LinearLayout>
Activity檔案
public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
private LinearLayout mLayout;
private Button mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = (LinearLayout) this.findViewById(R.id.mylayout);
mButton = (Button) this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
return false;
}
@Override
public void onClick(View v) {
Log.i(null, "OnClickListener--onClick--"+v);
}
}
以上程式碼很簡單,Activity中有一個LinearLayout(ViewGroup的子類,ViewGroup是View的子類)佈局,佈局中包含一個按鈕(View的子類),然後分別對這兩個控制元件設定了Touch與Click的監聽事件,具體執行結果如下:
1,當穩穩的點選Button時
2,當穩穩的點選除過Button以外的其他地方時:
3,當收指點選Button時按在Button上晃動了一下鬆開後
我們看下onTouch和onClick,從引數都能看出來onTouch比onClick強大靈活,畢竟多了一個event引數。這樣onTouch裡就可以處理ACTION_DOWN、ACTION_UP、ACTION_MOVE等等的各種觸控。現在來分析下上面的列印結果;在1中,當我們點選Button時會先觸發onTouch事件(之所以列印action為0,1各一次是因為按下擡起兩個觸控動作被觸發)然後才觸發onClick事件;在2中也同理類似1;在3中會發現onTouch被多次調運後才調運onClick,是因為手指晃動了,所以觸發了ACTION_DOWN->ACTION_MOVE…->ACTION_UP。
onTouch會有一個返回值,而且在上面返回了false。你可能會疑惑這個返回值有啥效果?那就驗證一下吧,我們將上面的onTouch返回值改為ture。如下:
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, "OnTouchListener--onTouch-- action="+event.getAction()+" --"+v);
return true;
}
顯示結果:
此時onTouch返回true,則onClick不會被調運了。
好了,經過這個簡單的例項驗證你可以總結髮現:
- Android控制元件的Listener事件觸發順序是先觸發onTouch,其次onClick。
如果控制元件的onTouch返回true將會阻止事件繼續傳遞,返回false事件會繼續傳遞。
事件流程
看上面的例子是不是有點困惑,為何OnTouch返回True,onClick就不執行,事件傳遞就中斷,在這裡需要引進一個場景,這樣解釋起來就更形象生動。
首先,請想象一下生活中常見的場景:假如你所在的公司,有一個總經理,級別最高,它下面有個部長,級別次之,最底層就是幹活的你,沒有級別。現在總經理有一個任務,總經理將這個業務佈置給部長,部長又把任務安排給你,當你完成這個任務時,就把任務反饋給部長,部長覺得這個任務完成的不錯,於是就簽了他的名字反饋給總經理,總經理看了也覺得不錯,就也簽了名字交給董事會,這樣,一個任務就順利完成了。這其實就是一個典型的事件攔截機制。
在這裡我們先定義三個類:
一個總經理—MyViewGroupA,最外層的ViewGroup
一個部長—MyViewGroupB,中間的ViewGroup
一個你—MyView,在最底層
根據以上的場景,我們可以繪製以下流程圖:
從圖中,我們可以看到在ViewGroup中,比View多了一個方法—onInterceptTouchEvent()方法,這個是幹嘛用的呢,是用來進行事件攔截的,如果被攔截,事件就不會往下傳遞了,不攔截則繼續。
如果我們稍微改動下,如果總經理(MyViewGroupA)發現這個任務太簡單,覺得自己就可以完成,完全沒必要再找下屬,因此MyViewGroupA就使用了onInterceptTouchEvent()方法把事件給攔截了,此時流程圖:
我們可以看到,事件就傳遞到MyVewGroupA這裡就不繼續傳遞下去了,就直接返回。
如果我們再改動下,總經理(MyViewGroupA)委託給部長(MyViewGroupB),部長覺得自己就可以完成,完全沒必要再找下屬,因此MyViewGroupB就使用了onInterceptTouchEvent()方法把事件給攔截了,此時流程圖:
我們可以看到,MyViewGroupB攔截後,就不繼續傳遞了,同理如果,到乾貨的我們上(MyView),也直接返回True的話,事件也是不會繼續傳遞的,如圖:
原始碼
分析Android View事件傳遞機制之前有必要先看下原始碼的一些關係,如下是幾個繼承關係圖:
看了官方這個繼承圖是不是明白了上面例子中說的LinearLayout是ViewGroup的子類,ViewGroup是View的子類,Button是View的子類關係呢?其實,在Android中所有的控制元件無非都是ViewGroup或者View的子類,說高尚點就是所有控制元件都是View的子類。
1,從View的dispatchTouchEvent方法說起
在Android中你只要觸控控制元件首先都會觸發控制元件的dispatchTouchEvent方法(其實這個方法一般都沒在具體的控制元件類中,而在他的父類View中),所以我們先來看下View的dispatchTouchEvent方法,如下:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
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;
}
dispatchTouchEvent的程式碼有點長,但可以挑幾個重點講講,if (onFilterTouchEventForSecurity(event))
語句判斷當前View是否沒被遮住等,然後定義ListenerInfo區域性變數,ListenerInfo是View的靜態內部類,用來定義一堆關於View的XXXListener等方法;接著if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
語句就是重點,首先li物件自然不會為null,li.mOnTouchListener呢?你會發現ListenerInfo的mOnTouchListener成員是在哪兒賦值的呢?怎麼確認他是不是null呢?通過在View類裡搜尋可以看到:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
li.mOnTouchListener是不是null取決於控制元件(View)是否設定setOnTouchListener監聽,在上面的例項中我們是設定過Button的setOnTouchListener方法的,所以也不為null,接著通過位與運算確定控制元件(View)是不是ENABLED 的,預設控制元件都是ENABLED 的,接著判斷onTouch的返回值是不是true。通過如上判斷之後如果都為true則設定預設為false的result為true,那麼接下來的if (!result && onTouchEvent(event))
就不會執行,最終dispatchTouchEvent也會返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
語句有一個為false則if (!result && onTouchEvent(event))
就會執行,如果onTouchEvent(event)返回false則dispatchTouchEvent返回false,否則返回true。
這下再看前面的例項部分明白了吧?控制元件觸控就會調運dispatchTouchEvent方法,而在dispatchTouchEvent中先執行的是onTouch方法,所以驗證了例項結論總結中的onTouch優先於onClick執行道理。如果控制元件是ENABLE且在onTouch方法裡返回了true則dispatchTouchEvent方法也返回true,不會再繼續往下執行;反之,onTouch返回false則會繼續向下執行onTouchEvent方法,且dispatchTouchEvent的返回值與onTouchEvent返回值相同
2,dispatchTouchEvent總結
在View的觸控式螢幕傳遞機制中通過分析dispatchTouchEvent方法原始碼我們會得出如下基本結論:
- 觸控控制元件(View)首先執行dispatchTouchEvent方法。
- 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。
- 如果控制元件(View)的onTouch返回false或者mOnTouchListener為null(控制元件沒有設定setOnTouchListener方法)或者控制元件不是enable的情況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回一樣。
- 如果控制元件不是enable的設定了onTouch方法也不會執行,只能通過重寫控制元件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回一樣。
- 如果控制元件(View)是enable且onTouch返回true情況下,dispatchTouchEvent直接返回true,不會呼叫onTouchEvent方法。
3,onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
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 & 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) {
// 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();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
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);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
首先地6到14行可以看出,如果控制元件(View)是disenable狀態,同時是可以clickable的則onTouchEvent直接消費事件返回true,反之如果控制元件(View)是disenable狀態,同時是disclickable的則onTouchEvent直接false。多說一句,關於控制元件的enable或者clickable屬性可以通過java或者xml直接設定,每個view都有這些屬性。
接著22行可以看見,如果一個控制元件是enable且disclickable則onTouchEvent直接返回false了;反之,如果一個控制元件是enable且clickable則繼續進入過於一個event的switch判斷中,然後最終onTouchEvent都返回了true。switch的ACTION_DOWN與ACTION_MOVE都進行了一些必要的設定與置位,接著到手擡起來ACTION_UP時你會發現,首先判斷了是否按下過,同時是不是可以得到焦點,然後嘗試獲取焦點,然後判斷如果不是longPressed則通過post在UI Thread中執行一個PerformClick的Runnable,也就是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;
}
這個方法也是先定義一個ListenerInfo的變數然後賦值,接著判斷li.mOnClickListener是不是為null,決定執行不執行onClick。你指定現在已經很機智了,和onTouch一樣,搜一下mOnClickListener在哪賦值的唄,結果發現:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
控制元件只要監聽了onClick方法則mOnClickListener就不為null,而且有意思的是如果調運setOnClickListener方法設定監聽且控制元件是disclickable的情況下預設會幫設定為clickable。
4,onTouchEvent小結
- onTouchEvent方法中會在ACTION_UP分支中觸發onClick的監聽。
- 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action。
小結
通過以上總結,Android中的事件攔截機制,其實跟我們生活中的上下級委託任務很像,領導可以處理掉,也可以下發給下屬員工處理,如果員工處理的好,領導才敢給你下發任務,如果你處理不好,則領導也不敢把任務交給你,這就像在中途把下發的任務的中途攔截掉了。通過流程和原始碼的分析,相信大家能比較容易瞭解事件的分發、攔截、處理事件的流程。在弄清楚順序機制之後,再配合原始碼看,你會更加深入的理解,為什麼流程會是這樣的,最先對流程有一個大致的認識之後,再去理解,這樣就不會一頭霧摸不著頭腦,進而會有更大的學習樂趣,畢竟在學習過程中,保持好奇心是很重要的。