在Android上,不止一個途徑來偵聽使用者和應用程式之間互動的事件。對於使用者介面裡的事件,偵聽方法就是從與使用者互動的特定視圖對象截獲這些事件。視圖類提供了相應的手段。
在各種用來組建布局的視圖類裡面,你可能會注意到一些公用的回調方法看起來對使用者介面事件有用。這些方法在該對象的相關動作發生時被Android架構調用。比如,當一個視圖(如一個按鈕)被觸摸時,該對象上的onTouchEvent()方法會被調用。不過,為了偵聽這個事件,你必須擴充這個類並重寫該方法。很明顯,擴充每個你想使用的視圖對象(只是處理一個事件)是荒唐的。這就是為什麼視圖類也包含了一個嵌套介面的集合,這些介面含有實現起來簡單得多的回呼函數。這些介面叫做事件接聽程式event listeners,是用來截獲使用者和你的介面互動動作的"門票"。
當你更為普遍的使用事件接聽程式來偵聽使用者動作時,總有那麼一次你可能得為了建立一個自訂群組件而擴充一個視圖類。也許你想擴充按鈕Button類來使某些事更花哨。在這種情況下,你將能夠使事件處理器event handlers類來為你的類定義預設事件行為。
事件接聽程式Event Listeners
事件接聽程式是視圖View類的介面,包含一個單獨的回調方法。這些方法將在視圖中註冊的接聽程式被使用者介面操作觸發時由Android架構調用。下面這些回調方法被包含在事件接聽程式介面中:
onClick():包含於View.OnClickListener。當使用者觸摸這個item(在觸摸模式下),或者通過瀏覽鍵或跟蹤球聚焦在這個item上,然後按下"確認"鍵或者按下跟蹤球時被調用。
onLongClick():包含於View.OnLongClickListener。當使用者觸摸並控制住這個item(在觸摸模式下),或者通過瀏覽鍵或跟蹤球聚焦在這個item上,然後保持按下"確認"鍵或者按下跟蹤球(一秒鐘)時被調用。
onFocusChange():包含於View.OnFocusChangeListener。當使用者使用瀏覽鍵或跟蹤球瀏覽進入或離開這個item時被調用。
onKey():包含於View.OnKeyListener。當使用者聚焦在這個item上並按下或釋放裝置上的一個按鍵時被調用。
onTouch():包含於View.OnTouchListener。當使用者執行的動作被當做一個觸摸事件時被調用,包括按下,釋放,或者螢幕上任何的移動手勢(在這個item的邊界內)。
onCreateContextMenu():包含於View.OnCreateContextMenuListener。當正在建立一個操作功能表的時候被調用(作為持續的"長點擊"動作的結果)。
這些方法是它們相應介面的唯一"住戶"。要定義這些方法並處理你的事件,在你的活動中實現這個嵌套介面或定義它為一個匿名類。然後,傳遞你的實現的一個執行個體給各自的View.set...Listener() 方法。(比如,調用setOnClickListener()並傳遞給它你的OnClickListener實現。)
下面的例子說明了如何為一個按鈕註冊一個點擊接聽程式:
// Create an anonymous implementation of OnClickListenerprivate OnClickListener mCorkyListener = new OnClickListener() { public void onClick(View v) { // do something when the button is clicked }}; protected void onCreate(Bundle savedValues) { ... // Capture our button from layout Button button = (Button)findViewById(R.id.corky); // Register the onClick listener with the implementation above button.setOnClickListener(mCorkyListener); ...}
你可能會發現把OnClickListener作為活動的一部分來實現會便利的多。這將避免額外的類載入和對象分配。比如:
public class ExampleActivity extends Activity implements OnClickListener { protected void onCreate(Bundle savedValues) { ... Button button = (Button)findViewById(R.id.corky); button.setOnClickListener(this); } // Implement the OnClickListener callback public void onClick(View v) { // do something when the button is clicked } ...}
注意上面例子中的onClick()回調沒有傳回值,但是一些其它事件接聽程式必須返回一個布爾值。原因和事件相關。對於其中一些,原因如下:
onLongClick() – 返回一個布爾值來指示你是否已經消費了這個事件而不應該再進一步處理它。也就是說,返回true 表示你已經處理了這個事件而且到此為止;返回false 表示你還沒有處理它和/或這個事件應該繼續交給其他on-click接聽程式。
onKey() –返回一個布爾值來指示你是否已經消費了這個事件而不應該再進一步處理它。也就是說,返回true 表示你已經處理了這個事件而且到此為止;返回false 表示你還沒有處理它和/或這個事件應該繼續交給其他on-key接聽程式。
onTouch() - 返回一個布爾值來指示你的接聽程式是否已經消費了這個事件。重要的是這個事件可以有多個彼此跟隨的動作。因此,如果當接收到向下動作事件時你返回false,那表明你還沒有消費這個事件而且對後續動作也不感興趣。那麼,你將不會被該事件中的其他動作調用,比如手勢或最後出現向上動作事件。
記住按鍵事件總是遞交給當前焦點所在的視圖。它們從視圖層次的頂層開始被分發,然後依次向下,直到到達恰當的目標。如果你的視圖(或者一個子視圖)當前擁有焦點,那麼你可以看到事件經由dispatchKeyEvent()方法分發。除了從你的視圖截獲按鍵事件,還有一個可選方案,你還可以在你的活動中使用onKeyDown() and onKeyUp()來接收所有的事件。
注意: Android 將首先呼叫事件處理器,其次是類定義中合適的預設處理器。這樣,從這些事情接聽程式中返回true 將停止事件向其它事件接聽程式傳播並且也會阻塞視圖中的缺事件處理器的回呼函數。因此當你返回true時確認你希望終止這個事件。
事件處理器Event Handlers
如果你從視圖建立一個自訂群組件,那麼你將能夠定義一些回調方法被用作預設的事件處理器。在建立自訂群組件Building Custom Components的文檔中,你將學習到一些用作事件處理的通用回呼函數,包括:
- onKeyDown(int, KeyEvent) - 當一個新的按鍵事件發生時被調用。
- onKeyUp(int, KeyEvent) - 當一個向上鍵事件發生時被調用。
- onTrackballEvent(MotionEvent) - 當一個跟蹤球運動事件發生時被調用。
- onTouchEvent(MotionEvent) - 當一個觸控螢幕移動事件發生時調用。
- onFocusChanged(boolean, int, Rect) - 當視圖獲得或者丟失焦點時被調用。
你應該知道還有一些其它方法,並不屬於視圖類的一部分,但可以直接影響你處理事件的方式。所以,當在一個布局裡管理更複雜的事件時,考慮一下這些方法:
- Activity.dispatchTouchEvent(MotionEvent) - 這允許你的活動可以在分發給視窗之前捕獲所有的觸摸事件。
- ViewGroup.onInterceptTouchEvent(MotionEvent) - 這允許一個視圖組ViewGroup 在分發給子視圖時觀察這些事件。
- ViewParent.requestDisallowInterceptTouchEvent(boolean) - 在一個父視圖之上調用這個方法來表示它不應該通過onInterceptTouchEvent(MotionEvent)來捕獲觸摸事件。
觸摸模式Touch Mode
當使用者使用方向鍵或跟蹤球瀏覽使用者介面時,有必要給使用者可操作的item(比如按鈕)設定焦點,這樣使用者可以知道哪個item將接受輸入。不過,如果這個裝置有觸摸功能,而且使用者通過觸摸來和介面互動,那麼就沒必要高亮items,或者設定焦點到一個特定的視圖。這樣,就有一個互動模式 叫"觸摸模式"。
對於一個具備觸摸功能的裝置,一旦使用者觸控螢幕幕,裝置將進入觸摸模式。自此以後,只有isFocusableInTouchMode()為真的視圖才可以被聚焦,比如文本編輯組件。其他可觸摸視圖,如按鈕,在被觸摸時將不會接受焦點;它們將只是在被按下時簡單的觸發on-click接聽程式。任何時候使用者按下方向鍵或滾動跟蹤球,這個裝置將退出觸摸模式,然後找一個視圖來接受焦點,使用者也許不會通過觸控螢幕幕的方式來恢複介面互動。
觸摸模式狀態的維護貫穿整個系統(所有視窗和活動)。為了查詢目前狀態,你可以調用isInTouchMode() 來查看這個裝置當前是否處於觸摸模式中。
處理焦點Handling Focus
架構將根據使用者輸入處理常規的焦點移動。這包含當視圖刪除或隱藏,或者新視圖出現時改變焦點。視圖通過isFocusable()方法表明它們想擷取焦點的意願。
要改變視圖是否可以接受焦點,可以調用setFocusable()。在觸摸模式中,你可以通過isFocusableInTouchMode()查詢一個視圖是否允許接受焦點。你可以通過setFocusableInTouchMode()方法來改變它。焦點移動基於一個在給定方向尋找最近鄰居的演算法。少有的情況是,預設演算法可能和開發人員的意願行為不匹配。在這些情況下,你可以通過下面布局檔案中的XML屬性提供顯式的重寫:nextFocusDown, nextFocusLeft, nextFocusRight, 和nextFocusUp。為失去焦點的視圖增加這些屬性之一。定義屬性值為擁有焦點的視圖的ID。比如:
<LinearLayout android:orientation="vertical" ... > <Button android:id="@+id/top" android:nextFocusUp="@+id/bottom" ... /> <Button android:id="@+id/bottom" android:nextFocusDown="@+id/top" ... /></LinearLayout>
通常,在這個豎向布局中,從第一個按鈕向上瀏覽或者從第二個按鈕向下都不會移動到其它地方。現在這個頂部按鈕已經定義了底部按鈕為nextFocusUp (反之亦然),瀏覽焦點將從上到下和從下到上迴圈移動。
如果你希望在使用者介面中聲明一個可設定焦點視圖(通常不是這樣),可以在你的布局定義中,為這個視圖增加android:focusable XML 屬性。把它的值設定成true。你還可以通過android:focusableInTouchMode在觸摸模式下聲明一個視圖為可聚焦。
想請求一個接受焦點的特定視圖,調用requestFocus()。
要偵聽焦時間點事件(當一個視圖獲得或者失去焦點時被通知到),使用onFocusChange(),如上面事件接聽程式Event Listeners所描述的那樣。
觸摸事件、點擊事件的區別
針對螢幕上的一個View控制項,Android如何區分應當觸發onTouchEvent,還是onClick,亦或是onLongClick事件?
在Android中,一次使用者操作可以被不同的View按次序分別處理,並將完全響應了使用者一次UI操作稱之為消費了該事件(consume),那麼Android是按什麼次序將事件傳遞的呢?又在什麼情況下判定為消費了該事件?
搞清楚這些問題對於編寫出能正確響應UI操作的代碼是很重要的,尤其當螢幕上的不同View需要針對此次UI操作做出各種不同響應的時候更是如此,一個典型例子就是使用者在案頭上放置了一個Widget,那麼當使用者針對widget做各種操作時,案頭本身有的時候要對使用者的操作做出響應,有時忽略。只有搞清楚事件觸發和傳遞的機制才有可能保證在介面布局非常複雜的情況下,UI控制項仍然能正確響應使用者操作。
1. onTouchEvent
onTouchEvent中要處理的最常用的3個事件就是:ACTION_DOWN、ACTION_MOVE、ACTION_UP。
這三個事件標識出了最基本的使用者觸控螢幕幕的操作,含義也很清楚。雖然大家天天都在用它們,但是有一點請留意,ACTION_DOWN事件作為起始事件,它的重要性是要超過ACTION_MOVE和ACTION_UP的,如果發生了ACTION_MOVE或者ACTION_UP,那麼一定曾經發生了ACTION_DOWN。
從Android的原始碼中能看到基於這種不同重要性的理解而實現的一些互動機制,SDK中也有明確的提及,例如在ViewGroup的onInterceptTouchEvent方法中,如果在ACTION_DOWN事件中返回了true,那麼後續的事件將直接發給onTouchEvent,而不是繼續發給onInterceptTouchEvent。
2. onClick、onLongClick與onTouchEvent
曾經看過一篇文章提到,如果在View中處理了onTouchEvent,那麼就不用再處理onClick了,因為Android只會觸發其中一個方法。這個理解是不太正確的,針對某個view,使用者完成了一次觸碰操作,顯然從感應器上得到的訊號是手指按下和抬起兩個操作,我們可以理解為一次Click,也可以理解為發生了一次ACTION_DOWN和ACTION_UP,那麼Android是如何理解和處理的呢?
在Android中,onClick、onLongClick的觸發是和ACTION_DOWN及ACTION_UP相關的,在時序上,如果我們在一個View中同時覆寫了onClick、onLongClick及onTouchEvent的話,onTouchEvent是最先捕捉到ACTION_DOWN和ACTION_UP事件的,其次才可能觸發onClick或者onLongClick。主要的邏輯在View.java中的onTouchEvent方法中實現的:
case MotionEvent.ACTION_DOWN: mPrivateFlags |= PRESSED; refreshDrawableState(); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { postCheckForLongClick(); } break; case MotionEvent.ACTION_UP: if ((mPrivateFlags & PRESSED) != 0) { boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (!mHasPerformedLongPress) { if (mPendingCheckForLongPress != null) { removeCallbacks(mPendingCheckForLongPress); } if (!focusTaken) { performClick(); } } … break;
可以看到,Click的觸發是在系統捕捉到ACTION_UP後發生並由performClick()執行的,performClick裡會調用先前註冊的監聽器的onClick()方法:
public boolean performClick() { … if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
LongClick的觸發則是從ACTION_DOWN開始,由postCheckForLongClick()方法完成:
private void postCheckForLongClick() { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout()); }
可以看到,在ACTION_DOWN事件被捕捉後,系統會開始觸發一個postDelayed操作,delay的時間在Eclair2.1上為500ms,500ms後會觸發CheckForLongPress線程的執行:
class CheckForLongPress implements Runnable { … public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } } … }
如果各種條件都滿足,那麼在CheckForLongPress中執行performLongClick(),在這個方法中將調用onLongClick():
public boolean performLongClick() { … if (mOnLongClickListener != null) { handled = mOnLongClickListener.onLongClick(View.this); } … }
從實現中可以看到onClick()和onLongClick()方法是由ACTION_DOWN和ACTION_UP事件捕捉後根據各種情況最終確定是否觸發的,也就是說如果我們在一個Activity或者View中同時監聽或者覆寫了onClick(),onLongClick()和onTouchEvent()方法,並不意味著只會發生其中一種。