如何以编程方式获取Android导航栏的高度和宽度?

157

在Android中,屏幕底部的黑色导航栏不容易删除。自从3.0版本以来,它已经成为 Android 的一部分,用于替换硬件按钮。这里是一张图片:

系统栏

我如何获取该UI元素的宽度和高度(以像素为单位)?


这个问题的最佳解决方案已经添加在这里。我们可以通过比较显示指标和实际指标来确定设备中是否存在导航栏。看看我的答案,我添加了完整的代码来查找整个Android设备的实际导航栏大小。 - Sino Raj
如果您正在使用SDK 30+,请参考此答案 - tuantv.dev
23个回答

211

请尝试以下代码:

Resources resources = context.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
    return resources.getDimensionPixelSize(resourceId);
}
return 0;

10
谢谢。+1。您知道这种方法有多稳定吗?资源标识符是否会在不同的平台版本之间发生更改? - Ben Pearson
75
请注意,此代码在没有导航栏的设备上不返回0。已在三星S2和S3上进行测试,结果分别为72和96。 - Egis
6
看我的回答,对于有物理导航的设备它会返回0。https://dev59.com/omIj5IYBdhLWcg3wb0rX#29938139 - Mdlc
1
这对我不起作用。我仍然会得到一个值,该值返回给屏幕上没有导航栏的设备。我的测试显示此代码返回的值为= Nexus5:144 | Nexus7:75 | SamsungS10:96 | SamsungNoteII:96。三星手机没有导航栏,因此使用此代码会让您认为它有导航栏。 - se22as
6
此代码展示了导航栏的默认大小(也可以尝试使用navigation_bar_height_landscape来查看横屏模式下导航栏的高度,或者使用navigation_bar_width来查看垂直导航栏的宽度)。您需要单独找出导航栏是否正在显示以及其显示位置,例如通过测试物理菜单按钮的存在来判断。也许您可以在Android源代码中找到其他方法,网址是https://android.googlesource.com/platform/frameworks/base/+/android-4.2.2_r1/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java - user149408
显示剩余9条评论

110

我通过比较应用可用屏幕尺寸和实际屏幕尺寸来获取导航栏大小。当应用可用屏幕尺寸小于实际屏幕尺寸时,我假设导航栏存在。然后我计算导航栏的大小。这种方法适用于API14及以上。

public static Point getNavigationBarSize(Context context) {
    Point appUsableSize = getAppUsableScreenSize(context);
    Point realScreenSize = getRealScreenSize(context);

    // navigation bar on the side
    if (appUsableSize.x < realScreenSize.x) {
        return new Point(realScreenSize.x - appUsableSize.x, appUsableSize.y);
    }

    // navigation bar at the bottom
    if (appUsableSize.y < realScreenSize.y) {
        return new Point(appUsableSize.x, realScreenSize.y - appUsableSize.y);
    }

    // navigation bar is not present
    return new Point();
}

public static Point getAppUsableScreenSize(Context context) {
    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = windowManager.getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    return size;
}

public static Point getRealScreenSize(Context context) {
    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = windowManager.getDefaultDisplay();
    Point size = new Point();

    if (Build.VERSION.SDK_INT >= 17) {
        display.getRealSize(size);
    } else if (Build.VERSION.SDK_INT >= 14) {
        try {
            size.x = (Integer) Display.class.getMethod("getRawWidth").invoke(display);
            size.y = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
        } catch (IllegalAccessException e) {} catch (InvocationTargetException e) {} catch (NoSuchMethodException e) {}
    }

    return size;
}

更新

针对考虑显示切口的解决方案,请查看John的答案


2
这是我在测试的所有设备上使用的唯一有效解决方案。我的要求是确定导航栏是否在屏幕底部,并考虑屏幕方向。我稍微调整了答案中的代码,只返回屏幕底部导航栏的大小,0表示不存在。我的测试显示 Nexus5 = 竖屏:144,横屏:0 | Nexus7 = 竖屏:96,横屏:96 | 三星Note II = 竖屏:0,横屏:0 | 三星S10 = 竖屏:0,横屏:0。 - se22as
2
我也可以确认Danylo的解决方案只适用于某些型号。Egidijus提供的这个解决方案更好,而且在迄今为止测试的所有型号上都有效。谢谢你。 - Bisclavret
2
状态栏是应用程序可用屏幕的一部分。 - Egis
1
当导航手势开启而非导航栏时,它在Android P上无法正常工作。您能否指导我如何解决这种情况? - Nik
2
这也没有考虑到DisplayCutout。 - Gauthier
显示剩余16条评论

47

NavigationBar的高度因设备而异,也因方向而异。首先你需要检查设备是否有NavigationBar,然后确定设备是平板还是非平板(手机),最后你需要查看设备的方向以获取正确的高度。

public int getNavBarHeight(Context c) {
         int result = 0;
         boolean hasMenuKey = ViewConfiguration.get(c).hasPermanentMenuKey();
         boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);

         if(!hasMenuKey && !hasBackKey) {
             //The device has a navigation bar
             Resources resources = c.getResources();

             int orientation = resources.getConfiguration().orientation;
             int resourceId;
             if (isTablet(c)){
                 resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape", "dimen", "android");
             }  else {
                 resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_width", "dimen", "android");     
             }

             if (resourceId > 0) {
                 return resources.getDimensionPixelSize(resourceId);
             }
         }
         return result;
} 


private boolean isTablet(Context c) {
    return (c.getResources().getConfiguration().screenLayout
            & Configuration.SCREENLAYOUT_SIZE_MASK)
            >= Configuration.SCREENLAYOUT_SIZE_LARGE;
}

这对我不起作用,Nexus5返回hasMenuKey和hasBackKey为true,因此它认为没有navBar。(Nexus7正确地返回hasMenuKey和hasBackKey为false) - se22as
5
我已在Nexus 5上测试了这段代码,并且两者都返回false。你确定你没有使用模拟器或ROM吗? - Mdlc
是的,我之前在使用模拟器,抱歉我之前的陈述中应该澄清这一点。 - se22as
2
不幸的是,在Sony Xperia Z3上无法工作
hasBackKey = false!,尽管它应该是true。
以下代码可以正常工作:
boolean navBarExists = getResources().getBoolean(getResources().getIdentifier("config_showNavigationBar", "bool", "android"));
- Hamzeh Soboh
无法在Pixel XL上使用,hasBackKey为true但应为false。 - Emil Adz
显示剩余6条评论

36

实际上,在平板电脑上(至少是Nexus 7),导航栏在纵向和横向时具有不同的大小,因此这个功能应该长成这样:

private int getNavigationBarHeight(Context context, int orientation) {
    Resources resources = context.getResources();

    int id = resources.getIdentifier(
            orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape",
            "dimen", "android");
    if (id > 0) {
        return resources.getDimensionPixelSize(id);
    }
    return 0;
}

而在 Kotlin 中:

private fun getNavigationBarHeight(): Int {
    val resources: Resources = requireContext().resources

    val resName = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
        "navigation_bar_height"
    } else {
        "navigation_bar_height_landscape"
    }

    val id: Int = resources.getIdentifier(resName, "dimen", "android")

    return if (id > 0) {
        resources.getDimensionPixelSize(id)
    } else {
        0
    }
}

8
要从上下文中获取方向,请使用 context.getResources().getConfiguration().orientation - Hugo Gresse
这个问题的最佳解决方案已经添加在这里。我们可以通过比较显示指标和实际指标来确定设备中是否存在导航栏。看看我的答案,我添加了完整的代码来找出整个Android设备的实际导航栏大小。 - Sino Raj

32

我认为这里的答案更好,因为它允许你获得切除高度。

获取你的根视图,并添加setOnApplyWindowInsetsListener(或者你可以重写onApplyWindowInsets),从中获取插入值。

在我的相机活动中,我将系统栏的底部填充到我的底部布局。最后,它修复了切口问题。

相机活动插入值

使用appcompat的话就像这样:

ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    binding.takePictureLayout.apply {
        setPaddingRelative(paddingStart, paddingTop, paddingEnd, systemBars.bottom)
    }
    return@setOnApplyWindowInsetsListener insets
}

没有 appcompat,这样做:

mCameraSourcePreview.setOnApplyWindowInsetsListener((v, insets) -> { ... })

可以使用 setOnApplyWindowInsetsListener 吗?如果可以,如何使用?为什么要区分LOLLIPOP和其他版本? - android developer
是的,这是可能的,而且更好。我会修正答案。不需要区分。 - John
我明白了。谢谢! - android developer
这是在API >= 20上的正确方法。请参见此视频的21:23。 - Lance Johnson
好的解决方案。但它无法立即确定NAV_BAR_HEIGHT。 - Ahamadullah Saikat
4
当我从onCreate()中调用它时,监听器从未被执行。我有什么遗漏吗? - howettl

15

我希望这可以帮助你。

public int getStatusBarHeight() {
    int result = 0;
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

public int getNavigationBarHeight()
{
    boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();
    int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android");
    if (resourceId > 0 && !hasMenuKey)
    {
        return getResources().getDimensionPixelSize(resourceId);
    }
    return 0;
}

7

2021年的新答案来拯救


受到Egis的回答的启发:

context.navigationBarHeight

扩展名获取器所在的位置

val Context.navigationBarHeight: Int
get() {
    val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

    return if (Build.VERSION.SDK_INT >= 30) {
        windowManager
                .currentWindowMetrics
                .windowInsets
                .getInsets(WindowInsets.Type.navigationBars())
                .bottom

    } else {
        val currentDisplay = try {
            display
        } catch (e: NoSuchMethodError) {
            windowManager.defaultDisplay
        }

        val appUsableSize = Point()
        val realScreenSize = Point()
        currentDisplay?.apply {
            getSize(appUsableSize)
            getRealSize(realScreenSize)
        }

        // navigation bar on the side
        if (appUsableSize.x < realScreenSize.x) {
            return realScreenSize.x - appUsableSize.x
        }

        // navigation bar at the bottom
        return if (appUsableSize.y < realScreenSize.y) {
            realScreenSize.y - appUsableSize.y
        } else 0
    }
}

测试环境:

  • 带有导航栏的模拟器
    • Pixel 3a (API 30)
    • Pixel 2 (API 28)
    • Pixel 3 (API 25)
    • Pixel 2 (API 21)
  • Xiaomi Poco F2 Pro,带或不带导航栏(全面屏)

1
你使用哪个编译SDK?我没有找到某些方法。 - jboxxpradhana
1
@jboxxpradhana 目标和编译的 SDK 版本均为 30。 - Haytham Anmar
3
它总是返回导航和状态。 - Tarif Chakder

5
这是我用来为View添加paddingRight和paddingBottom以避开导航栏的代码。我结合了这里的一些答案,并在横向方向与isInMultiWindowMode一起制作了一个特殊的子句。关键是要读取navigation_bar_height,但也要检查config_showNavigationBar以确保我们实际上应该使用高度。
之前的解决方案都不适用于我。自Android 7.0起,您必须考虑多窗口模式。这会破坏了通过display.realSize与display.size进行比较的实现,因为realSize会给出整个屏幕(两个拆分窗口)的尺寸,而size只会给出您的应用窗口的尺寸。将填充设置为这种差异将使整个视图都被填充。
/** Adds padding to a view to dodge the navigation bar.

    Unfortunately something like this needs to be done since there
    are no attr or dimens value available to get the navigation bar
    height (as of December 2016). */
public static void addNavigationBarPadding(Activity context, View v) {
    Resources resources = context.getResources();
    if (hasNavigationBar(resources)) {
        int orientation = resources.getConfiguration().orientation;
        int size = getNavigationBarSize(resources);
        switch (orientation) {
        case Configuration.ORIENTATION_LANDSCAPE:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
                context.isInMultiWindowMode()) { break; }
            v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
                         v.getPaddingRight() + size, v.getPaddingBottom());
            break;
        case Configuration.ORIENTATION_PORTRAIT:
            v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
                         v.getPaddingRight(), v.getPaddingBottom() + size);
            break;
        }
    }
}

private static int getNavigationBarSize(Resources resources) {
    int resourceId = resources.getIdentifier("navigation_bar_height",
                                             "dimen", "android");
    return resourceId > 0 ? resources.getDimensionPixelSize(resourceId) : 0;
}

private static boolean hasNavigationBar(Resources resources) {
    int hasNavBarId = resources.getIdentifier("config_showNavigationBar",
                                              "bool", "android");
    return hasNavBarId > 0 && resources.getBoolean(hasNavBarId);
}

2

获取导航栏高度的测试代码(以像素为单位):

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

获取状态栏高度(以像素为单位)的测试代码:

public static int getStatusBarHeight(Context c) {
    int resourceId = c.getResources()
                      .getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        return c.getResources().getDimensionPixelSize(resourceId);
    }
    return 0;
}

像素转换为dp:

public static int pxToDp(int px) {
    return (int) (px / Resources.getSystem().getDisplayMetrics().density);
}

2
我已经为所有设备(包括Nexus 5、三星Galaxy Nexus 6 edge+、三星S10、三星Note II等)解决了这个问题。我认为这将帮助你处理设备相关的问题。
下面我会添加两种代码:
Java代码(用于本机Android):
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.ViewConfiguration;
import android.view.WindowManager;

public class DeviceSpec {

    private int resourceID = -1;
    private Display display = null;
    private DisplayMetrics displayMetrics = null;
    private DisplayMetrics realDisplayMetrics = null;
    private Resources resources = null;
    private WindowManager windowManager = null;

    public double GetNavigationBarHeight(Context context) {
        try {
            windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            display = windowManager.getDefaultDisplay();
            displayMetrics = new DisplayMetrics();
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                realDisplayMetrics = new DisplayMetrics();
                display.getMetrics(displayMetrics);
                display.getRealMetrics(realDisplayMetrics);
                if(displayMetrics.heightPixels != realDisplayMetrics.heightPixels) {
                    resources = context.getResources();
                    return GetNavigationBarSize(context);
                }
            }
            else {
                resources = context.getResources();
                resourceID = resources.getIdentifier("config_showNavigationBar", "bool", "android");
                if (resourceID > 0 && resources.getBoolean(resourceID))
                    return GetNavigationBarSize(context);
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return 0;
    }

    private double GetNavigationBarSize(Context context) {
        resourceID = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceID > 0 && ViewConfiguration.get(context).hasPermanentMenuKey())
           return (resources.getDimensionPixelSize(resourceID) / displayMetrics.density);
        return 0;
    }
}

并且需要C#代码(适用于Xamarin Forms/Android)

int resourceId = -1;
        IWindowManager windowManager = null;
        Display defaultDisplay = null;
        DisplayMetrics displayMatrics = null;
        DisplayMetrics realMatrics = null;
        Resources resources = null;

        public double NavigationBarHeight
        {
            get
            {
                try
                {
                    windowManager = Forms.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
                    defaultDisplay = windowManager.DefaultDisplay;
                    displayMatrics = new DisplayMetrics();
                    if (Build.VERSION.SdkInt >= BuildVersionCodes.JellyBeanMr2)
                    {
                        realMatrics = new DisplayMetrics();
                        defaultDisplay.GetMetrics(displayMatrics);
                        defaultDisplay.GetRealMetrics(realMatrics);
                        if (displayMatrics.HeightPixels != realMatrics.HeightPixels)
                        {
                            resources = Forms.Context.Resources;
                            return GetHeightOfNivigationBar();
                        }
                    }
                    else {
                        resources = Forms.Context.Resources;
                        resourceId = resources.GetIdentifier("config_showNavigationBar", "bool", "android");
                        if (resourceId > 0 && resources.GetBoolean(resourceId))
                            return GetHeightOfNivigationBar();
                    }
                }
                catch (Exception e) { }
                return 0;
            }
        }

        private double GetHeightOfNivigationBar()
        {
            resourceId = resources.GetIdentifier("navigation_bar_height", "dimen", "android");
            if (!ViewConfiguration.Get(Forms.Context).HasPermanentMenuKey && resourceId > 0)
            {
                return resources.GetDimensionPixelSize(resourceId) / displayMatrics.Density;
            }
            return 0;
        }

Display.getRealMetrics() 需要 API 级别 17。 - Weizhi
如果 (Build.VERSION.SdkInt >= BuildVersionCodes.JellyBeanMr2) 已经被检查过了。 - Sino Raj
代码需要修复。我的LG Nexus 5X会因以下原因得到0.0: [1] 当screenOrientation为默认值时,只有在不旋转时hasPermanentMenuKeyfalse。 [2] 当screenOrientation为横向时,它会落入displayMetrics.heightPixels!= realDisplayMetrics.heightPixels)的else中。 [3] 当screenOrientation为纵向时,hasPermanentMenuKeyfalse - 林果皞
它可以工作,但在4.x上,如果(hasPermanentMenuKey)是浪费的,请将其注释掉。 - djdance
你的条件应该是 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)。要执行 public void getRealMetrics (DisplayMetrics outMetrics),需要 API 17。请参阅文档:https://developer.android.com/reference/kotlin/android/util/DisplayMetrics。 - portfoliobuilder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接