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

隨手記技術團隊發表於2017-12-05

歡迎關注微信公眾號「隨手記技術團隊」,檢視更多隨手記團隊的技術文章。
本文作者:劉玲
原文連結:mp.weixin.qq.com/s/d6D_rYmzl…

1-前言

關於“沉浸式狀態列”這種叫法,有的朋友可能會覺得不妥。但是目前網上大部分講到“沉浸式狀態列”基本都是指“透明狀態列”,所以這裡就不討論其對錯了(其實有時候錯的多了,也就成了對的了),大家知道是說的“透明狀態列”就行了,下文都是稱這種效果為“沉浸式狀態列”。

在Android 4.4之前,所有應用都是無法設定狀態列的背景顏色的,都是跟著系統來的(黑色背景狀態列),一塊黑色的狀態列和應用非常不搭調。為了提供更好的互動效果,Google在Android 4.4之後提供了設定沉浸式狀態列的方法。支援沉浸式狀態列的App的介面顯得逼格更高一點,因此隨手記Android客戶端也在年初的時候也支援了沉浸式狀態列。在實現沉浸式狀態列效果的過程中踩了不少的坑,特此記錄下來。 下圖為隨手記Android客戶端設定沉浸式狀態列前後的效果對比圖:

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

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

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

2.1-Android 4.4以上實現方式

由於沉浸式狀態列設定是在Android 4.4之後才提供的,所以我們需要對Android 4.4以上的系統做適配。Android 4.4有兩種方式可以實現沉浸式狀態列,一種是在資原始檔中設定,一種是在程式碼中設定。

2.1.1-資原始檔中設定沉浸式狀態列

首先,我們要修改values/styles.xml,在裡面新增一個空的style,繼承自BaseTheme。

<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);
}
複製程式碼

通過上述兩種方法設定之後,效果圖如下:

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…

相關文章