1. 程式人生 > >適配安卓沉浸式狀態列的新姿勢

適配安卓沉浸式狀態列的新姿勢

Github Demo: https://github.com/lliuguangbo/AutoSystemBar

針對狀態列,官方從4.4版本開始支援,但是4.4和5.0以上API是不同的,6.0以上提供了兩種狀態列圖示樣式
分別是白色和黑色樣式。

針對狀態列圖示樣式的修改,小米和魅族提供額外的API,在6.0以下都支援,可以參考它們的文件:
- https://dev.mi.com/console/doc/detail?pId=1159
- http://open-wiki.flyme.cn/index.php?title=%E7%8A%B6%E6%80%81%E6%A0%8F%E5%8F%98%E8%89%B2

Android4.4狀態列的API:
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

設定該屬性後,Activity的Layout會嵌入到狀態列下,
也就是setContentView(View)的set進去的view高度比之前高出了狀態列的高度。

Android5.0以上狀態列的API:
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

window.setStatusBarColor(Color.TRANSPARENT);

5.0 以上 FLAG_TRANSLUCENT_STATUS 與 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS是有衝突的,需要呼叫clearFlags清楚這個flag否則會沒有效果。 通過呼叫setStatusBarColor()來改變狀態列顏色。
但是當設定的顏色不是全透明時,Activity的setContentView(View)的set進去的view高度是沒有發生變化的,如果設定的顏色是去透明時,setContentView(View)的set進去的view高度同樣比之前高出了狀態列的高度。

官方Android6.0修改狀態列圖示樣式API
View decorView = window.getDecorView();
if
(darkFont){ //黑色樣式 decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); }else { //白色樣式 decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); }
適配狀態列的方案思考
  • 現在越來越多的應用開始適配狀態列了,不再是黑黑的狀態列;
  • 越來越多的應用會使用透明狀態列,把圖片或者視屏嵌入到狀態列下充分利用空間,通常這個介面是可以滾動的,當滾動一定的位置時改變狀態列顏色。
  • 狀態列圖示樣式的適配

開發者通常的適配方法是:
- 修改狀態列顏色,4.4版本需要額外新增一個狀態列高度的View來填充,通過改變這個View的背景來實現;5.0以上有兩種方法,一種是呼叫setStatusBarColor(), 另一種和4.4的方案一致。
- 這種情況適配起來還是挺麻煩的,透明狀態列時,activity的contentView的高度高出了狀態列的高度會引起一些佈局上的問題,例如圖片嵌入狀態列下,當滾動時ActionBar會重新出現,會導致ActionBar也嵌入狀態列下,很難看。適配時還要對不同的版本進行佈局的調整(因為4.4以下沒問題)
- 對於狀態列圖示樣式的適配,如果你的ActionBar背景是白色的,狀態列改為白色,那麼狀態列圖示樣式就需要改成黑色,如果還是白色會導致看不清狀態列圖示

總的來說,對於第二種情況時適配還是挺麻煩的。

  • 採用額外新增一個狀態列高度的View來填充的方法可以統一4.4和5.0版本;
  • 獲取ActionBar下最多的顏色,使用該顏色當做狀態列顏色,這時想到了官方提供com.android.support:palette-v7:26.1.0
  • 狀態列圖示的樣式根據狀態列顏色做出調整。
程式碼的實現

先擼個自定義ViewGroup,繼承RelativeLayout。
下面是這個自定義ViewGroup需要載入的佈局檔案,分三部分:狀態列View,底部導航欄View, 中間內容部分

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

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/status_view"
        android:layout_above="@+id/navigation_view"
        >
    </FrameLayout>

    <cn.albert.autosystembar.SystemBarView
        android:id="@+id/status_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_alignParentTop="true"
        />

    <cn.albert.autosystembar.SystemBarView
        android:id="@+id/navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_alignParentBottom="true"
        />

</merge>

下面是自定義ViewGroup關鍵程式碼,命名為InternalLayout,在構造器裡對狀態列View,底部導航欄View初始化高度。

class InternalLayout extends RelativeLayout{
    // 部分關鍵程式碼
    public InternalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        Utils.initializeStatusBar(this);
        Utils.initializeNavigationBar(this);

        ViewCompat.setFitsSystemWindows(this, true);
        ViewCompat.requestApplyInsets(this);
        ViewCompat.setOnApplyWindowInsetsListener(this, new android.support.v4.view.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                return insets.consumeSystemWindowInsets();
            }
        });
        inflate(context, R.layout.layout_content, this);
        mStatusView = findViewById(R.id.status_view);
        mStatusView.getLayoutParams().height = Utils.sStatusBarHeight;
        mNavigationView = findViewById(R.id.navigation_view);
        mNavigationView.getLayoutParams().height = Utils.sNavigationBarHeight;

        mContentLayout = (ViewGroup) findViewById(R.id.content);

    }


    void setContentView(View content) {
        if(content.getParent() == null){
            mContentLayout.addView(content);
        }
    }
}

下面是SystemBarView的程式碼, android4.4以下SystemBarView高度,寬頻都為0。

class SystemBarView extends View{

    public SystemBarView(Context context) {
        super(context);
    }

    public SystemBarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }else {
            setMeasuredDimension(0, 0);
            setVisibility(GONE);
        }
    }
}

InternalLayout內部有一個FrameLayout,用來裝Activity的setContentView()裡View,程式碼如下:

final View decorView = window.getDecorView();
final View androidContent = window.getDecorView().findViewById(Window.ID_ANDROID_CONTENT);
final ViewGroup realContent = ((ViewGroup) androidContent);
View content = realContent.getChildAt(0);
//content是Activity的setContentView()裡的View
realContent.removeView(content);
InternalLayout layout = new InternalLayout(activity);
layout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
layout.setContentView(content);
realContent.addView(layout);

接著實現在ActionBar裡獲取主顏色作為狀態列的顏色:

//decorView是Activity的DecorView, 獲取decorView的bitmap
decorView.setDrawingCacheEnabled(true);
final Bitmap bitmap = Bitmap.createBitmap(decorView.getDrawingCache());
decorView.setDrawingCacheEnabled(false);


int top = Utils.sStatusBarHeight + PADDING;
int bottom = (int) (top + ACTION_BAR_DEFAULT_HEIGHT * decorView.getResources().getDisplayMetrics().density);
//PADDING = 10, ACTION_BAR_DEFAULT_HEIGHT = 48, rect 記錄了一個矩形,在狀態列下面的一個矩形,10, 48 我隨便定義的距離,這個矩形不用太精準,不用就是ActionBar的位置。
Rect rect = new Rect(0, top, bitmap.getWidth(), bottom);

//利用Palette的API來獲取顏色
mBuilder = new Palette.Builder(bitmap)
                .clearFilters()
                .addFilter(FILTER)
                .setRegion(rect.left, rect.top, rect.right, rect.bottom);
Palette p = mBuilder.generate()
//這裡會獲取一個List<Palette.Swatch>, Swatch就是代表獲取到一種顏色,
// getPopulation() :返回這顏色的數量。
// getRgb(): 返回rgb顏色

List<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());                

//給swatches排排序,獲取數量最大的那個
Collections.sort(swatches, new Comparator<Palette.Swatch>() {
    @Override
    public int compare(Palette.Swatch lhs, Palette.Swatch rhs) {
        if (lhs == null && rhs != null) {
            return 1;
        } else if (lhs != null && rhs == null) {
            return -1;
        } else if (lhs == null) {
            return 0;
        } else {
            return rhs.getPopulation() - lhs.getPopulation();
        }
    }
});
populationSwatch = swatches.get(0);
color = populationSwatch.getRgb();//color就是我需要的顏色,把它設定到狀態列即可

根據顏色去判斷使用狀態列圖示樣式:


private static final float BLACK_MAX_LIGHTNESS = 0.05f;
private static final float WHITE_MIN_LIGHTNESS = 0.95f;

// 將color轉成hsl色彩模式
private boolean isDarkStyle(int color) {
    boolean isDarkStyle = false;
    //mTemp是一個float陣列,陣列長度為3,陣列的值依次為對色相(H)、飽和度(S)、明度(L)
    ColorUtils.colorToHSL(color, mTemp);
    if (mTemp[2] <= BLACK_MAX_LIGHTNESS) {
        isDarkStyle = false; // 白色
    } else if (mTemp[2] >= WHITE_MIN_LIGHTNESS) {
        isDarkStyle = true;  // 黑色
    }
    return isDarkStyle;
}

最後我將實踐寫成了一個庫: https://github.com/lliuguangbo/AutoSystemBar

使用起來很簡單, 有問題可以提issue, 也可以star以下表示支援,謝謝.

//1.
SystemBarHelper.Builder().into(activity)

//2.
SystemBarHelper.Builder()
                .statusBarColor()    // 設定狀態列顏色
                .statusBarFontStyle()  // 設定狀態列時間,電量的風格, 6.0以上, 部分國產機可以不用6.0以上.
                .navigationBarColor()  // 設定導航欄顏色
                .enableImmersedStatusBar()  // 佈局嵌入狀態列,例如圖片嵌入狀態列
                .enableImmersedNavigationBar()  // 佈局嵌入導航欄,例如圖片嵌入導航欄
                .enableAutoSystemBar(false)  // 根據狀態列下面的背景顏色自動調整狀態列的顏色, 自動調整狀態列時間,電量的風格, 預設是開啟的
                .into(this)

//3.                
SystemBarHelper helper = SystemBarHelper.Builder().into(activity);
helper.setNavigationBarColor()
helper.setStatusBarColor()
helper.statusBarFontStyle()
helper.enableImmersedStatusBar()
helper.enableImmersedNavigationBar()