本篇記錄下Android軟鍵盤的簡單使用和一些注意事項,包括如何獲取軟鍵盤輸入內(nèi)容、打開彈窗自動進入編輯狀態(tài)、點擊空白處收起軟鍵盤。
軟鍵盤簡單使用
軟鍵盤可以通過InputMethodManager來控制鍵盤的顯示和隱藏狀態(tài),鍵盤輸入內(nèi)容可以通過重寫dispatchKeyEvent()方法來獲取。
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="show"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<Button
android:id="@+id/hide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hide"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<MainActivity.kt>
class MainActivity : AppCompatActivity() {
private lateinit var imm : InputMethodManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
show.setOnClickListener {
// 顯示軟鍵盤
imm.toggleSoftInput(0, InputMethodManager.SHOW_IMPLICIT)
}
hide.setOnClickListener {
// 隱藏軟鍵盤
imm.toggleSoftInput(0, 0)
}
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
Log.d("MainActivity", "dispatchKeyEvent = $event ")
if (event != null) {
text.text = event.characters
}
return super.dispatchKeyEvent(event)
}
}
打開彈窗進入編輯狀態(tài)
編輯彈窗在一打開的時候自動拉起軟鍵盤,可以給用戶提供更好的體驗,比如修改密碼時,打開彈窗無需點擊編輯框,直接輸入內(nèi)容,體驗感會更好。
接下來實現(xiàn)功能:進入編輯彈窗,EditText獲取焦點,軟鍵盤顯示。
1. 首先編輯好Dialog的布局文件<dialog_layout.xml>
Dialog包含一個EditText和一個關(guān)閉按鈕。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Button
android:id="@+id/dismiss_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="dismiss"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<EditText
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="請輸入內(nèi)容"
android:textSize="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
2. 自定義DialogFragment<MyDialogFragment.java>
其中編寫了一個startEdit方法,功能就是為傳入的view獲取焦點,并顯示軟鍵盤。
public class MyDialogFragment extends DialogFragment {
@Override
public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_layout, container, false);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initView(view);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
// 點擊外部區(qū)域是否dismiss dialog
dialog.setCanceledOnTouchOutside(false);
// 設(shè)置背景
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.GREEN));
return dialog;
}
@Override
public void onStart() {
super.onStart();
Window window = getDialog().getWindow();
// 設(shè)置dialog寬高
window.setLayout(500, 300);
// 設(shè)置dialog顯示位置
window.setGravity(Gravity.CENTER);
}
private void initView(View view){
EditText editText = view.findViewById(R.id.edit);
startEdit(editText);
view.findViewById(R.id.dismiss_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
}
// 獲取焦點,顯示軟鍵盤
private void startEdit(View view){
view.requestFocus();
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
}
3. 在主程序中觸發(fā)彈窗<MainActivity.java>
在MainActivity中添加一個按鈕,點擊按鈕顯示編輯彈窗MyDialogFragment。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView(){
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MyDialogFragment dialog = new MyDialogFragment();
dialog.show(getSupportFragmentManager(), "test_dialog");
}
});
}
}
4. 問題:功能未實現(xiàn)
運行程序之后發(fā)現(xiàn),打開彈窗并未顯示軟鍵盤,但當在MyDialogFragment中添加按鈕來調(diào)用startEdit()方法時,方法是可以實現(xiàn)的,EditText獲取了焦點,軟鍵盤也正常顯示出來了。
這可能是由于在MyDialogFragment剛打開時布局還沒有完全初始化就調(diào)用startEdit()方法,導致方法未實現(xiàn),修改MyDialogFragment中的initView,延遲一段時間再調(diào)用startEdit()方法就可以實現(xiàn)顯示軟鍵盤的效果。
private void initView(View view){
EditText editText = view.findViewById(R.id.edit);
// 添加延遲任務(wù)來確保視圖已經(jīng)初始化好
new Handler().postDelayed(() -> {
// 獲取焦點,顯示軟鍵盤
startEdit(editText);
}, 500);
view.findViewById(R.id.dismiss_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
}
在MyDialogFragment銷毀時記得要釋放Handler資源。
點擊空白處收起軟鍵盤
點擊空白處收起軟鍵盤有多種方式可以實現(xiàn),這里主要介紹兩種:
(1)點擊根布局時收起軟鍵盤,但當有其他布局覆蓋在根布局上方時,點擊事件被上方的布局攔截,無法觸發(fā)根布局的點擊事件而收起軟鍵盤。
(2)通過觸摸事件實現(xiàn),當點擊EditText以外的的區(qū)域時收起軟鍵盤,此方法更靈活。
下面是分別在Activity、Fragment和DialogFregment中實現(xiàn)點擊空白處收起軟鍵盤的方法,其中Activity和Fragment使用的是(2)觸摸事件實現(xiàn)的方法,DialogFragment使用的是(1)點擊根布局的方法實現(xiàn)。
1. 在Activity中實現(xiàn)
Activity有dispatchTouchEvent(),可以全局性地攔截和處理觸摸事件。
這里重寫Activity中的dispatchTouchEvent()方法,獲取頁面當前焦點View,如果是EditText的話才執(zhí)行 if 中判斷和隱藏軟鍵盤的邏輯。
當前焦點View是EditText,然后獲取EditText的位置和點擊的坐標,如果點擊位置在EditText的范圍內(nèi),并且當前軟鍵盤處于顯示狀態(tài),才通過InputMethodManager 來隱藏軟鍵盤。
這里使用getGlobalVisibleRect() 返回 EditText 控件的實際屏幕位置,使用MotionEvent.getX()和MotionEvent.getY()獲取點擊位置的坐標。
我的理解,getGlobalVisibleRect() 和 MotionEvent.getX() 獲取的是絕對位置,假設(shè)顯示軟鍵盤的時候把EditView的控件頂?shù)巾撁嫔戏?,這時再獲取控件位置和點擊位置坐標與不顯示軟鍵盤時的值是一樣的。而 getLocationInWindow() 和MotionEvent.getRawX() 獲取的是相對位置,軟鍵盤顯示前后EditView相對屏幕的位置不同,獲取到的值也是不同的。
<MainActivity.java>
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
View v = getCurrentFocus();
if (v != null && v instanceof EditText) {
Rect outRect = new Rect();
v.getGlobalVisibleRect(outRect);
// 判斷點擊位置是否在 EditText 范圍內(nèi)
Log.d("===", "outRect: left = " + outRect.left + ", right = " + outRect.right + ", top = " + outRect.top + ", bottom = " + outRect.bottom);
Log.d("===", "ev.getX() = " + ev.getX() + ", ev.getY() = " + ev.getY());
if (!isShouldHideInput(v, ev)) {
// 點擊在EditText范圍內(nèi)
Log.d("===", "is in EditText ");
} else if(isSoftShowing()){
Log.d("===", "isSoftShowing ");
// 如果點擊在EditText范圍外并且軟鍵盤正在顯示,可以隱藏軟鍵盤
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
}
return super.dispatchTouchEvent(ev);
}
上述方法可以實現(xiàn)點擊EditText以外的區(qū)域收起軟鍵盤,但當編輯框是一個如下圖所示的自定義View時,編輯到一半點擊右側(cè)眼睛圖標查看密碼時軟鍵盤就會收起來,或者點叉刪除EditText內(nèi)容重新輸入時軟鍵盤也會收起來,交互體驗就很差。

這種情況就需要增加變量來控制某些控件的點擊不觸發(fā)收起軟鍵盤的操作。
這里增加了變量isShouldHideSoftInput 做為收起軟鍵盤的其中一個判斷變量,只有當同時滿足 軟鍵盤正在顯示 + 點擊區(qū)域在EditText之外 + isShouldHideSoftInput 可以隱藏軟鍵盤 時才能調(diào)用InputMethodManager 方法隱藏軟鍵盤。
當點擊EditText范圍外的某個View不需要收起軟鍵盤時,可以調(diào)用notHideSoftInputOnTouch()方法給這個View設(shè)置觸摸事件的監(jiān)聽器。
notHideSoftInputOnTouch()方法內(nèi)部時通過setOnTouchListener()方法設(shè)置View的觸摸監(jiān)聽,并在onTouch中處理觸摸事件,這里的處理就是將變量isShouldHideSoftInput 設(shè)置為false,這樣在dispatchTouchEvent()中處理觸摸事件時就無法進入 if 中收起軟鍵盤了,并在MotionEvent.ACTION_UP最后恢復isShouldHideSoftInput 的默認值,避免影響下一次操作。
這里要注意,觸摸事件正常的傳遞順序是由Activity的dispatchTouchEvent()一層一層傳遞到View的onTouchEvent()的,但這里是調(diào)用setOnTouchListener()設(shè)置View的觸摸事件監(jiān)聽,觸摸事件會先由Activity的dispatchTouchEvent()傳遞到View的onTouch()。

無論是按下ACTION_DOWN、移動ACTION_MOVE 還是 抬起ACTION_UP 都是一層一層的傳遞觸摸事件的,所以可以在 按下ACTION_DOWN事件 傳遞到View的onTouch()時設(shè)置變量isShouldHideSoftInput 為false,然后在 Activity的dispatchTouchEvent() 中處理后面的 抬起ACTION_UP事件時再判斷是否需要隱藏軟鍵盤,并在最后恢復變量isShouldHideSoftInput 的值,這樣就可以保證變量isShouldHideSoftInput 僅在一次按下抬起期間生效。
<BaseActivity.java>
public class BaseActivity extends AppCompatActivity {
private boolean isShouldHideSoftInput = true;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
View currentFocus = getCurrentFocus();
if (currentFocus != null) {
boolean isOut = isOutOfFocusedEditText(currentFocus, ev);
Log.d("===", "dispatchTouchEvent: " + isSoftShowing() + ", " + isOut + ", " + isShouldHideSoftInput);
if (isSoftShowing() && isOut && isShouldHideSoftInput) {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0);
}
}
}
isShouldHideSoftInput = true;
}
return super.dispatchTouchEvent(ev);
}
public boolean isSoftShowing() {
Rect rect = new Rect();
Window window = getWindow();
if (window != null) {
int screenHeight = window.getDecorView().getHeight();
window.getDecorView().getWindowVisibleDisplayFrame(rect);
return screenHeight - rect.bottom != 0;
}
return false;
}
private boolean isOutOfFocusedEditText(View v, MotionEvent event) {
if (v != null && v instanceof EditText) {
int[] leftTop = new int[2];
v.getLocationInWindow(leftTop);
int left = leftTop[0];
int top = leftTop[1];
int bottom = top + v.getHeight();
int right = left + v.getWidth();
Log.d("===", "left = " + left + ", right = " + right + ", top = " + top + ", bottom = " + bottom);
Log.d("===", "event.getRawX() = " + event.getRawX() + ", event.getRawY() = " + event.getRawY());
return !(event.getRawX() > left && event.getRawX() < right && event.getRawY() > top && event.getRawY() < bottom);
}
return false;
}
public void notHideSoftInput(){
isShouldHideSoftInput = false;
}
@SuppressLint("ClickableViewAccessibility")
public void notHideSoftInputOnTouch(View view) {
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("===", "onTouch View: " + v.toString());
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Context context = v.getContext();
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
if (context instanceof BaseActivity) {
((BaseActivity) context).notHideSoftInput();
}
}
// 在接收到按下事件時返回false,則不會接收到后續(xù)移動和抬起事件
return false;
}
});
}
}
2. 在Fragment中實現(xiàn)
由于Fragment是Activity中的組件,所有Activity中dispatchTouchEvent()可以傳遞并作用于其中的Fragment,實現(xiàn)點擊EditText以外區(qū)域收起軟鍵盤。
如果點擊某些控件不需要收起軟鍵盤的話,F(xiàn)ragment可以獲取Activity的實例并調(diào)用其中的方法,同樣可以調(diào)用BaseActivity中的notHideSoftInputOnTouch()方法實現(xiàn)該需求。
這里MainActivity繼承BaseActivity,MainActivity中添加了MainFragment。
<MainFragment.java>
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ImageView checkIcon= view.findViewById(R.id.check_icon);
ImageView clearIcon= view.findViewById(R.id.clear_icon);
Activity activity = getActivity();
if (activity != null && activity instanceof MainActivity) {
((MainActivity) activity).notHideSoftInputOnTouch(checkIcon);
((MainActivity) activity).notHideSoftInputOnTouch(clearIcon);
}
}
3. 在DialogFragment中實現(xiàn)
彈窗Dialog有兩種實現(xiàn)方式,一種是繼承Dialog,一種是繼承DialogFragment。
Dialog依賴于Activity,也可以通過重寫dispatchTouchEvent()實現(xiàn)收起軟鍵盤的功能。而DialogFragment依賴于Fragment,目前我能想到的方法就是通過點擊根布局來收起軟鍵盤,這種方法沒辦法控制點擊EditText以外的某個控件不會收起軟鍵盤。
<MyDialog.java>
@SuppressLint({"ClickableViewAccessibility", "UseCompatLoadingForDrawables"})
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
// 點擊外部區(qū)域是否dismiss dialog
dialog.setCanceledOnTouchOutside(false);
// 設(shè)置背景
dialog.getWindow().setBackgroundDrawable(requireContext().getResources().getDrawable(R.drawable.dialog_shape));
// 設(shè)置對話框顯示時的監(jiān)聽器
dialog.setOnShowListener(dialogInterface -> {
// 獲取對話框的根視圖
View rootView = dialog.getWindow().getDecorView();
// 設(shè)置觸摸事件監(jiān)聽器
rootView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
View currentFocus = Objects.requireNonNull(getDialog()).getCurrentFocus();
if (currentFocus != null) {
boolean isOut = isOutOfFocusedEditText(currentFocus, event);
Log.d("===", "dispatchTouchEvent: " + isSoftShowing() + ", " + isOut);
if (isSoftShowing() && isOut) {
InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0);
}
return false;
}
}
}
return false;
});
});
return dialog;
}
public boolean isSoftShowing() {
Rect rect = new Rect();
Window window = Objects.requireNonNull(getDialog()).getWindow();
if (window != null) {
int screenHeight = window.getDecorView().getHeight();
window.getDecorView().getWindowVisibleDisplayFrame(rect);
return screenHeight - rect.bottom != 0;
}
return false;
}
private boolean isOutOfFocusedEditText(View v, MotionEvent event) {
if (v != null && v instanceof EditText) {
int[] leftTop = new int[2];
v.getLocationInWindow(leftTop);
int left = leftTop[0];
int top = leftTop[1];
int bottom = top + v.getHeight();
int right = left + v.getWidth();
Log.d("===", "left = " + left + ", right = " + right + ", top = " + top + ", bottom = " + bottom);
Log.d("===", "event.getRawX() = " + event.getRawX() + ", event.getRawY() = " + event.getRawY());
return !(event.getRawX() > left && event.getRawX() < right && event.getRawY() > top && event.getRawY() < bottom);
}
return false;
}