Android -- 设置透明状态栏后弹出键盘的时候Toolbar被拉伸或者移出屏幕的问题解决

问题描述

一般我们设置透明状态栏的时候都是通过下面代码进行设置

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}

然后在使用Toolbar的时候设置fitsSystemWindows="true"就可以成功设置透明状态栏了,这里还通过设置android:minHeight和maxButtonHeight将默认的Toolbar的高度56dp改为48dp。

    <style name="toolbarStyle" parent="Base.Widget.AppCompat.Toolbar" >
        <item name="android:background">@color/colorPrimary</item>

        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_width">match_parent</item>
        <item name="android:fitsSystemWindows">true</item>
        <item name="theme">@style/ToolbarTheme</item>
        <item name="popupTheme">@style/ToolbarPopupTheme</item>
        <item name="titleTextAppearance">@style/ToolbarTitle</item>
        <item name="android:minHeight">48dp</item>
        <item name="maxButtonHeight">48dp</item>
    </style>

然而这种设置方法在有EditText的布局中就会出现问题,假如有以下带有EditText的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            style="@style/toolbarStyle">

    </android.support.v7.widget.Toolbar>

    <EditText
            android:id="@+id/edittext"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_alignParentBottom="true"
            android:background="#cccccc"
            android:hint="我是EditText"
            android:textColor="@android:color/black"/>
    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:layout_below="@id/toolbar"
            android:layout_above="@id/edittext"
            android:text=""/>

</RelativeLayout>

没有弹出键盘的情况下,这个布局的效果如下图所示。

效果图1
效果图1

当Activity设置android:windowSoftInputMode="adjustPan"时,则会出现Toolbar被移出屏幕的情况。

效果图2
效果图2

当Activity设置android:windowSoftInputMode="adjustResize"时,则会出现EditText被键盘覆盖、Toolbar被拉伸的情况


效果图3
效果图3

问题分析

fitsSystemWindow属性

根据官方对fitsSystemWindows属性(链接)的描述,当View的fitsSystemWindows设置为true的时候,系统会自动为该View设置相应的padding以适应键盘、状态栏、导航栏等系统窗口,这就可以解释为什么给Toolbar设置fitsSystemWindows之后Toolbar会自动加上paddingTop以适应状态栏,如果没有加上fitsSystemWindows=true,Toolbar则会有部分被状态栏覆盖。

Called by the view hierarchy when the content insets for a window have changed, to allow it to adjust its content to fit within those windows. The content insets tell you the space that the status bar, input method, and other system windows infringe on the application's window.

You do not normally need to deal with this function, since the default window decoration given to applications takes care of applying it to the content of the window. If you use SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION this will not be the case, and your content can be placed under those system elements. You can then use this method within your view hierarchy if you have parts of your UI which you would like to ensure are not being covered.

关于fitsSystemWindows的源码实现可以看这篇文章(链接)

EditText问题分析

当设置android:windowSoftInputMode="adjustPan"时,根据官方对adjustPan(链接)的描述,设置这个属性之后 Activity 主窗口的尺寸不会调整,而是会自动平移窗口的内容使EditText永远不会被键盘覆盖,这就是为什么Toolbar会被移出屏幕的原因。

“adjustPan”
不调整 Activity 主窗口的尺寸来为软键盘腾出空间, 而是自动平移窗口的内容,使当前焦点永远不被键盘遮盖,让用户始终都能看到其输入的内容。 这通常不如尺寸调正可取,因为用户可能需要关闭软键盘以到达被遮盖的窗口部分或与这些部分进行交互。

当设置设置android:windowSoftInputMode="adjustResize"时,根据官方的解释,此时Activity窗口的尺寸会调整而为屏幕上的软键盘腾出空间,由于Toolbar设置了fitsSystemWindows为true且Toolbar的高度设置为wrap_content,因此Toolbar会被设置了一定的paddingBottom造成被拉伸。

“adjustResize”
始终调整 Activity 主窗口的尺寸来为屏幕上的软键盘腾出空间。

问题解决

首先不能在Toolbar设置fitsSystemWindows="true"让系统自动给Toolbar设置paddingTop,我们可以自己手动给Toolbar添加状态栏高度的paddingTop,具体代码如下。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        toolbar.setTitle("测试");
        //设置透明状态栏
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            toolbar.setPadding(0, getStatusBarHeight(this), 0, 0);    //给Toolbar设置paddingTop
        }
    }

    //通过反射获取状态栏高度,默认25dp
    private static int getStatusBarHeight(Context context) {
        int statusBarHeight = dip2px(context, 25);
        try {
            Class<?> clazz = Class.forName("com.android.internal.R$dimen");
            Object object = clazz.newInstance();
            int height = Integer.parseInt(clazz.getField("status_bar_height")
                    .get(object).toString());
            statusBarHeight = context.getResources().getDimensionPixelSize(height);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return statusBarHeight;
    }

    //根据手机的分辨率从 dp 的单位 转成为 px(像素)
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    
}

首先是android:windowSoftInputMode="adjustPan",其实我们不应该这一个布局文件设置为adjustPan,如果确实需要解决这个问题,那么应该给Toolbar下面的所有View用ScrollView给包括进去,这样自动平移只会移动ScrollView里面的内容。

对于android:windowSoftInputMode="adjustResize",由于现在我们没有对Toolbar设置fitsSystemWindows="true",Toolbar没有被拉伸,但是EditText却被键盘覆盖住,解决这个问题最好的方法就是给最外层的View设置fitsSystemWindows="true",此时虽然EditText在键盘上面没有被覆盖住,但是最外层的View由于设置了fitsSystemWindows="true"从而导致系统会给最外层的View设置paddingTop,导致的效果如下。


效果图4
效果图4

解决这个问题的方法就是让最外层的View不去添加系统给的padding,通过重写View的两个方法就可以实现,这两个方法中fitSystemWindows在5.0以后就不支持了,因此5.0以后需要重写onApplyWindowInsets来进行适配,代码如下。

    @Override
    protected boolean fitSystemWindows(Rect insets) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            insets.left = 0;
            insets.top = 0;
            insets.right = 0;
        }
        return super.fitSystemWindows(insets);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));
        } else {
            return insets;
        }
    }

最终的效果图如下,顺利达到我们想要的效果。

效果图5
效果图5

代码

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<com.sparrow.toolbar.SoftInputRelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:fitsSystemWindows="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            style="@style/toolbarStyle">

    </android.support.v7.widget.Toolbar>

    <EditText
            android:id="@+id/edittext"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_alignParentBottom="true"
            android:background="#cccccc"
            android:hint="我是EditText"
            android:textColor="@android:color/black"/>
    <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="16sp"
            android:layout_below="@id/toolbar"
            android:layout_above="@id/edittext"
            android:text="The default implementation works well for a situation where it is used with a container that covers the entire window, allowing it to apply the appropriate insets to its content on all edges. If you need a more complicated layout (such as two different views fitting system windows, one on the top of the window, and one on the bottom), you can override the method and handle the insets however you would like. Note that the insets provided by the framework are always relative to the far edges of the window, not accounting for the location of the called view within that window. (In fact when this method is called you do not yet know where the layout will place the view, as it is done before layout happens.)The default implementation works well for a situation where it is used with a container that covers the entire window, allowing it to apply the appropriate insets to its content on all edges. If you need a more complicated layout (such as two different views fitting system windows, one on the top of the window, and one on the bottom), you can override the method and handle the insets however you would like. Note that the insets provided by the framework are always relative to the far edges of the window, not accounting for the location of the called view within that window. (In fact when this method is called you do not yet know where the layout will place the view, as it is done before layout happens.)"
            android:textColor="@android:color/black"/>

</com.sparrow.toolbar.SoftInputRelativeLayout>

SoftInputRelativeLayout.java

public class SoftInputRelativeLayout  extends RelativeLayout{
    public SoftInputRelativeLayout(Context context) {
        super(context);
    }

    public SoftInputRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SoftInputRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public SoftInputRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected boolean fitSystemWindows(Rect insets) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            insets.left = 0;
            insets.top = 0;
            insets.right = 0;
        }
        return super.fitSystemWindows(insets);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));
        } else {
            return insets;
        }
    }
}

MainActivity.java


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        toolbar.setTitle("测试");
        //设置透明状态栏
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            toolbar.setPadding(0, getStatusBarHeight(this), 0, 0);
        }
    }

    //获取状态栏高度
    private static int getStatusBarHeight(Context context) {
        int statusBarHeight = dip2px(context, 25);
        try {
            Class<?> clazz = Class.forName("com.android.internal.R$dimen");
            Object object = clazz.newInstance();
            int height = Integer.parseInt(clazz.getField("status_bar_height")
                    .get(object).toString());
            statusBarHeight = context.getResources().getDimensionPixelSize(height);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return statusBarHeight;
    }

    //根据手机的分辨率从 dp 的单位 转成为 px(像素)
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}

styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>          <!-- 标题栏颜色 -->
        <item name="colorPrimaryDark">@color/colorAccent</item>   <!-- 状态栏颜色 -->
        <item name="colorAccent">@color/colorAccent</item>              <!-- 控件颜色 -->
        <item name="android:textColorSecondary">#ffffff</item>          <!-- 返回按钮和三点更多按钮颜色 -->
        <item name="android:textColorPrimary">@android:color/white</item>       <!--标题文字颜色-->
    </style>

    <!-- toolbar标题样式 -->
    <style name="ToolbarTitle" parent="@style/TextAppearance.Widget.AppCompat.Toolbar.Title">
        <item name="android:textSize">16sp</item>
    </style>

    <style name="myPopupMenu" parent="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"></style>

    <style name="MyPopupMenu" parent="Base.Widget.AppCompat.PopupMenu">

    </style>

    <!-- toolbar菜单文字颜色 -->
    <style name="ToolbarTheme" parent="@style/ThemeOverlay.AppCompat.ActionBar">
        <item name="actionMenuTextColor">@android:color/white</item>       <!-- 菜单文字颜色 -->
        <item name="actionMenuTextAppearance">@style/ToolbarMenuTextSize</item>
    </style>

    <!-- toolbar菜单文字尺寸 -->
    <style name="ToolbarMenuTextSize" parent="@style/TextAppearance.AppCompat.Menu">
        <item name="android:textSize">10sp</item>
    </style>

    <!-- toolbar弹出菜单样式 -->
    <style name="ToolbarPopupTheme" parent="@style/ThemeOverlay.AppCompat">
        <item name="android:colorBackground">#212121</item>             <!-- 弹窗菜单背景颜色 -->
        <item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item>
    </style>

    <style name="OverflowMenuStyle" parent="Widget.AppCompat.Light.PopupMenu.Overflow">
        <item name="overlapAnchor">false</item>  <!--把该属性改为false即可使menu位置位于toolbar之下-->
    </style>

    <style name="toolbarStyle" parent="Base.Widget.AppCompat.Toolbar" >
        <item name="android:background">@color/colorPrimary</item>

        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_width">match_parent</item>
        <!--<item name="android:fitsSystemWindows">true</item>-->
        <item name="theme">@style/ToolbarTheme</item>
        <item name="popupTheme">@style/ToolbarPopupTheme</item>
        <item name="titleTextAppearance">@style/ToolbarTitle</item>
        <item name="android:minHeight">48dp</item>
        <item name="maxButtonHeight">48dp</item>
    </style>

</resources>

新的问题

在使用的过程中发现有用户反馈,说只要进入我们采用该布局的页面就会崩溃,我们查看了崩溃日志,发现有部分手机都使用了相同的一个安卓系统,并且版本都是19,android4.4.x,YunOS系统,异常信息为 java.lang.ClassNotFoundException: Didn't find class "android.view.WindowInsets" 。

这应该是该系统的虚拟机加载类的方法不一样,在加载一个类的时候也将函数涉及到的其他类也一起进行加载了,因为WindowInsets是Api为20才添加的,所有才会出现ClassNotFoundException这一个异常,解决方法网上有人给出方法,就是增加layout_v20文件夹,针对不同的版本写不一样的布局,分别为api 20以上与20以下提供不同的布局,这是采用系统的限定符实现的,之后20以上的原样采用上述的方式,20以下去掉onApplyWindowInsets复写,这样不同的版本加载不同的代码就OK了。

具体可以查看一下这篇博客(链接)

参考资料

  1. fitSystemWindows
  2. android:fitSystemWindows详解
  3. android:windowSoftInputMode
  4. 令人困惑的fitsSystemWindows属性
  5. Android输入法弹出时覆盖输入框问题的解决方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容