聊聊获取屏幕高度这件事
问题的起因是我发现 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时,使用getRealMetrics
(getRealSize
)来获取屏幕高度。那么这里发生了什么,为什么会这样?
其实在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一下的机子了吧,不会吧不会吧。。。所以历史的包袱可以去掉了。
上面方法名都是getScreenHeight
,可是这个高度范围到底和你需要的是否一致。这个需要开发时注意,我的习惯是ScreenHeight指应用显示的高度,不包括导航栏(非全屏下),RealHeight来指包含导航栏和状态栏的高度(getRealMetrics
)。
PS:以前也使用过AndroidUtilCode这个工具库,里面将前者方法名定义为getAppScreenHeight
,后者为getScreenHeight
。也是很直观的方法。
下文中我会以自己的习惯,使用ScreenHeight
和RealHeight
来代表两者。
我印象中华为手机很早就使用了虚拟导航键,如下图(图片来源):
比较特别的是,当时华为的导航栏还可以显示隐藏,注意图中左下角的箭头。点击可以隐藏,上滑可以显示。即使这样,使用getScreenHeight
也可以准确获取高度,隐藏了ScreenHeight
就等于RealHeight
。
上述的这一切在“全面屏”时代没有到来之前,没有什么问题。
2.立足当下
小米MIX的发布开启了全面屏时代(16年底),以前的手机都是16:9的,记得雷布斯在发布会上说过,他们费了很大的力气说服了谷歌去除了16:9的限制(从Android 7.0开始)
全面屏手机是真的香,不过随之也带来适配问题。首当其冲的就是刘海屏,各家有各自的获取刘海区域大小的方法。主要原因还是国内竞争的激烈,各家为了抢占市场,先于谷歌定制了自己的方案。这一点让人想起了万恶的动态权限适配。。。
其实在刘海屏之下,还隐藏一个导航栏的显示问题,也就是本篇的重点。全面屏追求更多的显示区域,随之带来了手势操作。在手势操作模式下,导航栏是隐藏状态。
本想着可以和上面提到的华为一样,隐藏获取的就是RealHeight
,显示就是减去导航栏高度的ScreenHeight
。然而现实并不是这样,下表是我收集的一些全面屏手机各高度的数据。
机型 | 系统 | ScreenHeight | RealHeight | NavigationBar | StatusBar | 是否有刘海 |
---|---|---|---|---|---|---|
vivo Z3x | Funtouch OS_10(Android 10) | 2201(2075) | 2280 | 126 | 84 | 是 |
Xiaomi MIX 2s | MIUI 12(Android 10) | 2030(2030) | 2160 | 130 | 76 | 否 |
Redmi Note 8Pro | MIUI 11.0.3(Android 10) | 2134(2134) | 2340 | 130 | 76 | 是 |
Redmi K30 5G | MIUI 12.0.3(Android 10) | 2175(2175) | 2400 | 130 | 95 | 是 |
Honor 10 Lite | EMUI 10(Android 10) | 2259(2139) | 2340 | 120 | 81 | 是 |
华为畅享 20 | EMUI 10.1.1(Android 10) | 1552(1472) | 1600 | 80 | 48 | 是 |
OPPO Find X | ColorOS 7.1(Android 10) | 2340(2208) | 2340 | 132 | 96 | 否 |
OnePlus 6 | H2OS 10.0.8(Android 10) | 2201(2159,2075) | 2280 | 126(42) | 80 | 否 |
ScreenHeight
一栏中括号内表示显示导航栏时获取的屏幕高度。
大致的规律总结如下:
- 在有刘海的手机上,ScreenHeight不包含状态栏高度。
- 小米手机在隐藏显示导航栏时,ScreenHeight不变,且不包含导航栏高度。
其中vivo手机,屏幕高度加状态栏高度大于真实高度(2201 + 84 > 2280)。本以为差值79是刘海高度,但查看vivo文档后发现,vivo刘海固定27dp(81px),也还是对不上。。。
一加6最奇怪,三种设置模式。使用侧边全屏手势时底部有一个小条,NavigationBar高度变为42。(2159 + 42 = 2075 + 126 = 2201)也就是说这种模式也属于有导航栏的情况。
这时如果你需要获取准确的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;}}
}
此方法通过比较ScreenHeight
和RealHeight
是否相等来判断。如果对比上面表中的数据,那只有OPPO Find X可以判断成功。也有一些方法通过ScreenHeight
和RealHeight
差值来计算导航栏高度。显然这些方法已无法再使用。
所以搜索了一下相关信息,得到了下面的代码:
/*** 是否隐藏了导航键** @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;} elsereturn 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") || BRAND.contains("realme");}public static boolean isHuawei() {return BRAND.contains("huawei") || BRAND.contains("honor");}public static boolean isOneplus(){return BRAND.contains("oneplus");}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 boolean isGoogle(){return BRAND.contains("google");}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 其他值 不显示 0显示 -1 未知*/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 (isOneplus()) {return oneplusNavigationEnabled(context);}if (isSamsung()) {return samsungNavigationEnabled(context);}if (isSmartisan()) {return smartisanNavigationEnabled(context);}if (isNokia()) {return nokiaNavigationEnabled(context);}if (isGoogle()) {// navigation_mode 三种模式均有导航栏,只是高度不同。return 0;}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);}/*** @param context* @return 0虚拟导航键 2为手势导航*/private static int oneplusNavigationEnabled(Context context) {int result = Settings.Secure.getInt(context.getContentResolver(), "navigation_mode", 0);if (result == 2) {// 两种手势 0有按钮, 1没有按钮if (Settings.System.getInt(context.getContentResolver(), "buttons_show_on_screen_navkeys", 0) != 0) {return 0;}}return result;}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.86return height / width >= 1.86f;}}/*** 获取去除导航栏高度的剩余高度(含状态栏)* @param context* @return*/public static int getScreenContentHeight(Context context) {if (isAllScreenDevice(context)) {int result = isNavBarHide(context);int result = isNavBarHide(context);if (result == 0) {return getRealScreenHeight(context) - getNavigationBarHeight(context);} else if (result == -1){// 未知return getScreenHeight(context);} else {return getRealScreenHeight(context);}} else {return getScreenHeight(context);}}
}
有人会问,这些key都是哪里来的?毕竟我在厂商文档也没有翻到。
我能想到的办法是查看SettingsProvider
,它是提供设置数据的Provider
,分有Global
、System
、Secure
三种类型,上面代码中可以看到不同品牌存放在的类型都不同。我们可以通过adb命令查看所有数据,根据navigation等关键字去寻找。比如查看Secure
的数据:
adb shell settings list secure
或者:
ContentResolver cr = context.getContentResolver();Uri uri = Uri.parse("content://settings/secure/");Cursor cursor = cr.query(uri, null, null, null, null);while (cursor.moveToNext()) {String name = cursor.getString(cursor.getColumnIndex("name"));String value = cursor.getString(cursor.getColumnIndex("value"));Log.d("settings:", name + "=" + value);}cursor.close();
这样如果有上面兼容不到的机型,可以使用这个方法适配。也欢迎你的补充反馈。
费了这么大的劲获取到了准确的高度,可能你会说,还不如直接获取ContentView
的高度:
public static int getContentViewHeight(Activity activity) {View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);return contentView.getHeight();}
这个结果和上述计算的高度一致,唯一的限制是需要在onWindowFocusChanged
之后调用,否则高度为0。这个我们可以根据实际情况自行选用。
2021-04-06补充:
发现在Android 11中可以使用WindowMetrics获取窗口边界距四边的距离大小,我们使用bottom获取导航栏真实高度:
@RequiresApi(api = Build.VERSION_CODES.Q)public static int getRealNavHeight(Context context) {WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();WindowInsets windowInsets = windowMetrics.getWindowInsets();Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());return insets.bottom;}
这样一来可以不需要判断当前导航栏是否显示来获取高度了。不过只适用于Android 11及以上,对于中间的8、9、10看来还需要以上的方法处理。
3.已知问题
网上有许多同类代码,发现会将vivo和oppo都使用
navigation_gesture_on
这一个key。我在oppo Find x中发现此key并不存在,不知是否和系统版本有关。如果是的话,又需要判断oppo的系统版本了。上面提到的获取导航栏高度的方法在部分手机中无效,无效的原因是因为导航栏隐藏时,获取高度就为0。所以判断是否显示导航栏是关键。
刘海的出现,很多人会吐槽丑,所以厂家想到了隐藏刘海的方式(掩耳盗铃),比如下面是
Redmi K30
的设置页面:
第二种没啥特别,就是状态栏强制为黑色。这里我怀疑因为这个设置,导致在有刘海的手机上,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
弹出高度不正确的问题,过了好久才修复了。
至于屏幕宽度,也不清楚随着折叠屏、环绕屏的到来会不会造成影响。但愿不要吧,碎片化原来越严重了。。。
最后,如果本文对你有启发有帮助,点个赞和收藏可好?
参考
- Android 获取屏幕高度,虚拟导航键检测
聊聊获取屏幕高度这件事相关推荐
- 华为手机获取状态栏高度是错误的_聊聊获取屏幕高度这件事
问题的起因是我发现 PopupWindow弹出位置不正确时发现的.其实早在两年多前,我就发现我手上的小米MIX2s 获取屏幕高度不正确,后面参考V2EX 的这篇帖子处理了.最近又一次做到类似功能,发现 ...
- 「技术播客月」Day 10: Meta Podcast: 聊聊播客这件事
首届·技术播客月第10天的直播正在火热进行中~ 今晚将由 「Nebula Graph 星球」 「编码人声」 「开源面对面」 三档播客节目为我们带来精彩的议题: 「Meta Podcast: 聊聊播客这 ...
- 聊聊网络游戏同步那点事
0x00 前言 16年年底的时候我从当时的公司离职,来到了目前任职的一家更专注于游戏开发的公司.接手的是一个platform游戏项目,基本情况是之前的团队完成了第一个版本,即单人模式的基础玩法,但是之 ...
- 经验分享:聊聊多人游戏同步那点事
16年年底的时候我从当时的公司离职,来到了目前任职的一家更专注于游戏开发的公司.接手的是一个platform游戏项目,基本情况是之前的团队完成了第一个版本,即单人模式的基础玩法,但是之后对该项目的定位 ...
- 通过阿里P9代考这件事,聊聊职级
通过阿里P9代考这件事,聊聊职级 近日,阿里P9员工代考事引发热议. 有同学就问,P9在阿里是什么等级?为什么阿里要设置这样的等级? 因此,小师妹特意在朋友圈做了个小的研究,发现很多人对此并不是很清楚 ...
- 聊聊Uber公司迁移数据库这件事
先声明,我没有被盗号--只是想趁着周末没多少人看,今天来写写技术.是的,本帐号在极其偶然的情况下也会写一些纯粹的技术问题,虽然我知道我的读者并不都是技术从业者.不过--谁让我现在就这一个写文章的地方呢 ...
- 中科柏诚获“专精特新”称号,把一件事做到极致就是价值
中科柏诚荣获北京市2022年第一批"专精特新"称号,把一件事做到极致就是价值. 什么叫"专精特新"? "专精特新",是指具有专业化.精细化. ...
- 表白这件事,比解 bug 要难多少?
情人节快乐!我是可爱无敌的阿里妹. 今天是个粉红色日子,我们来聊聊和技术无关的"技术活",比如: "如何表白?" 当技术人碰上动心的姑娘,他的浪漫开关就打开了. ...
- 她破解哈希函数算法:坚持10年做一件事一定能做成
今年9月7日,2019未来科学大奖揭晓,这项设立4年的大奖迎来首位女性得主--密码学家王小云.在信息时代,金融服务.网络安全等背后都离不开密码系统的"护航".在大众眼中,密码带着神 ...
最新文章
- Hexo集成Algolia实现搜索功能
- phpstudy安装imagick扩展库怎么装
- uva 610(割边)
- sift的java实现解述
- pip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='f 的解决办法
- 连接MySQL数据库时报 Public Key Retrieval is not allowed 异常
- 源代码下载 第六章 注解式控制器详解
- java 读取 邮件 附件,JavaMail 中对附件下载的处理
- var let this的区别
- 字符串固定长度 易语言_易语言宽字符数据类型怎么设置
- android获取屏幕宽高与获取控件宽高
- XML 文档四种解析放式
- 【Python爬虫】淘宝商品比价定向爬虫
- Git之(三)辅助命令
- 人对光波的三种特性_光的特点是什么?
- 《哈利·波特:霍格沃茨之谜》推出二月支线任务“天界舞会”,带来新的选择、挑战与服装
- Hillstone 基础上网配置
- 验证码图片显示不出文字
- 关于计算机专业英语的小短文,有没人有计算机专业英语的短文,200字左右。
- CART决策树算法Python实现 (人工智能导论作业)