js获取微信状态栏高度_聊聊获取屏幕高度这件事

问题的起因是我发现 PopupWindow弹出位置不正确时发现的。其实早在两年多前,我就发现我手上的小米MIX2s 获取屏幕高度不正确,后面参考V2EX 的这篇帖子处理了。最近又一次做到类似功能,发现小米、vivo都出现了问题。所以有了今天的内容。

1.回顾过去

说起获取屏幕高度,不知道你是如何理解这个高度范围的?是以应用显示区域高度作为屏幕高度还是手机屏幕的高度。

那么我们先看一下平时使用获取高度的方法:

public static int getScreenHeight(Context context) {
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();
    DisplayMetrics dm = new DisplayMetrics();
    display.getMetrics(dm);
    return dm.heightPixels;
}

//或
public static int getScreenHeight(Context context) {
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Point point = new Point();
    wm.getDefaultDisplay().getSize(point);
    return point.y;
}

// 或
public static int getScreenHeight(Context context) {
    return context.getResources().getDisplayMetrics().heightPixels;
}
// 貌似还有更多的方法
复制代码

以上三种效果一致,只是写法略有不同。

当然你或许使用的是这种:

public static int getScreenHeight(Context context) {
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();
    DisplayMetrics dm = new DisplayMetrics();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        display.getRealMetrics(dm);
    } else {
        display.getMetrics(dm);
    }
    return dm.heightPixels;
}
// 其他几种写法大同小异
...
复制代码

这个方法判断了系统大于等于Android 4.2时,使用getRealMetricsgetRealSize)来获取屏幕高度。那么这里发生了什么,为什么会这样?

其实在Andoird 4.0时,引入了虚拟导航键,如果你继续使用getMetrics之类的方式,获取的高度是去除了导航栏的高度的。

当时因为在4.0和4.2之间还没有的getRealMetrics这个方法,所以甚至需要添加下面的适配代码:

try {
     heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
} catch (Exception e) {
}
复制代码

现在不会还有人适配4.4甚至5.0一下的机子了吧,不会吧不会吧。。。所以历史的包袱可以去掉了。

37c19693269677248c11793b8a0769d7.png


在这里插入图片描述

上面方法名都是getScreenHeight,可是这个高度范围到底和你需要的是否一致。这个需要开发时注意,我的习惯是ScreenHeight指应用显示的高度,不包括导航栏(非全屏下),RealHeight来指包含导航栏和状态栏的高度(getRealMetrics)。

PS:以前也使用过AndroidUtilCode这个工具库,里面将前者方法名定义为getAppScreenHeight,后者为getScreenHeight。也是很直观的方法。

下文中我会以自己的习惯,使用ScreenHeightRealHeight来代表两者。

我印象中华为手机很早就使用了虚拟导航键,如下图(图片来源):

a79b5dbd9505a871769b132feee6003c.png


华为手机

比较特别的是,当时华为的导航栏还可以显示隐藏,注意图中左下角的箭头。点击可以隐藏,上滑可以显示。即使这样,使用getScreenHeight也可以准确获取高度,隐藏了ScreenHeight就等于RealHeight

上述的这一切在“全面屏”时代没有到来之前,没有什么问题。

2.立足当下

小米MIX的发布开启了全面屏时代(16年底),以前的手机都是16:9的,记得雷布斯在发布会上说过,他们费了很大的力气说服了谷歌去除了16:9的限制(从Android 7.0开始)

30a26e1ca4e3d04b9faad3e27bae9a86.png


MIX 2发布会

4a377326f3ec1a5e7e43b67f3959248e.png


MIX 2发布会

全面屏手机是真的香,不过随之也带来适配问题。首当其冲的就是刘海屏,各家有各自的获取刘海区域大小的方法。主要原因还是国内竞争的激烈,各家为了抢占市场,先于谷歌定制了自己的方案。这一点让人想起了万恶的动态权限适配。。。

其实在刘海屏之下,还隐藏一个导航栏的显示问题,也就是本篇的重点。全面屏追求更多的显示区域,随之带来了手势操作。在手势操作模式下,导航栏是隐藏状态。

本想着可以和上面提到的华为一样,隐藏获取的就是RealHeight,显示就是减去导航栏高度的ScreenHeight。然而现实并不是这样,下表是我收集的一些全面屏手机各高度的数据。

机型系统ScreenHeightRealHeightNavigationBarStatusBar是否有刘海

ScreenHeight一栏中括号内表示显示导航栏时获取的屏幕高度。

大致的规律总结如下:

  • 在有刘海的手机上,ScreenHeight不包含状态栏高度。
  • 小米手机在隐藏显示导航栏时,ScreenHeight不变,且不包含导航栏高度。

其中vivo手机最奇怪,屏幕高度加状态栏高度大于真实高度(2201 + 84 > 2280)。本以为差值79是刘海高度,但查看vivo文档后发现,vivo刘海固定27dp(81px),也还是对不上。。。

这时如果你需要获取准确的ScreenHeight,只有通过RealHeight - NavigationBar来实现了。

所以首先需要判断当前导航栏是否显示,再来决定是否减去NavigationBar高度。

先看看老牌的判断方法如下:

public boolean isNavigationBarShow(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        Point realSize = new Point();
        display.getSize(size);
        display.getRealSize(realSize);
        return realSize.y!=size.y;
    } else {
        boolean menu = ViewConfiguration.get(this).hasPermanentMenuKey();
        boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
        if(menu || back) {
            return false;
        }else {
            return true;
        }
    }
}

复制代码

此方法通过比较ScreenHeightRealHeight是否相等来判断。如果对比上面表中的数据,那只有OPPO Find X可以判断成功。也有一些方法通过ScreenHeightRealHeight差值来计算导航栏高度。显然这些方法已无法再使用。

所以搜索了一下相关信息,得到了下面的代码:

/**
     * 是否隐藏了导航键
     *
     * @param context
     * @return
     */
    public static boolean isNavBarHide(Context context) {
        try {
            String brand = Build.BRAND;
            // 这里做判断主要是不同的厂商注册的表不一样
            if (!StringUtils.isNullData(brand) && (Rom.isVivo() || Rom.isOppo())) {
                return Settings.Secure.getInt(context.getContentResolver(), getDeviceForceName(), 0) != 0;
            } else if (!StringUtils.isNullData(brand) && Rom.isNokia()) {
                //甚至 nokia 不同版本注册的表不一样, key 还不一样。。。
                return Settings.Secure.getInt(context.getContentResolver(), "swipe_up_to_switch_apps_enabled", 0) == 1
                        || Settings.System.getInt(context.getContentResolver(), "navigation_bar_can_hiden", 0) != 0;
            } else
                return Settings.Global.getInt(context.getContentResolver(), getDeviceForceName(), 0) != 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 各个手机厂商注册导航键相关的 key
     *
     * @return
     */
    public static String getDeviceForceName() {
        String brand = Build.BRAND;
        if (StringUtils.isNullData(brand))
            return "navigationbar_is_min";
        if (brand.equalsIgnoreCase("HUAWEI") || "HONOR".equals(brand)) {
            return "navigationbar_is_min";
        } else if (Rom.isMiui()||Rom.check("XIAOMI")) {
            return "force_fsg_nav_bar";
        } else if (Rom.isVivo()) {
            return "navigation_gesture_on";
        } else if (Rom.isOppo()) {
            return "hide_navigationbar_enable";
        } else if (Rom.check("samsung")) {
            return "navigationbar_hide_bar_enabled";
        } else if (brand.equalsIgnoreCase("Nokia")) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
                return "navigation_bar_can_hiden";
            } else {
                return "swipe_up_to_switch_apps_enabled";
            }
        } else {
            return "navigationbar_is_min";
        }
    }

复制代码

可以看到包含了华为、小米、vivo、oppo 、三星甚至诺基亚的判断。这就是适配的现实状况,不要妄想寻找什么通用方法,老老实实一个个判断吧。毕竟幺蛾子就是这些厂家搞出来的,厂家魔改教你做人。

这种方法在上面的测试机中都亲测准确有效。

不过这个判断方法不够严谨,比如其他品牌手机使用此方法,那么结果都是false。用这样的结果来计算高度显得不够严谨。

根据前面提到问题发生的原因是全面屏带来的(7.0及以上)。所以我们可以先判断是否是全面屏手机(屏幕长宽比例超过1.86以上),然后判断是否显示导航栏,对于不确定的机型,我们还是使用原先的ScreenHeight。尽量控制影响范围。

我整理的代码如下(补充了锤子手机判断):

/**
 * @author weilu
 **/
public class ScreenUtils {

    private static final String BRAND = Build.BRAND.toLowerCase();

    public static boolean isXiaomi() {
        return Build.MANUFACTURER.toLowerCase().equals("xiaomi");
    }

    public static boolean isVivo() {
        return BRAND.contains("vivo");
    }

    public static boolean isOppo() {
        return BRAND.contains("oppo");
    }

    public static boolean isHuawei() {
        return BRAND.contains("huawei") || BRAND.contains("honor");
    }

    public static boolean isSamsung(){
        return BRAND.contains("samsung");
    }

    public static boolean isSmartisan(){
        return BRAND.contains("smartisan");
    }

    public static boolean isNokia() {
        return BRAND.contains("nokia");
    }

    public static int getRealScreenHeight(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        DisplayMetrics dm = new DisplayMetrics();
        display.getRealMetrics(dm);
        return dm.heightPixels;
    }

    public static int getRealScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        DisplayMetrics dm = new DisplayMetrics();
        display.getRealMetrics(dm);
        return dm.widthPixels;
    }

    public static int getScreenHeight(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        DisplayMetrics dm = new DisplayMetrics();
        display.getMetrics(dm);
        return dm.heightPixels;
    }

    /**
     * 判断设备是否显示NavigationBar
     *
     * @return 1 不显示 0显示 2 未知
     */
    public static int isNavBarHide(Context context) {
        // 有虚拟键,判断是否显示
        if (isVivo()) {
            return vivoNavigationEnabled(context);
        }
        if (isOppo()) {
            return oppoNavigationEnabled(context);
        }
        if (isXiaomi()) {
            return xiaomiNavigationEnabled(context);
        }
        if (isHuawei()) {
            return huaWeiNavigationEnabled(context);
        }
        if (isSamsung()) {
            return samsungNavigationEnabled(context);
        }
        if (isSmartisan()) {
            return smartisanNavigationEnabled(context);
        }
        if (isNokia()) {
            return nokiaNavigationEnabled(context);
        }
        return 2;
    }

    /**
     * 判断当前系统是使用导航键还是手势导航操作
     *
     * @param context
     * @return 0 表示使用的是虚拟导航键,1 表示使用的是手势导航,默认是0
     */
    public static int vivoNavigationEnabled(Context context) {
        return Settings.Secure.getInt(context.getContentResolver(), "navigation_gesture_on", 0);
    }

    public static int oppoNavigationEnabled(Context context) {
        return Settings.Secure.getInt(context.getContentResolver(), "hide_navigationbar_enable", 0);
    }

    public static int xiaomiNavigationEnabled(Context context) {
        return Settings.Global.getInt(context.getContentResolver(), "force_fsg_nav_bar", 0);
    }

    private static int huaWeiNavigationEnabled(Context context) {
        return Settings.Global.getInt(context.getContentResolver(), "navigationbar_is_min", 0);
    }

    public static int samsungNavigationEnabled(Context context) {
        return Settings.Global.getInt(context.getContentResolver(), "navigationbar_hide_bar_enabled", 0);
    }

    public static int smartisanNavigationEnabled(Context context) {
        return Settings.Global.getInt(context.getContentResolver(), "navigationbar_trigger_mode", 0);
    }

    public static int nokiaNavigationEnabled(Context context) {
        boolean result = Settings.Secure.getInt(context.getContentResolver(), "swipe_up_to_switch_apps_enabled", 0) != 0
                || Settings.System.getInt(context.getContentResolver(), "navigation_bar_can_hiden", 0) != 0;

        if (result) {
            return 1;
        } else {
            return 0;
        }
    }

    public static int getNavigationBarHeight(Context context){
        int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return context.getResources().getDimensionPixelSize(resourceId);
        }
        return 0;
    }

    private static boolean isAllScreenDevice(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            // 7.0放开限制,7.0以下都不为全面屏
            return false;
        } else {
            int realWidth = getRealScreenWidth(context);
            int realHeight = getRealScreenHeight(context);

            float width;
            float height;
            if (realWidth < realHeight) {
                width = realWidth;
                height = realHeight;
            } else {
                width = realHeight;
                height = realWidth;
            }
            // Android中默认的最大屏幕纵横比为1.86
            return height / width >= 1.86f;
        }
    }

    /**
     * 获取去除导航栏高度的剩余高度(含状态栏)
     * @param context
     * @return
     */
    public static int getScreenContentHeight(Context context) {

        if (isAllScreenDevice(context)) {

            int result = isNavBarHide(context);

            if (result == 0) {
                return getRealScreenHeight(context) - getNavigationBarHeight(context);
            } else if (result == 1){
                return getRealScreenHeight(context);
            } else {
                // 未知
                return getScreenHeight(context);
            }
        } else {
            return getScreenHeight(context);
        }

    }
}
复制代码

有人会问,这些key都是哪里来的?毕竟我在厂商文档也没有翻到。

我能想到的办法是查看SettingsProvider,它是提供设置数据的Provider,分有GlobalSystemSecure三种类型,上面代码中可以看到不同品牌存放在的类型都不同。我们可以通过adb命令查看所有数据,根据navigation等关键字去寻找。比如查看Secure的数据:

adb shell settings list secure
复制代码

这样如果有上面兼容不到的机型,可以使用这个方法适配。也欢迎你的补充反馈。

费了这么大的劲获取到了准确的高度,可能你会说,还不如直接获取ContentView的高度:

public static int getContentViewHeight(Activity activity) {
        View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
        return contentView.getHeight();
    }
复制代码

这个结果和上述计算的高度一致,唯一的限制是需要在onWindowFocusChanged之后调用,否则高度为0。这个我们可以根据实际情况自行选用。

3.已知问题

  • 网上有许多同类代码,发现会将vivo和oppo都使用navigation_gesture_on这一个key。我在oppo Find x中发现此key并不存在,不知是否和系统版本有关。如果是的话,又需要判断oppo的系统版本了。
  • 上面提到的获取导航栏高度的方法在部分手机中无效,无效的原因是因为导航栏隐藏时,获取高度就为0。所以判断是否显示导航栏是关键。
  • 刘海的出现,很多人会吐槽丑,所以厂家想到了隐藏刘海的方式(掩耳盗铃),比如下面是Redmi K30的设置页面:

e342f8904a7c6d729533211d380b631d.png


设置刘海显示页

第二种没啥特别,就是状态栏强制为黑色。这里我怀疑因为这个设置,导致在有刘海的手机上,ScreenHeight不包含状态栏高度。

最糟糕的是第三种,隐藏后状态栏在刘海外。例如Redmi K30在开启后,ScreenHeight 为2174,RealHeight为2304,而关闭时为2175 和 2400。这下连万年不变的RealHeight也变化了,这太不real了,大家自行体会。不过目前发现未影响适配方案,不知其他手机如何。

对于是否隐藏刘海,其实也是有各家的判断的,比如小米:

// 0:显示刘海,1:隐藏刘海
    Settings.Global.getInt(context.getContentResolver(), "force_black", 0);
复制代码
  • 有些App会使用修改density的屏幕适配方案,这会影响获取导航栏高度的方法。比如130px的导航栏适配后获取到的是136px。所以这里需要使用getSystem中的density转换回去:
public static int getNavigationBarHeight(Context context){
        int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            int height = context.getResources().getDimensionPixelSize(resourceId);
            // 兼容屏幕适配导致density修改
            float density = context.getResources().getDisplayMetrics().density;
            if (DENSITY != density) {
                return dpToPx(px2dp(context, height));
            }
            return height;
        }
        return 0;
    }

    public static final float DENSITY = Resources.getSystem().getDisplayMetrics().density;

    public static int dpToPx(int dpValue) {
        return (int) (dpValue * DENSITY + 0.5f);
    }

    public static int px2dp(Context context, int px) {
        return (int) (px / context.getResources().getDisplayMetrics().density + 0.5);
    }
复制代码

getSystem源码如下:

/**
     * Return a global shared Resources object that provides access to only
     * system resources (no application resources), is not configured for the
     * current screen (can not use dimension units, does not change based on
     * orientation, etc), and is not affected by Runtime Resource Overlay.
     */
    public static Resources getSystem() {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                ret = new Resources();
                mSystem = ret;
            }
            return ret;
        }
    }
复制代码

它不受资源覆盖的影响,我们可以通过它将值转换回来。

4.展望未来

本篇看似聊的获取高度这件事,其实伴随导航栏的发展演进,核心是是如何判断导航栏是否显示。

通过上面的介绍,总结一下就是在“全面屏时代”,如果你想获取屏幕高度,就不要使用ScreenHeight了。否则会出现UI展示上的问题。而且这种问题,线上也不会崩溃,难以发现。以前在支付宝中就发现过 PopupWindow弹出高度不正确的问题,过了好久才修复了。

至于屏幕宽度,也不清楚随着折叠屏、环绕屏的到来会不会造成影响。但愿不要吧,碎片化原来越严重了。。。

作者:唯鹿
链接:https://juejin.cn/post/6908862707374489607
来源:掘金

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值