继续创造,加速成长!这是我参与「日新计划 10 月更文应战」的第8天,点击检查活动详情

前语

前段时间呈现了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+沉溺形式下输入框被软键盘挡住的进程:

  1. window.getAttributes().softInputMode设置成SOFT_INPUT_ADJUST_RESIZE
  2. 设置view的fitSystemWindows为true,我这儿是webview里边的输入框被挡住,设的便是webview而不是父View
  3. 重写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。假如它是内部自己写的一个类去完成这个操作呢?

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

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

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

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,虽然用不上,但也是个思路。最后发现能够直接重写这个办法就行,我反倒还把问题想杂乱了。