最近项目里需要适配虚拟导航键,以及获取导航键的高度,来适配界面布局的高度。
不得不说,国内由于不同手机厂商对系统做了不同的修改,对系统界面底部的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
false
网络上流传的大多数的方法 都是获取
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;
}
}
先这吧 ,如果碰上别的系统适配的方法可以拿出来,一起分享下。