【Android】透明狀態欄在App中的實現與接口設計

文章目錄
  1. 1. 認識透明狀態欄
  2. 2. 透明狀態欄Api及特性
  3. 3. 設置透明狀態欄
  4. 4. 處理消失的系統狀態欄區域
  5. 5. fitsSystemWindows
  6. 6. Activity中的接口設計
  7. 7. Fragment中的接口設計
  8. 8. 白色Titlebar的處理
  9. 9. React-Native的處理
  10. 10. 小米 與 魅族 與 (莫名其妙的)華爲
  11. 11. 騰訊優測UTest

GitHub源碼:TransparentStatusbar
源碼中分兩個app

  • TestBasic:

    1. 透明狀態欄實現的示例,方便debug
    2. 白色/紅色Titlebar的不同處理方式
    3. paddingTopfitsSystemWindows的對比
    4. layer-list分層背景的使用
  • TitlebarBelowTransparentStatusBar

    1. 示例App中統一的處理方式
    2. Activity中的接口設計
    3. Fragment中的接口設計

認識透明狀態欄

Android4.4開始引入了透明狀態欄的新特性.
見下圖,左邊爲傳統的Android系統狀態欄,右邊爲透明狀態欄.   
 
what is transparent status bar

  • 正常顯示狀態欄的圖標/文字  
  • 狀態欄的背景是透明的,能透出應用的背景色.而不像之前一樣是默認的黑色不可編輯.

透明狀態欄Api及特性

Android 4.4(v19)開始,透明狀態欄特性變化很頻繁,直到Android 6.0(v23)才真正完善穩定.  
 
下表展示各版本所引入的新Api或特性.

Version/levelFeaturesDescription
4.4/v19WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS狀態欄是漸變色的半透明 
4.4_Watch/v20OnApplyWindowInsetsListener能夠區分多個Inset事件與Rect信息(PS.系統狀態欄屬於插入區Inset的一種)
5.0/v21WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
允許自定義狀態欄背景色了,但無法控制狀態欄上的文字/圖標顏色
6.0/v23View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR狀態欄上的圖標/文字顏色的亮色模式,即顏色是暗色

設置透明狀態欄

根據多個版本間的Api及特性,Java代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Activity.java
// onCreate(Bundle bundle)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Android 5.0
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6.0
// 亮色模式,避免系統狀態欄的圖標不可見
// visibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
window.getDecorView().setSystemUiVisibility(visibility);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// 自定義狀態欄背景色
window.setStatusBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Android 4.4
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}

注意: 要設置透明狀態欄的Activitytheme須是NoTitleBar.

1
2
3
4
5
6
7
8
9
10
// AndroidManifest.xml
<activity android:name="MyActivity"
android:theme="@android:style/Theme.NoTitleBar"
/>
// Or / 或
// MyActivity.java
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
requestWindowFeature(Window.FEATURE_NO_TITLE);
}

代碼執行後,界面顯示效果如下圖:
status_bar_space
可以發現系統狀態欄的區域已經消失,ActivitycontentView頂上去佔據了原來屬於系統狀態欄的區域.
導致雖然Back Title雖然仍在Titlebar區域垂直居中,但視覺效果上受狀態欄圖標的影響,卻不是垂直居中的效果.

所以接下來第二步就是: 以何種方式處理消失的系統狀態欄區域?


處理消失的系統狀態欄區域

處理方式可以有:

  1. 就讓activitycontentView頂上去吧,不需要修改.
    • 這種場景用於一些可全屏瀏覽圖片/觀看視頻等界面.
  2. 調整Titlebar的高度.
    • 可以通過設置paddingTop/layout_height.
    • 對於不同的Android版本,可以通過版本適配文件,在values/dimens.xmlvalues-v19/dimens.xml分別定義具體數值.
    • 調整paddingTop,有可能導致Titlebar中的內容不會再垂直居中.
    • 不適應無Titlebaractivity.
  3. android:fitsSystemWindows && OnApplyWindowInsetsListener
    • fitsSystemWindows標籤可以直接對View添加paddingXXX
    • activity的佈局嵌套結構中只對第一個設置fitsSystemWidnowsView有效,無法設置設置到多個View
    • 方式單一,對於指定的界面實現簡單,但要應用於整個App超多Activitylayout.xml,不夠靈活.
    • 不夠靈活還表現在:有些activity的複雜效果可能會有多個View同時或分場合佔據系統狀態欄的空間,需要留出額外的修改接口.
  4. activitycontentView的頂部再addView直接填充原來狀態欄的區域.
  5. OnApplyWindowInsetsListener可以回調給開發者當前WindowInset的區域類型與區域寬高Rect信息
    • fitsSystemWindows一樣,多個View設置該監聽但也只有最外層的view會被調用執行.

在實踐中,本人採用了1 2 5這三種方式配合使用.


fitsSystemWindows

我並不想用該屬性.
這裏只記錄一下系統源碼中的相應的方法:

1
2
3
4
5
6
View.java
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets)
private boolean fitSystemWindowsInt(Rect insets)
// 這個方法是真正爲View添加paddingXXX的地方
protected void internalSetPadding(int left, int top, int right, int bottom)

調試系統源碼的一個方法:
使用Android自帶模擬器Debug,斷點跟進執行過程.要注意,模擬器的apk level要和compileSdkVersionbuildToolsVersion相對應.


Activity中的接口設計

接口設計的原則:

  1. 對正常的業務佈局xml的編寫沒有強制要求
    如不要求強制使用fitsSystemWindows
  2. 不影響正常的業務Activity的java代碼編寫
    如業務Actiivty不需要額外的編碼量即可實現透明狀態欄效果.特殊的動效Activity除外.
  3. 提供靈活的處理方式
    可方便的開啓或關閉透明狀態欄功能.

類圖如下:
Activity

  • BaseActivity 是App中所有Activity的父類.
    由於透明狀態欄與Activity相關,所以對應的接口聲明都放在BaseActivity中.
    默認Activity的透明狀態欄功能是開啓的.
    該類中幾個重要函數的調用順序爲:
1
2
`onCreate` `setContentView` `isFixTransparentStatusbar`
└──true`fixTransparentStatusbar`

具體代碼實現爲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// WhateverActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// BaseActivity.java
@Override
public void setContentView(View view) {
rootView = view;
super.setContentView(view);
if (isFixTransparentStatusBar()) {
Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 亮色模式,避免系統狀態欄的圖標不可見
visibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
window.getDecorView().setSystemUiVisibility(visibility);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
fixTransparentStatusBar(view);
// 最後fix一下狀態欄背景白色與系統的文字圖標白色的問題
fixTransparentStatusBarWhiteTextColor(view, viewStatusbarBackground);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// WindowManager.LayoutParams localLayoutParams = window.getAttributes();
// localLayoutParams.flags = (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | localLayoutParams.flags);
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
fixTransparentStatusBar(view);
// 最後fix一下狀態欄背景白色與系統的文字圖標白色的問題
fixTransparentStatusBarWhiteTextColor(view, viewStatusbarBackground);
} else {
setStatusbarBackgroundGone();
}
} else {
setStatusbarBackgroundGone();
}
}

注意 : 由於透明狀態欄是執行Window.addFlags()實現的,該方法又調用了Window.setFlags().
閱讀該Api文檔,發現推薦先執行setContentView後執行Window.setFlags().
Window.setFlags

  • TitlebarActivity 是通用的包含TitlebarActivity.
    重載了setContentView(),實現自動添加Titlebar這個通用組件,當然不需要Titlebar時也可以使用setContentViewNoTitlebar().
    擴展此功能,即在添加通用Titlebar前先添加上通用的viewStatusbarBackground.
    setContentView()的實現爲:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void setContentView(View view) {
contentView = view;
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(this).inflate(R.layout.transparent_status_bar_bg_view, linearLayout, true);
viewStatusbarBackground = linearLayout.findViewById(R.id.status_bar_background);
LayoutInflater.from(this).inflate(R.layout.titlebar_original, linearLayout, true);
viewTitlebar = linearLayout.findViewById(R.id.titlebar_layout);
initTitlebarIDs(viewTitlebar);
linearLayout.addView(contentView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
super.setContentView(linearLayout);
}

其中,爲了便於定位到TitlebarviewStatusbarBackground,這兩個組件的id都被預先定義在attrs.xml中.

1
2
3
4
5
// values/attrs.xml
<resources>
<item name="status_bar_background" type="id"/>
<item name="titlebar_layout" type="id"/>
</resources>

Fragment中的接口設計

有的Activity的顯示主體是Fragment,接口設計的觀點爲不應干擾Fragment正常的onCreateView()的實現流程.
那麼在哪個時機處理FragmentcontentView呢?
閱讀Api發現了Fragment::onViewCreated(View view)這個方法,該方法會在onCreateView()返回後,立即執行,且方法參數爲onCreateView()所返回的View.

Fragment

Java代碼實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BaseFragment extends Fragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
&& isFixTransparentStatusBar()) {
fixTransparentStatusBar(view);
}
super.onViewCreated(view, savedInstanceState);
}
/**
* 是否需要改變status bar背景色,對於某些機型手機(如oppo)無法改變狀態欄字體顏色,
* 會被當前狀態欄擋住字體顏色,因此修改透明狀態欄背景色
* @return true: 調用fixTransparentStatusBar()
*/
protected boolean isFixTransparentStatusBar(){
return false;
}
/**
* @param view {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}中返回的view.
* */
protected void fixTransparentStatusBar(View view) {
}
}

通過fixTransparentStatusBar(),即可以調整Fragment的界面顯示,無論是往狀態欄區域添加一個填充View或根據id再調整寬高或padding都是可以的.


白色Titlebar的處理

Android 6.0及以上可以使用亮色模式.
但在是低版本的手機中,Titlebar如果是白色的,或者說App的主題是白色的,則會出現狀態欄的白色文字和圖標被淹沒在Titlebar中無法閱讀.如下圖:
white.text.and.icon.invisible
這時可以通過layer-list來設置分層背景,不必新增額外的View填充系統狀態欄區域.
見如下代碼或TestBasic/res/drawable/title_layout_white3.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/black" />
</shape>
</item>
<item android:bottom="1dp">
<shape android:shape="rectangle">
<solid
android:color="@android:color/white" />
</shape>
</item>
<!-- 48dp爲標題欄高度 -->
<item android:bottom="48dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@android:color/white"
android:centerColor="@color/middleColor"
android:endColor="@android:color/darker_gray"
android:angle="90"
/>
</shape>
</item>
</layer-list>
  • 第一個item爲黑色背景,效果爲Titlebar底下的黑色分隔線.
  • 第二個item爲常規的Titlebar背景.
  • 第三個item爲狀態欄的過濾漸變背景色.

最張效果見下圖:
white.Titlebar.perfect


React-Native的處理

React-Nativejs代碼,怎麼辦?
不不,React是表象,Native是實質。一樣處理掉。
React-Nativeroot的組件界面是ReactRootView,可以在顯示的Activity里布局使用LinearLayoutorientationVERTICAL,
TransparentStatusBarReactRootView一併添加爲子View,設置該LinearLayoutActivitycontentView即可。

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ElnReactBaseActivity extends BaseActivity {
private ReactInstanceManager mReactInstanceManager;
private ReactRootView mReactRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
viewStatusbarBackground = LayoutInflater.from(this).inflate(R.layout.transparent_status_bar_bg_view, linearLayout, false);
linearLayout.addView(viewStatusbarBackground);
mReactRootView = new ReactRootView(this);
LinearLayout.LayoutParams layParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
linearLayout.addView(mReactRootView, layParams);
mReactInstanceManager = ReactHelper.getInstance().getReactManager();
Bundle bundle = getExtra();
mReactRootView.startReactApplication(mReactInstanceManager, "ELearning", bundle);
setContentView(linearLayout);
}
}

小米 與 魅族 與 (莫名其妙的)華爲

小米 與 魅族都能通過自各的反射方法實現狀態欄的亮色模式,解決白色Titlebar的問題.
這點兩家做得很好.這裏直接給出官方文檔說明了.

小米狀態欄變色
魅族狀態欄變色

上述的代碼也整合進了GitHub中的工程TitlebarBelowTransparentStatusBar.

至於華爲,額…大部分華爲機子都是好機,但華爲榮耀6 Plus(PE-TL10,EMUI3.1,Android 5.1.1)明明是Android 5.1,但使用5.1的代碼無效,得使用4.4的實現方式.


騰訊優測UTest

一個方便使用的App遠程測試平臺,機型多,Android版本齊全.
出了華爲這檔子事,就把App上傳試了下其它各種手機,還好還好,沒發現其它妖娥子.

uTest


About Sodino

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章