Show soft (virtual) keyboard when EditText got focused
最近寫 app 時碰到一個需求: layout 裡面有一個搜尋欄位 (EditText), 當開啟 activity 時, 希望那個 EditText 能取得焦點並且彈出虛擬鍵盤.
看似簡單的需求其實搞了一整晚, 解法有很多種, 端看 app 需求.
最簡單的方式是以 ScrollView 當底, 裡面的 layout 只要有放置 EditText, activity 開啟後便會自動取得 focus, 無需特別設定. 但如果你的 layout 裡面有多個 EditText 欄位, 肯定不希望一開啟就彈出虛擬鍵盤, 這時候可以透過兩種方式關閉. 一種是 AndroidManifest.xml 裡面在該 activity 的項目設定 android:windowSoftInputMode="stateHidden", 另一種是在程式裡面透過 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) 關閉虛擬鍵盤.
如果不想用 ScrollView 呢 ? 能否直接在 onResume() 時, 透過 requestFocus() 的方式讓虛擬鍵盤彈出來 ? 這說法有點怪, 因為原始的需求中只有一個 EditText, activity 打開本來就會 focus 到 EditText 上. 但有趣的地方在於即使 EditText 有 focus, 虛擬鍵盤仍舊不見蹤影. 依照前面的邏輯, 我們只要在 AndroidManifest.xml 裡面設定 android:windowSoftInputMode="stateVisible" 或者在 onResume() 透過 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) 便可以叫出虛擬鍵盤.
但事情沒有這麼簡單, 這麼做會帶出兩個問題:
- 當程式轉換到下一個畫面, 透過 back 按鈕回到這個 fragment 時, 虛擬鍵盤不會再彈出.
- 因為這是個 fragment, 所以當程式跳到別的畫面 (fragment) 時, 如果按 Home 或者 Multitask 鈕暫時跳出 app, 之後再由其他地方跳回 app, 虛擬鍵盤會莫名其妙的出現.
翻了下文件, 我們可以在 InputMethodManager 找到一個有趣的 method: showSoftInput 感覺上只要EditText 先 requestFocus(), 再把 EditText 與參數帶入, 就可以彈出虛擬鍵盤. 遺憾的是不管怎麼做 (onResume()), 虛擬鍵盤就是不會出來, 而透過 isActive() 會得到 false 的傳回值. 但是在已經出現的畫面中另外執行 showSoftInput() (比如 Button 的 clicklistener), 居然可以成功讓虛擬鍵盤彈出, 而呼叫 showSoftInput() 之前 isActive() 的回傳值竟然是 true ! 花了點時間在 AOSP 尋找答案, 推測在 app 開啟後 layout 畫完時, InputMethodService 尚未完全 ready, 如果太早 (onCreate/onResume/onStart) 對他進行相關請求, 基本上都會失敗. 而且悲劇的是目前也沒有 callback 可以讓 app 知道 InputMethdoService 什麼時候 ready, stackoverflow 上廣為流傳的 workaround 跟我的想法一樣:
mEditText.requestFocus();
mEditText.postDelayed(new Runnable() {
@Override
public void run() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, 0);
}
}, 200);
不過我個人比較傾向另一種作法, 一樣是 onResume() 要求 focus, 但另外 setOnFocusChangeListener 讓 EditText 取得 focus 時彈出虛擬鍵盤:
private void requestSoftInput(View v) {
mIsSoftInputFromImm = imm.isActive();
if (mIsSoftInputFromImm) {
imm.showSoftInput(v, 0);
} else {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
}
@Override
public void onResume() {
super.onResume();
if (mEditText.hasFocus()) {
requestSoftKeyboard(mEditText);
} else {
mEditText.requestFocus();
}
}
@Override
public void onPause() {
super.onPause();
if (mIsSoftInputFromImm) {
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
} else {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
...
mEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
requestSoftInput(v);
}
}
});
}
這麼做有幾個好處:
- 由系統自行決定調出虛擬鍵盤的時機, 而非透過 timer, 避免不必要的小問題.
- 當程式由另一個 fragment 回來時, 仍舊可以藉由 focus 的取得彈出虛擬鍵盤.
- 無論離開時有無顯示虛擬鍵盤, 回來時鍵盤一定會出現.
- 切換 fragment 時不會因為錯誤的狀態導致虛擬鍵盤在奇怪的點彈出.
小小一個功能眉角還挺多的, 如果有朋友碰過類似的問題有更 solid 的 solution 也歡迎提供 :)