隨手記Android沉浸式狀態欄的踩坑之路

歡迎關注微信公衆號「隨手記技術團隊」,查看更多隨手記團隊的技術文章。
本文做者:劉玲
原文連接:mp.weixin.qq.com/s/d6D_rYmzl…android

1-前言

關於「沉浸式狀態欄」這種叫法,有的朋友可能會以爲不妥。可是目前網上大部分講到「沉浸式狀態欄」基本都是指「透明狀態欄」,因此這裏就不討論其對錯了(其實有時候錯的多了,也就成了對的了),你們知道是說的「透明狀態欄」就好了,下文都是稱這種效果爲「沉浸式狀態欄」。windows

在Android 4.4以前,全部應用都是沒法設置狀態欄的背景顏色的,都是跟着系統來的(黑色背景狀態欄),一塊黑色的狀態欄和應用很是不搭調。爲了提供更好的交互效果,Google在Android 4.4以後提供了設置沉浸式狀態欄的方法。支持沉浸式狀態欄的App的界面顯得逼格更高一點,所以隨手記Android客戶端也在年初的時候也支持了沉浸式狀態欄。在實現沉浸式狀態欄效果的過程當中踩了很多的坑,特此記錄下來。 下圖爲隨手記Android客戶端設置沉浸式狀態欄先後的效果對比圖:bash

隨手記沉浸式和非沉浸式對比圖

對比兩種效果,很明顯下面設置了沉浸式狀態欄的看上去更協調、更美觀一點。微信

2-如何實現沉浸式狀態欄

2.1-Android 4.4以上實現方式

因爲沉浸式狀態欄設置是在Android 4.4以後才提供的,因此咱們須要對Android 4.4以上的系統作適配。Android 4.4有兩種方式能夠實現沉浸式狀態欄,一種是在資源文件中設置,一種是在代碼中設置。app

2.1.1-資源文件中設置沉浸式狀態欄

首先,咱們要修改values/styles.xml,在裏面添加一個空的style,繼承自BaseTheme。ide

<resources>
    <!-- Base application theme. -->
    <style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme" parent="BaseTheme" />
</resources>
複製代碼

而後在values-v19目錄下的styles.xml文件(若是項目中沒有就新建一個,在4.4以上的系統就會讀取該目錄下的資源文件)添加以下代碼:佈局

<resources>
    <style name="AppTheme" parent="BaseTheme">
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>
複製代碼

而後將App的主題設置爲AppTheme便可。 注:android:windowTranslucentStatus這個屬性是v19開始引入的。測試

2.1.2-在代碼中設置

在代碼中實現更爲方便一點,咱們只須要在BaseActivity中添加一個FLAG_TRANSLUCENT_STATUS的flag便可。字體

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

經過上述兩種方法設置以後,效果圖以下:ui

ToolBar頂上去

咱們會發現,僅僅經過上述設置Toolbar會頂到狀態欄裏面去。一般你們會想到使用fitsSystemWindows屬性來解決此問題。

fitSystemWindows官方描述:Boolean internal attribute to adjust view layout based on system windows such as the status bar. If true, adjusts the padding of this view to leave space for the system windows. Will only take effect if this view is in a non-embedded activity. 簡單描述:這個屬性的做用是讓view能夠根據系統窗口(如status bar)來調整本身的佈局,若是值爲true,就會調整view的paingding屬性來給system windows留出空間(即給view添加一個值爲狀態欄高度的top padding)。

咱們試着給Toolbar設置一下fitsSystemWindows屬性爲true。佈局代碼以下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <android.support.v7.widget.Toolbar
        android:id="@+id/my_toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
 
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:padding="16dp">
 
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#e0e0e0"
            android:layout_gravity="bottom">
            <EditText
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:fitsSystemWindows="true"
                android:background="@drawable/edit_text_rect_bg" />
        </RelativeLayout>
    </FrameLayout>
</LinearLayout>
複製代碼

上面代碼在Android 4.4和Android 5.0+上面對比效果圖以下:

4.4和5.0對比效果

由上面對比圖咱們能夠看出來,在Android 4.4上面狀態欄是全透明的,而在Android 5.0+上面狀態欄是半透明的。

注:有些4.4的系統上面狀態欄並非全透明的,而是漸變的。

2.2-Android 5.0以上實現方式

上面已經實現了沉浸式狀態欄的效果了,可是若是運行在Android 5.0以上的機器上面,會發現大部分手機會出現狀態欄是半透明的。

也有些App在Android 5.0以上就是這種狀態欄半透明的效果,好比QQ。可是有些產品和設計就是想統一風格,所有都實現全透明的狀態欄。那怎麼辦呢?Android自5.0起,又爲咱們提供了設置狀態欄顏色的API,咱們能夠本身設置狀態欄的顏色。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    window.setStatusBarColor(Color.TRANSPARENT);
 
}
複製代碼

添加上述代碼後再在Android 5.0+上運行看效果,狀態欄已經變成全透明瞭,和上圖Android 4.4效果同樣的,這裏就再也不附圖了。

2.3-Android 6.0以上設置狀態欄字體顏色

大部分手機默認狀態欄字體顏色是白色的,若是Toolbar或者界面頭部的顏色較淺,那麼狀態欄上白色的字看不怎麼清楚。 Android 6.0之後,咱們可使用代碼將狀態欄字體的顏色設置爲黑色了,代碼以下:

window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
複製代碼

3-踩過的坑

本覺得上面基本已經完美實現了沉浸式狀態欄了,沒想到測試的時候仍是發現了一系列的坑。

3.1-軟鍵盤彈出時Toolbar被頂上去了

若是在界面中有EditText或者其餘輸入框的話,會發現當軟件盤彈出的時候Toolbar裏面的內容都被頂上去了,以下圖所示:

ToolBar被軟鍵盤頂上去

這是爲何呢?經研究發現原來是fitsSystemWindows屬性搞的鬼。哪一個View設置了fitsSystemWindows=true,這個View就會被軟件盤頂上去。因此說,fitsSystemWindows不能亂用,會有意想不到的坑。 那能不能不用fitsSystemWindows呢?固然能夠。前面也說了,fitsSystemWindows=true的做用是給View增長值爲狀態欄高度的padding,那咱們何不本身手動給Toolbar添加padding呢? 咱們去掉Toolbar上的fitsSystemWindows屬性,並設置一下Toolbar的padding,代碼以下:

protected void setStatusBarPaddingAndHeight(View toolBar) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        if (toolBar != null) {
            int statusBarHeight = getSystemBarHeight(this);
            toolBar.setPadding(toolBar.getPaddingLeft(), statusBarHeight, toolBar.getPaddingRight(),
                    toolBar.getPaddingBottom());
            toolBar.getLayoutParams().height = statusBarHeight +
                    (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 45, getResources().getDisplayMetrics());
        }
    }
}
複製代碼

去掉Toolbar的fitsSystemWindows屬性,並加上上面的代碼,軟鍵盤彈出時Toolbar正常了。 目前隨手記Android項目中就是使用代碼添加padding的方式替代fitsSystemWindows屬性的。

3.2-軟鍵盤彈出時EditText等輸入框會被軟件盤覆蓋掉

上面軟件盤將Toolbar頂上去的示例圖中,咱們還會發現一個問題,就是軟鍵盤彈出時EditText並無跟着彈出來而是被軟鍵盤覆蓋掉了。

上面說Toolbar加了fitsSystemWindows屬性以後會被軟鍵盤頂上去,那麼咱們給輸入框加一個fitsSystemWindows屬性是否恰好就能解決輸入框被覆蓋的問題呢?果斷試一下!

輸入框有padding

試了以後發現,果真能夠,可是輸入框的高度變了,實際上是輸入框的padding增長了狀態欄的高度。很顯然,這並非一個很好的解決方式。 後來在stackoverflow上找到了一個解決方法:解決FLAG_TRANSLUCENT_STATUS致使輸入框被軟鍵盤覆蓋的解決方案

咱們對其作了點調整,代碼以下:

public class AndroidBug5497Workaround {

    public static void assistActivity(View content) {
        new AndroidBug5497Workaround(content);
    }

    private View mChildOfContent;
    private int usableHeightPrevious;
    private ViewGroup.LayoutParams frameLayoutParams;

    private AndroidBug5497Workaround(View content) {
        if (content != null) {
            mChildOfContent = content;
            mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                public void onGlobalLayout() {
                    possiblyResizeChildOfContent();
                }
            });
            frameLayoutParams = mChildOfContent.getLayoutParams();
        }
    }

    private void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            //若是兩次高度不一致
            //將計算的可視高度設置成視圖的高度
            frameLayoutParams.height = usableHeightNow;
            mChildOfContent.requestLayout();//請求從新佈局
            usableHeightPrevious = usableHeightNow;
        }
    }

    private int computeUsableHeight() {
        //計算視圖可視高度
        Rect r = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(r);
        return r.bottom;
    }

}
複製代碼

添加上面的類,而後在Activity的onCreate方法中的setContentView後面加上以下代碼:

AndroidBug5497Workaround.assistActivity(findViewById(android.R.id.content));
複製代碼

而後運行,輸入框可以正常被頂上去,並且輸入框的佈局又沒有受到影響。

軟鍵盤正常彈出

該方案的原理是,給界面的根佈局設置一個監聽器,當界面大小有變化的時候,如鍵盤彈出的時候,從新設置一下根佈局的高度,再調用requestLayout對界面進行重繪。

目前隨手記Android就是使用這個方案,截止到目前也沒有發現這種方案會帶來其餘什麼問題。

3.3-華爲EMUI3.1上的坑

將上面的沉浸式代碼放在EMUI3.1系統的手機(如華爲榮耀7)上面跑,會發現根本沒有沉浸式效果,狀態欄是透明的,顯示的是桌面上的顏色,以下圖:

EMUI3.1沉浸式問題

經驗證,原來是EMUI3.1系統的緣由,不少App也是在EMUI3.0上有沉浸式的效果,到了EMUI3.1卻沒有效果了。在EMUI3.1沒有沉浸式效果若是和4.4之前同樣是黑的也就算了,這樣透明的顯示桌面顏色實在難看。 後來發現去掉下面這句代碼,可讓其有沉浸式的效果。

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
複製代碼

效果以下:

EMUI3.1沉浸式問題修復後

不過它的狀態欄不是全透明的,而是像某些4.4的系統同樣是漸變的,不過總比顯示桌面顏色的效果好。 這裏咱們加一個判斷,判斷若是不是EMUI3.1的系統,才調用clearFlags清除掉FLAG_TRANSLUCENT_STATUS。 具體代碼以下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
   Window window = getWindow();
   window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      // 由於EMUI3.1系統與這種沉浸式方案API有點衝突,會沒有沉浸式效果。
      // 因此這裏加了判斷,EMUI3.1系統不清除FLAG_TRANSLUCENT_STATUS
      if (!isEMUI3_1()) {
         window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
      }
      window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
      window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
      window.setStatusBarColor(Color.TRANSPARENT);
   }
}
 
public static boolean isEMUI3_1() {
    if ("EmotionUI_3.1".equals(getEmuiVersion())) {
        return true;
    }
    return false;
}
 
private static String getEmuiVersion(){
    Class<?> classType = null;
    try {
        classType = Class.forName("android.os.SystemProperties");
        Method getMethod = classType.getDeclaredMethod("get", String.class);
        return (String)getMethod.invoke(classType, "ro.build.version.emui");
    } catch (Exception e){
    }
    return "";
}
複製代碼

3.4-CoordinatorLayout+AppBarLayout滾動隱藏導航欄遇到沉浸式狀態欄的坑

這個坑主要是在作理財頭條需求的時候碰到的。

需求背景:頭條功能須要實現二級TabLayout導航,第一級是Toolbar(頭條、產品和發現),第二級是是頭條裏面各個欄目切換的TabLayout。須要實現的效果是,在頭條Fragment中,滑動帖子列表能夠隱藏和顯示一級導航Toolbar。一級導航Toolbar顯示的時候,左右滑動是切換一級導航的Tab(即頭條、發現和產品)。當在頭條Fragment中上滑滾動帖子列表隱藏一級導航Toolbar後,左右滑動是切換二級導航的tab(即頭條各個欄目)。效果見下圖。

理財頭條效果1

滾動列表隱藏和顯示Toolbar,首先確定是想到CoordinatorLayout+AppBarLayout。基於項目中已實現的沉浸式效果,添加修改Activity中的佈局:

<android.support.design.widget.CoordinatorLayout
   android:id="@+id/coordinator_layout"
   android:layout_height="match_parent"
   android:layout_width="match_parent">
   <android.support.design.widget.AppBarLayout
       android:id="@+id/appbar_layout"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:elevation="0dp">
       <android.support.v7.widget.Toolbar
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           app:contentInsetStart="0dip"
           app:contentInsetLeft="0dip"
           app:contentInsetEnd="0dip"
           app:layout_scrollFlags="scroll|enterAlways">
           ...部分代碼省略...
          <android.support.design.widget.TabLayout
              android:id="@+id/tab_layout"
              android:layout_width="wrap_content"
              android:layout_height="match_parent"
              android:layout_centerHorizontal="true"
              app:tabBackground="@null"
              app:tabIndicatorColor="@color/tab_text_selected_color"
              app:tabIndicatorHeight="2dip"
              app:tabMode="fixed"
              app:tabGravity="fill"
              app:tabPaddingStart="14dp"
              app:tabPaddingEnd="14dp"
              app:tabTextAppearance="@style/FinanceTabTextAppearance"
              app:tabSelectedTextColor="@color/tab_text_selected_color"
              app:tabTextColor="@color/tab_text_unselected_color" />
       </android.support.v7.widget.Toolbar>
   </android.support.design.widget.AppBarLayout>
   <android.support.v4.view.ViewPager
       android:id="@+id/pager"
       android:layout_width="match_parent"
       android:paddingBottom="50dp"
       android:layout_height="match_parent"
       android:overScrollMode="never"
       app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
複製代碼

佈局是在Toolbar中添加一個TabLayout做爲一級導航的tab。而後使用一個ViewPager,給該ViewPager添加了三個Fragment,分別是頭條、產品和發現的Fragment。其中,頭條Fragment中又嵌套了TabLayout和ViewPager。 基於沉浸式的實現方案,在代碼中給AppBarLayout添加一個狀態欄高度的padding。本覺得能夠大功告成了,結果發現運行以後,在上滑隱藏AppBarLayout以後再下拉,會超出下拉範圍,也就是下拉的時候會多出一條狀態欄高度的空白,效果以下圖頂部:

理財頭條滑動問題

通過不斷嘗試和探索,發現給Activity添加以下flag便可:

this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
複製代碼

嗯,不錯,滑動問題解決了。但內心老是不安,總感受有坑。後面發現確實有坑,添加了這個flag後,部分帶虛擬按鍵的華爲手機出現虛擬按鍵擋住底部佈局的問題,經驗證只有EMUI3.1纔有這個問題(又是EMUI3.1,已無力吐槽)。 最後百般周折,終於找到有效解決CoordinatorLayout+AppBarLayout並給AppBarLayout設置paddingtop以後的滑動問題的方法了。

this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
複製代碼

本覺得上面解決方案已經完美沒有任何問題了,沒想到仍是有坑。不久後測試發現一個現網問題:當WebView中的輸入框獲取焦點軟鍵盤彈出後,退出界面時底部佈局出現軟鍵盤大小的黑塊。以下圖所示:

附件1

經排查,此問題就是因爲上面那段代碼引發的。 沒辦法,只能去掉上面那段代碼,尋找另外的解決方案來處理CoordinatorLayout+AppBarLayout並給AppBarLayout設置paddingtop的滑動問題了。 後來在發如今Activity的onCreate方法中加上下面一段代碼就能夠完美解決這個問題。

if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root_container_layout), new android.support.v4.view.OnApplyWindowInsetsListener() {
        @Override
       public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
            return insets.consumeSystemWindowInsets();
       }
    });
}
複製代碼

4-總結

上面就是隨手記Android項目中沉浸式狀態欄實現過程當中遇到的坑以及解決方案。最終隨手記Android實現狀態欄效果後在不一樣機型上面效果圖以下:

4.2-6.0對比圖

通過沉浸式狀態欄的開發,發現幾個容易踩的坑須要注意: 1.fitsSystemWindows=true要慎用,不少坑。好比WebView中輸入框獲取焦點彈出軟鍵盤時出現抖動,還有哪一個View設置了fitsSystemWindows=true軟鍵盤彈出時哪一個View就會被頂上去; 2.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS不要用,會致使EMUI3.1的系統下面虛擬按鍵擋住佈局;

5-參考文檔

stackoverflow.com/questions/7…

相關文章
相關標籤/搜索