Android:实现类似QQ、微信的表情输入键盘

阅读时间 约 1 分钟

终于有时间继续写博客了,上一篇博客中提到的使用「adjustPan」模式来实现表情输入键盘的思路,在这一段时间的使用过程中出现了很多兼容性问题,各种机型(有无虚拟按键)、系统版本(高度返回值是否包含状态栏)返回的软键盘高度与实际的软键盘高度不同,监听的调用方式也不尽相同,最后导致界面出现错位。起始回头仔细想想,让软键盘覆盖表情键盘这种办法从代码角度看确实很傻。。。不够优雅。

研究了一段时间、翻看了一些开源项目的实现原理,特别是著名的「四次元」,给了我很大启发,我重写了一遍依赖库,给北邮人论坛客户端更新后,再没有人反馈表情键盘有问题了,下面简单谈谈实现的思路。

demo

SoftInputMode 模式

首先,Android系统在界面上弹出软键盘时会将整个Activity的高度压缩,即默认的SoftInputMode是「AdjustResize」,直觉上表情键盘的高度应该设置得和软键盘相同,显示表情键盘时的同时将软键盘收起。这种思路比较直观,比「AdjustPan」模式好的地方在于表情键盘只有两种状态(Visible/Gone),而不是三种(Visible/Invisible/Gone),处理起来逻辑上会更简单。

表情键盘的高度

如果按照上面的思路直接写,应该是这样:

emotionButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (mEmotionLayout.isShown()) {
            hideEmotionLayout();
        } else {
            showEmotionLayout();
        }
    }
});

代码很简单,几乎没有别的逻辑判断,同样的,我们在显示表情键盘之前,需要判断一下软键盘的高度,将其设置给表情键盘:

private void showEmotionLayout() {
    int softInputHeight = getSupportSoftInputHeight();
    if (softInputHeight == 0) {
        softInputHeight = sp.getInt(SHARE_PREFERENCE_TAG, 400);
    }
    hideSoftInput();
    mEmotionLayout.getLayoutParams().height = softInputHeight;
    mEmotionLayout.setVisibility(View.VISIBLE);
}

private void hideEmotionLayout(boolean showSoftInput) {
    if (mEmotionLayout.isShown()) {
        mEmotionLayout.setVisibility(View.GONE);
        if (showSoftInput) {
            showSoftInput();
        }
    }
}

界面跳动

但是运行起来会发现一个问题,点击按钮打开表情键盘时,输入框会上下跳动一下。分析一下原因,点击按钮后,先收起了软键盘,当前Activity的高度变高,输入框回到了界面底部;再打开表情键盘时,输入框又被顶上来,所有看起来点击按钮后输入框会上下跳动一下。无论是先隐藏软键盘还是先打开表情键盘都会有这个问题。

如果这时候去纠结隐藏软键盘和打开表情键盘如何同步的话就会走进一个牛角尖,去处理不同机型之间的兼容性问题了。其实解决思路非常简单,输入框不是会上下跳么,那固定它的位置不就好了?

举个例子,如果整个界面的根布局是LinearLayout,那么一个控件的位置其实是由它上面所有控件的高度决定的,如果它上面所有控件的高度都不变化,那即使整个Activity的高度变化(开/关软键盘)也不会影响这个控件的位置,也就不会发生跳动了。

我们假设有这样两个方法lockContentHeight()unlockContentHeight(),用来锁定和解锁输入框上面的所有控件的高度,那么点击按钮的监听就应该这样写:

@Override
public void onClick(View v) {
    if (mEmotionLayout.isShown()) {
        lockContentHeight();
        hideEmotionLayout(true);
        unlockContentHeight();
    } else {
        if (isSoftInputShown()) {
            lockContentHeight();
            showEmotionLayout();
            unlockContentHeight();
        } else {
            showEmotionLayout();
        }
    }
}

为了更好的表现形式,这里的unlockContentHeight()可以替换成unlockContentHeightDelayed(),即延迟一会(例如200ms)再解锁高度,留出播放软键盘收回动画的时间。

锁定和解锁高度

现在整个问题的关键就在于锁定和解锁控件的高度了,那么如何实现lockContentHeight()unlockContentHeight()这两个函数呢?我们仍以根布局是LinearLayout为例,一个典型的界面布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />

    <include
        layout="@layout/reply_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

其中ListView的layout_height为0dp、layout_weight为1,这样这个ListView就会自动充满整个布局,这里ListView可以替换成任意控件(比如一个RelativeLayout,内部有更复杂的布局)。

当你需要锁定这个ListView的高度时:

private void lockContentHeight() {
    LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
    params.height = mContentView.getHeight();
    params.weight = 0.0F;
}

将weight置0,然后将height设置为当前的height,在父控件(LinearLayout)的高度变化时它的高度也不再会变化。而解锁高度时这样做:

private void unlockContentHeightDelayed() {
    mEditText.postDelayed(new Runnable() {
        @Override
        public void run() {
            ((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
        }
    }, 200L);
}

在上面的函数中解锁高度其实只有一句话:LinearLayout.LayoutParams.weight = 1.0F;,在Java代码里动态更改LayoutParam的weight,会导致父控件重新onLayout(),从而达到改变控件的高度的目的。

总结

整体思路基本上就是:

  1. 点击表情按钮
  2. 锁定内容高度
  3. 收起软键盘
  4. 显示表情键盘
  5. 解锁内容高度

是不是比上一篇博客中分析三种显示状态,监听软键盘变化而做同步变化的方法要简单明了很多?

详细代码见我的Github:dss886/Android-EmotionInputDetector