在上篇文章中已經了解到界面Activity
的繪制完全依賴其加載的視圖組件View
,不僅如此,用戶的每次觸摸操作都可以在界面Activity
內接收並響應,也可以直接傳遞給其中的某個視圖View
響應。那么對於用戶的操作,應該如何響應,而同一個操作到底是作用於界面,還是界面中的某一個子視圖?針對用戶的操作對象所產生的交互方式不同,本文將分別展開介紹。
界面內交互
界面響應
說到界面交互,很容易想到用戶在設備屏幕上的觸摸操作。可是屏幕那么大要怎么確定用戶觸摸的位置呢?Android系統定義了一套屏幕坐標規則,該規則不僅適用於當前的屏幕交互,在后文提及的動畫繪制及其他屏幕相關操作等都同樣適用。該規則將屏幕的左上角作為屏幕坐標的原點,從左上角往右上角延伸的方向作為屏幕坐標的x軸,從左上角往左下角延伸的方向作為屏幕坐標的y軸。
比如針對一款 1024x512 尺寸的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_HOME
HOME鍵,KeyEvent.KEYCODE_POWER
電源鍵,KEYCODE_VOLUME_UP
音量增加鍵等。 -
boolean onKeyUp(int keyCode, KeyEvent event)
在子視圖沒有處理的情況下,用戶對硬件設備按鍵的每一次抬起釋放行為,都會回調一次該方法。其兩個參數與上述onKeyDown()
中的兩個參數類似。
視圖響應
相對來說,界面內的視圖響應要繁瑣一些,而能實現的效果也更多樣化。當把視圖View
作為用戶的操作對象時,仍然可以重寫上述界面響應的三個方法,但是系統視圖往往也封裝了一層更加簡單粗暴的響應方法。
在視圖中重寫界面響應的三個方法后,如果返回的結果為true,則上文界面響應中的三個方法將不會被回調。
對於重寫界面響應中的三個方法的方式,有兩種代碼實現方式。一是針對已定義的視圖View
類,通過調用類似 setOnTouchListener (View.OnTouchListener l)
等監聽方法,在監聽類中實現相關響應方法即可。或者將已定義的視圖View
作為父類,重新創建一個自定義視圖View
,在新的自定義View
中可以直接重寫相關響應方法,而在聲明使用原視圖View
類的地方,修改為使用新的自定義View
類。通常針對聲明為final
的類只能使用方式一實現。
在
View.OnTouchListener
類中實現的onTouch(View v, MotionEvent event)
方法,會優先於視圖View
內部的onTouchEvent(MotionEvent event)
被調用,因此如果onTouch(View v, MotionEvent event)
返回結果為true,將不會回調視圖View
內部的onTouchEvent(MotionEvent event)
,而且該視圖View
內部的其他響應交互類監聽(如setOnClickListener(View.OnClickListener l)、setOnLongClickListener (View.OnLongClickListener l)等)也不會被調用。
為什么需要封裝一層響應方法呢?用戶對視圖的操作,往往就是點擊(短時間內執行按下行為和抬起釋放行為),長按(在執行按下行為后等待一段時間再執行抬起釋放行為),拖拽(在執行按下行為后執行一段滑動行為之后再執行抬起釋放行為)這些固定操作類型。如果每個視圖都要細分用戶的操作行為,就會有大量冗余的操作類型判斷代碼,所以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.Acitivty
和android.view.View
中,由於事件攔截只會發生在子視圖的傳遞過程中,在界面中並不需要,所以事件攔截對應的方法只在android.view.GroupView
中重寫。
boolean dispatchTouchEvent (MotionEvent event)
當某個事件被分發到該視圖時,系統回調視圖中的該方法。返回結果表示當前事件是否被處理。boolean onInterceptTouchEvent(MotionEvent event)
當某個事件被分發到該視圖后,系統會回調視圖中的該方法,根據其返回結果判斷是否攔截該事件交由當前視圖處理。默認返回結果為false,表示不攔截該事件,將會繼續回調子視圖的dispatchTouchEvent()
。返回結果為true時,表示攔截該事件,將會回調當前視圖的onTouchEvent()
.boolean onTouchEvent (MotionEvent event)
當某個事件輪到該視圖被處理時,會在該視圖的上述事件分發方法boolean dispatchTouchEvent (MotionEvent event)
中回調到該方法。返回結果表示當前事件是否被處理。
界面間交互
上文介紹了針對一個界面Activity
的交互響應,那么兩個界面Activity
之間如何交互呢?這就用到在加載界面一文中啟動Activity
所使用的android.content.Intent
意圖類了。不同於用戶與界面的交互,界面間交互主要是變量數據的共享,所以通過Intent
支持的交互數據類型是有限的。
發送數據界面
在啟動一個界面Activity
之前要先創建意圖對象,在該意圖對象調用putExtras(Bundle bundle)
方法,可以將要發送的數據打包成android.os.Bundle類型的實例存入。
而該Bundle
對象可以存儲的數據類型支持包括boolean
、char
、byte
、short
、int
、float
、double
、long
八種基本數據類型,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_USER
和android.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)
設置返回參數即可。