类微信聊天界面输入法随动布局实现原理分析

需求

类似于微信聊天界面的布局,有一些特殊的编辑功能,要求有功能布局随着输入法键盘显示,一部分要显示在键盘上方,在某些模式切换后,输入法键盘区域要替换成相应的功能布局。

输入法键盘本质是个dialog,由系统控制,作为第三方应用能够使上力的地方并不多,然而业界有一句经典的话很著名,叫“XX能实现,我们为什么不行”,确实,解决方案还是能够找到的。

源码

实际上这类型的需求也是很多第三方输入法要做的基本功能,在github上找到了一个开源的输入法项目 XhsEmoticonsKeyboard ,其中找到了实现的上述功能的办法,本文是针对该项目上述功能实现的方案分析。另外,感谢项目贡献者 zhongdaxia的开源分享。

实现分析

整个开源工程的功能不少,大约有四个界面。针对我所遇到的需求这里只分析com.keyboard.activity.ChattingListActivity 这个界面,是一个类似于微信聊天界面的ACTIVITY。跟所有网上的输入法使用demo一致,该功能的实现也是基于manifest中定义adjustResize:

android:windowSoftInputMode="adjustResize"

而此定义有效的前提是相对应的activity不能设为全屏模式。

结构和简析

源码目录

核心源码在XhsEmoticonsKeyboard库工程中,整个随动功能在红框框出的几个类中实现,它们的类图关系如下:

可以看到功能是基于ReleativeLayout实现的。其中ResizeLayout复写了onSizeChanged、onMeasure和onLayout方法,并定义了OnResizeListener接口:

public interface OnResizeListener {

    /** 软键盘弹起 */
    void OnSoftPop(int height);

    /** 软键盘关闭 */
    void OnSoftClose(int height);

    /** 软键盘高度改变 */
    void OnSoftChanegHeight(int height);
}

实现了布局变化的监听,并且拥有判别软键盘弹起、关闭和改变的能力,通过接口回调给使用者;

AutoHeightLayout 则在 ResizeLayout 的基础上复写了 addView 方法,通过有条件的布局规范约束、对OnResizeListener的实现从而在合适的时机动态布局需要变化的部分,实现了需求中“随动”的部分;

至于 XhsEmoticonsKeyBoardBar ,则是最重工程代码中使用的类,它包装了很多比较定制化的接口和功能,整个布局的初始化和回调处理可以再这里分析。

代码分析

ResizeLayout —— 布局变化监听的实现,通过三个函数的复写:

  1. onSizeChanged:记录初始高度(在本工程中则是全屏高度)到变量mMaxParentHeight;
  2. onMeasure:在测量函数中将高度强制写死到初始高度,manifest中的配置使输入法布局发生变化时activitty的整个布局会跟随重新测绘,这里的复写保证本布局不会在这个过程中被压缩或者平移;
  3. onLayout:布局过程的复写其实没有布局的事儿,只是通过测量时保存的高度列表heightList,来分析当前布局的状态 —— 也对应着输入法键盘的状态,从而通过回调接口返回当前的输入法状态;

AutoHeightLayout —— 如何随动起来

@Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        int childSum = getChildCount();
        if (getChildCount() > 1) {
            throw new IllegalStateException("can host only one direct child");
        }

    super.addView(child, index, params);
    if (childSum == 0) {
        //设置第一个子view 为RelativeLayout.ALIGN_PARENT_BOTTOM 并记录ID
        mAutoHeightLayoutId = child.getId();
        if (mAutoHeightLayoutId < 0) {
            child.setId(ID_CHILD);
            mAutoHeightLayoutId = ID_CHILD;
        }
        RelativeLayout.LayoutParams paramsChild = (LayoutParams) child.getLayoutParams();
        paramsChild.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        child.setLayoutParams(paramsChild);
    } else if (childSum == 1) {
        //唯一可以有的第二个子view 设置居于AutoHeightLayout之上
        RelativeLayout.LayoutParams paramsChild = (LayoutParams) child.getLayoutParams();
        paramsChild.addRule(RelativeLayout.ABOVE, mAutoHeightLayoutId);
        child.setLayoutParams(paramsChild);
    }
}

其一、在于对addView的复写限制了这个Layout的子view数量,实际上也变相的要求继承者在本身的布局设置上要设定好自动调节高度(AutoHeight)这一块,而其子view限定必须放在自动调节高度布局之上。那么在AutoHeight高度发生变化时,子view就能够跟随AutoHeight来变化了;

其二、AutoHeightLayout实现了OnResizeListener,并且在所有其所有接口中都调用了下面这个函数:

private void setAutoViewHeight(final int height) {
    int heightDp = Utils.px2dip(mContext, height);
    if (heightDp > 0 && heightDp != mAutoViewHeight) {
        mAutoViewHeight = heightDp;
        Utils.setDefKeyboardHeight(mContext, mAutoViewHeight);
    }

    if (mAutoHeightLayoutView != null) {
        mAutoHeightLayoutView.setVisibility(View.VISIBLE);
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mAutoHeightLayoutView.getLayoutParams();
        params.height = height;
        mAutoHeightLayoutView.setLayoutParams(params);
    }
}

可以看出,在键盘布局发生变化时,mAutoHeightLayoutView被重新设置了布局参数,重新测绘,而mAutoHeightLayoutView需要外部通过set来设定。也就是说AutoHeightLayout本身不能直接使用,实现功能的同时也对其下一步的实现者作出了一定的约束,并且这些约束是由代码逻辑决定的,比较隐晦。

XhsEmoticonsKeyBoardBar 最终的实现

这部分是最终定制功能的实现部分,实际上对于最初的需求实现我们只要关心几个点就足够了。AutoHeightLayout对应本身的布局定义、调用的setAutoHeightLayoutView设置进去的是哪一块view、该Layout包裹的子view定义(实际上这部分在这个工程中就是Activity的布局)。

对于前两个点、在 XhsEmoticonsKeyBoardBar 得到构造中就能够找到,本身在构造的过程中通过 inflate 工具解析加载 R.layout.view_keyboardbar 布局文件、并在接下来的初始化过程中将ID为 R.id.ly_foot_func 的 view 进行了 setAutoHeightLayoutView设置。代码比较简单,直接看这两处布局的定义:

整个布局会在addView的复写代码中(childSum == 0)这个分支上强制布局为向父布局底部对齐,其中表情区的部分被设置为AutoHeightLayoutView。那么表情区这一部分随着输入法的变化而变化,可以预见的是:当输入法隐藏时,表情区高度重新布局为0,输入区则会紧贴父布局底部;当输入法显示是,表情区高度随输入法布局高度变化而变化,至于是否显示要依靠功能逻辑,输入区则一直紧靠输入法上端显示。

至于对应Activity的布局,其中唯一一个子布局(XhsEmoticonsKeyBoardBar中也仅能包含一个)会在addView的复写代码中(childSum == 1)这个分支中,布局为处于第一个子view上放,在这里也就是始终显示在输入框、分割线以上,保证了内部显示内容的输入法随动。

总结

  1. 移植过程中,我们只是针对XhsEmoticonsKeyBoardBar这一部分,根据自己的不同的功能部分进行改造,当然,要留意上文所提到的一些约束。再上层的两个类直接引用即可;
  2. 对于输入法相关的布局问题,根据监听布局变化来动态布局这个解决方案的难点实际上都在两个基础类中实现,解决了很大一部分问题,避开坑之后使用起来还是比较顺手、兼容性也不错;
  3. 最大的前提是要设置 adjustResize 属性,所以缺憾之处在于界面无法设置为全屏。
感谢您赏个荷包蛋~