咱們先一塊兒來回顧一下實現沉浸式狀態欄的通常套路。在Android上,關於對StatusBar(狀態欄)的操做,一直都在不斷改善,而且表現愈來愈好,在Android4.4 如下,咱們能夠對StatusBar和 NavigationBar進行顯示和隱藏操做。可是直到Android4.4,咱們才能真正意義上的實現沉浸式狀態欄。從Android4.4 到如今(Android 9),關於沉浸式大概能夠分紅三個階段:java
總結:這三個階段的Android上API版本混亂,各類Flag林立。再加上各大廠商的定製化可謂是火上澆油,讓安卓開發者異常頭疼。android
咱們將從沉浸式支持的三個階段和支持的功能出發,去了解出現的相關背景,而後去了解怎麼實現三個階段的沉浸式。git
在android4.4及以上版本中爲 setSystemUiVisibility() 方法引入了一個新的flag:SYSTEM_UI_FLAG_IMMERSIVE,它可使你的app實現真正意義上的全屏體驗。當SYSTEM_UI_FLAG_IMMERSIVE、SYSTEM_UI_FLAG_HIDE_NAVIGATION 和SYSTEM_UI_FLAG_FULLSCREEN三個flag一塊兒使用的時候,能夠隱藏狀態欄與導航欄,同時讓你的app能夠捕捉到用戶的全部觸摸屏事件。從Android4.4以上版本纔是真正的能夠設置沉浸式體驗,但也僅僅是操做狀態欄和導航欄的顯示與隱藏。github
當沉浸式全屏模式啓用的時候,你的activity會繼續接受各種的觸摸事件。用戶能夠經過在狀態欄與導航欄原來區域的邊緣向內滑動讓系統欄從新顯示。這個操做清空了SYSTEM_UI_FLAG_HIDE_NAVIGATION和SYSTEM_UI_FLAG_FULLSCREEN,若是沒有兩個標誌的話,系統欄從新變得可見。若是設置了兩個標籤的話,這個操做同時也觸發了View.OnSystemUiVisibilityChangeListener。然而, 若是你想讓系統欄在一段時間後自動隱藏的話,你應該使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY標籤。segmentfault
展現了各類不一樣的「沉浸式」狀態api
在上圖中:app
注意,immersive類的標籤只有在與SYSTEM_UI_FLAG_HIDE_NAVIGATION,SYSTEM_UI_FLAG_FULLSCREEN中一個或兩個一塊兒使用的時候纔會生效。你能夠只使用其中的一個,可是通常狀況下你須要同時隱藏狀態欄和導航欄以達到沉浸的效果。ide
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
<style name="Theme" parent="Theme.Design.Light.NoActionBar"> <item name="android:windowTranslucentStatus">true</item> </style>
效果以下:佈局
效果如上圖,能夠看出,沉浸式的效果是出來了,可是也有一個問題,咱們的標題欄和狀態欄重疊了,至關於整個佈局上移了StatusBar 的高度。測試
爲了讓標題欄回到原來的位置而且適應標題欄的顏色,咱們在標題欄的上方添加一個大小和StatusBar大小同樣假的狀態欄View,View 的BackgroundColor 能夠本身設置成標題欄同樣的顏色也能夠是其餘顏色,這個View起到一個佔位的做用。這個時候,標題欄就會下移StatusBar的高度,回到正常的位置。
經過設置paddingTop從新繪製標題欄高度代碼以下:
View statusBarView = mDecorView.findViewById(IMMERSION_STATUS_BAR_VIEW); if (statusBarView == null) { statusBarView = new View(mActivity); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, mBarConfig.getStatusBarHeight()); params.gravity = Gravity.TOP; statusBarView.setLayoutParams(params); statusBarView.setVisibility(View.VISIBLE); statusBarView.setId(IMMERSION_STATUS_BAR_VIEW); mDecorView.addView(statusBarView); } if (mBarParams.statusBarColorEnabled) { statusBarView.setBackgroundColor(ColorUtils.blendARGB(mBarParams.statusBarColor, mBarParams.statusBarColorTransform, mBarParams.statusBarAlpha)); } else { statusBarView.setBackgroundColor(ColorUtils.blendARGB(mBarParams.statusBarColor, Color.TRANSPARENT, mBarParams.statusBarAlpha)); }
添加上述代碼後,效果以下:
經過以上就能夠實現Android 4.4 上的沉浸式狀態欄。
小結:Android4.4-Android5.0的步驟就是爲window添加FLAG_TRANSLUCENT_STATUS的Flag,而後添加一個假的狀態欄,經過上述方法設置的沉浸式在Android4.4-Android5.0之間的效果如貼圖,狀態欄頂部是有一個黑色陰影漸變,在5.0版本版本以上被修復了。
若是是一張圖片沉浸到狀態欄則不須要設置這個假的狀態欄,只須要設置,FLAG_TRANSLUCENT_STATUS就OK。而且在Android4.4-Android5.0是沒有提供改變狀態顏色的屬性,因此只能經過新增長一個假的狀態欄方式改變背景顏色。
Android 5.0 是一個里程碑式的版本,從Android 5.0開始,Google 推出了全新的設計規範 Material Design,而且原生控件就能夠實現一些炫酷的UI動效。從這個版本開始,google 加入了一個比較重要的方法setStatusBarColor (對應屬性:android:statusBarColor),經過這個方法,能夠很輕鬆地實現沉浸式狀態欄。方法以下
public abstract void setStatusBarColor(@ColorInt int color);
注意看這個方法的註釋,想要這個方法生效,必須還要配合一個Flag一塊兒使用,必須設置FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS ,而且不能設置FLAG_TRANSLUCENT_STATUS(Android 4.4才用這個),因此和4.4互斥不能共用。因此Android5.0以上能夠設置狀態欄和導航欄字體顏色。
解釋:設置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,代表會Window負責系統bar的background 繪製,繪製透明背景的系統bar(狀態欄和導航欄),而後用getStatusBarColor()和getNavigationBarColor()的顏色填充相應的區域。這就是Android 5.0 以上實現沉浸式導航欄的原理。
實現沉浸式添加以下代碼:
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); //注意要清除 FLAG_TRANSLUCENT_STATUS flag getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); getWindow().setStatusBarColor(getResources().getColor(android.R.color.holo_red_light));
效果以下:
若是在開發的時候是經過設置主題的方式設置,則須要在values-v21文件夾下添加以下主題,達到兼容目的
<style name="Theme" parent="Theme.Design.Light.NoActionBar"> <item name="android:windowTranslucentStatus">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:statusBarColor">@android:color/holo_red_light</item> </style>
在Android 5.0 使圖片延伸到狀態欄,只需設置windowTranslucentStatus,將 statusBarColor 設置爲透明便可。
主題方式設置以下:
<style name="ImageTranslucentTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <item name="android:windowTranslucentNavigation">true</item> <item name="android:windowTranslucentStatus">true</item> <!-- 設置statusBarColor 爲透明--> <item name="android:statusBarColor">@android:color/transparent</item> </style>
在開發過程當中,使用代碼設置windowTranslucentStatus須要經過版本號的判斷兼容 Android5.0如下和Android 5.0以上。
使用Android6.0如下版本沉浸式的時候會遇到一個問題,那就是Android 系統狀態欄的字色和圖標顏色爲白色,當狀態欄顏色接近淺色的時候,狀態欄上的內容就看不清了。Android 6.0 新添加了一個屬性來解決這個問題,屬性是SYSTEM_UI_FLAG_LIGHT_STATUS_BAR。
解釋:爲setSystemUiVisibility(int)方法添加的Flag,請求status bar 繪製模式,它能夠兼容亮色背景的status bar 。要在設置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDSflag ,同時清除了FLAG_TRANSLUCENT_STATUSflag 纔會生效。
經過代碼設置:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); }
效果以下
而且還能夠在主題中使用屬性,而且該主題須要放在values-v23文件夾下相應Android6.0以上才能生效:
<style name="Theme" parent="Theme.Design.Light.NoActionBar"> <item name="android:windowTranslucentStatus">false</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:statusBarColor">@android:color/holo_red_light</item> <!-- Android 6.0以上 狀態欄字色和圖標爲淺黑色--> <item name="android:windowLightStatusBar">true</item> </style>
在實際開發過程當中,咱們不只僅只會遇到以上三種版本兼容問題,還須要考慮如:不一樣手機品牌,動態該狀態欄背景,以及Fragment中須要有本身的狀態欄顏色場景。因此須要綜合考慮多種場景,達到能適配多種開發狀況的要求。
總結出如下場景:
綜合以上場景而且參考github例子進行封裝以後獲得了ZanImmersionBar這個輪子
咱們但願將設置沉浸式效果都封裝在一個類裏面,想達到全部的效果經過一個方法設置,但不少的效果中都會有重複的設置步驟,而且每一個效果的方法太多則分不清使用哪一個,因此將設置沉浸式效果拆分紅幾個步驟,而想要設置個性效果,經過方法設置參數,最後經過init方法收集全部參數讓後統一設置參數屬性。
ZanImmersionBar.with(this).init();//該方法將進行如下步驟處理沉浸式 public void init() { //更新Bar的參數 updateBarParams(); //設置沉浸式 setBar(); //適配狀態欄與佈局重疊問題 fitsLayoutOverlap(); //適配軟鍵盤與底部輸入框衝突問題 fitsKeyboard(); //變色view transformView(); }
沉浸式設置流程以下圖:
這幾個步驟其中,獲取參數和設置沉浸式是必須通過,下面三種設置是在開發中可能遇到的狀況,也是設置參數,若是匹配到了則會進行三種設置的處理,接下來主要分析下第一個步驟和第二步驟。
咱們使用一個對象用於存儲用戶設置的bar參數,這些參數有狀態欄和導航欄顏色、透明度、顯示隱藏等等,經過該對象中的參數來分別設置
public class BarParams implements Cloneable { /** * 狀態欄顏色 */ @ColorInt int statusBarColor = Color.TRANSPARENT; /** * 導航欄顏色 */ @ColorInt int navigationBarColor = Color.BLACK; /** * The Default navigation bar color. */ int defaultNavigationBarColor = Color.BLACK; /** * 狀態欄透明度 */ @FloatRange(from = 0f, to = 1f) float statusBarAlpha = 0.0f; /** * 導航欄透明度 */ @FloatRange(from = 0f, to = 1f) float navigationBarAlpha = 0.0f; //等其餘屬性 ... }
而這些屬性能夠經過如下ZanImmersionBar提供的方法進行個性化設置,而這些方法只是將須要設置的參數添加到BarParams對象中,最後必須調用init將參數設置上去。
ZanImmersionBar.with(this) .transparentStatusBar() //透明狀態欄,不寫默認透明色 .transparentNavigationBar() //透明導航欄,不寫默認黑色(設置此方法,fullScreen()方法自動爲true) .transparentBar() //透明狀態欄和導航欄,不寫默認狀態欄爲透明色,導航欄爲黑色(設置此方法,fullScreen()方法自動爲true) .statusBarColor(R.color.colorPrimary) //狀態欄顏色,不寫默認透明色 .navigationBarColor(R.color.colorPrimary) //導航欄顏色,不寫默認黑色 .barColor(R.color.colorPrimary) //同時自定義狀態欄和導航欄顏色,不寫默認狀態欄爲透明色,導航欄爲黑色 .statusBarAlpha(0.3f) //狀態欄透明度,不寫默認0.0f .navigationBarAlpha(0.4f) //導航欄透明度,不寫默認0.0F .barAlpha(0.3f) //狀態欄和導航欄透明度,不寫默認0.0f .statusBarDarkFont(true) //狀態欄字體是深色,不寫默認爲亮色 .navigationBarDarkIcon(true) //導航欄圖標是深色,不寫默認爲亮色 //等一些其餘方法 .init(); //必須調用方可沉浸式
設置完參數後,則須要收集這些參數,若是在Fragment中使用,則須要Activity同步Fragment的BarParams參數
//得到Bar相關信息 //若是在Fragment中使用,讓Activity同步Fragment的BarParams參數 if (mIsFragment) { ZanImmersionBar immersionBar = mImmersionBarMap.get(mActivity.toString()); if (immersionBar != null) { immersionBar.mBarParams = mBarParams; } }
mImmersionBarMap是個Map,用於存儲每一個Activity對應的ZanImmersionBar對象
關閉銷燬:在activity的onDestroy方法中執行,由於mImmersionBarMap存儲了每一個Activity對應的ZanImmersionBar對象,因此當Activity關閉了須要及時釋放,否則mImmersionBarMap會存在過多無用的ZanImmersionBar對象。
ZanImmersionBar.with(this).destroy(); //必須調用該方法,防止內存泄漏 public void destroy() { //取消監聽 cancelListener(); //刪除當前界面對應的ImmersionBar對象 Iterator<Map.Entry<String, ZanImmersionBar>> iterator = mImmersionBarMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, ZanImmersionBar> entry = iterator.next(); if (entry.getKey().contains(mImmersionBarName) || (entry.getKey().equals(mImmersionBarName))) { iterator.remove(); } } }
該方法則是將上一步的參數進行初始化,初始化過程則會根據沉浸式三個階段和不一樣廠商進行區分設置。須要注意小米手機系統有本身定製化設置的屬性,因此須要分區設置
private void setBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !OSUtils.isEMUI3_x()) { //適配劉海屏 fitsNotchScreen(); //初始化5.0以上,包含5.0 uiFlags = initBarAboveLOLLIPOP(uiFlags); //android 6.0以上設置狀態欄字體爲暗色 uiFlags = setStatusBarDarkFont(uiFlags); //android 8.0以上設置導航欄圖標爲暗色 uiFlags = setNavigationIconDark(uiFlags); } else { //初始化5.0如下,4.4以上沉浸式 initBarBelowLOLLIPOP(); } ... } if (OSUtils.isMIUI6Later()) { //修改miui狀態欄字體顏色 setMIUIBarDark(mWindow, MIUI_STATUS_BAR_DARK, mBarParams.statusBarDarkFont); //修改miui導航欄圖標爲黑色 if (mBarParams.navigationBarEnable) { setMIUIBarDark(mWindow, MIUI_NAVIGATION_BAR_DARK, mBarParams.navigationBarDarkIcon); } } ... }
咱們能夠重點看下5.0以上狀態欄和導航欄初始化,設置初始化window屬性讓後設置導航欄和狀態欄顏色
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private int initBarAboveLOLLIPOP(int uiFlags) { //Activity全屏顯示,但狀態欄不會被隱藏覆蓋,狀態欄依然可見,Activity頂端佈局部分會被狀態欄遮住。 uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; mWindow.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); //判斷是否存在導航欄 if (mBarConfig.hasNavigationBar()) { mWindow.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } //須要設置這個才能設置狀態欄和導航欄顏色 mWindow.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); //設置狀態欄顏色 if (mBarParams.statusBarColorEnabled) { mWindow.setStatusBarColor(ColorUtils.blendARGB(mBarParams.statusBarColor, mBarParams.statusBarColorTransform, mBarParams.statusBarAlpha)); } else { mWindow.setStatusBarColor(ColorUtils.blendARGB(mBarParams.statusBarColor, Color.TRANSPARENT, mBarParams.statusBarAlpha)); } ... return uiFlags; }
經過代碼分析能夠看到其實ZanImmersionBar所作的事情就是將設置沉浸式方法進行步驟拆分和增長個性屬性設置,而且將沉浸式三個階段和不一樣廠商進行區分獨立方法設置調用。
public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 全部子類都將繼承這些相同的屬性,請在設置界面以後設置 ZanImmersionBar.with(this).init(); } @Override protected void onDestroy() { super.onDestroy(); // 必須調用該方法,防止內存泄漏 ZanImmersionBar.with(this).destroy(); } }
注意在Fragment中使用ZanImmersionBar須要在承載的Activity中初始化ZanImmersionBar,否則會拋出異常
public abstract class BaseFragment{ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initImmersionBar(); } @Override public void initImmersionBar() { ZanImmersionBar.with(this).keyboardEnable(true).init(); } }
具體的Fragment調用如下方法:
@Override public void initImmersionBar() { super.initImmersionBar(); ZanImmersionBar.with(this) .statusBarDarkFont(true) .statusBarColor(R.color.btn1) .navigationBarColor(R.color.btn1) .init(); }
在Dialog中設置ZanImmersionBar方式和在Fragment或者Activity同樣,若是在Fragment或者Activity中有設置而且dialog出現不須要改變狀態欄則不用設置ZanImmersionBar,若是須要作定製化上面的高級用法在Dialog也支持
ZanImmersionBar.with(this).init();
ZanImmersionBar.with(this, dialog).init();
重點是調用如下方法,可是此方法會致使有導航欄的手機底部佈局會被導航欄覆蓋,還有底部輸入框沒法根據軟鍵盤彈出而彈出。這個屬性在頂部彈出的時候是須要使用,若是是底部彈框須要看狀況而定。
popupWindow.setClippingEnabled(false);
正常使用ZanImmersionBar通常不須要考慮重疊問題但在項目中接入ZanImmersionBar而且頁面沒有考慮給頭部控件預留出狀態欄的高度,而且須要將頁面內容沉浸到狀態欄或者作定製化狀態欄,這種狀況下須要考慮重疊問題。以前說到Android4.4版本的時候解決重疊的方式是一種,也能夠參考一下幾種方式解決狀態欄與佈局頂部重疊問題。
在values-v19/dimens.xml文件下
<dimen name="status_bar_height">25dp</dimen>
在values/dimens.xml文件下
<dimen name="status_bar_height">0dp</dimen>
而後在佈局界面添加view標籤,高度指定爲status_bar_height
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/darker_gray" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="@dimen/status_bar_height" android:background="@color/colorPrimary" /> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" app:title="方法一" app:titleTextColor="@android:color/white" /> </LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true"> </LinearLayout>
而後使用ImmersionBar時候必須指定狀態欄顏色
ZanImmersionBar.with(this) .statusBarColor(R.color.colorPrimary) .init();
注意:ZanImmersionBar必定要在設置完佈局之後使用
ZanImmersionBar.with(this) .fitsSystemWindows(true) //使用該屬性,必須指定狀態欄顏色 .statusBarColor(R.color.colorPrimary) .init();
在標題欄的上方增長View標籤,高度指定爲0dp
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/darker_gray" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/colorPrimary" /> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" app:title="方法四" app:titleTextColor="@android:color/white" /> </LinearLayout>
而後使用ZanImmersionBar的statusBarView方法,指定view就能夠啦
ZanImmersionBar.with(this) .statusBarView(view) .init(); //或者 //ZanImmersionBar.setStatusBarView(this,view);
ZanImmersionBar.with(this) .titleBar(view) //能夠爲任意view,若是是自定義xml實現標題欄的話,最外層節點不能爲RelativeLayout .init(); //或者 //ZanImmersionBar.setTitleBar(this, view);
ZanImmersionBar.with(this) .titleBarMarginTop(view) //能夠爲任意view .statusBarColor(R.color.colorPrimary) //指定狀態欄顏色,根據狀況是否設置 .init(); //或者使用靜態方法設置 //ZanImmersionBar.setTitleBarMarginTop(this,view);
在處理Android沉浸式狀態欄和導航欄開始會很頭大,而且會不理解相關設置的window的FLAG屬性,想要分清楚這些屬性的大意須要從出現背景出發拆封,先熟悉沉浸式出現的3個階段的屬性和版本能作什麼和不能作什麼,而後再去了解各個廠家定製化的屬性在哪些版本階段使用,以及是否須要對異形屏適配,最後纔是在實際開發和需求中對狀態欄和導航欄處理。不過在Android6.0之後版本廠家的定製化狀態已經愈來愈沒有意義了,之後Android原生趨勢也將會讓開發者使用沉浸式愈來愈舒服。