Android 虚拟导航键适配

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> 
  1. 我们可以从"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;
    }
}



先这吧 ,如果碰上别的系统适配的方法可以拿出来,一起分享下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章