前言
现在很多应用都会有评论功能,方便用户随时反馈使用效果,为了避免文本评论的单调问题,往往都会支持一些自定义的表情图片输入软键盘。但是这些表情图片键盘往往不需要像真正的中文输入法那样作为单独的应用安装,这时就需要客户端针对Android软件盘做适配处理,方便用户切换输入法。
实现效果
实现接口
用户评论开发过程中通常需要和EditText控件打交道,编辑框通常编程过程中最常见的就是和软键盘做交互操作。在用户最初进入编辑评论时有时希望直接展示软键盘,减少用户选中输入框操作,最好是保证输入框不被软件盘覆盖住。android:windowSoftInputMode属性就包含了实现这些交互方式的属性,主要分成两个部分,一个是控制软件盘展示或隐藏的state开头属性值,一个是控制展示输入框的Window适配效果的adjust开头属性值,通常都是两个使用或(|)指定键盘盘和Windwo适配效果,最常见的Adjust属性值解释如下:
属性值 | 意义 |
---|---|
adjustPan | 如果Window内容布局可以滚动,尽量将输入框展示在软件盘之上,不能保证软件盘没有遮盖其他内容布局 |
adjustResize | Window内容布局整体压缩,展示软件盘的地方不会展示内容布局 |
adjustNothing | Window布局不做任何调整 |
adjustUnspecified | 根据情况做adjustPan或者adjustResize |
控制软件盘展示交互的state属性如下:
属性值 | 意义 |
---|---|
stateVisible | 初次进入界面展示软键盘 |
stateAlwaysVisible | 不管是初次进入还是导航到别的Activity重新回来,都展示软件盘 |
stateHide | 初次键入不展示软件盘 |
stateAlwaysHide | 不论初次还是从别的Activity导航回来都不展示软件盘 |
stateUnchanged | 软键盘保持在前一个Activity里的状态不论展开还是关闭 |
stateUnspecified | 未指定软键盘状态 |
考虑到要实现软件盘的切换,但是自定义的表情布局是需要展示在用户的界面上,但是要覆盖在系统软件盘界面之上,最重要的一点就是确定软件盘的高度和展示关闭状态。但是Android的内部实现并没有提供软件盘打开关闭的回调功能,这时adjustResize就起到了关键性的作用,可以在adjustResize模式下通过监控Window内容布局整体的高度变化来确定软件盘高度和开闭状态。
考虑到adjustResize会导致整个Window界面的压缩,所以切换的时候难免会引起界面跳动,如果避免这个情况需要了解系统提供的隐藏软键盘的接口,主要有下面两种:
public static void hideSoftInput(EditText editText) {
InputMethodManager inputManager = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromInputMethod(editText.getWindowToken(), 0);
}
public static void hideSoftInputFromWindow(EditText editText) {
InputMethodManager inputManager = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
}
hideSoftInputFromInputMethod调用之后会发现软键盘并没有被关闭,点击输入也没有问题;hideSoftInputFromWindow调用后真的把软键盘关闭了,而且整个Window界面也随之变成了全屏状态。二者的区别在哪里呢,如果你定义一个PopupWindow,在第一种方式关闭的Window上展示在DecorView底部,会发现PopupWindow展示在软件盘的位置,但是第二种PopupWindow会展示在软件盘之上,也就是说如果希望切换的自定义表情键盘展示在软件位置,需要用第一种隐藏方式,尽管看起来实际上并没有被隐藏,但是自定义表情布局会遮盖住它。
实现过程
首先需要实现对软件盘的展示关闭监听操作,这里使用自定以控件来实现。自定义控件需要和Window的根部局同样大小,同时在onSizeChanged方法中监听控件大小的改变,只有高度改变绝对值超过1/5屏幕宽高的时候才认定软件盘状态发生了改变,XML中布局如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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">
// 监控软键盘展示关闭的自定义布局
<com.example.comment.KeyBoardLayout
android:id="@+id/key_board_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// 其他内容布局
....
</FrameLayout>
自定义布局在onSizeChangde中监听大小变化,同时使用接口通知Activity软件盘当前状态:
public class KeyBoardLayout extends View {
private static final String TAG = "KeyBoardLayout";
private KeyBoardListener keyBoardListener;
// 软件盘监听回调接口
public interface KeyBoardListener {
// 当前软件盘是否展示,active为true表示展示,keyBoardHeight表明软件盘高度
void onKeyBoardEvent(boolean active, int keyBoardHeight);
}
public KeyBoardLayout(Context context) {
super(context);
}
public KeyBoardLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public KeyBoardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(TAG, "onSizeChanged(): w = " + w + ", h = " + h + ", oldw = " + oldw + ", oldh = " + oldh);
super.onSizeChanged(w, h, oldw, oldh);
int threshold = CommonUtils.getScreenHeight() / 5;
// 如果旧高度和新高度都是有效值
if (oldh > 0 && h > 0) {
// 旧高度比新高度要大,表明键盘弹起
int keyboardHeight = oldh - h;
if (keyboardHeight > threshold) {
notifyKeyBoardShow(keyboardHeight);
} else if (keyboardHeight < 0) {
notifyKeyBoardHide(-keyboardHeight);
}
}
}
private void notifyKeyBoardHide(int keyboardHeight) {
if (keyBoardListener != null) {
keyBoardListener.onKeyBoardEvent(false, keyboardHeight);
}
}
private void notifyKeyBoardShow(int keyboardHeight) {
if (keyBoardListener != null) {
keyBoardListener.onKeyBoardEvent(true, keyboardHeight);
}
}
public void setOnKeyBoardShow(KeyBoardListener keyBoardListener) {
this.keyBoardListener = keyBoardListener;
}
第二步在HintPopupWindow里自定义表情布局,内部使用PopupWindow做展示操作,在软件盘之上有一个控制布局,控制布局下面才是自定义的表情布局,这里只使用一副图片来代替自定义表情布局。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--软件盘上方的控制布局,负责展示软件盘或者隐藏软件盘 -->
<LinearLayout
android:background="@android:color/white"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:layout_width="match_parent"
android:layout_height="@dimen/comment_hint_height">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/comment_hint"
android:layout_height="match_parent" />
<!--负责切换软件盘和自定义表情布局的按钮-->
<Button
android:id="@+id/comment_hint_btn"
android:text="@string/comment_hide_keyboard"
android:layout_width="wrap_content"
android:layout_height="@dimen/comment_hint_height" />
</LinearLayout>
<!--实际展示的表情布局-->
<ImageView
android:id="@+id/comment_custom_pic"
android:src="@drawable/cute_cat"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
在负责切换自定义表情和软键盘的按钮回调事件里,更新自定义布局的宽高:
@Override
public void onClick(View v) {
// 如果自定义布局高度等于控制部分的高度,也就是当前展示的是软件盘
if (popupWindow.getHeight() == originHeight) {
if (keyboardHeight > 0) {
button.setText(R.string.comment_open_keyboard);
imageView.getLayoutParams().height = keyboardHeight;
popupWindow.setHeight(v.getResources().getDimensionPixelSize(R.dimen.comment_hint_height)
+ imageView.getLayoutParams().height);
// 设置自定义表情布局可见,并且更新PopupWindow的高度
popupWindow.update(CommonUtils.getScreenWidth(), popupWindow.getHeight());
// 隐藏软件盘
KeyboardUtils.hideSoftInput(editText);
}
} else {
// 设置软件盘高度为控制部分的高度,并且打开软件盘
button.setText(R.string.comment_hide_keyboard);
popupWindow.setHeight(originHeight);
popupWindow.update(CommonUtils.getScreenWidth(), popupWindow.getHeight());
KeyboardUtils.showSoftInput(editText);
}
}
最后需要在Activity里设置软件盘的监听事件,并且根据软件盘展示和消失控制自定义表情布局的展示和消失,确保二者不会出现展示冲突。
@Override
public void onKeyBoardEvent(boolean active, int keyBoardHeight) {
isSoftActive = active;
if (active) {
onKeyBoardShow(keyBoardHeight);
} else {
onKeyBoardHide(keyBoardHeight);
}
}
public void onKeyBoardShow(final int keyBoardHeight) {
Log.d(TAG, "onKeyBoardShow(): keyBoardHeight = " + keyBoardHeight);
final int hintTop = CommonUtils.getScreenHeight() - keyBoardHeight - hintPopupWindow.getHeight();
hintPopupWindow.setKeyboardHeight(keyBoardHeight);
hintPopupWindow.show(getWindow().getDecorView(), 0, hintTop);
}
private void onKeyBoardHide(int keyBoardHeight) {
getWindow().getDecorView().postDelayed(new Runnable() {
@Override
public void run() {
hintPopupWindow.hide();
}
}, 100);
}
以上就是实现的主要代码逻辑,详细的内部实现请查看软件盘切换。