The problem started when I discovered that the PopupWindow was in the wrong popup position. In fact, as early as more than two years ago, I found that the screen height of mi MIX2s in my hand is not correct, which will be dealt with by referring to this post of V2EX. Recently, similar functions were done again, and xiaomi and Vivo were found to have problems. So there you have it.

1. Look back

Speaking of getting screen height, how do you understand the height range? Is the height of the app display area the height of the screen or the height of the phone’s screen?

So let’s take a look at the usual method of obtaining height:

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

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

/ / or
public static int getScreenHeight(Context context) {
	return context.getResources().getDisplayMetrics().heightPixels;
}
// There seems to be more to it
Copy the code

The above three effects are the same, but the writing is slightly different.

Of course you might use this:

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;
}
// The other ways are similar.Copy the code

This method uses getRealMetrics (getRealSize) to measure screen height when the system is greater than or equal to Android 4.2. So what happened here? Why did it happen?

In fact, in Andoird 4.0, virtual navigation keys were introduced. If you continue to use getMetrics or something like that, the height obtained is stripped of the height of the navigation bar.

Since getRealMetrics was not available between 4.0 and 4.2, the following adaptation code even needed to be added:

try {
     heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
} catch (Exception e) {
}
Copy the code

There is no one who ADAPTS 4.4 or even 5.0 now, no, no… So the baggage of history can be removed.

GetScreenHeight = getScreenHeight (); I use ScreenHeight to show the height of the app, not including the navigation bar (not in full screen), and RealHeight to include the height of the navigation bar and the status bar (getRealMetrics).

AndroidUtilCode is also used before, which defines the former method name as getAppScreenHeight and the latter as getScreenHeight. It’s also an intuitive way to do it.

In the following I will use my own habitsScreenHeightandRealHeightTo represent both.

In my impression, Huawei phones have used virtual navigation buttons for a long time, as shown below (photo source) :

In particular, huawei’s navigation bar at that time can also show hidden, notice the arrow in the lower left corner of the picture. Click to hide, slide up to show. Even then, you can get the exact height using getScreenHeight. Hiding ScreenHeight equals RealHeight.

All of this was fine until the age of “full screen”.

2. Be present

The mi MIX launch kicked off the full screen era (late 2016), previous phones were 16:9, remember Rebus said at the launch that they had a hard time convincing Google to remove the 16:9 limit (starting with Android 7.0).

Full-screen phones are really sweet, but then there are adaptation issues. The first is the bangs, and each country has its own method for determining the size of the bangs. The main reason is the fierce domestic competition, in order to seize the market, before Google customized their own programs. This is reminiscent of the evil dynamic permission adaptation…

In fact, under the bangs screen, there is also a hidden navigation bar display problem, which is the focus of this article. The full screen seeks more display areas, which brings with it gestures. In gesture mode, the navigation bar is hidden.

The RealHeight is hidden and the ScreenHeight is subtracted from the navigation bar height. However, this is not the case. Here are some of the stats I collected on the height of the full-screen phone.

models system ScreenHeight RealHeight NavigationBar StatusBar Do you have bangs
vivo Z3x OS_10 Funtouch OS_10 2201 (2075). 2280 126 84 is
Xiaomi MIX 2s MIUI 12 (Android 10) 2030 (2030). 2160 130 76 no
Redmi Note 8Pro MIUI 11.0.3 (Android 10) 2134 (2134). 2340 130 76 is
Redmi K30 5G MIUI 12.0.3 (Android 10) 2175 (2175). 2400 130 95 is
Honor 10 Lite EMUI 10 Android 10 2259 (2139). 2340 120 81 is
Huawei enjoys 20 EMUI 10.1.1 for Android 10 1552 (1472). 1600 80 48 is
OPPO Find X ColorOS 7.1 (Android 10) 2340 (2208). 2340 132 96 no
OnePlus 6 H2OS 10.0.8 (Android 10) 2201 (2159207) 2280 126 (42) 80 no

The parentheses in ScreenHeight indicate the ScreenHeight obtained when the navigation bar is displayed.

The general rules are summarized as follows:

  • On phones with bangs, ScreenHeight does not include the status bar height.
  • When the mi mobile phone hides the navigation bar, the ScreenHeight remains unchanged and does not include the height of the navigation bar.

In vivo mobile phone, the screen height plus the status bar height is greater than the real height (2201 + 84 > 2280). I thought the difference value 79 was the height of the bangs, but after checking the document of Vivo, I found that the bangs of Vivo were fixed at 27dp (81px), but it was still not correct…

Oneplus 6 is the weirdest, with three Settings. There is a small bar at the bottom when using the side full screen gesture and the NavigationBar height changes to 42. (2159 + 42 = 2075 + 126 = 2201) in other words, this mode also belongs to the case with the navigation bar.

The realheight-Navigationbar is the only way to get the exact ScreenHeight.

So you need to check whether the current NavigationBar is displayed, and then decide whether to subtract the NavigationBar height.

Here’s the old method:

public boolean isNavigationBarShow(a){
    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);
        returnrealSize.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; }}}Copy the code

This method checks whether ScreenHeight and RealHeight are equal. If you compare the data in the table above, only OPPO Find X can determine success. There are also ways to calculate the height of the navigation bar using the difference between ScreenHeight and RealHeight. It is clear that these methods are no longer available.

So a search for relevant information yielded the following code:

	/** * Whether the navigation key ** is hidden@param context
     * @return* /
    public static boolean isNavBarHide(Context context) {
        try {
            String brand = Build.BRAND;
            // Different vendors register different tables
            if(! StringUtils.isNullData(brand) && (Rom.isVivo() || Rom.isOppo())) {return Settings.Secure.getInt(context.getContentResolver(), getDeviceForceName(), 0) != 0;
            } else if(! StringUtils.isNullData(brand) && Rom.isNokia()) {// Even different versions of Nokia register different tables and keys...
                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;
    }
 
    /** * Each handset manufacturer registers the navigation key related key **@return* /
    public static String getDeviceForceName(a) {
        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"; }}Copy the code

You can see the judgments of Huawei, Xiaomi, Vivo, Oppo, Samsung and even Nokia. This is the reality of adaptation. Don’t try to find a universal solution. After all, unskilled is these manufacturers come out, manufacturers magic change to teach you life.

This method is accurate and effective in the above test machine.

However, this judgment method is not rigorous enough. For example, if other brands of mobile phones use this method, the result will be false. It is not rigorous to calculate height with such results.

As mentioned above, the problem is caused by the full screen (7.0 and above). So we can first determine if it is a full-screen phone (with a screen width ratio of more than 1.86 or above) and then determine whether to display a navigation bar. For uncertain phones, we use the original ScreenHeight. Try to control the sphere of influence.

I collated the code as follows (added the one plus, hammer mobile phone judgment) :

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

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

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

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

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

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

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

	public static boolean isNokia(a) {
		return BRAND.contains("nokia");
	}
    
    	public static boolean isGoogle(a){
		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;
	}


	/** * Check whether the device displays NavigationBar **@returnOther values are not displayed. 0 is displayed. -1 Unknown */
	public static int isNavBarHide(Context context) {
		// There are virtual keys, check whether to display
		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 all have navigation bar, but different height.
			return 0;
		}
		return -1;
	}

	/** * Determine whether the current system uses navigation keys or gesture navigation operation **@param context
	 * @return0 indicates that the virtual navigation button is used. 1 indicates that the gesture navigation button is used. The default value is 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
	 * @return0 Virtual navigation key 2: gesture navigation */
	private static int oneplusNavigationEnabled(Context context) {
		int result = Settings.Secure.getInt(context.getContentResolver(), "navigation_mode".0);
		if (result == 2) {
			// There are two gestures 0 with a button and 1 without a button
			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 let go of the restrictions
			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;
			}
			// The default maximum aspect ratio on Android is 1.86
			return height / width >= 1.86 f; }}/** * Gets the remaining height (including the status bar) * after removing the navigation bar height@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) {/ / unknown
				return getScreenHeight(context);
			} else {
				returngetRealScreenHeight(context); }}else {
			returngetScreenHeight(context); }}}Copy the code

One might ask, where did all these keys come from? After all, I didn’t find it in the manufacturer’s documentation.

The solution I can think of is to look at SettingsProvider. It is a Provider that provides Settings data. There are three types of Provider: Global, System, and Secure. We can view all data via ADB command and search for it according to navigation and other keywords. For example, look at Secure’s data:

	adb shell settings list secure
Copy the code

Or:

	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();		
Copy the code

In this way, if there is a model that is not compatible with the above, you can use this method to adapt. Your additional feedback is also welcome.

After all this effort to get the exact height, you might say, better to get the height of the ContentView directly:

	public static int getContentViewHeight(Activity activity) {
		View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
		return contentView.getHeight();
	}
Copy the code

This result is the same height as the above calculation, with the only limitation that it needs to be called after onWindowFocusChanged, otherwise the height is 0. We can choose this by ourselves according to the actual situation.


The 2021-04-06 supplement:

In Android 11, we can use WindowMetrics to get the distance between the four edges of the window. We can use bottom to get the real height of the navigation bar:

	@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;
	}
Copy the code

This eliminates the need to determine whether the current navigation bar is displayed to get altitude. It only works with Android 11 and above, but it also needs the above method for the middle 8, 9, and 10.

3. Known problems

  • Navigation_gesture_on navigation_geSTUre_on navigation_geSTUre_on I found in Oppo Find X that this key does not exist, I wonder if it is related to the system version. If so, the system version of OpPO needs to be determined.

  • The navigation bar height method mentioned above does not work on some phones because the navigation bar is hidden and the height is 0. So deciding whether to display the navigation bar is key.

  • The appearance of bangs, many people will laugh at ugly, so manufacturers think of a way to hide bangs (to hide the alarm), such as the following is Redmi K30 Settings page:

The second, unremarkable, is to force the status bar to be black. I suspect that ScreenHeight does not include the status bar height on phones with bangs because of this setting.

Worst of all is the third, which hides the post status bar outside the bangs. For example, when Redmi K30 is on, ScreenHeight is 2174, RealHeight is 2304, and when off, ScreenHeight is 2175 and 2400. Now the real height, which has been constant for thousands of years, has changed, so it’s not real, so you can see for yourself. However, the current discovery does not affect the adaptation scheme, I do not know how other mobile phones.

For whether to hide bangs, in fact, there are various judgments, such as Xiaomi:

	// 0: show bangs, 1: hide bangs
	Settings.Global.getInt(context.getContentResolver(), "force_black".0);
Copy the code
  • Some apps use modificationsdensityThis affects the way you get the height of the navigation bar. For example, the 130px navigation bar is adjusted to 136px. So we need to use thisgetSystemDensity conversion back to:
	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);
			// Compatibility screen adaptation results in density modification
			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.5 f);
	}

	public static int px2dp(Context context, int px) {
		return (int) (px / context.getResources().getDisplayMetrics().density + 0.5);
	}
Copy the code

GetSystem source code:

	/** * 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(a) {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                ret = new Resources();
                mSystem = ret;
            }
            returnret; }}Copy the code

It is not affected by resource overwrite, and we can convert values back through it.

Look to the future

This article seems to talk about the acquisition of height this matter, in fact, with the development and evolution of the navigation bar, the core is how to judge whether the navigation bar is displayed.

The bottom line is that in the “full screen era”, if you want to get ScreenHeight, don’t use ScreenHeight. Otherwise, there will be UI presentation problems. And this kind of problem, online also won’t crash, difficult to find. The PopupWindow height error was found in Alipay before and it took a long time to fix it.

As for screen width, it’s also unclear whether it will be affected by the arrival of folding and wrapping screens. Hopefully not, fragmentation is getting worse…

Finally, if this article is inspiring and helpful to you, please like and bookmark it.

reference

  • Android gets screen height, virtual navigation key detection