
問題的起因是我發現 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;
} 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") || 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.86
return 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,這個我們可以根據實際情況自行選用,
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 獲取螢屏高度,虛擬導航鍵檢測
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/239084.html
標籤:其他
上一篇:Zabbix部署
下一篇:支付寶小程式下載圖片到服務器
