AutoJs6 icon indicating copy to clipboard operation
AutoJs6 copied to clipboard

怎样获得屏幕底部高度?

Open wengzhenquan opened this issue 2 months ago • 6 comments

场景: 因为微信小程序在不同设备可能存在布局分析失效,导致只能用坐标点击(当布局分析失效时)。 需要点击的是底部的导航键,比如点击“论坛”、“我的”,大概位置就是在微信底部“通讯录”、“发现”、“我”。

当然,如果高度使用0.98*device.height,理论上是没问题的。 但是,如果出现虚拟按键,就不适用了。

此前,通过ui.statusBarHeight可以获得状态栏高度。所以我想问问能否得到软件底部的位置(或虚拟按键上方边界)?

如果是全面屏手势模式,底部的位置肯定是device.height,那么稍微上移那么一点点,就可以得到需要点击的高度。

如果是底部虚拟按键模式,我也需要得到按键上方那条横线的位置,然后上移一点点也可以得到需要点击的高度。

那么怎么得到这条横线的位置呢? 不论是从上往下寻找,还是从下往上寻找,都可以。

wengzhenquan avatar Oct 19 '25 13:10 wengzhenquan

解决了

wengzhenquan avatar Oct 19 '25 16:10 wengzhenquan

未来 AutoJs6 将新增 ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度.

另外也欢迎将你的解决方案在此处分享给其他有需要的用户及开发者.

SuperMonster003 avatar Oct 29 '25 12:10 SuperMonster003

// 状态栏高度
const statusBarHeight = ui.statusBarHeight;

// 导航栏高度
const navBarHeight = getNavigationBarHeight();

// 底部可用最低位置(就是导航栏上方,(隐藏)手势提示线位置,或者虚拟按键上方)
const navBarY = dheight - navBarHeight;

// 判断当前是否为全面屏手势模式
const gestureMode = isGestureMode();

// 获取导航栏高度(虚拟按键/手势条高度)
function getNavigationBarHeight() {
    try {
        // 方法1:通过系统资源获取
        let resources = context.getResources();
        let resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return resources.getDimensionPixelSize(resourceId);
        }
    } catch (e) {}
    return 0;
}


// 判断当前是否为全面屏手势模式
function isGestureMode() {
    //  let navBarHeight = getNavigationBarHeight();
    // 将高度转换为dp
    const navBarHeightDp = navBarHeight * 160 / device.density;
    // 手势模式下导航栏高度通常小于48
    return navBarHeightDp < 35;
}


存在一个问题: 当是全面手势导航的时候, "navigation_bar_height" 存在隐藏手势提示线。

即便手机设置里面已经隐藏了,依旧存在高度。 还有,判断手势模式,我把像素转化成dp再判断高度,高度取<35,不保证正确。

wengzhenquan avatar Oct 29 '25 13:10 wengzhenquan

使用 WindowInsets 可以获取系统栏的高度, 但它依赖于一个 Anchor View, 可以理解为一个锚点视图, 典型的秒点视图是 android.view.Window#getDecorView() 得到的视图. 有了上述锚点视图, 就可以准确获取导航栏及状态栏的高度了.

一个 Kotlin 示例代码:

val window = getWindowForContext(context)

// [1] Status bar height solution based on WindowInsets.
// zh-CN: 基于 WindowInsets 的状态栏高度方案.

val maxInsets = window?.decorView?.let { decor ->
    ViewCompat.getRootWindowInsets(decor)
        ?.getInsets(WindowInsetsCompat.Type.navigationBars())
        ?.let { insets ->
            // Compatible with horizontal screen navigation bars: take the maximum value of bottom/left/right.
            // zh-CN: 兼容横屏左右侧导航栏: 取 bottom/left/right 最大值.
            maxOf(insets.bottom, insets.left, insets.right)
        }
} ?: 0
if (maxInsets > 0) return maxInsets

对于 AutoJs6 的 UI 模式, 自然可以利用 activity.window.decorView 获取需要的锚点视图. 但很多时候 AutoJs6 需要在本体应用外部执行脚本, 此时只有一个 context (等价于 applicationContext) 可以使用. 这种情况下就只能依赖 context 进行内部 dimen 资源查询进行状态栏或导航栏的高度值估算了, 这算是一种 "尽力而为" 的策略, 因此无法保证完全准确.

一个 Kotlin 实例代码:

fun getNavigationBarHeightByDimen(context: Context, withComputedOnPreR: Boolean = true): Int {
    val res = context.resources

    // [1] If system declares not to show navigation bar, return 0 early (non-public resource, may be inaccurate).
    // zh-CN: 如果系统声明不显示导航栏, 尽早返回 0 (非公开资源, 可能不准确).

    val showNavResId = res.getIdentifier("config_showNavigationBar", "bool", "android")
    if (showNavResId > 0) runCatching {
        if (!res.getBoolean(showNavResId)) return 0
    }

    // [2] Priority read from internal dimen to try to cover portrait/landscape differences.
    // zh-CN: 优先从内部 dimen 读取, 尽量覆盖横竖屏差异.

    val isLandscape = res.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
    val dimenNameCandidates = when (isLandscape) {
        true -> listOf("navigation_bar_height_landscape", "navigation_bar_height", "navigation_bar_height_portrait")
        else -> listOf("navigation_bar_height_portrait", "navigation_bar_height", "navigation_bar_height_landscape")
    }

    val dimenHeight = dimenNameCandidates.asSequence().map { res.queryDimen(it) }.firstOrNull { it > 0 } ?: 0
    if (dimenHeight > 0) return dimenHeight

    // [3] Try to estimate using DisplayMetrics on pre-Android R.
    // zh-CN: 在 Android R 以下, 尝试用 DisplayMetrics 估算.

    if (!withComputedOnPreR) return 0
    val (systemUiWidthInset, systemUiHeightInset) = getSystemUiInsetsOnPreR(context) ?: return 0

    // Status bar estimated value.
    // zh-CN: 状态栏估算值.
    val statusBar = getStatusBarHeightByDimen(context, false)

    val bottomNavGuess = (systemUiHeightInset - statusBar).coerceAtLeast(0)
    val sideNavGuess = systemUiWidthInset.coerceAtLeast(0)

    // A navigation bar will be more likely on the side in landscape, and more likely on the bottom in portrait.
    // zh-CN: 导航栏横屏更可能在侧边, 竖屏更可能在底部.
    val computed = maxOf(bottomNavGuess, sideNavGuess)

    // Prevent abnormal values (navigation bar usually not exceeding 96dp).
    // zh-CN: 防止异常值 (导航栏通常不超过 96dp).
    val maxReasonable = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, res.displayMetrics).toInt()

    if (computed in 1..maxReasonable) return computed

    // [4] Finally return 0, no constant forced.
    // zh-CN: 最终返回 0, 仅依赖 context 且不强制指定常量.

    return 0
}

如果你需要将上面相关的 Kotlin 代码转换为可以执行的 JavaScript 脚本代码, 我可以尝试帮你转换. 另外, 目前还没有官方公开且稳定的 Android API 可以直接判定系统正在使用手势导航还是按键导航.

SuperMonster003 avatar Oct 29 '25 14:10 SuperMonster003

找到了新的方式,通过UI获取,而且更加准确。 在全面手势模式下,隐藏手势条,能获取到导航栏高度为0。可以兼容全面手势和传统导航模式。 但我不确定设备兼容性。

所以,我将代码改为如下(方法一):

// 获取导航栏高度(虚拟按键/手势条高度)
function getNavigationBarHeight() {

    try {
        // 方法一:通过UI获取
        //      准确获取导航栏高度,如果是全面手势模式,并隐藏手势条,能获取到0
        
        // 获取屏幕总高度
        let screenHeight = device.height;

        // 获取应用可用高度(不包括系统UI)
        var windowManager = context.getSystemService(context.WINDOW_SERVICE);
        var display = windowManager.getDefaultDisplay();
        var usableSize = new android.graphics.Point();
        display.getSize(usableSize);
        var usableHeight = usableSize.y;

        // 获取状态栏高度
        let statusBarHeight = ui.statusBarHeight


        // 计算底部系统UI高度
        // 底部系统UI高度 = 屏幕总高度 - 应用可用高度 - 状态栏高度
        var bottomUIHeight = screenHeight - usableHeight - statusBarHeight;

        // 确保高度不为负数
        if (bottomUIHeight < 0) {
            bottomUIHeight = 0;
        }

        // 打印结果
        //  log("底部系统UI高度: " + bottomUIHeight + "px");

        return bottomUIHeight;
    } catch (e) {
        try {
            // 方法二:通过id获取
            //        即便隐藏手势条,依旧能获取手势条高度
            
            var resources = context.getResources();

            // 按优先级尝试传统导航栏ID
            var idsToTry = [
                "navigation_bar_height",
                "navigation_bar_frame_height",
                "navigation_bar_height_landscape",
                "config_navBarInteractionFrame"
            ];

            for (var i = 0; i < idsToTry.length; i++) {
                var id = idsToTry[i];
                var resourceId = resources.getIdentifier(id, "dimen", "android");
                if (resourceId <= 0 && id === "config_navBarInteractionFrame") {
                    resourceId = resources.getIdentifier(id, "config", "android");
                }

                if (resourceId > 0) {
                    let height = resources.getDimensionPixelSize(resourceId);
                    if (height > 0) {
                        //  log("使用 " + id + " 作为导航栏高度: " + height + "px");
                        return height;
                    }
                }
            }
        } catch (e) {}
    }

    return 0;
}

就是通过系统UI获取的。

如果设备兼容性没问题,我这不是可以考虑去除方法二

wengzhenquan avatar Oct 30 '25 21:10 wengzhenquan

另外,通过deepseek查询,导航栏高度情况如下:

设备类型 常见高度 说明 手机(标准) 48 dp 最常见的传统导航栏高度 手机(紧凑型) 42 dp 一些较小屏幕或特定厂商的设备 平板电脑 56-64 dp 平板设备通常有更高的导航栏 横屏模式 36-42 dp 横屏时高度通常会减少

<36dp,理论上包括所有情况。

至少我的手机传统导航模式高度是42dp(我的全面手势模式的手势条只有8dp)

wengzhenquan avatar Oct 30 '25 22:10 wengzhenquan