Android 虛擬導航鍵適配
最近項目裏需要適配虛擬導航鍵,以及獲取導航鍵的高度,來適配界面佈局的高度。
判斷虛擬導航鍵是否存在
不得不說,國內由於不同手機廠商對系統做了不同的修改,對系統界面底部的NavigationBar處理方式也就各不相同,有些手機系統有NavigationBar,有些手機沒有,還有則是在設置增加開關,讓用戶選擇是否啓用NavigationBar。因此,對弈APP開發者來說,完美適配虛擬導航鍵也是一件比較有挑戰性的事。
首先,我們來看看android源碼有沒有提供公共API來判斷當前系統是否存在NavigationBar。
分析源碼
通過查閱Android源碼,我們發現在WindowManagerService.java下面有一個方法是hasNavigationBar:
@Override
public boolean hasNavigationBar() {
return mPolicy.hasNavigationBar();
}
但是,WindowManagerService是系統服務,我們無法直接調用這個方法。那我繼續看這個方法的具體實現。
mPolicy是什麼呢?看源碼:final WindowManagerPolicy mPolicy;,WindowManagerPolicy只是一個接口,具體的實現是在哪裏呢?
它的實現類是PhoneWindowManager,所以最終是調到了PhoneWindowManager的hasNavigationBar()
// Use this instead of checking config_showNavigationBar so that it can be consistently
// overridden by qemu.hw.mainkeys in the emulator.
@Override
public boolean hasNavigationBar() {
return mHasNavigationBar;
}
再看看PhoneWindowManager中給mHasNavigationBar賦值的地方在哪裏:
public void setInitialDisplaySize(Display display, int width, int height, int density) {
...
...
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
// Allow a system property to override this. Used by the emulator.
// See also hasNavigationBar().
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
mHasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
mHasNavigationBar = true;
}
...
...
}
從上面代碼可以看到mHasNavigationBar的值的設定是由兩處決定的:
1.首先從系統的資源文件中取設定值config_showNavigationBar, 這個值的設定的文件路徑是frameworks/base/core/res/res/values/config.xml
<!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
autodetected from the Configuration. -->
<bool name="config_showNavigationBar">false</bool>
- 我們可以從"qemu.hw.mainkeys" 處着手, 這個值可能會覆蓋上面獲取到的mHasNavigartionBar的值。 如果“qemu.hw.mainkeys”獲取的值不爲空的話,不管值是true還是false,都要依據後面的情況來設定。
所以上面的兩處設定共同決定了NavigationBar的顯示與隱藏。
實現判斷NavigationBar 的方法
網絡上流傳的大多數的方法 都是獲取
rs.getIdentifier("config_showNavigationBar", "bool", "android");
hasNavigationBar = rs.getBoolean(id);
根據hasNavigationBar的狀態來確定是否有虛擬鍵盤。
下面這個方法把網絡上的方法合成一下基本如此 。
//判斷是否存在NavigationBar
public static boolean hasNavigationBar(Context context) {
boolean hasNavigationBar = false;
Resources rs = context.getResources();
int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
if (id > 0) {
hasNavigationBar = rs.getBoolean(id);
}
try {
//反射獲取SystemProperties類,並調用它的get方法
Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
Method m = systemPropertiesClass.getMethod("get", String.class);
String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
hasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
hasNavigationBar = true;
}
} catch (Exception e) {
e.printStackTrace();
}
return hasNavigationBar;
}
從原理上講到此爲止了吧,在我的小米手機和錘子手機上實體物理鍵, 以及Android 8, 9 的虛擬機上跑了一下,美滋滋, 感覺沒事了,世界和平了。然而現實並不是如此。
自從設計拿了個華爲全面屏和Redmi7 , 發現這個方法沒用了。 瞬間感覺世界黯淡了。
全面屏手機虛擬導航鍵的開關
由於全面屏手機都沒有底部的Home,Back等實體按鍵,因此,大多數全面屏手機都是支持虛擬導航鍵,即通過上面的方法hasNavigationBar獲取的返回值都是true。
而在國內的全面屏手機中,大多數如果是全面屏,底部NavigationBar會佔用一些屏幕空間, 一直顯示出來, 這就失去了全面屏的意義, 用戶體驗並不好。
現在很多手機 例如華爲P20 ,30 ,小米手機、VIVO x20 ,X20 Plus 就會再系統進入以及設置中增加了是否啓用NavigationBar的開關, 以及手勢的滑動 是否顯示。
例如VIVO 開關在設置-> 導航鍵
當隱藏虛擬導航鍵時,用戶可以通過底部上滑的手勢實現導航鍵同樣的功能,非常便利。
感覺這種設計貌似是蘋果先帶的頭吧 。
那麼是不是有什麼可以判斷呢? 必須有了, 例如VIVO 就是在Setting 中這個開關的值, 可以在系統setting.xml
中找到該屬性。 看一下兼容代碼:
vivo 適配代碼
private static final String NAVIGATION_GESTURE = "navigation_gesture_on";
private static final int NAVIGATION_GESTURE_OFF = 0;
/**
* 獲取vivo手機設置中的"navigation_gesture_on"值,判斷當前系統是使用導航鍵還是手勢導航操作
* @param context app Context
* @return 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;
}
完了以後 那麼總結出來方法就是
//vivoNavigationGestureEnabled()從設置中取不到值的話,返回false,因此也不會影響在其他手機上的判斷
boolean hasNavigationBar = hasNavigationBar(this) && !vivoNavigationGestureEnabled(this);
網上又找了個 MIUI 的適配方法。
private static final String XIAOMI_FULLSCREEN_GESTURE = "force_fsg_nav_bar";
public static boolean xiaomiNavigationGestureEnabled(Context context) {
int val = Settings.Global.getInt(context.getContentResolver(), XIAOMI_FULLSCREEN_GESTURE, 0);
return val != 0;
}
這裏在放一個獲取常見系統類型的判斷類。 挺好使 ,
/**
系統信息以及系統類型判斷。
*/
public class OSInfo {
public enum OSType {
OS_TYPE_OTHER(0),
OS_TYPE_EMUI(1),
OS_TYPE_MIUI(2),
OS_TYPE_FLYME(3),
OS_TYPE_COLOR(4),
OS_TYPE_FUNTOUCH(5);
private final int value;
OSType(int value) {
this.value = value;
}
}
/** SharedPreferences標識 */
public static final String OS_SP = "com_github_xubo_statusbarutils_os_sp";
/** MIUI標識(小米) */
private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
/** EMUI標識(華爲) */
private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
/** Flyme標識(魅族) */
public static final String KEY_VERSION_FLYME = "ro.meizu.setupwizard.flyme";
/** color標識(oppo) */
private static final String KEY_VERSION_COLOR = "ro.build.version.opporom";
/** color標識(funtouch) */
private static final String KEY_VERSION_FUNTOUCH = "ro.vivo.os.version";
public static OSType getRomType(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(OS_SP, Context.MODE_PRIVATE);
int osTypeValue = sharedPreferences.getInt("os_type", -1);
if (osTypeValue == -1) {
String display = Build.DISPLAY;
if (display.toUpperCase().contains("FLYME")) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_FLYME.value).commit();
return OSType.OS_TYPE_FLYME;
} else {
if (!TextUtils.isEmpty(getProp(KEY_VERSION_MIUI))) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_MIUI.value).commit();
return OSType.OS_TYPE_MIUI;
} else if (!TextUtils.isEmpty(getProp(KEY_VERSION_EMUI))) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_EMUI.value).commit();
return OSType.OS_TYPE_EMUI;
} else if (!TextUtils.isEmpty(getProp(KEY_VERSION_FLYME))) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_FLYME.value).commit();
return OSType.OS_TYPE_FLYME;
} else if (!TextUtils.isEmpty(getProp(KEY_VERSION_COLOR))) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_COLOR.value).commit();
return OSType.OS_TYPE_COLOR;
} else if (!TextUtils.isEmpty(getProp(KEY_VERSION_FUNTOUCH))) {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_FUNTOUCH.value).commit();
return OSType.OS_TYPE_FUNTOUCH;
} else {
sharedPreferences.edit().putInt("os_type", OSType.OS_TYPE_OTHER.value).commit();
return OSType.OS_TYPE_OTHER;
}
}
} else {
OSType osType;
switch (osTypeValue) {
case 0:
osType = OSType.OS_TYPE_OTHER;
break;
case 1:
osType = OSType.OS_TYPE_EMUI;
break;
case 2:
osType = OSType.OS_TYPE_MIUI;
break;
case 3:
osType = OSType.OS_TYPE_FLYME;
break;
case 4:
osType = OSType.OS_TYPE_COLOR;
break;
case 5:
osType = OSType.OS_TYPE_FUNTOUCH;
break;
default:
osType = OSType.OS_TYPE_OTHER;
break;
}
return osType;
}
}
public static String getProp(String name) {
String line;
BufferedReader input = null;
try {
Process p = Runtime.getRuntime().exec("getprop " + name);
input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
line = input.readLine();
input.close();
} catch (IOException ex) {
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return line;
}
}
先這吧 ,如果碰上別的系統適配的方法可以拿出來,一起分享下。