本篇记录下Android软键盘的简单使用和一些注意事项,包括如何获取软键盘输入内容、打开弹窗自动进入编辑状态、点击空白处收起软键盘。
软键盘简单使用
软键盘可以通过InputMethodManager来控制键盘的显示和隐藏状态,键盘输入内容可以通过重写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)
}
}
打开弹窗进入编辑状态
编辑弹窗在一打开的时候自动拉起软键盘,可以给用户提供更好的体验,比如修改密码时,打开弹窗无需点击编辑框,直接输入内容,体验感会更好。
接下来实现功能:进入编辑弹窗,EditText获取焦点,软键盘显示。
1. 首先编辑好Dialog的布局文件<dialog_layout.xml>
Dialog包含一个EditText和一个关闭按钮。
<?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="请输入内容"
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);
// 点击外部区域是否dismiss dialog
dialog.setCanceledOnTouchOutside(false);
// 设置背景
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.GREEN));
return dialog;
}
@Override
public void onStart() {
super.onStart();
Window window = getDialog().getWindow();
// 设置dialog宽高
window.setLayout(500, 300);
// 设置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. 在主程序中触发弹窗<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. 问题:功能未实现
运行程序之后发现,打开弹窗并未显示软键盘,但当在MyDialogFragment中添加按钮来调用startEdit()方法时,方法是可以实现的,EditText获取了焦点,软键盘也正常显示出来了。
这可能是由于在MyDialogFragment刚打开时布局还没有完全初始化就调用startEdit()方法,导致方法未实现,修改MyDialogFragment中的initView,延迟一段时间再调用startEdit()方法就可以实现显示软键盘的效果。
private void initView(View view){
EditText editText = view.findViewById(R.id.edit);
// 添加延迟任务来确保视图已经初始化好
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资源。
点击空白处收起软键盘
点击空白处收起软键盘有多种方式可以实现,这里主要介绍两种:
(1)点击根布局时收起软键盘,但当有其他布局覆盖在根布局上方时,点击事件被上方的布局拦截,无法触发根布局的点击事件而收起软键盘。
(2)通过触摸事件实现,当点击EditText以外的的区域时收起软键盘,此方法更灵活。
下面是分别在Activity、Fragment和DialogFregment中实现点击空白处收起软键盘的方法,其中Activity和Fragment使用的是(2)触摸事件实现的方法,DialogFragment使用的是(1)点击根布局的方法实现。
1. 在Activity中实现
Activity有dispatchTouchEvent(),可以全局性地拦截和处理触摸事件。
这里重写Activity中的dispatchTouchEvent()方法,获取页面当前焦点View,如果是EditText的话才执行 if 中判断和隐藏软键盘的逻辑。
当前焦点View是EditText,然后获取EditText的位置和点击的坐标,如果点击位置在EditText的范围内,并且当前软键盘处于显示状态,才通过InputMethodManager 来隐藏软键盘。
这里使用getGlobalVisibleRect() 返回 EditText 控件的实际屏幕位置,使用MotionEvent.getX()和MotionEvent.getY()获取点击位置的坐标。
我的理解,getGlobalVisibleRect() 和 MotionEvent.getX() 获取的是绝对位置,假设显示软键盘的时候把EditView的控件顶到页面上方,这时再获取控件位置和点击位置坐标与不显示软键盘时的值是一样的。而 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 范围内
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范围内
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);
}
上述方法可以实现点击EditText以外的区域收起软键盘,但当编辑框是一个如下图所示的自定义View时,编辑到一半点击右侧眼睛图标查看密码时软键盘就会收起来,或者点叉删除EditText内容重新输入时软键盘也会收起来,交互体验就很差。
这种情况就需要增加变量来控制某些控件的点击不触发收起软键盘的操作。
这里增加了变量isShouldHideSoftInput 做为收起软键盘的其中一个判断变量,只有当同时满足 软键盘正在显示 + 点击区域在EditText之外 + isShouldHideSoftInput 可以隐藏软键盘 时才能调用InputMethodManager 方法隐藏软键盘。
当点击EditText范围外的某个View不需要收起软键盘时,可以调用notHideSoftInputOnTouch()方法给这个View设置触摸事件的监听器。
notHideSoftInputOnTouch()方法内部时通过setOnTouchListener()方法设置View的触摸监听,并在onTouch中处理触摸事件,这里的处理就是将变量isShouldHideSoftInput 设置为false,这样在dispatchTouchEvent()中处理触摸事件时就无法进入 if 中收起软键盘了,并在MotionEvent.ACTION_UP最后恢复isShouldHideSoftInput 的默认值,避免影响下一次操作。
这里要注意,触摸事件正常的传递顺序是由Activity的dispatchTouchEvent()一层一层传递到View的onTouchEvent()的,但这里是调用setOnTouchListener()设置View的触摸事件监听,触摸事件会先由Activity的dispatchTouchEvent()传递到View的onTouch()。
无论是按下ACTION_DOWN、移动ACTION_MOVE 还是 抬起ACTION_UP 都是一层一层的传递触摸事件的,所以可以在 按下ACTION_DOWN事件 传递到View的onTouch()时设置变量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,则不会接收到后续移动和抬起事件
return false;
}
});
}
}
2. 在Fragment中实现
由于Fragment是Activity中的组件,所有Activity中dispatchTouchEvent()可以传递并作用于其中的Fragment,实现点击EditText以外区域收起软键盘。
如果点击某些控件不需要收起软键盘的话,Fragment可以获取Activity的实例并调用其中的方法,同样可以调用BaseActivity中的notHideSoftInputOnTouch()方法实现该需求。
这里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中实现
弹窗Dialog有两种实现方式,一种是继承Dialog,一种是继承DialogFragment。
Dialog依赖于Activity,也可以通过重写dispatchTouchEvent()实现收起软键盘的功能。而DialogFragment依赖于Fragment,目前我能想到的方法就是通过点击根布局来收起软键盘,这种方法没办法控制点击EditText以外的某个控件不会收起软键盘。
<MyDialog.java>
@SuppressLint({"ClickableViewAccessibility", "UseCompatLoadingForDrawables"})
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
// 点击外部区域是否dismiss dialog
dialog.setCanceledOnTouchOutside(false);
// 设置背景
dialog.getWindow().setBackgroundDrawable(requireContext().getResources().getDrawable(R.drawable.dialog_shape));
// 设置对话框显示时的监听器
dialog.setOnShowListener(dialogInterface -> {
// 获取对话框的根视图
View rootView = dialog.getWindow().getDecorView();
// 设置触摸事件监听器
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;
}