6.7.0 - Alpha10 - 新增 ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度 (issue #456)

This commit is contained in:
SuperMonster003
2025-10-29 22:25:43 +08:00
parent c446ffba1f
commit 6221196950
6 changed files with 230 additions and 51 deletions

View File

@@ -12,6 +12,7 @@
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))", "s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
"UiObject#isShifted 方法, 用于检测控件位置变化", "UiObject#isShifted 方法, 用于检测控件位置变化",
"device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_", "device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_",
"ui.navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
"http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_", "http 模块请求相关方法获取的 body 对象增加 stream/saveToFile/close 方法 _[`issue #452`](http://issues.autojs6.com/452)_",
"http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)", "http 模块请求相关方法支持缓存控制选项参数 (cacheBody/bodyCacheThresholdBytes)",
"http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_", "http 模块请求相关方法支持不安全选项参数 (isInsecure/insecure), 用于忽略证书相关异常 _[`issue #417`](http://issues.autojs6.com/417)_",
@@ -51,6 +52,7 @@
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题" "构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
], ],
"improvement": [ "improvement": [
"ui.statusBarHeight 属性 (getter) 增强一定程度的兼容性",
"console.log 等方法打印全局对象 (images, app, ocr 等) 时支持显示详细信息", "console.log 等方法打印全局对象 (images, app, ocr 等) 时支持显示详细信息",
"android.graphics.Paint#setColor 支持正常解析 ColorInt/ColorHex/ColorName 等颜色参数", "android.graphics.Paint#setColor 支持正常解析 ColorInt/ColorHex/ColorName 等颜色参数",
"内置模块相关方法实参类型的异常消息增加类型摘要信息", "内置模块相关方法实参类型的异常消息增加类型摘要信息",

View File

@@ -135,8 +135,11 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
(activity as? ScriptExecuteActivity)?.eventEmitter (activity as? ScriptExecuteActivity)?.eventEmitter
}, },
"statusBarHeight" to Supplier { "statusBarHeight" to Supplier {
ViewUtils.getStatusBarHeightByDimen(globalContext) ViewUtils.getStatusBarHeight(globalContext)
}, },
"navigationBarHeight" to Supplier {
ViewUtils.getNavigationBarHeight(globalContext)
}
) )
init { init {

View File

@@ -51,7 +51,7 @@ open class LayoutBoundsView : View {
init { init {
this.setWillNotDraw(false) this.setWillNotDraw(false)
statusBarHeight = ViewUtils.getStatusBarHeightByWindow(context) statusBarHeight = ViewUtils.getStatusBarHeight(context)
} }
fun setOnNodeInfoSelectListener(onNodeInfoSelectListener: OnNodeInfoSelectListener?) { fun setOnNodeInfoSelectListener(onNodeInfoSelectListener: OnNodeInfoSelectListener?) {

View File

@@ -155,7 +155,7 @@ public class LayoutHierarchyView extends MultiLevelListView {
mPaint.setStyle(Paint.Style.STROKE); mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true); mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(3); mPaint.setStrokeWidth(3);
mStatusBarHeight = ViewUtils.getStatusBarHeightByWindow(getContext()); mStatusBarHeight = ViewUtils.getStatusBarHeight(getContext());
} }
public Paint getBoundsPaint() { public Paint getBoundsPaint() {

View File

@@ -10,6 +10,7 @@ import android.content.ContextWrapper
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Resources
import android.graphics.ColorFilter import android.graphics.ColorFilter
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
@@ -64,14 +65,6 @@ import kotlin.math.roundToInt
@Suppress("unused") @Suppress("unused")
object ViewUtils { object ViewUtils {
enum class MODE(val key: String) {
DAY(key(R.string.key_night_mode_always_off)),
NIGHT(key(R.string.key_night_mode_always_on)),
FOLLOW(key(R.string.key_night_mode_follow_system)),
NULL(key(R.string.key_night_mode_null)),
;
}
private const val TAG_STATUS_BAR_SCRIM = "status_bar_scrim" private const val TAG_STATUS_BAR_SCRIM = "status_bar_scrim"
@JvmStatic @JvmStatic
@@ -103,47 +96,78 @@ object ViewUtils {
return view.findViewById(resId) return view.findViewById(resId)
} }
// FIXME by Stardust on Jan 23, 2018.
// ! Not working in some devices.
// ! https://github.com/hyb1996/Auto.js/issues/268
// ! zh-CN (translated by SuperMonster003 on Jul 29, 2024):
// ! 在一些设备上无法正常获取结果.
// ! 参阅: https://github.com/hyb1996/Auto.js/issues/268
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun getStatusBarHeightByDimen(context: Context, withFallback: Boolean = true): Int { fun getStatusBarHeight(context: Context, withFallback: Boolean = true): Int {
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") val window = getWindowForContext(context)
val dimenStatusBarHeight = when (resourceId > 0) {
true -> context.resources.getDimensionPixelSize(resourceId) // [1] Status bar height solution based on WindowInsets, compatible with display cutout.
else -> 0 // zh-CN: 基于 WindowInsets 的状态栏高度方案, 兼容刘海 (display cutout).
}
return when { val insetsTop = window?.decorView?.let { decor ->
dimenStatusBarHeight > 0 -> dimenStatusBarHeight ViewCompat.getRootWindowInsets(decor)?.let { insets ->
withFallback -> TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, context.resources.displayMetrics).toInt() val statusTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
else -> 0 val cutoutTop = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
} insets.displayCutout?.safeInsetTop ?: 0
} else 0
maxOf(statusTop, cutoutTop)
}
} ?: 0
if (insetsTop > 0) return insetsTop
// [2] Visible area top solution, may be 0 in some fullscreen/immersive modes.
// zh-CN: 可见区域 top 方案, 在某些全屏/沉浸式下可能为 0.
val rectTop = window?.decorView?.let { decor ->
Rect().also { rect -> decor.getWindowVisibleDisplayFrame(rect) }.top
} ?: 0
if (rectTop > 0) return rectTop
// [3] Internal dimen solution, only as an estimate.
// zh-CN: 内部 dimen 方案, 仅作为估值.
if (withFallback) return getStatusBarHeightByDimen(context)
// [4] Finally return 0, no constant forced.
// zh-CN: 最终返回 0, 不强制指定常量.
return 0
} }
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun getStatusBarHeightByWindow(context: Context, withFallback: Boolean = true): Int { fun getNavigationBarHeight(context: Context, withFallback: Boolean = true): Int {
val windowStatusBarHeight = getWindowForContext(context)?.decorView?.let { decorView -> val window = getWindowForContext(context)
Rect().also { rect ->
decorView.getWindowVisibleDisplayFrame(rect) // [1] Status bar height solution based on WindowInsets.
}.top // 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 } ?: 0
return when { if (maxInsets > 0) return maxInsets
windowStatusBarHeight > 0 -> windowStatusBarHeight
withFallback -> TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, context.resources.displayMetrics).toInt() // [2] Internal dimen solution, only as an estimate.
else -> 0 // zh-CN: 内部 dimen 方案, 仅作为估值.
}
if (withFallback) return getNavigationBarHeightByDimen(context)
// [3] Finally return 0, no constant forced.
// zh-CN: 最终返回 0, 不强制指定常量.
return 0
} }
fun getTitleBarHeight(context: Context): Int { fun getTitleBarHeight(context: Context): Int {
val window = getWindowForContext(context) val window = getWindowForContext(context)
val contentViewTop = window?.findViewById<View>(Window.ID_ANDROID_CONTENT)?.top ?: 0 val contentViewTop = window?.findViewById<View>(Window.ID_ANDROID_CONTENT)?.top ?: 0
return contentViewTop - getStatusBarHeightByWindow(context) return contentViewTop - getStatusBarHeight(context)
} }
fun getScreenHeight(activity: Activity) = DisplayMetrics().let { metrics -> fun getScreenHeight(activity: Activity) = DisplayMetrics().let { metrics ->
@@ -307,7 +331,7 @@ object ViewUtils {
val navBarOverlay = View(activity).apply { val navBarOverlay = View(activity).apply {
layoutParams = FrameLayout.LayoutParams( layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, MATCH_PARENT,
navBarInsets.bottom, // 导航栏高度 navBarInsets.bottom, // Navigation bar height. (zh-CN: 导航栏高度.)
).apply { gravity = Gravity.BOTTOM } ).apply { gravity = Gravity.BOTTOM }
setBackgroundColor(color) setBackgroundColor(color)
} }
@@ -652,10 +676,12 @@ object ViewUtils {
} }
/** /**
* 显示软键盘. * Show soft keyboard. (zh-CN: 显示软键盘.)
* *
* @param target 需要获取输入的视图 (如 EditText) * @param target The view that needs input (like EditText).
* @param useForced 是否使用 SHOW_FORCED 方式强制弹出, 默认 false, 代表由系统自行判断 * zh-CN: @param target 需要获取输入的视图 (如 EditText).
* @param useForced Whether to use SHOW_FORCED to force showing, default false, let system decide.
* zh-CN: @param useForced 是否使用 SHOW_FORCED 方式强制弹出, 默认 false, 代表由系统自行判断.
*/ */
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@@ -675,9 +701,10 @@ object ViewUtils {
} }
/** /**
* 隐藏软键盘. * Hide soft keyboard. (zh-CN: 隐藏软键盘.)
* *
* @param target 当前持有输入焦点的视图, 也可以是任意位于同一窗口的 View * @param target The view currently holding input focus, or any view in the same window.
* zh-CN: @param target 当前持有输入焦点的视图, 也可以是任意位于同一窗口的 View.
*/ */
@JvmStatic @JvmStatic
fun hideSoftInput(target: View) { fun hideSoftInput(target: View) {
@@ -685,6 +712,154 @@ object ViewUtils {
imm.hideSoftInputFromWindow(target.windowToken, 0) imm.hideSoftInputFromWindow(target.windowToken, 0)
} }
// @Comment by SuperMonster003 on Oct 29, 2025.
// ! Replaced with getStatusBarHeightByWindow and improved getStatusBarHeightByDimen methods.
// ! zh-CN: 已替换为 getStatusBarHeightByWindow 和已改进的 getStatusBarHeightByDimen 方法.
// FIXME by Stardust on Jan 23, 2018.
// ! Not working in some devices.
// ! https://github.com/hyb1996/Auto.js/issues/268
// ! zh-CN (translated by SuperMonster003 on Jul 29, 2024):
// ! 在一些设备上无法正常获取结果.
// ! 参阅: https://github.com/hyb1996/Auto.js/issues/268
// # @SuppressLint("InternalInsetResource", "DiscouragedApi")
// # @JvmStatic
// # @JvmOverloads
// # fun getStatusBarHeightByDimen(context: Context, withFallback: Boolean = true): Int {
// # val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
// # val dimenStatusBarHeight = when (resourceId > 0) {
// # true -> context.resources.getDimensionPixelSize(resourceId)
// # else -> 0
// # }
// # return when {
// # dimenStatusBarHeight > 0 -> dimenStatusBarHeight
// # withFallback -> TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, context.resources.displayMetrics).toInt()
// # else -> 0
// # }
// # }
private fun getStatusBarHeightByDimen(context: Context, withComputedOnPreR: Boolean = true): Int {
val res = context.resources
// [1] 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("status_bar_height_landscape", "status_bar_height", "status_bar_height_default", "status_bar_height_portrait")
else -> listOf("status_bar_height_portrait", "status_bar_height", "status_bar_height_default", "status_bar_height_landscape")
}
val dimenHeight = dimenNameCandidates.asSequence().map { res.queryDimen(it) }.firstOrNull { it > 0 } ?: 0
if (dimenHeight > 0) return dimenHeight
// [2] Try to estimate using DisplayMetrics on pre-Android R.
// zh-CN: 在 Android R 以下, 尝试用 DisplayMetrics 估算.
if (!withComputedOnPreR) return 0
val (_, systemUiHeightInset) = getSystemUiInsetsOnPreR(context) ?: return 0
// Navigation bar estimated value.
// zh-CN: 导航栏估算值.
val navBar = getNavigationBarHeightByDimen(context, false).coerceAtLeast(0)
// "Status bar height" is approximately "vertical system area height" minus "navigation bar height".
// zh-CN: "状态栏高度" 近似视为 "垂直系统区域高度" 减去 "导航栏高度".
val statusGuess = (systemUiHeightInset - navBar).coerceAtLeast(0)
// Prevent abnormal values (status bar usually not exceeding 48dp).
// zh-CN: 防止异常值 (状态栏通常不超过 48dp).
val maxReasonable = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, res.displayMetrics).toInt()
if (statusGuess in 1..maxReasonable) return statusGuess
// [3] Finally return 0, no constant forced.
// zh-CN: 最终返回 0, 仅依赖 context 且不强制指定常量.
return 0
}
@SuppressLint("InternalInsetResource", "DiscouragedApi")
private 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
}
@SuppressLint("InternalInsetResource", "DiscouragedApi")
private fun Resources.queryDimen(name: String, def: (() -> Number)? = null): Int = runCatching {
val id = this.getIdentifier(name, "dimen", "android")
return if (id > 0) this.getDimensionPixelSize(id) else 0
}.getOrDefault(def?.invoke()?.toInt() ?: 0)
@Suppress("DEPRECATION")
private fun getSystemUiInsetsOnPreR(context: Context): Pair<Int, Int>? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return null
val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager ?: return null
val display = wm.defaultDisplay ?: return null
val real = DisplayMetrics().also { display.getRealMetrics(it) }
val usable = DisplayMetrics().also { display.getMetrics(it) }
val systemUiWidthInset = (real.widthPixels - usable.widthPixels).coerceAtLeast(0)
val systemUiHeightInset = (real.heightPixels - usable.heightPixels).coerceAtLeast(0)
return Pair(systemUiWidthInset, systemUiHeightInset)
}
enum class MODE(val key: String) {
DAY(key(R.string.key_night_mode_always_off)),
NIGHT(key(R.string.key_night_mode_always_on)),
FOLLOW(key(R.string.key_night_mode_follow_system)),
NULL(key(R.string.key_night_mode_null)),
;
}
class AutoNightMode { class AutoNightMode {
companion object { companion object {
@@ -704,7 +879,7 @@ object ViewUtils {
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
class TextSizeScaleDetector(context: Context, textView: TextView) :ScaleGestureDetector(context, TextSizeScaleListener(textView)) { class TextSizeScaleDetector(context: Context, textView: TextView) : ScaleGestureDetector(context, TextSizeScaleListener(textView)) {
init { init {
textView.apply { textView.apply {
@@ -726,7 +901,6 @@ object ViewUtils {
} }
// Scaling & Text Size Handling
private class TextSizeScaleListener(private val textView: TextView) : ScaleGestureDetector.SimpleOnScaleGestureListener() { private class TextSizeScaleListener(private val textView: TextView) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
private val mMinTextSize = 4.0f private val mMinTextSize = 4.0f

View File

@@ -1,5 +1,5 @@
#Wed Oct 29 12:59:31 CST 2025 #Wed Oct 29 20:13:49 CST 2025
BUILD_TIME=1761713971603 BUILD_TIME=1761740029614
COMPILE_SDK_VERSION=36 COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125 IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36 TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29 TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3451 VERSION_BUILD=3452
VERSION_NAME=6.7.0 Alpha10 VERSION_NAME=6.7.0 Alpha10
VSCODE_EXT_REQUIRED_VERSION=1.0.8 VSCODE_EXT_REQUIRED_VERSION=1.0.8