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;
    }
}



先這吧 ,如果碰上別的系統適配的方法可以拿出來,一起分享下。

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