手機正朝着全面屏的方向演進,與此同時也給開發者帶來了不少適配上的新問題,虛擬導航欄就是其中一個。最近在糗百的項目中,就有相關的適配問題,我查閱了目前關於虛擬導航欄適配的相關文章,基本上在全面屏手機裏都有不一樣程度的失效,這使我不由開始思考這個問題,android
爲何咱們對虛擬導航欄的判斷在全面屏中失效了?今天咱們就從虛擬導航欄的來歷和發展,詳細聊聊虛擬導航欄的適配。bash
最初搭載Android系統的手機使用的是全鍵盤(物理按鍵),到後來廠商們發現不須要那麼多按鍵,在摸索中逐漸減小至三個功能按鍵: 返回鍵/Home鍵/任務鍵, app
基本上全部的Android廠商發佈的手機,不管使用原生系統,仍是自研的ROM,都會帶上這三個按鍵。ide
後面,Android爲了提升屏佔比,減少手機下巴的高度,支持手機廠商把這幾個按鍵集成到屏幕中,佈局
就出現了現在的虛擬導航欄。ui
在全面屏流行以前,Android主推的虛擬導航欄並非手機主流,不少手機依然是把三個功能按鍵做爲物理按鍵放在手機下巴處。使用物理按鍵的手機和虛擬導航欄的手機在市場上能夠說是一半一半。什麼?你見過又有虛擬按鍵又有物理按鍵??別擔憂,這樣的廠商已經基本倒閉了spa
到這裏,咱們能夠肯定,Android手機中這三個頗具特點的功能按鍵要麼是物理按鍵,要麼是集成在屏幕中做爲虛擬按鍵。設計
咱們先來問一個問題:3d
-咱們是否須要虛擬導航欄的適配?code
-答案是:未必,
由於咱們徹底有方法避免虛擬導航欄致使的種種問題。那就是經過各類設置,把虛擬導航欄的屏幕和APP顯示區域徹底割裂開。像這樣:
這兩塊顯示區域相互並不干擾,是否存在虛擬導航欄就不重要了。
可是這樣雖然省事,可是不少時候會致使APP缺少美感(設計師就是這麼和我說的),設計師每每但願APP的顯示區域伸入到虛擬導航欄中,達到一種沉浸感:
像這種:
面對設計師的這個小小的要求,咱們能說不麼?顯然不能。那麼這個時候,就須要考慮適配的問題了。
咱們須要知道當前界面是否存在虛擬導航欄,以及虛擬導航欄的高度,以便於對咱們的佈局作必定的調整,不然這二者就會重疊。這就是虛擬導航欄的適配。
關於虛擬導航欄的適配,咱們須要明確一點,虛擬導航欄適配的核心問題並非如何獲取虛擬導航欄的高度,而在於判斷當前虛擬導航欄是否存在或正在顯示,由於導航欄的高度屬於系統設置的一個值,是不可改變的。獲取這個尺寸上並無什麼難度,咱們只須要把這個值讀出來便可。真正的核心在於,怎樣判斷當前虛擬導航欄是否存在。
在全面屏手機以前,咱們對虛擬導航欄的判斷就有不少種方法,
好比方法1:
{
// 判斷系統是否寫入了關於定義虛擬導航欄的高度相關變量。
//若是高度大於0,則表示該手機有虛擬導航欄
Resources res = activity.getResources();
int resourceId = res.getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
return res.getDimensionPixelSize(resourceId)>0;
}
}
複製代碼
又或者是這種方法2:
{
int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
// 判斷系統是否寫入了關因而否顯示虛擬導航欄的相關變量,若是爲true,表示有虛擬導航欄
return id > 0 && resources.getBoolean(id);
}
複製代碼
又或者方法3:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Display display = context.getWindowManager().getDefaultDisplay();
Point size = new Point();
Point realSize = new Point();
display.getSize(size); // app繪製區域
display.getRealSize(realSize);
return realSize.y != size.y;
} else {
boolean menu = ViewConfiguration.get(context).hasPermanentMenuKey();
boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);// 判斷是否存在物理按鍵
if (menu || back) {
return false;
} else {
return true;
}
}
複製代碼
以上三個方法,基本上都是看系統中是否有虛擬導航欄的相關定義,即若是咱們能發現系統中由虛擬導航欄相關的定義,就認定虛擬導航欄存在。這個思考方式源於手機的物理導航鍵和虛擬導航鍵一直以來都是對立存在的,即去掉了物理導航鍵,那麼就會使用虛擬導航欄,若是存在虛擬導航欄,那麼就沒有物理按鍵。有了A就沒有B,若是存在了B,那就沒有A。在這種前提下,那種思考方式不會有什麼問題。
然而全面屏手機打破了這種對立存在的格局,去掉了物理導航鍵,但同時也隱去了虛擬導航欄(即手機確實集成了虛擬導航欄,可是沒有使用),取而代之的是經過全面屏手勢實現三個按鍵的功能。因此說,全面屏手機+全面屏手勢。是致使以往判斷方法失效的緣由。
回過頭想,致使判斷失效更本質的緣由,實際上是由於咱們的判斷方法都是間接判斷,是去尋找必要條件,而非充分條件,就比如咱們在夜晚看到了月亮的光芒,並不能證實月亮是自發光的物體,除非假設一個前提:能發光的物體都是自發光的。證實才能成立。而全面屏的到來,正好打破了這個前提,致使了咱們的推導出了問題。
如今,因爲全面屏手機裏通常都存在虛擬導航欄和全面屏手勢這兩中操做方式,且兩者必取其一,所以,網上就又出現了另外一種間接判斷法,即判斷當前手機是否在用全面屏手勢,若是否,則表示在用虛擬導航欄。
如下是針對vivo,小米的全面屏虛擬導航欄的判斷方法:
/**
* @returnv false 表示使用的是虛擬導航鍵(NavigationBar), true 表示使用的是手勢, 默認是false
*/
public static boolean vivoNavigationGestureEnabled(Context context) {
int val = Settings.Secure.getInt(context.getContentResolver(), NAVIGATION_GESTURE, NAVIGATION_GESTURE_OFF);
return val != NAVIGATION_GESTURE_OFF;
}
public static boolean isXiaoMiNavigationBarShow(Activity context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Settings.Global.getInt(context.getContentResolver(), "force_fsg_nav_bar", 0) != 0) {
//開啓手勢,不顯示虛擬鍵
return false;
}
}
}
複製代碼
可是這種方法也有一些缺陷,好比,判斷方法都是廠商方面給出的,也就是說沒有通用性,還有其餘廠商系統判斷方法未知;並且,這種方法很難判斷那些可隱藏/呼出的虛擬導航欄。更重要的是,經過必要條件作間接判斷始終是有隱患的。
所以,爲了尋找一個更加通用,準確的判斷方法,咱們嘗試進入Android系統層面去嘗試尋找判斷虛擬導航欄的方案。
虛擬導航欄也是一個View,若是這個View繪製了本身,並顯示在Window佈局中,那麼虛擬導航欄就必定存在。也就是說,咱們只要找到這個View,並證實它是否存在便可。
因而咱們嘗試經過Layout Inspector分析了虛擬導航欄的佈局層級,發現它是DecorView的Child View(Android5.0以上是這樣),同時咱們在DecorView中找到了表明虛擬導航欄的View,那麼,接下來的問題就很簡單了咯。代碼以下:
{
private static final String NAVIGATION= "navigationBarBackground";
// 該方法須要在View徹底被繪製出來以後調用,不然判斷不了
//在好比 onWindowFocusChanged()方法中能夠獲得正確的結果
public static boolean isNavigationBarExist(@NonNull Activity activity){
ViewGroup vp = (ViewGroup) activity.getWindow().getDecorView();
if (vp != null) {
for (int i = 0; i < vp.getChildCount(); i++) {
vp.getChildAt(i).getContext().getPackageName();
if (vp.getChildAt(i).getId()!= NO_ID && NAVIGATION.equals(activity.getResources().getResourceEntryName(vp.getChildAt(i).getId()))) {
return true;
}
}
}
return false;
}
}
複製代碼
固然,還有一種判斷方案,也很好。
public static void isNavigationBarExist(Activity activity, final OnNavigationStateListener onNavigationStateListener) {
if (activity == null) {
return;
}
final int height = getNavigationHeight(activity);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
activity.getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets windowInsets) {
boolean isShowing = false;
int b = 0;
if (windowInsets != null) {
b = windowInsets.getSystemWindowInsetBottom();
isShowing = (b == height);
}
if (onNavigationStateListener != null && b <= height) {
onNavigationStateListener.onNavigationState(isShowing, b);
}
return windowInsets;
}
});
}
}
public static int getNavigationHeight(Context activity) {
if (activity == null) {
return 0;
}
Resources resources = activity.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height",
"dimen", "android");
int height = 0;
if (resourceId > 0) {
//獲取NavigationBar的高度
height = resources.getDimensionPixelSize(resourceId);
}
return height;
}
複製代碼
這種方法是判斷系統窗口占用區域,底部可能出現的系統窗口除了虛擬導航欄,可能還存在虛擬鍵盤,彷佛不太好判斷,可是因爲咱們能夠獲得系統配置的虛擬導航欄的高度,因此在這些系統佔用的窗口高度中咱們能夠篩選出虛擬導航欄的高度。所以,總的來說,這種判斷也是很不錯的。
虛擬導航欄的適配原本只是一個小問題,可是仔細深究之下,發現還有頗有意思,因此才經過大篇幅幫你們簡單的梳理整個虛擬導航欄的由來,發展以及適配工做,
手機形態的演進,其實對於Android系統,APP,用戶的影響都是明顯的,做爲開發者的咱們,更加不能輕視這些改變。