沉浸式設計以及兼容

什么是沉浸式

可以參考SystemBarTint

狀態欄 —— StausBar

通過colorPrimaryDark屬性修改狀態欄,只適用于5.0以后

你可以通過修改主題下的屬性,來改變狀態欄的顏色

<style name="AppTheme" parent="Theme.AppCompat.Light.[圖片上傳中...(windowTranslucentStatus_4.4.png-3332a0-1511580222129-0)]
">
    <item name="colorPrimaryDark">@color/red</item>
</style>
版本 說明 對比
4.3 colorPrimaryDark屬性
colorPrimary_4.3.png
4.4 colorPrimaryDark屬性
colorPrimary_4.4.png
5.0 colorPrimaryDark屬性
colorPrimary_5.0.png

那么如何兼容4.4呢?

通過android:windowTranslucentStatus屬性兼容4.4

v19\styles.xml中給主題添加以下屬性

<item name="android:windowTranslucentStatus">true</item>
版本 說明 對比
4.3 android:windowTranslucentStatus屬性
windowTranslucentStatus_4.3.png
4.4 android:windowTranslucentStatus屬性
windowTranslucentStatus_4.4.png
5.0 android:windowTranslucentStatus屬性
windowTranslucentStatus_5.0.png

如果要用此方法兼容4.4版本,則許創建v21\styles.xml,給5.0以后的版本提供以下主題:

<item name="colorPrimaryDark">@color/red</item>

否則,5.0以上會沿用v19\styles.xml中定義的主題屬性,效果如下圖

bad_5.0.png

如果讓主題去掉ActionBar,會是怎么樣呢?

NoActionBar

如果設置成NoActionBar的主題風格,把布局改成如下:

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/yellow"
        android:gravity="top|center_horizontal"
        android:text="@string/content" />

</LinearLayout>

版本 說明 對比
4.3 NoActionBar
noActionBar_4.3.png
4.4 NoActionBar
noActionBar_4.4.png
5.0 NoActionBar
noActionBar_5.0.png

從結果可以看出4.4版本界面的內容會覆蓋StatusBar,而5.0以上則不會出現,那為什么會出現這種情況?通過setContentView源碼分析了解,在5.0版本后,PhoneWindow類的installDecor方法中調用了:

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();

這個方法是在ViewGroup中實現的:

final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
    children[i].makeOptionalFitsSystemWindows();
}

從這個實現來看,就是遍歷DecorView的子類添加fitsSystemWindows標記,這個標記是用來干嘛的呢?

fitsSystemWindow解決內容布局和StatusBar重合

fitsSystemWindow 如果設置為true,就是組件都在屏幕內,但是不包括statusBar。設置成false后,整個屏幕都可以放置組件,沒有statusBarwindow之分

這樣,如果在布局中給根布局加上fitsSystemWindow屬性,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

在4.4系統上會顯示成下面這樣:

fitsSystemWindows_4.4.png

并沒有達到我們想要的效果,雖然能和StatusBar區域分隔開,但是StatusBar的顏色卻變為了白色,這是為什么呢?雖然我們通過fitsSystemWindow屬性讓內容部分與StatusBar隔開一定的間距,但是由于4.4中我們給StatusBar設置成了透明色,所以StatusBar區域顯示了根布局的背景顏色。

修改根布局背景顏色達到沉浸式效果

因此修改根布局的背景顏色為黃色,則能達到我們想要的效果:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/yellow"
    android:fitsSystemWindows="true">
final_4.4.png

這樣子雖然能達到沉浸式效果,但是背景顏色卻固定了,而且每個界面的根布局都得添加fitsSystemWindow屬性很麻煩,如何統一有效得兼容4.4以上的系統來達到沉浸式效果呢?

最終兼容方案

針對5.0以上的系統

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(barColor);
} 

針對4.4到5.0之間的系統

  1. StatusBar設置為透明
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
  1. 獲取到根布局mContentRoot,設置mContentRoot的FitsSystemWindows
mContentRoot = findViewById(android.R.id.content);
mContentRoot.setFitsSystemWindows(true);
  1. 創建一個和StatusBar高度一樣的幀布局作為假的StatusBar,修改其背景顏色達到沉浸效果
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
        getStatusBarHeight());
FrameLayout frameLayout = new FrameLayout(context);
frameLayout.setLayoutParams(lp);
frameLayout.setBackgroundColor(barColor);
mContentRoot.addView(frameLayout);
如何獲取StatusBar的高度

這個高度是在frameworks/base/core/res/res/values/dimens.xml里面定義的:

<!-- Height of the status bar -->
<dimen name="status_bar_height">25dip</dimen>

這個資源文件是系統的,不會生成在項目的R文件中,所以在項目中通過getDimen是找不到這個資源的,只能在系統編譯生成的資源文件中找到這個資源。保存在out/target/common/R/com/android/internal/R.java下,

  1. 可以通過反射拿到想要的資源的值
public int getSystemDimenPx(String field) {
    int result = 0;
    try {
        Class clz = Class.forName("com.android.internal.R$dimen");
        Object obj = clz.newInstance();
        int resourceId = Integer.parseInt(clz.getField(field).get(obj).toString());
        if (resourceId > 0)
            result = getResources().getDimensionPixelSize(resourceId);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}
  1. 可以通過getIdentifier方法獲得系統資源id
int result = 0;
int resourceId = Resources.getSystem().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0)
     result = getResources().getDimensionPixelSize(resourceId);

導航欄 —— NavigationBar

和狀態欄的實現類似

最終兼容方案

針對5.0以上的系統

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setNavigationBarColor(barColor);
} 

針對4.4到5.0之間的系統

  1. NavagationBar設置為透明
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
  1. 判斷是否存在導航欄,有些手機沒有導航欄,通過物理屏幕減去內容屏幕的相應寬高判斷
    private boolean hasNavigationBar(Activity activity) {
        WindowManager windowManager = activity.getWindowManager();
        Display d = windowManager.getDefaultDisplay();

        // 真實物理屏幕
        DisplayMetrics realDisplayMetrics = new DisplayMetrics();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            d.getRealMetrics(realDisplayMetrics);
        }

        int realHeight = realDisplayMetrics.heightPixels;
        int realWidth = realDisplayMetrics.widthPixels;

        // 屏幕內容高度
        DisplayMetrics displayMetrics = new DisplayMetrics();
        d.getMetrics(displayMetrics);

        int displayHeight = displayMetrics.heightPixels;
        int displayWidth = displayMetrics.widthPixels;

        // 防止橫屏,寬高都判斷
        return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
    }
  1. 如果存在導航欄,則在導航欄對應的位置上創建一個和NavagationBar高度一樣的幀布局作為假的NavagationBar,修改其背景顏色達到沉浸效果
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
        getNavigationBarHeight());
lp.gravity = Gravity.BOTTOM;
FrameLayout frameLayout = new FrameLayout(activity);
frameLayout.setLayoutParams(lp);
frameLayout.setBackgroundColor(barColor);
mContentRoot.addView(frameLayout);

最終效果如圖:

final_navigation_4.4.png

擴展 —— 如何實現圖片全屏沉浸式效果

結合之前分析的如何實現顏色沉浸是效果,得出:

  1. StatusBarNavagationBar設置為透明
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
  1. 獲得根布局mContentRoot,遍歷將mContentRoot的子View的FitsSystemWindows設置為false,參考ViewGroup的隱藏方法makeOptionalFitsSystemWindows
final int count = mContentRoot.getChildCount();
for (int i = 0; i < count; i++) {
    mContentRoot.getChildAt(i).setFitsSystemWindows(false);
}
  1. 給mContentRoot的上內邊距置為0,否則圖片頂不到StatusBar
mContentRoot.setPadding(mContentRoot.getPaddingLeft(), 0,
                    mContentRoot.getPaddingRight(), mContentRoot.getPaddingBottom());
  1. 讓設置了背景圖片的View的上內邊距加上StatusBar的高度,否則內容會和狀態欄重合
drawableView.setPadding(drawableView.getPaddingLeft(), drawableView.getPaddingTop() + getStatusBarHeight(),
                    drawableView.getPaddingRight(), drawableView.getPaddingBottom());

最后的效果:

版本 對比
4.4
pic_immrison_4.4.png
5.0
pic_immrison_5.0.png

細心的已經發現了5.0的版本的狀態欄和4.4的有些不一樣,5.0的狀態欄是半透明的,為什么會這樣呢?在代碼中針對4.4和5.0同樣設置的狀態欄透明標記,顯示的效果卻不同。讓我們來看看源碼:

5.0如何實現全屏圖片

我們知道FLAG_TRANSLUCENT_STATUS是在Window類中通過addFlags添加的,而它的使用則是在DecorView中,DecorView中通過calculateStatusBarColor方法計算狀態欄的顏色

calculateStatusBarColor是如何計算狀態欄的顏色的

(flags & FLAG_TRANSLUCENT_STATUS) != 0 ? semiTransparentStatusBarColor
 : (flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 ? statusBarColor
 : Color.BLACK;
  1. 首先它會判斷是否有FLAG_TRANSLUCENT_STATUS標記,如果有這個標記,則會返回semiTransparentStatusBarColor,這個顏色值是個常量mSemiTransparentStatusBarColor,這個常量是在創建DecorView的時候賦值為R.color.system_bar_background_semi_transparent可以看到在資源文件中的顏色值是百分之40透明度黑色的值,這就是為什么狀態欄在5.0顯示半透明的原因
    <!-- Status bar color for semi transparent mode. -->
    <color name="system_bar_background_semi_transparent">#66000000</color> <!-- 40% black -->
  1. 否則會判斷是否有FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS標記,如果有這個標記,則會返回
    statusBarColor,這個顏色是由PhoneWindowsetStatusBarColor方法決定的

修改狀態欄半透明的解決方案

可以從calculateStatusBarColor方法的第二個判斷條件入手:

  1. 5.0以上移除FLAG_TRANSLUCENT_STATUS標記
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
  1. 添加FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS標記
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
  1. 設置狀態欄顏色為透明色
getWindow().setStatusBarColor(Color.TRANSPARENT);

導航欄修改類似,最終代碼:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mContentRoot = (ViewGroup) findViewById(android.R.id.content);
            makeOptionalFitsSystemWindows();
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
                if (hasNavigationBar(this)) {
                    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
                }
                int uiFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
                uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
                uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
                uiFlags |= View.SYSTEM_UI_FLAG_VISIBLE;
                getWindow().getDecorView().setSystemUiVisibility(
                        uiFlags | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
                getWindow().setStatusBarColor(Color.TRANSPARENT);
                getWindow().setNavigationBarColor(Color.TRANSPARENT);
            }
            mContentRoot.setPadding(mContentRoot.getPaddingLeft(), 0,
                    mContentRoot.getPaddingRight(), mContentRoot.getPaddingBottom());
            drawableView.setPadding(drawableView.getPaddingLeft(), drawableView.getPaddingTop() + getStatusBarHeight(),
                    drawableView.getPaddingRight(), drawableView.getPaddingBottom());
        }

運行后的效果:


final_5.0.png

總結

  1. 對于5.0以上主要通過getWindow().setStatusBarColor(barColor);設置狀態欄的顏色;4.4到5.0之間主要在狀態欄的位置添加一個FrameLayout作為假狀態欄,通過改變這個FrameLayout的背景顏色來改變狀態欄的顏色,注意的是狀態欄必須設置為透明
  2. 對于圖片沉浸式,主要是通過設置狀態欄透明,并將布局的FitsSystemWindows設置為false,讓statusBar和整個組件都在屏幕內,就可以控制根布局的內邊距來實現
  3. FitsSystemWindows屬性的作用,設置為true,則組件都在屏幕內,但是不包括statusBar;設置成false后,整個屏幕都可以放置組件,沒有statusBar和window之分

參考

與Status Bar和Navigation Bar相關的一些東西
全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容