Android全面屏虛擬導航欄適配

拉丁吳發表於2018-11-07

手機正朝著全面屏的方向演進,與此同時也給開發者帶來了很多適配上的新問題,虛擬導航欄就是其中一個。最近在糗百的專案中,就有相關的適配問題,我查閱了目前關於虛擬導航欄適配的相關文章,基本上在全面屏手機裡都有不同程度的失效,這使我不禁開始思考這個問題,

Android全面屏虛擬導航欄適配

為什麼我們對虛擬導航欄的判斷在全面屏中失效了?今天我們就從虛擬導航欄的來歷和發展,詳細聊聊虛擬導航欄的適配。

Android全面屏虛擬導航欄適配

關於虛擬導航欄

最初搭載Android系統的手機使用的是全鍵盤(物理按鍵),到後來廠商們發現不需要那麼多按鍵,在摸索中逐漸減少至三個功能按鍵: 返回鍵/Home鍵/任務鍵,

Android全面屏虛擬導航欄適配

基本上所有的Android廠商釋出的手機,無論使用原生系統,還是自研的ROM,都會帶上這三個按鍵。

後面,Android為了提高屏佔比,減小手機下巴的高度,支援手機廠商把這幾個按鍵整合到螢幕中,

Android全面屏虛擬導航欄適配

就出現瞭如今的虛擬導航欄。

在全面屏流行之前,Android主推的虛擬導航欄並不是手機主流,很多手機依然是把三個功能按鍵作為物理按鍵放在手機下巴處。使用物理按鍵的手機和虛擬導航欄的手機在市場上可以說是一半一半。什麼?你見過又有虛擬按鍵又有物理按鍵??別擔心,這樣的廠商已經基本倒閉了

Android全面屏虛擬導航欄適配
Android全面屏虛擬導航欄適配

到這裡,我們可以確定,Android手機中這三個頗具特色的功能按鍵要麼是物理按鍵,要麼是整合在螢幕中作為虛擬按鍵。

關於虛擬導航欄的適配

我們先來問一個問題:

-我們是否需要虛擬導航欄的適配?

-答案是:未必,

因為我們完全有方法避免虛擬導航欄導致的種種問題。那就是通過各種設定,把虛擬導航欄的螢幕和APP顯示區域完全割裂開。像這樣:

Android全面屏虛擬導航欄適配

這兩塊顯示區域相互並不干擾,是否存在虛擬導航欄就不重要了。

但是這樣雖然省事,但是很多時候會導致APP缺乏美感(設計師就是這麼和我說的),設計師往往希望APP的顯示區域伸入到虛擬導航欄中,達到一種沉浸感:

像這種:

Android全面屏虛擬導航欄適配

面對設計師的這個小小的要求,我們能說不麼?顯然不能。那麼這個時候,就需要考慮適配的問題了。

Android全面屏虛擬導航欄適配

我們需要知道當前介面是否存在虛擬導航欄,以及虛擬導航欄的高度,以便於對我們的佈局做一定的調整,否則這兩者就會重疊。這就是虛擬導航欄的適配。

關於虛擬導航欄的適配,我們需要明確一點,虛擬導航欄適配的核心問題並不是如何獲取虛擬導航欄的高度,而在於判斷當前虛擬導航欄是否存在或正在顯示,因為導航欄的高度屬於系統設定的一個值,是不可改變的。獲取這個尺寸上並沒有什麼難度,我們只需要把這個值讀出來即可。真正的核心在於,怎樣判斷當前虛擬導航欄是否存在。

判斷虛擬導航欄的老方法

在全面屏手機之前,我們對虛擬導航欄的判斷就有很多種方法,

比如方法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。在這種前提下,那種思考方式不會有什麼問題。

然而全面屏手機打破了這種對立存在的格局,去掉了物理導航鍵,但同時也隱去了虛擬導航欄(即手機確實整合了虛擬導航欄,但是沒有使用),取而代之的是通過全面屏手勢實現三個按鍵的功能。所以說,全面屏手機+全面屏手勢。是導致以往判斷方法失效的原因。

Android全面屏虛擬導航欄適配

回過頭想,導致判斷失效更本質的原因,其實是因為我們的判斷方法都是間接判斷,是去尋找必要條件,而非充分條件,就好比我們在夜晚看到了月亮的光芒,並不能證明月亮是自發光的物體,除非假設一個前提:能發光的物體都是自發光的。證明才能成立。而全面屏的到來,正好打破了這個前提,導致了我們的推匯出了問題。

現在,由於全面屏手機裡一般都存在虛擬導航欄和全面屏手勢這兩中操作方式,且二者必取其一,因此,網上就又出現了另一種間接判斷法,即判斷當前手機是否在用全面屏手勢,如果否,則表示在用虛擬導航欄。

以下是針對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,使用者的影響都是明顯的,作為開發者的我們,更加不能輕視這些改變。

相關文章