作者:孙有军


当一个activity中含有输入框时,我们点击输入框,会弹出输入法界面,整个界面的变化效果与manifest中对应设置的android:windowSoftInputMode属性有关,一般可以设置的值如下,

<activity android:windowSoftInputMode=["stateUnspecified","stateUnchanged”,  "stateHidden", "stateAlwaysHidden”,  "stateVisible","stateAlwaysVisible”,  "adjustUnspecified", "adjustResize”,  "adjustPan"] …… >
具体怎么设置可以查看官方文档。今天主要解决当输入法弹出时会覆盖输入框的问题。

什么情况会覆盖?

当android的应用中如果一个activity设置了全屏属性Theme.Light.NotittleBar.Fullscreen或者设置了activity对应的主题中android:windowTranslucentStatus属性,设置方式为:`<item name="android:windowTranslucentStatus">true</item>`,这是如果对应的页面上含有输入框,将会导致点击输入框时软键盘弹出后键盘覆盖输入框,导致输入框看不见。

为什么?

这其实是因为在全屏时,adjustResize属性已经失效了,该问题是系统的一个bug,[参考链接](http://code.google.com/p/android/issues/detail?id=5497)。adjustResize不生效,那有没有其他方法来解决呐? 这时我们可以设置adjust属性为adjustPan属性,该属性不会失效,但是由于adjustPan会将页面整体平移,以留出输入法空间,会有一个抖动的效果,体验很差,哪有没有体验效果更好的方法呐?

解决方案:

如果跟布局采用FrameLayout,则可以复写一个自定义FrameLayout,同时设置FrameLayout的android:fitsSystemWindows属性为true。xml设置如下
<com.sample.ui.widget.InsetFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"                    android:layout_width="match_parent"                    android:layout_height="match_parent"                    android:fitsSystemWindows="true”>
我们自定义该FrameLayout为InsetFrameLayout,InsetFrameLayout 代码如下:
public final class InsetFrameLayout extends FrameLayout {    private int[] mInsets = new int[4];    public InsetFrameLayout(Context context) {        super(context);     }    public InsetFrameLayout(Context context, AttributeSet attrs) {        super(context, attrs);     }    public InsetFrameLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);     }    public final int[] getInsets() {        return mInsets;     }    @Override     protected final boolean fitSystemWindows(Rect insets) {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {            // Intentionally do not modify the bottom inset. For some reason,             // if the bottom inset is modified, window resizing stops working.              mInsets[0] = insets.left;             mInsets[1] = insets.top;             mInsets[2] = insets.right;              insets.left = 0;             insets.top = 0;             insets.right = 0;         }        return super.fitSystemWindows(insets);     }    @Override     public final WindowInsets onApplyWindowInsets(WindowInsets insets) {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {             mInsets[0] = insets.getSystemWindowInsetLeft();             mInsets[1] = insets.getSystemWindowInsetTop();             mInsets[2] = insets.getSystemWindowInsetRight();            return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0,                     insets.getSystemWindowInsetBottom()));         } else {            return insets;         }     }  }

官方解决方案:

官方其实也发现了问题,因此在android.support.design.internal下也重写了FrameLayout来解决该问题,但是该类被标记了hide。
/*  * Copyright (C) 2015 The Android Open Source Project  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  *      http://www.apache.org/licenses/LICENSE-2.0  *  * Unless required by applicable law or agreed to in writing, software  * distributed under the License is distributed on an "AS IS" BASIS,  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  */package android.support.design.internal;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Rect;import android.graphics.drawable.Drawable;import android.support.annotation.NonNull;import android.support.design.R;import android.support.v4.view.ViewCompat;import android.support.v4.view.WindowInsetsCompat;import android.util.AttributeSet;import android.view.View;import android.widget.FrameLayout;/**  * @hide  */public class ScrimInsetsFrameLayout extends FrameLayout {    private Drawable mInsetForeground;    private Rect mInsets;    private Rect mTempRect = new Rect();    public ScrimInsetsFrameLayout(Context context) {        this(context, null);     }    public ScrimInsetsFrameLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);     }    public ScrimInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        final TypedArray a = context.obtainStyledAttributes(attrs,                 R.styleable.ScrimInsetsFrameLayout, defStyleAttr,                 R.style.Widget_Design_ScrimInsetsFrameLayout);         mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsFrameLayout_insetForeground);         a.recycle();         setWillNotDraw(true); // No need to draw until the insets are adjusted          ViewCompat.setOnApplyWindowInsetsListener(this,                new android.support.v4.view.OnApplyWindowInsetsListener() {                    @Override                     public WindowInsetsCompat onApplyWindowInsets(View v,                             WindowInsetsCompat insets) {                        if (null == mInsets) {                             mInsets = new Rect();                         }                         mInsets.set(insets.getSystemWindowInsetLeft(),                                 insets.getSystemWindowInsetTop(),                                 insets.getSystemWindowInsetRight(),                                 insets.getSystemWindowInsetBottom());                         setWillNotDraw(mInsets.isEmpty() || mInsetForeground == null);                         ViewCompat.postInvalidateOnAnimation(ScrimInsetsFrameLayout.this);                        return insets.consumeSystemWindowInsets();                     }                 });     }    @Override     public void draw(@NonNull Canvas canvas) {        super.draw(canvas);        int width = getWidth();        int height = getHeight();        if (mInsets != null && mInsetForeground != null) {            int sc = canvas.save();             canvas.translate(getScrollX(), getScrollY());            // Top             mTempRect.set(0, 0, width, mInsets.top);             mInsetForeground.setBounds(mTempRect);             mInsetForeground.draw(canvas);            // Bottom             mTempRect.set(0, height - mInsets.bottom, width, height);             mInsetForeground.setBounds(mTempRect);             mInsetForeground.draw(canvas);            // Left             mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom);             mInsetForeground.setBounds(mTempRect);             mInsetForeground.draw(canvas);            // Right             mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom);             mInsetForeground.setBounds(mTempRect);             mInsetForeground.draw(canvas);              canvas.restoreToCount(sc);         }     }    @Override     protected void onAttachedToWindow() {        super.onAttachedToWindow();        if (mInsetForeground != null) {             mInsetForeground.setCallback(this);         }     }    @Override     protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        if (mInsetForeground != null) {             mInsetForeground.setCallback(null);         }     }  }
采用如上其中的任何一种方法就可以解决输入法弹出后覆盖输入框问题。

其他问题?

在我们使用的过程中发现有用户反馈,说只要进入我们采用该布局的页面就会崩溃,我们查看了崩溃日志,发现有部分手机都使用了相同的一个安卓系统,并且版本都是19,android4.4.x,一个被重写过的系统,该系统的代码加载方式被重写了。

为什么会崩溃?

我们代码使用到了WindowInsets,该类是api 20才提供的,因此19的系统中其实是没有该代码的,但是该系统在xml的inflate的时候就解析了该类,导致classNotFound。

新的解决方案!

新的解决方案还是采用了上述的方式,不过会针对不同的版本写不一样的布局,分别为api 20以上与20以下提供不同的布局,这是采用系统的限定符实现的,之后20以上的原样采用上述的方式,20以下去掉onApplyWindowInsets复写,这样不同的版本加载不同的代码就OK了。
@Override     public final WindowInsets onApplyWindowInsets(WindowInsets insets) {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {             mInsets[0] = insets.getSystemWindowInsetLeft();             mInsets[1] = insets.getSystemWindowInsetTop();             mInsets[2] = insets.getSystemWindowInsetRight();            return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0,                     insets.getSystemWindowInsetBottom()));         } else {            return insets;         }     }

总结

到此整个解决方案已经完成了,如过有更新的解决方式望大家分享。