判斷虛擬導航欄的老方法
在全面屏手機之前,我們對虛擬導航欄的判斷就有很多種方法,
比如方法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;
}