Android開發筆記(一)沉浸式狀態欄

在Android開發中我們越來越重視用戶的App操作體驗,在使用App中我們主張減少對用戶的干擾,經常會提到一致性體驗。爲了追求界面的風格的一致性,Google官方在Android 4.4 開始,支持了系統最上方的狀態欄(StatusBar)和最下方的導航欄(Navigation Bar)可以被透明化,使得APP中的設計可以過渡更加平滑,不像之前那樣的割裂感,讓整個APP更加一致。而且後續的系統版本中,持續增加了對狀態欄操作的api接口,但這也導致瞭如果直接使用4.4的方法在Android 5.0 以上會導致顯示效果不一致的問題。爲了避免這個情況,那麼開發者需要考慮版本的兼容性,對不同的系統版本進行分別處理。下面將從不同的系統版本中介紹如何實現沉浸式狀態欄的效果。

代碼實例:沉浸式狀態欄

4.4(KITKAT)

在設置沉浸式狀態欄時,首先要將對應Activity設置一個主題(theme),該主題直接繼承Theme.AppCompat.Light.NoActionBar,然後直接在主題中設置狀態欄透明:

<activity
    android:name=".MainActivity"
    android:theme="@style/AppTheme.NoActionBar">
    ....
</activity>

.....

<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- 設置系統Status Bar顏色透明 -->
    <item name="android:windowTranslucentStatus">true</item>
    <!-- 設置系統Navigation Bar顏色透明 -->
    <item name="android:windowTranslucentNavigation">true</item>
</style>

當直接使用如上設置時,細心的你可能會發現一個問題:本來在狀態欄下面顯示的文字,盡然都跑到狀態欄中去了而且與狀態欄中信息發生重疊。你一看,感覺這操作還不如剛纔呢。別急,系統早已爲我們提供了一個解決方案,那就是在主題中補充一個android:fitsSystemWindows = true 的屬性或者加在Activity對應的佈局文件的根屬性上。這樣就避免了這個問題。

fitSystemWindows屬性:

該屬性的作用是設置爲true時,可以避免應用內容和系統的窗口(statusbar)發生重疊,通過在View上設置和系統窗口一樣高度的邊框(padding)來確保應用內容不會出現到系統窗口中。這樣使得系統會自己計算好佈局距狀態欄的高度,使界面內容佈局位於狀態欄下方和導航欄上方。

如果一個佈局中的多個view都設置了android:fitsSystemWindows="true"的屬性,那麼只有第一個View會生效,其他view的設置無效。而且這個View中,再設置android:padding屬性會失效。

代碼實現

那麼一切按照如上配置操作,我們的確可以在4.4中實現狀態欄的透明效果,但是並不能隨心所欲的達到自定義設置狀態欄顏色的效果,而且不具有我們所提倡的插拔式體驗。所以我們從代碼層面尋求更好的設計。

我們同樣可以通過代碼來實現windowTranslucentStatus 的效果,如下:

public void setTranslucentStatus(Activity activity, boolean on) {
    Window win = activity.getWindow();
    WindowManager.LayoutParams winParams = win.getAttributes();
    final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
    if (on) {
        winParams.flags |= bits;
    } else {
        winParams.flags &= ~bits;
    }
    win.setAttributes(winParams);
}

因爲我們在4.4中不能直接修改狀態欄的顏色,所以可以通過創建一個View然後設置爲系統狀態欄同樣的高度,接着將它置於DecorView窗口的頂部將真正的狀態欄覆蓋,這樣就可以改變這個View的背景色來實現狀態欄顏色改變。然後設置根佈局fitSystemWindows,這樣就實現了4.4中的沉浸式狀態欄。

public void compat(Activity activity, int statusColor) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        Window win = activity.getWindow();
        setTranslucentStatus(activity,true);
        ViewGroup decorView = (ViewGroup) win.getDecorView();
        decorView.addView(createStatusBarView(activity, statusColor));
        setRootView(activity, true);
    }

}
    
/**
 * create a view which it's height equal system status bar's height
 *
 * @param context
 * @param color
 * @return
 */
private View createStatusBarView(Context context, @ColorInt int color) {
    View statusBarView = new View(context);
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams
            (FrameLayout.LayoutParams.MATCH_PARENT, getStatusBarHeight(context));
    params.gravity = Gravity.TOP;
    statusBarView.setLayoutParams(params);
    statusBarView.setBackgroundColor(color);
    return statusBarView;
}

/**
 * sets whether or not the root view of layout fitSystemWindows.
 *
 * @param activity
 * @param fitSystemWindows
 */
private void setRootView(Activity activity, boolean fitSystemWindows) {
    ViewGroup parent = activity.findViewById(Window.ID_ANDROID_CONTENT);
    for (int i = 0; i < parent.getChildCount(); i++) {
        View childView = parent.getChildAt(i);
        if (childView instanceof ViewGroup) {
            childView.setFitsSystemWindows(fitSystemWindows);
            ((ViewGroup) childView).setClipToPadding(fitSystemWindows);
        }
    }
}

/**
 * receive the status bar height of the system.
 *
 * @param context
 * @return
 */
private int getStatusBarHeight(Context context) {
    int result = 0;
    int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = context.getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}
5.0(LOLLIPOP)

對於在4.4系統上,我們需要做的很多但是效果卻很少,所幸的是隻要Android還在發展低版本系統所佔的份額只會越來越少以至慢慢被淘汰。而Google官方也已經意識到對開發者開放狀態欄操作接口是必要的,因此在5.0的版本上,開發者無需做過多操作就可以直接修改狀態欄顏色。

Google直接在Window類中提供了setStatusBarColor方法:

public abstract class Window {

    .....
    
    /**
     * Sets the color of the status bar to {@code color}.
     *
     * For this to take effect,
     * the window must be drawing the system bar backgrounds with
     * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
     * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set.
     *
     * If {@code color} is not opaque, consider setting
     * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
     * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
     * <p>
     * The transitionName for the view background will be "android:status:background".
     * </p>
     */
    public abstract void setStatusBarColor(@ColorInt int color);
}

1:設置狀態欄顏色

方法中註釋中說明了,在設置狀態欄顏色的同時,還需要同步設置WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS這個Window Flag,並且需要保證WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS這個Window Flag沒有被設置。否則,不會生效。

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.setStatusBarColor(statusColor);

2:設置狀態欄透明

如果我們需要實現狀態欄的透明效果同時上浮在佈局內容上方,我們可以考慮這樣做:

/**
 * set status bar transparent .
 *
 * @param activity
 */
public void setStatusBarTransparent(Activity activity) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
        window.setStatusBarColor(Color.TRANSPARENT);
        decorView.setSystemUiVisibility(option);

    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        setTranslucentStatus(activity, true);
    }
}

/**
 * set status bar translucent or not.
 *
 * @param activity
 * @param on
 */
private void setTranslucentStatus(Activity activity, boolean on) {
    Window win = activity.getWindow();
    WindowManager.LayoutParams winParams = win.getAttributes();
    final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
    if (on) {
        winParams.flags |= bits;
    } else {
        winParams.flags &= ~bits;
    }
    win.setAttributes(winParams);
}

注:

這裏出現了setSystemUiVisibility中兩個View的標記:
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:視圖內容延伸至狀態欄區域,狀態欄上浮於視圖之上。這時再配合View.SYSTEM_UI_FLAG_LAYOUT_STABLE 標記,就能夠在status bar隱藏和顯示時,內容區域不會改變大小,從而保證佈局的穩定。所以這兩者經常聯合使用。

3:設置全屏
在設置全局顯示時,我們同樣既可以通過主題設置,也可以通過代碼動態設置:

<style name="AppTheme.FullScreen" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowFullscreen">true</item>
</style>

這裏我們主要分析代碼設置的方法:

/**
 * set systemUI hide or not
 *
 * @param activity
 */
public void setFullScreen(Activity activity, boolean fullScreen) {

    if (fullScreen) {
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        // Set the IMMERSIVE flag.
        // Set the content to appear under the system bars so that the content
        // doesn't resize when the system bars hide and show.
        int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        decorView.setSystemUiVisibility(option);
    } else {
        Window window = activity.getWindow();
        window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

        //don't work
//            View decorView = window.getDecorView();
//            int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
//                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
//                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
//            decorView.setSystemUiVisibility(option);
    }
}

View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:視圖延伸至導航欄區域,導航欄上浮於視圖之上;

View.SYSTEM_UI_FLAG_HIDE_NAVIGATION:暫時隱藏導航欄, 由於導航欄的重要性,當與用戶交互後,比如單擊屏幕,都可能會導致navigation bar重新出現,源於系統clear掉該標誌與SYSTEM_UI_FLAG_FULLSCREEN 標誌,同SYSTEM_UI_FLAG_IMMERSIVE 標誌一起使用可避免被clear;

View.SYSTEM_UI_FLAG_FULLSCREEN:隱藏狀態欄,效果同設置WindowManager.LayoutParams.FLAG_FULLSCREEN;

View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY:沉浸式效果,當用戶在系統欄區域向內滑動時,系統欄會顯示幾秒鐘然後重新消失;

View.SYSTEM_UI_FLAG_IMMERSIVE:沉浸式效果,當用戶在系統欄區域向內滑動時,系統欄會重新顯示並保持可見;(注意與STICKY的區別)

6.0(M)

在Android 6.0中,系統又提供新的方法來改變狀態欄中的字體顏色,這樣便能夠更好的適應系統狀態欄背景色。通過setSystemUiVisibility方法設置 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 可以自行修改狀態欄中字體爲黑色或者白色。
看API中文檔對此的解釋有:

/**
 * Flag for {@link #setSystemUiVisibility(int)}: Requests the status bar to draw in a mode that
 * is compatible with light status bar backgrounds.
 *
 * <p>For this to take effect, the window must request
 * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
 *         FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not
 * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS
 *         FLAG_TRANSLUCENT_STATUS}.
 *
 * @see android.R.attr#windowLightStatusBar
 */
public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000;

提示了在繪製狀態欄背景色時,它可以兼容light的模式。而且同樣需要設置WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS這個Window Flag,並且需要保證WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS這個Window Flag沒有被設置。否則,不會生效。

計算狀態欄背景色的light和dark,可以使用系統提供的方法,所以進行如下設置:

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.setStatusBarColor(statusColor);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    View decorView = window.getDecorView();
    if (decorView != null) {
        int vis = decorView.getSystemUiVisibility();
        if (isLightColor(statusColor)) {
            vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; //black
        } else {
            vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; //white
        }
        decorView.setSystemUiVisibility(vis);
    }
}


/**
 * calculate the color is light or dark.
 *
 * @param color
 * @return
 */
private boolean isLightColor(@ColorInt int color) {
    return ColorUtils.calculateLuminance(color) >= 0.5;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章