Android 输入框被挡问题完美解决方案

目录
  • 前言
  • 正常情况下的输入框被挡
  • Webview的输入框被挡
    • 小结
  • WindowInsets
  • 源码解析
  • 扩展

前言

前段时间出现了webview的输入框被软键盘挡住的问题,处理之后顺便对一些列的输入框被挡住的情况进行一个总结。

正常情况下的输入框被挡

正常情况下,输入框被输入法挡住,一般给window设softInputMode就能解决。

window.getAttributes().softInputMode = WindowManager.LayoutParams.XXX

有3种情况:

(1)SOFT_INPUT_ADJUST_RESIZE: 布局会被软键盘顶上去

(2)SOFT_INPUT_ADJUST_PAN:只会把输入框给顶上去(就是只顶一部分距离)

(3)SOFT_INPUT_ADJUST_NOTHING:不做任何操作(就是不顶)

SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE的不同在于SOFT_INPUT_ADJUST_PAN只是把输入框,而SOFT_INPUT_ADJUST_RESIZE会把整个布局顶上去,这就会有种布局高度在输入框展示和隐藏时高度动态变化的视觉效果。

如果你是出现了输入框被挡的情况,一般设置SOFT_INPUT_ADJUST_PAN就能解决。如果你是输入框没被挡,但是软键盘弹出的时候会把布局往上顶,如果你不希望往上顶,可以设置SOFT_INPUT_ADJUST_NOTHING。

softInputMode是window的属性,你给在Mainifest给Activity设置,也是设给window,你如果是Dialog或者popupwindow这种,就直接getWindow()来设置就行。正常情况下设置这个属性就能解决问题。

Webview的输入框被挡

但是Webview的输入框被挡的情况下,设这个属性有可能会失效。

Webview的情况下,SOFT_INPUT_ADJUST_PAN会没效果,然后,如果是Webview并且你还开沉浸模式的情况的话,SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_PAN都会不起作用。

我去查看资料,发现这就是经典的issue 5497, 网上很多的解决方案就是通过AndroidBug5497Workaround,这个方案很容易能查到,我就不贴出来了,原理就是监听View树的变化,然后再计算高度,再去动态设置。这个方案的确能解决问题,但是我觉得这个操作不可控的因素比较多,说白了就是会不会某种机型或者情况下使用会出现其它的BUG,导致你需要写一些判断逻辑来处理特殊的情况。

解法就是不用沉浸模式然后使用SOFT_INPUT_ADJUST_RESIZE就能解决。但是有时候这个window显示的时候就需要沉浸模式,特别是一些适配刘海屏、水滴屏这些场景。

setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)

那我的第一反应就是改变布局

window. setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);

这样是能正常把弹框顶上去,但是控件内部用的也是WRAP_CONTENT导致SOFT_INPUT_ADJUST_RESIZE改变布局之后就恢复不了原样,也就是会变形。而不用WRAP_CONTENT用固定高度的话,SOFT_INPUT_ADJUST_RESIZE也是失效的。

没事,还要办法,在MATCH_PARENT的情况下我们去设置fitSystemWindows为true,但是这个属性会让出一个顶部的安全距离,效果就是向下偏移了一个状态栏的高度。

这种情况下你可以去设置margin来解决这个顶部偏移的问题。

params.topMargin = statusHeight == 0 ? -120 : -statusHeight;
view.setLayoutParams(params);

这样的操作是能解除顶部偏移的问题,但是布局有可能被纵向压缩,这个我没完全测试过,我觉得如果你布局高度是固定的,可能不会受到影响,但我的webview是自适应的,webview里面的内容也是自适应的,所以我这出现了布局纵向压缩的情况。

举个例子,你的view的高度是800,状态栏高度是100,那设fitSystemWindows之后的效果就是view显示700,paddingTop 100,这样的效果,设置params.topMargin =-100,之后,view显示700,paddingTop 100。大概是这个意思:能从视觉上消除顶部偏移,但是布局纵向被压缩的问题没得到处理

所以最终的解决方法是改WindowInsets的Rect (这个我等下会再解释是什么意思)

具体的操作就是在你的自定义view中加入下面两个方法

@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
    fitSystemWindows = true;
    super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
    Log.v("mmp", "测试顶部偏移量:  "+insets.top);
    insets.top = 0;
    return super.fitSystemWindows(insets);
}

小结

解决WebView+沉浸模式下输入框被软键盘挡住的步骤:

  • window.getAttributes().softInputMode设置成SOFT_INPUT_ADJUST_RESIZE
  • 设置view的fitSystemWindows为true,我这里是webview里面的输入框被挡住,设的就是webview而不是父View
  • 重写fitSystemWindows方法,把insets的top设为0

WindowInsets

根据上面的3步操作,你就能处理webview输入框被挡的问题,但是如果你想知道为什么,这是什么原理。你就需要去了解WindowInsets。我们的沉浸模式的操作setSystemUiVisibility和设置fitSystemWindows属性,还有重写fitSystemWindows方法,都和WindowInsets有关。

WindowInsets是应用于窗口的系统视图的插入。例如状态栏STATUS_BAR和导航栏NAVIGATION_BAR。它会被view引用,所以我们要做具体的操作,是对view进行操作。

还有一个比较重要的问题,WindowInsets的不同版本都是有一定的差别,Android28、Android29、Android30都有一定的差别,例如29中有个android.graphics.Insets类,这是28里面没有的,我们可以在29中拿到它然后查看top、left等4个属性,但是只能查看,它是final的,不能直接拿出来修改。

但是WindowInsets这块其实能讲的内容比较多,以后可以拿出来单独做一篇文章,这里就简单介绍下,你只需要指定我们解决上面那些问题的原理,就是这个东西。

源码解析

大概对WindowInsets有个了解之后,我再带大家简单过一遍setFitsSystemWindows的源码,相信大家会印象更深。

public void setFitsSystemWindows(boolean fitSystemWindows) {
    setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}

它这里只是设置一个flag而已,如果你看它的注释(我这里就不帖出来了),他会把你引导到protected boolean fitSystemWindows(Rect insets)这个方法(我之后会说为什么会到这个方法)

@Deprecated
protected boolean fitSystemWindows(Rect insets) {
    if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
        if (insets == null) {
            // Null insets by definition have already been consumed.
            // This call cannot apply insets since there are none to apply,
            // so return false.
            return false;
        }
        // If we're not in the process of dispatching the newer apply insets call,
        // that means we're not in the compatibility path. Dispatch into the newer
        // apply insets path and take things from there.
        try {
            mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
            return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
        } finally {
            mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
        }
    } else {
        // We're being called from the newer apply insets path.
        // Perform the standard fallback behavior.
        return fitSystemWindowsInt(insets);
    }
}

(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0 这个判断后面会简单讲,你只需要知道正常情况是执行fitSystemWindowsInt(insets)

而fitSystemWindows又是哪里调用的?往前跳,能看到是onApplyWindowInsets调用的,而onApplyWindowInsets又是由dispatchApplyWindowInsets调用的。其实到这里已经没必要往前找了,能看出这个就是个分发机制,没错,这里就是WindowInsets的分发机制,和View的事件分发机制类似,再往前找就是viewgroup调用的。前面说了WindowInsets在这里不会详细说,所以WindowInsets分发机制这里也不会去展开,你只需要先知道有那么一回事就行。

public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    try {
        mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
        if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
            return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
        } else {
            return onApplyWindowInsets(insets);
        }
    } finally {
        mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
    }
}

假设mPrivateFlags3是0,PFLAG3_APPLYING_INSETS是20,0和20做或运算,就是20。然后判断是否有mOnApplyWindowInsetsListener,这个Listener就是我们有没有在外面做

setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
    @Override
    public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
        ......
        return insets;
    }
});

假设没有,调用onApplyWindowInsets

public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
        // We weren't called from within a direct call to fitSystemWindows,
        // call into it as a fallback in case we're in a class that overrides it
        // and has logic to perform.
        if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
            return insets.consumeSystemWindowInsets();
        }
    } else {
        // We were called from within a direct call to fitSystemWindows.
        if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
            return insets.consumeSystemWindowInsets();
        }
    }
    return insets;
}

mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS就是20和40做与运算,那就是0,所以调用fitSystemWindows。

而fitSystemWindows的(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)就是20和20做与运算,不为0,所以调用fitSystemWindowsInt。

分析到这里,就需要结合我们上面解决BUG的思路了,我们其实是要拿到Rect insets这个参数,并且修改它的top。

setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
    @Override
    public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
        ......
        return insets;
    }
});

setOnApplyWindowInsetsListener回调中的insets可以拿到android.graphics.Insets这个类,但是你只能看到top是多少,没办法修改。当然你可以看到top是多少,然后按我上面的做法Margin设置一下

params.topMargin = -top;

如果你的布局不发生纵向变形,那倒没有多大关系,如果有变形,那就不能用这个做法。从源码看,这个过程主要涉及3个方法。我们能看出最好下手的地方就是fitSystemWindows。因为onApplyWindowInsets和dispatchApplyWindowInsets是分发机制的方法,你要在这里下手的话可能会出现流程混乱等问题。

所以我们这样做来解决fitSystemWindows = true出现的顶部偏移。

@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
    fitSystemWindows = true;
    super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
    Log.v("mmp", "测试顶部偏移量:  "+insets.top);
    insets.top = 0;
    return super.fitSystemWindows(insets);
}

扩展

上面已经解决问题了,这里是为了扩展一下思路。
fitSystemWindows方法是protected,导致你能重写它,但是如果这个过程我们没办法用继承来实现呢?

其实这就是一个解决问题的思路,我们要知道为什么会出现这种情况,原理是什么,比如这里我们知道这个fitSystemWindows导致的顶部偏移是insets的top导致的。你得先知道这一点,不然你不知道怎么去解决这个问题,你只能去网上找别人的方法一个一个试。那我怎么知道是insets的top导致的呢?这就需要有一定的源码阅读能力,还要知道这个东西设计的思想是怎样的。当你知道有这么一个东西之后,再想办法去拿到它然后改变数据。

这里我我们是利用继承protected方法这个特性去获取到insets,那如果这个过程没办法通过继承实现怎么办?比如这里是因为fitSystemWindows是view的方法,而我们自定义view正好继承view。如果它是内部自己写的一个类去实现这个操作呢?

这种情况下一般两种操作比较万金油:

  • 你写一个类去继承它那个类,然后在你写的类里面去改insets,然后通过反射的方式把它注入给View
  • 动态代理

我其实一开始改这个的想法就是用动态代理,所以马上把代码撸出来。

public class WebViewProxy implements InvocationHandler {
    private Object relObj;
    public Object newProxyInstance(Object object){
        this.relObj = object;
        return Proxy.newProxyInstance(relObj.getClass().getClassLoader(), relObj.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if ("fitSystemWindows".equals(method.getName()) && args != null && args.length == 1){
                Log.v("mmp", "测试代理效果 "+args);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return proxy;
    }
}
WebViewProxy proxy = new WebViewProxy();
View viewproxy = (View) proxy.newProxyInstance(mWebView);

然后才发现fitSystemWindows不是接口方法,白忙活一场,但是如果fitSystemWindows是接口方法的话,我这里就可以用通过动态代理加反射的操作去修改这个insets,虽然用不上,但也是个思路。最后发现可以直接重写这个方法就行,我反倒还把问题想复杂了。

以上就是Android 输入框被挡问题完美解决方案的详细内容,更多关于Android 输入框被挡的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android输入框实时模糊搜索效果的示例代码

    Android输入框实时模糊搜索 很多开发场景会用到搜索框实时模糊搜索来帮助用户输入内容,如图 思路是在EditText 字符变动的时候 弹出ListPopupwindow并更新列表,这样的做法google已经封装为AutoCompleteTextView 用法 mAutoCompleteTextView.setAdapter(adapter); mAutoCompleteTextView.setFocusable(true); mAutoCompleteTextView.setOnItemCl

  • Android 详解自定义圆角输入框和按钮的实现流程

    Android-自定义圆角输入框和按钮 我们的征程是星辰大海,而非人间烟尘 自定义圆角输入框 效果 1.在drawable/下面new Drawable Resources File 2.新建shape文件,在里面自定义xml文件样式 代码文件 <!-- res/drawable/button_shape_normal.xml --> <shape xmlns:android="http://schemas.android.com/apk/res/android" a

  • Android EditText输入框实现下拉且保存最近5个历史记录思路详解

    文章结构: 后面又添加了清空历史记录的标签,就是在每一次添加更新后台数组后,数组的下一个标签为清空历史记录. s_btnDown.setOnClickListener(this); //对其进行焦点监听 case R.id.btnDown: showListPopulWindow(); //调用显示PopuWindow 函数 break; 点击后触发PopuWindow函数,也就是将其下拉框,绑定到TextBox标签的下面. private void showListPopulWindow()

  • Android WebView软键盘遮挡输入框方案详解

    目录 背景 纪实 方案 实现 总结 背景 笔者在使用 WebView 加载含有输入框的 H5 页面时,点击输入框后,输入框会被软键盘遮挡住,无法看到输入的内容,这很影响用户体验. 笔者想着这种业务场景比较常见,遂上网搜索一番,果不其然,有不少同志遇到这个问题,想来这个问题很好解决了.笔者一一尝试了同志们提供的解决方案,结果要不是没有作用,要不是效果不太满意,只好自己另辟蹊径了. 注:在笔者的业务场景中,App是全屏的,即没有顶部的系统栏,也没有底部的导航栏,所以笔者的解决方案,可能不适用于其他场

  • Android实现短信验证码输入框

    本文实例为大家分享了Android实现短信验证码输入框的具体代码,供大家参考,具体内容如下 其实用官方自定的那个inputEditText默认带下划线的,然后自己再实行焦点和输入框弹出等操作也可以. 写这个自定义View主要是为了练习. /** * 实现了粘贴事件监听回调的 EditText */ open class ListenPasteEditTextTest : AppCompatEditText { constructor(context: Context): super(contex

  • Android Compose自定义TextField实现自定义的输入框

    目录 概览 简单自定义BasicTextField示例 实现自定义样式的BasicTextField 使用BasicTextField自定义百度输入框 概览 众所周知Compose中默认的TextField和OutlineTextField样式并不能满足所有的使用场景,所以自定义TextField就成了必备技能,本文就揭露一下自定义TextField的实现.自定义TextField主要使用BasicTextField实现. 简单自定义BasicTextField示例 1.代码 var text

  • 浅谈android获取设备唯一标识完美解决方案

    本文介绍了浅谈android获取设备唯一标识完美解决方案,分享给大家,具体如下: /** * deviceID的组成为:渠道标志+识别符来源标志+hash后的终端识别符 * * 渠道标志为: * 1,andriod(a) * * 识别符来源标志: * 1, wifi mac地址(wifi): * 2, IMEI(imei): * 3, 序列号(sn): * 4, id:随机码.若前面的都取不到时,则随机生成一个随机码,需要缓存. * * @param context * @return */ p

  • Android软键盘遮挡的四种完美解决方案

    一.问题概述 在编辑框输入内容时会弹出软键盘,而手机屏幕区域有限往往会遮住输入界面,我们先看一下问题效果图: 输入用户名和密码时,系统会弹出键盘,造成系统键盘会挡住文本框的问题,如图所示: 输入密码时输入框被系统键盘遮挡了,大大降低了用户操作体验,这就是开发中非常常见的软键盘遮挡的问题,该如何解决? 二.简单解决方案 方法一 在你的activity中的oncreate中setContentView之前写上这个代码 getWindow().setSoftInputMode(WindowManage

  • Android 6.0调用相机图册崩溃的完美解决方案

    最近客户更新系统发现,以前的项目在调用相机的时候,闪退掉了,很奇怪,后来查阅后发现,Android 6.0以后需要程序授权相机权限,默认会给出提示,让用户授权,个人感觉这一特性很好,大概如下: 导入Android V4, V7包! Android Studio 导入很简单,右键项目后找到dependency就ok了. 继承AppCompatActivity public class MainActivity extends AppCompatActivity 引入需要的类库 import and

  • Android开发软键盘遮挡登陆按钮的完美解决方案

    在应用登陆页面我们需要填写用户名和密码.当填写这些信息的时候,软键盘会遮挡登陆按钮,这使得用户体验较差,所以今天就来解决这个问题 1:登陆布局界面如下 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="

  • Android TimePicker 直接输入的问题解决方案

    Android TimePicker 直接输入的问题解决方案 TimePicker 提供了上下的按钮,点击按钮,相关操作都是正常的.但是如果直接在输入框中修改小时或分钟后直接点击按钮取值,会发现不能真正改变时间. 以下代码得不到预期结果. @Override public void onClick(View v) { int i = timePicker1.getCurrentHour(); int j = timePicker1.getCurrentMinute(); startPoint.s

  • AndroidStudio构建项目提示错误信息“unable to find valid certification”的完美解决方案

    手抖了一下,把AS升级到了最新版本,然后就悲剧了,公司的项目跑不起来,提示"unable to find valid certification",新建项目也是一样的提示.之前总结的解决方案都用了,没一个好使的,经过两个下午的折腾,终于还是被我整好了,感动的泪水都要流出来了 (╥╯^╰╥) 直接放我的最新解决方案,look~ 第一步:如下所示,在项目的build.gradle的两个repositories中添加阿里public镜像,最好放在google() 前面: buildscrip

  • Ajax跨域的完美解决方案

    公司要做一个活动页面,在其过程中发现所有的接口,ajax请求跨域.这里对跨域做个简单介绍以及提供几种解决办法. 由于浏览器实现的同源策略的限制,XmlHttpRequest只允许请求当前源(域名.协议.端口)的资源,所以AJAX是不允许跨域的.这里提供自己常用的三种方法: 1.jsonp访问 JSONP(JSON with Padding)是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问: 实现方式 1) <

  • Ubuntu Server 16.04安装MySQL设置远程访问出现问题的完美解决方案(error:10061)

    说明: 一个朋友在使用Ubuntu Server 16.04安装MySQL,设置远程访问的时候出现了问题,请我帮忙.但是,我也没有使用过Ubuntu安装MySQL,于是乎搜索了很多技术文件,比着葫芦画瓢.但是,由于MySQL版本的差异,导致在安装设置的过程中出现了一些问题:就是不能远程访问. 一.安装mysql 1. 安装需要使用root账号,如果不会设置root账号的请参考Linux公社的其他文章.安装mysql过程中,需要设置mysql的root账号的密码,不要忽略了. sudo apt-g

  • Java Web开发防止多用户重复登录的完美解决方案

    目前web项目中,很多情况都是可以让同一个账户信息在不同的登录入口登录这次,这样子就不那么美好了. 推荐阅读: Java 多用户登录限制的实现方法 现在有两种解决方案: 1.将用户的登录信息用一个标志位的字段保存起来,每次登录成功就标记1,注销登录就标记为0,当标记为1的时候不允许别人登录. 2.将用户的登录信息保存在application内置作用域内, 然后利用session监听器监听每一个登录用户的登录情况. 很显然,第一种方式 每次登录 都需要操作数据库,多了一些不必要的性能开销,而且在登

  • Windows 64位下装安装Oracle 11g,PLSQL Developer的配置问题,数据库显示空白的完美解决方案(图文教程)

    安装pl sql 后,若下图的数据库处为空.则需要安装32位的客户端,说明pl sql不支持64位客户端连接. 解决办法: 1.下载32位Oracle客户端,并安装 2.设置PLSQL Developer 打开pl sql 在"工具" - "首选项" - "连接"中,设置 OCI库 (即oracle 32位的安装位置) D:\app\Administrator\product\11.2.0\client_1\oci.dll 如下图: 3.添加环境

随机推荐