Android系統編程入門系列之介面Activity交互響應

上篇文章中已經了解到介面Activity的繪製完全依賴其載入的視圖組件View,不僅如此,用戶的每次觸摸操作都可以在介面Activity內接收並響應,也可以直接傳遞給其中的某個視圖View響應。本文將針對這兩種用戶交互方式分別展開介紹。

介面內交互

介面響應

說到介面交互,很容易想到用戶在設備螢幕上的觸摸操作。可是螢幕那麼大要怎麼確定用戶觸摸的位置呢?Android系統定義了一套螢幕坐標規則,該規則不僅適用於當前的螢幕交互,在後文提及的動畫繪製及其他螢幕相關操作等都同樣適用。該規則將螢幕的左上角作為螢幕坐標的原點,從左上角往右上角延伸的方向作為螢幕坐標的x軸,從左上角往左下角延伸的方向作為螢幕坐標的y軸

比如針對一款 1024×512 尺寸的TV設備,其左下角的螢幕坐標值為 (0, 512),右下角的螢幕坐標值為 (1024, 512),右上角的螢幕坐標值為 (1024, 0),左上角的螢幕坐標值為 (0, 0)。

對螢幕的觸摸位置有了衡量標準,是不是就可以根據不同的位置做觸摸操作了呢?說到觸摸操作,也需要細化之後單獨處理。Android系統將用戶操作行為,大致分為三種:按下行為滑動行為抬起釋放行為。這樣系統就可以根據每一個操作行為做單獨的響應處理了。

另外,用戶的操作對象,除了上文提到的硬體設備螢幕以外,還有硬體設備的按鍵(包括硬體按鍵和虛擬按鍵)。只不過對按鍵的操作行為只有按下行為抬起釋放行為兩種,而且按鍵的操作不需要用到螢幕坐標相關內容。

基於上文的介紹,可以在介面Activity中可以分別重寫下邊三個方法對用戶的介面操作交互做出響應。

  • boolean onTouchEvent(MotionEvent event)
    在子視圖沒有處理的情況下,用戶對硬體設備螢幕的每一個操作,都會回調一次該方法。

    其參數android.view.MotionEvent事件類的實例化對象event。
    event.getAction()方法可以獲取當前事件行為,包括MotionEvent.ACTION_DOWN按下行為MotionEvent.ACTION_MOVE滑動行為MotionEvent.ACTION_UP抬起釋放行為等。
    event.getX()方法獲取當前操作的螢幕坐標x軸值。
    同理event.getY()方法獲取當前操作的螢幕坐標y軸值。

  • boolean onKeyDown(int keyCode, KeyEvent event)
    在子視圖沒有處理的情況下,用戶對硬體設備按鍵的每一次按下行為,都會回調一次該方法。

    參數一int類型的keyCode指定按鍵類型,一般其值與參數二event.getKeyCode()相等。
    參數二android.view.KeyEvent類的實例化對象event。
    event.getAction()方法同樣可以獲取當前事件行為,只有KeyEvent.ACTION_DOWN按下行為KeyEvent.ACTION_UP抬起釋放行為兩個行為值。
    event.getKeyCode()方法可以獲取觸發當前事件的按鍵類型,其值包括KeyEvent.KEYCODE_HOMEHOME鍵KeyEvent.KEYCODE_POWER電源鍵KEYCODE_VOLUME_UP音量增加鍵等。

  • boolean onKeyUp(int keyCode, KeyEvent event)
    在子視圖沒有處理的情況下,用戶對硬體設備按鍵的每一次抬起釋放行為,都會回調一次該方法。其兩個參數與上述onKeyDown()中的兩個參數類似。

視圖響應

相對來說,介面內的視圖響應要繁瑣一些,而能實現的效果也更多樣化。當把視圖View作為用戶的操作對象時,仍然可以重寫上述介面響應的三個方法,但是系統視圖往往也封裝了一層更加簡單粗暴的響應方法。

在視圖中重寫介面響應的三個方法後,如果返回的結果為true,則上文介面響應中的三個方法將不會被回調。

為什麼需要封裝一層響應方法呢?用戶對視圖的操作,往往就是點擊(短時間內執行按下行為抬起釋放行為),長按(在執行按下行為後等待一段時間再執行抬起釋放行為),拖拽(在執行按下行為後執行一段滑動行為之後再執行抬起釋放行為)這些固定操作類型。如果每個視圖都要細分用戶的操作行為,就會有大量冗餘的操作類型判斷程式碼,所以AndroidSDK定義了一系列介面分別對應用戶的操作類型。視圖如果需要響應某個操作,只需要設置其操作類型介面的實例化對象,並在該對象中實現相關方法即可。而這些介面主要有以下三個。

  • View.OnClickListener介面
    需要實現onClick(View view)方法,在該方法內響應響應視圖View被用戶點擊後的程式碼邏輯。
  • View.OnLongClickListener介面
    需要實現onLongClick(View view)方法,在該方法內響應響應視圖View被用戶長按後的程式碼邏輯。
  • View.OnDragListener介面
    需要實現onDrag(View v, DragEvent event)方法,在該方法內響應視圖View被用戶拖拽後的程式碼邏輯。

另外,不同的系統視圖也可能有單獨設置的響應方法,或者自定義視圖也會提供單獨的響應方法,例如列表視圖中的某一行數據被單獨點擊後如何響應,這些都要根據具體的視圖類查找並使用對應的響應方法,這裡不再贅述。

事件傳遞機制

在上文介面響應的三個方法中,關於他們被回調的時機,有個前提是子視圖沒有處理,即子視圖的介面響應方法返回結果為false。這就涉及到Android系統的事件傳遞機制了。
我們知道介面Activity在創建之後會調用setContentView(int layoutId)載入根視圖View,而根視圖裡邊則可以內嵌一層層的子視圖。那麼,如果用戶將手指觸摸到螢幕上,會觸發按下行為,該行為作為事件首先傳遞到根視圖中,之後根視圖再將該事件傳遞給子視圖,子視圖再將該事件傳遞給子視圖的子視圖,這樣按照載入時的嵌套順序一層層傳遞事件,稱之為事件分發
直到該事件傳遞到最後一層子視圖,或者某一層視圖不再繼續傳遞該事件,那麼該事件將在最後傳遞到的這層視圖中被首先處理。而每層視圖在收到傳遞進來的事件後,都有兩條路可以選擇,要麼將該事件繼續傳遞給子視圖,要麼自己處理該事件,如果選擇第二條路不再繼續傳遞子視圖而是自己處理該事件,稱之為事件攔截
一旦某層視圖處理了該事件,那麼其父層視圖將繼續處理該事件,之後是父層的父層視圖處理該事件,事件被這樣一層層處理,直到根視圖處理該事件結束,稱之為事件處理
在經歷了事件分發事件處理之後,這樣的一個事件傳遞機制就算完成了。而上文提到的每一個事件,都是如此。

上述過程在程式碼中的實現,只需要針對事件分發事件攔截事件處理分別定義一個可重寫的方法即可。能夠重寫該方法的位置主要是android.app.Acitivtyandroid.view.View中,由於事件攔截只會發生在子視圖的傳遞過程中,在介面中並不需要,所以事件攔截對應的方法只在android.view.GroupView中重寫。

  • boolean dispatchTouchEvent (MotionEvent event)
    當某個事件被分發到該視圖時,系統回調視圖中的該方法。返回結果表示當前事件是否被處理。
  • boolean onInterceptTouchEvent(MotionEvent event)
    當某個事件被分發到該視圖後,系統會回調視圖中的該方法,根據其返回結果判斷是否攔截該事件交由當前視圖處理。默認返回結果為false,表示不攔截該事件,將會繼續回調子視圖的dispatchTouchEvent()。返回結果為true時,表示攔截該事件,將會回調當前視圖的onTouchEvent().
  • boolean onTouchEvent (MotionEvent event)
    當某個事件輪到該視圖被處理時,系統會回調視圖中的該方法。返回結果表示當前事件是否被處理。

介面間交互

上文介紹了針對一個介面Activity的交互響應,那麼兩個介面Activity之間如何交互呢?這就用到在載入介面一文中啟動Activity所使用的android.content.Intent意圖類了。不同於用戶與介面的交互,介面間交互主要是變數數據的共享,所以通過Intent支援的交互數據類型是有限的。

發送數據介面

在啟動一個介面Activity之前要先創建意圖對象,在該意圖對象調用putExtras(Bundle bundle)方法,可以將要發送的數據打包成android.os.Bundle類型的實例存入。

而該Bundle對象可以存儲的數據類型支援包括booleancharbyteshortintfloatdoublelong八種基本數據類型,String類型和實現Parcelable介面的任意類型,及其[]數組或ArrayList數組,和其他一些不常用類型。這些數據都是以key-value鍵值對的形式保存在Bundle對象中。對於要保存的不同數據類型,分別調用對應的putT(String key, T value)系列方法即可以參數一key和參數二value的形式存入,同樣可以調用對應的getT(String key)系列方法取出指定參數一key對應的value數據,這裡的T泛指支援的不同數據類型。
另外也可以在創建的意圖對象中直接調用putExtra(String key, T value)系列方法,將要發送的數據直接以key-value鍵值對的形式存入,同樣也可以使用getTExtra(String key)系列方法取出指定參數一key對應的value數據,這裡的T同樣泛指Bundle可支援的不同數據類型。

在打包所有的數據後,就可以在當前介面Activity中繼續調用startActivity(Intent intent)系列方法啟動Intent意圖參數中指定的另一介面Activity了。
這裡的startActivity(Intent)方法是最簡單的啟動方法,另外還有startActivity(Intent, Bundle)在啟動時將要發送的數據打包作為參數二傳入。
或者startActivityForResult(Intent intent, int requestCode)在啟動時傳入一個唯一值作為參數二,以區分啟動不同介面的意圖,在啟動的介面Activity返回後,系統會調用當前介面Activity中的onActivityResult(int requestCode, int resultCode, Intent data)方法,因此可以重寫該方法。並根據參數一的唯一性對之前啟動的不同介面意圖做區分處理。參數二是根據啟動介面不同關閉狀態所返回的結果值,默認為android.app.Activity.RESULT_CANCELED,另外也可以為android.app.Activity.RESULT_FIRST_USERandroid.app.Activity.RESULT_OK,其值需要在啟動介面返回時設置。參數三是從啟動介面返回的Intent類型,主要使用其中的Bundle打包數據類型對象,同樣其值可以在啟動介面返回時設置。

接收數據介面

作為接收數據的啟動介面Activity,在其綁定上下文環境之後,一般是在onCreate(Bundle savedInstanceState)方法中,可以使用getIntent()方法獲取傳遞進來的Intent意圖對象,獲取該對象之後自然就可以通過getBExtras()或一系列getTExtra(String key)獲取到打包的數據,這樣在啟動介面中就可以使用在啟動之前上一個介面Activtiy中的變數數據了。

而當啟動介面Activity在被用戶操作返回時,系統將回調該啟動介面的onBackPressed()方法,之後將該Activity從棧中移出並銷毀。所以可以重寫onBackPressed()方法,在該方法中調用setResult(int resultCode, Intent data)設置上文提到的返回時參數。
或者在啟動介面Activity程式碼中也可以主動調用finish()方法,以關閉當前介面。因此在調用finish()方法之前先調用setResult(int resultCode, Intent data)設置返回參數即可。