Android 觸控模式(Touch Mode)
什麼是焦點?
在非觸屏手機時代或電腦上,我們通常需要用鍵盤、 滑鼠、軌跡球(trackball)與介面進行互動,當互動的時候必須使目標控制元件獲得焦點(比如高亮起來),這樣使用者才會注意到是什麼控制元件接受輸入。而如果是在觸屏時代,使用者可以直接用手指點選控制元件,這個時候就沒必要將目標高亮了(即獲取焦點)。這也就是接下來我們要講的觸控模式(Touch Mode)。
觸控模式
當用戶使用方向鍵或軌跡球導航使用者介面時,必須聚焦到可操作專案上(如按鈕),以便使用者看到將接受輸入的物件。 但是,如果裝置具有觸控功能且使用者開始通過觸控介面與之互動,則不再需要突出顯示專案或聚焦到特定檢視物件上。 因此,有一種互動模式稱為“觸控模式”(Touch Mode)
觸控模式是使用者和手機進行互動時view層次結構的一個狀態。代表了最近一次的互動是否是通過觸控式螢幕發生的,因為在Android裝置上還存在別的互動方式,比如鍵盤、等等。
對於支援觸控功能的裝置,當用戶觸控式螢幕幕時,裝置會立即進入觸控模式。無論何時,只要使用者點選方向鍵或滾動軌跡球,裝置就會退出觸控模式並找到一個檢視使其獲得焦點。 現在,使用者可在不觸控式螢幕幕的情況下繼續與使用者介面互動。
整個系統(所有視窗和 Activity)都將保持觸控模式狀態。要查詢當前狀態,您可以呼叫View#isInTouchMode() 來檢查裝置目前是否處於觸控模式。
焦點和觸控模式
在觸控模式下,沒有焦點,沒有選擇。同樣,任何已聚焦的控制元件當進入觸控模式時都變為未聚焦狀態。而當使用軌跡球和鍵盤時,就會立即離開觸控模式,控制元件就會變成聚焦的狀態。
現在我們知道了焦點不可以存在於觸控模式了吧,但是這並不完全正確,焦點其實是可以一種特殊的方式存在於觸控模式中的,我們稱之為聚焦( focusable)。這種特殊的模式是專門為可接收文字輸入的控制元件建立的,比如EditText。這就是為什麼使用者可以直接輸入文字到文字框中,而不必先用手指選擇其文字框的原因。
在觸控模式中,任何控制元件只要是可聚焦(focusable )的狀態,當用戶點選其控制元件時,該控制元件就會接收到其焦點。如果是不可聚焦的,點選控制元件將不會接收到焦點。如下所示,當用戶點選EditText時,EditText會接收到焦點:
對於支援觸控功能的裝置,當用戶觸控式螢幕幕時,裝置會立即進入觸控模式。 自此以後,只有 isFocusableInTouchMode() 為 true 的檢視才可聚焦,如文字編輯小部件EditText。其他可觸控的檢視(如按鈕Button)在使用者觸控時不會獲得焦點;按下時它們只是觸發點選偵聽器。
在觸控模式下,只有少部分的控制元件預設是可聚焦的狀態,例如EditText。可以通過setFocusableInTouchMode或xml中android:focusableInTouchMode設定控制元件是否可聚焦。
setFocusableInTouchMode 和 setFocusable
很多人對這兩個方法有疑問其實很簡單:
- setFocusable:設定控制元件是否能獲取焦點。可以通過isFocusable()獲取其狀態。
- setFocusableInTouchMode:在觸控模式下,設定控制元件是否允許聚焦。可以通過isFocusableInTouchMode() 獲取其狀態。
在使用鍵盤或軌跡球的情況下,只有setFocusable為true的控制元件,才可以獲取焦點(選中時高亮)。而在觸控模式下,setFocusable為true,並無法保證控制元件可以獲取焦點。setFocusable為true只能保證在非觸控模式下,該控制元件可以允許獲取焦點。如果想在在觸控模式中,改變控制元件是否允許聚焦,請使用setFocusableInTouchMode進行更改。
從上面我們也可以看出,不管是否在觸控模式下,控制元件獲取焦點的前提是isFocusable()為true。而在觸控模式下,只有isFocusable()和isFocusableInTouchMode()都為ture的情況下,控制元件才允許聚焦。
下面通過設定setFocusableInTouchMode和setFocusable的先後順序,檢視控制元件的焦點狀態:
button.setFocusableInTouchMode(false);
button.setFocusable(true);
Log.d("cryc","isFocusable "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable true isFocusableInTouchMode false
button.setFocusableInTouchMode(true);
button.setFocusable(false);
Log.d("cryc","isFocusable "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable false isFocusableInTouchMode false
button.setFocusable(true);
button.setFocusableInTouchMode(false);
Log.d("cryc","isFocusable "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable true isFocusableInTouchMode false
button.setFocusable(false);
button.setFocusableInTouchMode(true);
Log.d("cryc","isFocusable "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable true isFocusableInTouchMode true
通過上面我們發現如下的規律:
setFocusableInTouchMode為true,會使isFocusable也變為true,而setFocusableInTouchMode為false並不影響isFocusable。
setFocusable為false,會使isFocusableInTouchMode變為false,而setFocusable為true並不影響isFocusableInTouchMode。
下面我列出了各種常用控制元件的預設初始狀態:
控制元件 | Focusable | FocusableInTouchMode | Clickable | LongClickable |
---|---|---|---|---|
View | false | false | false | false |
TextView | false | false | false | false |
EditText | true | true | true | true |
Button | true | false | true | false |
ImageButton | true | false | true | false |
ImageView | false | false | false | false |
CheckBox | true | false | true | false |
RadioButton | true | false | true | false |
ProgressBar | false | false | false | false |
LinearLayout | false | false | false | false |
RelativeLayout | false | false | false | false |
其他Layout都幾乎一樣 | false | false | false | false |
從上面我們可以看出,大部分的控制元件FocusableInTouchMode屬性都為false。只有類似EditText這種控制元件才為true,因為EditText需要提供在沒使用者點選的條件下,彈出一個軟鍵盤進行輸入的功能。
控制元件可聚焦的注意點
我們現在有如下的佈局,設定如下的兩個Button都是可聚焦的:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.mytesttwo.MainActivity">
<Button
android:layout_width="match_parent"
android:text="one"
android:focusableInTouchMode="true"
android:id="@+id/btnOne"
android:layout_height="wrap_content" />
<Button
android:layout_width="match_parent"
android:text="two"
android:focusableInTouchMode="true"
android:id="@+id/btnTwo"
android:layout_height="wrap_content" />
</LinearLayout>
並設定了兩個button點選和焦點的監聽事件:
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("cryc","btnOne onClick");
}
});
btnOne.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
Log.d("cryc","btnOne onFocusChange "+ hasFocus);
}
});
btnTwo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("cryc","btnTwo onClick");
}
});
btnTwo.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
Log.d("cryc","btnTwo onFocusChange "+hasFocus);
}
});
當首次進後,應用介面是下面這樣的
這時候焦點在按鈕one上,使按鈕的背景變成了橘黃色,Log也打印出了資訊。
btnOne onFocusChange true
這時候當我們點選按鈕two的時候,焦點切換到按鈕two上:
btnOne onFocusChange false
btnTwo onFocusChange true
注意Log打印出的資訊,當用戶點選button two的時候,點選事件並沒有響應,只有焦點事件響應了。這時候有的開發者會覺得很奇怪?通常我們不是點選一下就可以響應按鈕的點選事件嗎? 因為當前的按鈕處於可聚焦的狀態,要讓按鈕響應事件必須點選兩下:
- 第一下,使焦點聚焦於button上。
- 第二下,才是真正響應點選的事件。
從上面我們也可以看出有時候設定focusableInTouchMode為true真的不能亂用,不然可能導致上面的情況發生。
在預設擁有可聚焦屬性的EditText也是如此,當進入帶有EditText控制元件的頁面時,EditText會自動獲取焦點(黃色邊框+游標閃爍),但此時輸入法軟鍵盤是不會自動彈起的(除非你特殊設定),也是需要使用者點選EditText才會彈起輸入法:
java EditTextOne onFocusChange true
但是當我們已經處在頁面中時,使用者只要點選一次第二個的輸入框時,軟鍵盤就會彈起。不要以為第二個的輸入框接收了點選事件(onClick),它接收的還是焦點事件,之所以會彈起,因為系統內部做了處理。
EditTextOne onFocusChange false
EditTextTwo onFocusChange true
那系統內部如何做處理的呢?我認為可能是在onTouchEvent方法中做處理,注意了只要有觸控控制元件的操作都會觸發onTouchEvent方法,當手指接觸控制元件的那一刻先響應的是onTouchEvent方法,然後觸發的才是onFocusChange方法或onClick方法。
注意:要想讓控制元件不觸發onTouchEvent方法,設定控制元件disable是沒有效果的,控制元件的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE這三個狀態必須都為false才行。還有一個方法就是可以通過父控制元件進行截獲輸入事件。
在專案中,一進入一個頁面, EditText預設就會自動獲取焦點。那麼如何取消這個預設行為呢?有人在EditText的父級控制元件中找一個,設定成可聚焦的狀態:android:focusableInTouchMode=”true” 把EditText的焦點進行截獲,使焦點轉移到父控制元件上:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:focusableInTouchMode="true"
>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
雖然解決了當前的問題,但是這裡卻有個隱含的風險,在有些低版本的系統中,當你把焦點轉移到父控制元件的身上時,如果從後臺切換到前臺,會導致整個介面傳送抖動。而這個bug通常很少人會得出原因,因此在轉移焦點的時候要特別注意。
由於設定了focusableInTouchMode屬性後會引起和android正常互動行為的不一致,所以android建議我們保守地使用這個屬性,在你確定要用它之前最好三思而後行。