Android 简单沉浸式弹出输入框

来自:Gxinyu

前言

最近公司项目在写IM聊天室功能,刚开始使用dialog方式,让dialog居底部显示,但是项目中需要文本和表情切换发送消息,但是因为软键盘本来就是一种特殊的dialog,dialog具有优先级,软键盘的优先级总是高于我们平时用到的dialog,所以出现显示消失的时候,因为软键盘与自定义的输入弹出框消失显示时机问题,导致闪烁问题。

网上还有一种方式是在activity的底部直接添加一个view的方式,通过约束其上的布局来解决闪烁问题,但是这种情况输入弹窗会把整个布局顶上去,感觉体验不是特别好。

所以决定去看看其他APP的效果,发现头条和简书的弹出输入框都是都不错,如下图所示:但是头条的弹出输入框在弹出时,系统状态栏会变色,感觉影响体验,简书的弹出输入框是在底部直接添加view的方式,只不过是增加了遮罩层。如何做到效果和dialog一致,点击外部和返回键收起,然后还能切换文本和表情输入,另外不会引起系统状态栏变色,。

头条.png

简书输入框.png

布局添加View

因为要实现当输入框弹出的时候一是不能让原来的内容顶上去,二是输入框内容要覆盖在当前内容的上层。所以不能采取直接在activity的布局底部直接填加view的方式,也不能是dialog的形式,所以只能添加到activity的顶层布局,activity的顶层布局是decorView,然后添加到底部,操作软键盘的隐藏显示来调取弹出输入框,显示软键盘的时候添加,弹出框消失的时候移除。

添加到DecorView

/**
* 添加
*/

if (mContext instanceof Activity) {
    Activity activity = (Activity) mContext;
     final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
     final ViewGroup content = (ViewGroup) decorView.findViewById(android.R.id.content);
     if (getParent() == null) {
           content.addView(this);
     }
}

从DecorView移除

//移除掉当前的view
        ViewParent parent = getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(this);
        }

注意事项:因为弹出框是在下方显示,所以添加到DecorView的顶级View要位于DecorView底部

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    //添加到底部
    android:layout_gravity="bottom"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

</LinearLayout>

点击外部消失

如果点击输入框外面,取消输出框体验更好。为此我们监听当前弹出框的onTouchEvent事件:

/**
     * 处理点击范围,如果在不在就取消
     *
     * @param event
     * @return
     */

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Rect rect = new Rect();
        getInputView().getGlobalVisibleRect(rect);
        if (!WindowUtil.touchIsInRect(event.getX(), event.getY(), rect)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = event.getX();
                    downY = event.getY();
                    downTime = System.currentTimeMillis();
                    break;
                case MotionEvent.ACTION_UP:
                    float dx = event.getX() - downX;
                    float dy = event.getY() - downY;
                    float distance = (float) Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
                    if (distance < touchSlop && (System.currentTimeMillis() - downTime) < isClickTime) {
                        //点击外部就消失
                        onInputListener.onCanceledOnTouchOutside();
                    }
                    downX = 0;
                    downY = 0;
                    downTime = 0;
                    break;
            }
        }
        return true;
    }

触点说明:
1、WindowUtil.touchIsInRect(event.getX(), event.getY(), rect) 检测当前触点是否在弹出框之中
2、onTouchEvent返回值设置为true,响应事件

点击返回键消失

 getInputView().setOnKeyListener(new OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
                    if (onInputListener != null) {
                        onInputListener.onBackPressed();
                    }
                    return true;
                }
                return false;
            }
        });

返回值一定要为true,否则失效.

如果你设置了这个方法,但是发现没有响应,是因为其他地方抢占了焦点,可能是edittext或者是软键盘,这里说明一下软键盘也是一种dialog,dialog默认抢占焦点,所有添加如下方法:

View inputView = getInputView();
            inputView.requestFocus();
            inputView.setFocusable(true);
            inputView.setFocusableInTouchMode(true);

当前弹出框抢占焦点之后,点击back键就会取消软键盘,由于软键盘消失有动画,需要时间,所以有两种方式可以取消弹出框,第一种发送延迟消息的方式,这种方式由于软键盘弹出时间不确定并且如果多次主动弹出会造成消息冲突,如果采用这种方式,尽量去使用防止重复点击监听,第二种是定义中间变量,再监听软键盘消失赋值为消失状态,这时候是消失状态但不是销毁状态。

/**
 * 输入框的四种状态
 */

public static final int INITIALIZE = 0;
public static final int SHOWING = 1;
public static final int DISMISSED= 2;
public static final int DISMISS_OUTSIDE = 100;
public static final int DISMISS_BACKKEY = 101;
public static final int DISMISS_KEYBOARD = 103;
private int currentState = INITIALIZE;

说明:因为之前为了响应返回键和touch事件,强制获取焦点,当弹出框消失的时候主动把焦点返回给当前页面,如果不这么做,可能会遇到其他问题,比如RecyclerView滚动事件等。

     clearFocus();
     View contentView = ((Activity) getContext()).findViewById(android.R.id.content);
     contentView.setFocusable(true);
     contentView.setFocusableInTouchMode(true);

模式切换

查看其它APP,有时候会切换到表情页面,有些APP的软键盘表情页面跟软键盘高度不一致导致有错位的感觉,如果表情页面跟软键盘高度一致体验会好很多,所以我在初始化的时候设置表情页面的高度。

        rlSpecialContent.setVisibility(INVISIBLE);
        ViewGroup.LayoutParams linearParams = (ViewGroup.LayoutParams) rlSpecialContent.getLayoutParams();
        int lastHeight = linearParams.height;
        if (bottomHeight != lastHeight) {
            //不重复设置
            linearParams.height = bottomHeight;
            rlSpecialContent.setLayoutParams(linearParams);
        }

说明:布局中关于表情页面设置Gone,减少绘制,但是显示弹出框的时候一定要给表情页面占位rlSpecialContent.setVisibility(INVISIBLE);并且之后切换的过程中也是如此,不能设置为Gone,否则有闪动错位效果,影响体验。

其他

由于本人比较懒,个人觉得如果弹出框加上动画可能更好一些,不过毕竟动画非常消耗资源,暂且就没加,可以自行添加,由于软键盘方式和各个手机厂商对于Rom的修改,不知道在其他型号手机上是否会出现未知问题,大家如果遇到问题,及时反馈给我,在此感谢大家阅读本文。

最后附上代码
https%3A%2F%2Fgithub.com%2FGxinyu%2FFineInput

推荐↓↓↓
安卓开发
上一篇:已有 Android 项目集成 Flutter 寻坑记 下一篇:Android 技术选型闲聊