適配安卓沉浸式狀態列的新姿勢
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()