6.7.0 - Alpha13 - 增强 ui.get(Status/Navigation)BarHeight 兼容性 (issue #456)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$data": {
|
"$data": {
|
||||||
"v6.7.0": {
|
"v6.7.0": {
|
||||||
"released_date": "2025/12/30",
|
"released_date": "2025/12/31",
|
||||||
"feature": [
|
"feature": [
|
||||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
||||||
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
|
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
|
||||||
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
|
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
|
||||||
"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)_",
|
"ui.getNavigationBarHeight 方法/navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
|
||||||
|
"ui.getVisible(Status/Navigation)BarHeight 方法/visible(Status/Navigation)BarHeight 属性 (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)_",
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
],
|
],
|
||||||
"improvement": [
|
"improvement": [
|
||||||
"runtime.loadJar 方法增强兼容性 (by [LYS86](https://github.com/LYS86)) _[`pr #475`](http://pr.autojs6.com/475)_",
|
"runtime.loadJar 方法增强兼容性 (by [LYS86](https://github.com/LYS86)) _[`pr #475`](http://pr.autojs6.com/475)_",
|
||||||
"ui.statusBarHeight 属性 (getter) 增强一定程度的兼容性",
|
"ui.getStatusBarHeight 方法/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 等颜色参数",
|
||||||
"app.launchDual/startDualActivity 等方法提升执行效率并增强兼容性",
|
"app.launchDual/startDualActivity 等方法提升执行效率并增强兼容性",
|
||||||
|
|||||||
@@ -24,8 +24,13 @@ import org.autojs.autojs.extension.AnyExtensions.isJsNullish
|
|||||||
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
import org.autojs.autojs.extension.AnyExtensions.jsBrief
|
||||||
import org.autojs.autojs.extension.AnyExtensions.jsSanitize
|
import org.autojs.autojs.extension.AnyExtensions.jsSanitize
|
||||||
import org.autojs.autojs.extension.AnyExtensions.jsUnwrapped
|
import org.autojs.autojs.extension.AnyExtensions.jsUnwrapped
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component1
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component2
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component3
|
||||||
|
import org.autojs.autojs.extension.FlexibleArray.Companion.component4
|
||||||
import org.autojs.autojs.extension.ScriptableExtensions.defineProp
|
import org.autojs.autojs.extension.ScriptableExtensions.defineProp
|
||||||
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
import org.autojs.autojs.extension.ScriptableExtensions.prop
|
||||||
|
import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire
|
||||||
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_GETTER_KEY
|
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_GETTER_KEY
|
||||||
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_OBJECT_KEY
|
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_OBJECT_KEY
|
||||||
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_SETTER_KEY
|
import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_SETTER_KEY
|
||||||
@@ -43,6 +48,7 @@ import org.autojs.autojs.util.RhinoUtils.callFunction
|
|||||||
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
import org.autojs.autojs.util.RhinoUtils.coerceBoolean
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
import org.autojs.autojs.util.RhinoUtils.coerceIntNumber
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
|
import org.autojs.autojs.util.RhinoUtils.coerceLongNumber
|
||||||
|
import org.autojs.autojs.util.RhinoUtils.coerceObject
|
||||||
import org.autojs.autojs.util.RhinoUtils.coerceString
|
import org.autojs.autojs.util.RhinoUtils.coerceString
|
||||||
import org.autojs.autojs.util.RhinoUtils.initNewBaseFunction
|
import org.autojs.autojs.util.RhinoUtils.initNewBaseFunction
|
||||||
import org.autojs.autojs.util.RhinoUtils.newBaseFunction
|
import org.autojs.autojs.util.RhinoUtils.newBaseFunction
|
||||||
@@ -120,6 +126,10 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
|
|||||||
::findView.name,
|
::findView.name,
|
||||||
::finish.name,
|
::finish.name,
|
||||||
::keepScreenOn.name,
|
::keepScreenOn.name,
|
||||||
|
::getStatusBarHeight.name,
|
||||||
|
::getVisibleStatusBarHeight.name,
|
||||||
|
::getNavigationBarHeight.name,
|
||||||
|
::getVisibleNavigationBarHeight.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
override val selfAssignmentGetters = listOf(
|
override val selfAssignmentGetters = listOf(
|
||||||
@@ -134,11 +144,25 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
|
|||||||
(activity as? ScriptExecuteActivity)?.eventEmitter
|
(activity as? ScriptExecuteActivity)?.eventEmitter
|
||||||
},
|
},
|
||||||
"statusBarHeight" to Supplier {
|
"statusBarHeight" to Supplier {
|
||||||
ViewUtils.getStatusBarHeight(globalContext)
|
val activity = getActivity(scriptRuntime)
|
||||||
|
val context = (activity as? ScriptExecuteActivity) ?: globalContext
|
||||||
|
ViewUtils.getStatusBarHeight(context)
|
||||||
|
},
|
||||||
|
"visibleStatusBarHeight" to Supplier {
|
||||||
|
val activity = getActivity(scriptRuntime)
|
||||||
|
val context = (activity as? ScriptExecuteActivity) ?: globalContext
|
||||||
|
ViewUtils.getStatusBarHeight(context, ignoreVisibility = false)
|
||||||
},
|
},
|
||||||
"navigationBarHeight" to Supplier {
|
"navigationBarHeight" to Supplier {
|
||||||
ViewUtils.getNavigationBarHeight(globalContext)
|
val activity = getActivity(scriptRuntime)
|
||||||
}
|
val context = (activity as? ScriptExecuteActivity) ?: globalContext
|
||||||
|
ViewUtils.getNavigationBarHeight(context)
|
||||||
|
},
|
||||||
|
"visibleNavigationBarHeight" to Supplier {
|
||||||
|
val activity = getActivity(scriptRuntime)
|
||||||
|
val context = (activity as? ScriptExecuteActivity) ?: globalContext
|
||||||
|
ViewUtils.getNavigationBarHeight(context, ignoreVisibility = false)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -678,6 +702,48 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
|
|||||||
UNDEFINED
|
UNDEFINED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoRuntimeFunctionInterface
|
||||||
|
fun getStatusBarHeight(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Int = ensureArgumentsAtMost(args, 1) { argList ->
|
||||||
|
val (options) = argList
|
||||||
|
val opt = coerceObject(options, newNativeObject())
|
||||||
|
val withComputed = opt.inquire("withComputed", ::coerceBoolean, true)
|
||||||
|
val withDimen = opt.inquire("withDimen", ::coerceBoolean, true)
|
||||||
|
val ignoreVisibility = opt.inquire("ignoreVisibility", ::coerceBoolean, true)
|
||||||
|
ViewUtils.getStatusBarHeight(globalContext, withComputed, withDimen, ignoreVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoRuntimeFunctionInterface
|
||||||
|
fun getVisibleStatusBarHeight(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Int = ensureArgumentsAtMost(args, 1) { argList ->
|
||||||
|
val (options) = argList
|
||||||
|
val opt = coerceObject(options, newNativeObject())
|
||||||
|
val withComputed = opt.inquire("withComputed", ::coerceBoolean, true)
|
||||||
|
val withDimen = opt.inquire("withDimen", ::coerceBoolean, true)
|
||||||
|
ViewUtils.getStatusBarHeight(globalContext, withComputed, withDimen, ignoreVisibility = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoRuntimeFunctionInterface
|
||||||
|
fun getNavigationBarHeight(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Int = ensureArgumentsAtMost(args, 1) { argList ->
|
||||||
|
val (options) = argList
|
||||||
|
val opt = coerceObject(options, newNativeObject())
|
||||||
|
val withComputed = opt.inquire("withComputed", ::coerceBoolean, true)
|
||||||
|
val withDimen = opt.inquire("withDimen", ::coerceBoolean, true)
|
||||||
|
val ignoreVisibility = opt.inquire("ignoreVisibility", ::coerceBoolean, true)
|
||||||
|
ViewUtils.getNavigationBarHeight(globalContext, withComputed, withDimen, ignoreVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RhinoRuntimeFunctionInterface
|
||||||
|
fun getVisibleNavigationBarHeight(scriptRuntime: ScriptRuntime, args: Array<out Any?>): Int = ensureArgumentsAtMost(args, 1) { argList ->
|
||||||
|
val (options) = argList
|
||||||
|
val opt = coerceObject(options, newNativeObject())
|
||||||
|
val withComputed = opt.inquire("withComputed", ::coerceBoolean, true)
|
||||||
|
val withDimen = opt.inquire("withDimen", ::coerceBoolean, true)
|
||||||
|
ViewUtils.getNavigationBarHeight(globalContext, withComputed, withDimen, ignoreVisibility = false)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initWidgetConstructor(ctor: BaseFunction) {
|
private fun initWidgetConstructor(ctor: BaseFunction) {
|
||||||
val prototype = ctor.prop("prototype") as? ScriptableObject ?: newNativeObject()
|
val prototype = ctor.prop("prototype") as? ScriptableObject ?: newNativeObject()
|
||||||
assignWidgetProperties(prototype)
|
assignWidgetProperties(prototype)
|
||||||
@@ -699,6 +765,7 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("EmptyRange")
|
||||||
private fun bindValueForApplyingAttribute(scriptRuntime: ScriptRuntime, value: String): String {
|
private fun bindValueForApplyingAttribute(scriptRuntime: ScriptRuntime, value: String): String {
|
||||||
val ctx = scriptRuntime.ui.bindingContext ?: return value
|
val ctx = scriptRuntime.ui.bindingContext ?: return value
|
||||||
val niceCtx = Context.toObject(ctx, ctx as? Scriptable ?: scriptRuntime.topLevelScope)
|
val niceCtx = Context.toObject(ctx, ctx as? Scriptable ?: scriptRuntime.topLevelScope)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapShader
|
import android.graphics.BitmapShader
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -20,11 +19,9 @@ import android.graphics.ColorMatrix
|
|||||||
import android.graphics.ColorMatrixColorFilter
|
import android.graphics.ColorMatrixColorFilter
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuff.Mode.SRC_IN
|
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.Shader
|
import android.graphics.Shader
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
@@ -51,11 +48,15 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
@@ -131,81 +132,495 @@ object ViewUtils {
|
|||||||
}
|
}
|
||||||
else -> bg?.let {
|
else -> bg?.let {
|
||||||
it.clearColorFilter()
|
it.clearColorFilter()
|
||||||
DrawableCompat.setTintMode(it, SRC_IN)
|
DrawableCompat.setTintMode(it, PorterDuff.Mode.SRC_IN)
|
||||||
DrawableCompat.setTint(it, color)
|
DrawableCompat.setTint(it, color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (reAssignment) view.background = bg
|
if (reAssignment) view.background = bg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Comment by SuperMonster003 on Nov 31, 2025.
|
||||||
|
// ! Replaced with improved getStatusBarHeight methods.
|
||||||
|
// ! zh-CN: 已替换为已改进的 getStatusBarHeight 方法.
|
||||||
|
// !
|
||||||
|
// 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
|
||||||
|
// !
|
||||||
|
// # fun getStatusBarHeight(context: Context): Int {
|
||||||
|
// # return context.resources.getIdentifier("status_bar_height", "dimen", "android").let { resourceId ->
|
||||||
|
// # when (resourceId > 0) {
|
||||||
|
// # true -> context.resources.getDimensionPixelSize(resourceId)
|
||||||
|
// # else -> 0
|
||||||
|
// # }
|
||||||
|
// # }
|
||||||
|
// # }
|
||||||
|
// !
|
||||||
|
/**
|
||||||
|
* Get status bar height in pixels.
|
||||||
|
*
|
||||||
|
* Strategy and semantics:
|
||||||
|
* 1. WindowInsets / WindowMetrics.
|
||||||
|
* - Uses Type.statusBars() as the primary source.
|
||||||
|
* - If ignoreVisibility is true, uses "ignoring visibility" insets to get a stable height even when the bar is hidden.
|
||||||
|
* - If ignoreVisibility is false, uses "visible" insets to reflect current visibility.
|
||||||
|
* - When ignoreVisibility is true, also considers Type.displayCutout() and returns max(statusBars.top, cutout.top).
|
||||||
|
* 2. Visible frame top, optional.
|
||||||
|
* - Only used when ignoreVisibility is true.
|
||||||
|
* - Reads decorView.getWindowVisibleDisplayFrame().top, may be 0 in immersive/fullscreen.
|
||||||
|
* 3. Computed (display diff), optional.
|
||||||
|
* - Enabled only when withComputed is true, and only on pre-Android R or when window is null.
|
||||||
|
* - Computes system UI vertical inset by (real metrics - usable metrics), then derives a status bar height.
|
||||||
|
* 4. Internal dimen, optional.
|
||||||
|
* - Reads internal android dimen resources as an estimate, may not reflect runtime visibility.
|
||||||
|
*
|
||||||
|
* Important notes:
|
||||||
|
* - If ignoreVisibility is false, this API is "visibility-pure":
|
||||||
|
* it will not fall back to visible frame, computed, or dimen.
|
||||||
|
* If the status bar is not visible or cannot be determined, it returns 0.
|
||||||
|
* - If you need a safe top inset for layout/clicking (including display cutout), prefer ignoreVisibility=true.
|
||||||
|
*
|
||||||
|
* zh-CN: 获取状态栏高度, 单位为 px.
|
||||||
|
*
|
||||||
|
* 策略与语义说明:
|
||||||
|
* 1. WindowInsets / WindowMetrics.
|
||||||
|
* - 以 Type.statusBars() 为主来源.
|
||||||
|
* - ignoreVisibility=true 时使用 "忽略可见性" 的 insets, 即使系统栏被隐藏也尽量返回稳定高度.
|
||||||
|
* - ignoreVisibility=false 时使用 "可见" 的 insets, 反映当前可见状态.
|
||||||
|
* - ignoreVisibility=true 时额外考虑 Type.displayCutout(), 返回 max(statusBars.top, cutout.top).
|
||||||
|
* 2. 可见区域 top, 可选.
|
||||||
|
* - 仅在 ignoreVisibility=true 时启用.
|
||||||
|
* - 读取 decorView.getWindowVisibleDisplayFrame().top, 在沉浸式/全屏下可能为 0.
|
||||||
|
* 3. computed (显示差值法), 可选.
|
||||||
|
* - 仅在 withComputed=true 且 pre-Android R 或 window==null 时启用.
|
||||||
|
* - 通过 real metrics 与 usable metrics 的差值计算系统 UI 垂直 inset, 再推导状态栏高度.
|
||||||
|
* 4. 内部 dimen, 可选.
|
||||||
|
* - 读取 android 内部 dimen 作为估值, 可能不反映运行时可见性.
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - ignoreVisibility=false 时该 API 语义更 "纯粹": 不会回退到可见区域, computed 或 dimen, 如果无法确定或不可见则返回 0.
|
||||||
|
* - 如需要用于布局或点击的安全 top inset (包含刘海安全区), 建议使用 ignoreVisibility=true.
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getStatusBarHeight(context: Context, withFallback: Boolean = true): Int {
|
fun getStatusBarHeight(
|
||||||
|
context: Context,
|
||||||
|
withComputed: Boolean = true,
|
||||||
|
withDimen: Boolean = true,
|
||||||
|
ignoreVisibility: Boolean = true,
|
||||||
|
): Int {
|
||||||
val window = getWindowForContext(context)
|
val window = getWindowForContext(context)
|
||||||
|
|
||||||
// [1] Status bar height solution based on WindowInsets, compatible with display cutout.
|
// [1] WindowInsets / WindowMetrics.
|
||||||
// zh-CN: 基于 WindowInsets 的状态栏高度方案, 兼容刘海 (display cutout).
|
// zh-CN: 通过 WindowInsets / WindowMetrics 获取状态栏高度.
|
||||||
|
val statusTop = window
|
||||||
val insetsTop = window?.decorView?.let { decor ->
|
?.getInsetsCompat(WindowInsetsCompat.Type.statusBars(), ignoreVisibility)?.top
|
||||||
ViewCompat.getRootWindowInsets(decor)?.let { insets ->
|
?: run {
|
||||||
val statusTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
// R+ fallback: WindowMetrics (no Activity/Window needed).
|
||||||
val cutoutTop = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
// zh-CN: R+ 降级方案: WindowMetrics (无需 Activity/Window).
|
||||||
insets.displayCutout?.safeInsetTop ?: 0
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
getInsetsFromWindowMetrics(context, WindowInsetsCompat.Type.statusBars(), ignoreVisibility).top
|
||||||
} else 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.
|
if (ignoreVisibility) {
|
||||||
// zh-CN: 可见区域 top 方案, 在某些全屏/沉浸式下可能为 0.
|
// Consider display cutout only for "stable" semantics.
|
||||||
|
// zh-CN: 仅在 "稳定语义" 下考虑刘海安全区.
|
||||||
|
val cutoutTop = window
|
||||||
|
?.getInsetsCompat(WindowInsetsCompat.Type.displayCutout(), true)?.top
|
||||||
|
?: run {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
getInsetsFromWindowMetrics(context, WindowInsetsCompat.Type.displayCutout(), true).top
|
||||||
|
} else 0
|
||||||
|
}
|
||||||
|
val stableTop = maxOf(statusTop, cutoutTop)
|
||||||
|
if (stableTop > 0) return stableTop
|
||||||
|
} else {
|
||||||
|
// Visible-only semantics.
|
||||||
|
// zh-CN: 仅可见语义.
|
||||||
|
if (statusTop > 0) return statusTop
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// [2] Visible frame top, only when ignoreVisibility is true.
|
||||||
|
// zh-CN: 可见区域 top, 仅在 ignoreVisibility=true 时启用.
|
||||||
val rectTop = window?.decorView?.let { decor ->
|
val rectTop = window?.decorView?.let { decor ->
|
||||||
Rect().also { rect -> decor.getWindowVisibleDisplayFrame(rect) }.top
|
Rect().also { rect -> decor.getWindowVisibleDisplayFrame(rect) }.top
|
||||||
} ?: 0
|
} ?: 0
|
||||||
if (rectTop > 0) return rectTop
|
if (rectTop > 0) return rectTop
|
||||||
|
|
||||||
// [3] Internal dimen solution, only as an estimate.
|
// [3] Computed (display diff), only on pre-R or when window is null.
|
||||||
// zh-CN: 内部 dimen 方案, 仅作为估值.
|
// zh-CN: 显示差值法, 仅在 pre-R 或 window==null 时启用.
|
||||||
|
if (withComputed && (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || window == null)) {
|
||||||
|
val computed = getStatusBarHeightByDisplayDiff(context)
|
||||||
|
if (computed > 0) return computed
|
||||||
|
}
|
||||||
|
|
||||||
if (withFallback) return getStatusBarHeightByDimen(context)
|
// [4] Internal dimen (estimate).
|
||||||
|
// zh-CN: 内部 dimen 估值.
|
||||||
|
if (withDimen) {
|
||||||
|
val dimen = getStatusBarHeightByDimen(context)
|
||||||
|
if (dimen > 0) return dimen
|
||||||
|
}
|
||||||
|
|
||||||
// [4] Finally return 0, no constant forced.
|
// [5] Finally return 0, no constant forced.
|
||||||
// zh-CN: 最终返回 0, 不强制指定常量.
|
// zh-CN: 最终返回 0, 不强制指定常量.
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the status bar height (px) by the difference of display metrics on pre-Android R.
|
||||||
|
*
|
||||||
|
* Heuristic:
|
||||||
|
* - If the vertical system-UI inset is already within a reasonable range, treat it as the status bar height.
|
||||||
|
* - Otherwise, subtract an estimated navigation bar height (from internal dimen only) and validate again.
|
||||||
|
*
|
||||||
|
* zh-CN: 通过 DisplayMetrics 差值在 Android R 以下估算状态栏高度 (单位: px).
|
||||||
|
*
|
||||||
|
* 经验规则 (heuristic):
|
||||||
|
* - 若系统 UI 的垂直 inset 已落在合理范围内, 则直接视为状态栏高度.
|
||||||
|
* - 否则先减去导航栏高度的估值 (仅来自内部 dimen), 再进行一次范围校验.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun getStatusBarHeightByDisplayDiff(context: Context): Int {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return 0
|
||||||
|
|
||||||
|
val insets = getSystemUiInsetsOnPreR(context) ?: return 0
|
||||||
|
val (_, systemUiHeightInset) = insets
|
||||||
|
|
||||||
|
val res = context.resources
|
||||||
|
val maxReasonable = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
48f,
|
||||||
|
res.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
|
// If it already looks like a status bar height, accept directly.
|
||||||
|
// zh-CN: 如果看起来已经像状态栏高度, 直接接受.
|
||||||
|
if (systemUiHeightInset in 1..maxReasonable) return systemUiHeightInset
|
||||||
|
|
||||||
|
// Otherwise, try to remove an estimated nav bar height (dimen only).
|
||||||
|
// zh-CN: 否则尝试减去导航栏估值(仅 dimen), 再次校验.
|
||||||
|
val navBar = getNavigationBarHeightByDimen(context, withComputedOnPreR = false).coerceAtLeast(0)
|
||||||
|
val guess = (systemUiHeightInset - navBar).coerceAtLeast(0)
|
||||||
|
|
||||||
|
return if (guess in 1..maxReasonable) guess else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read status bar height from internal android dimen resources.
|
||||||
|
*
|
||||||
|
* zh-CN: 从 android 内部 dimen 资源读取状态栏高度, 仅作为估值.
|
||||||
|
*/
|
||||||
|
private fun getStatusBarHeightByDimen(context: Context): Int {
|
||||||
|
val res = context.resources
|
||||||
|
|
||||||
|
// Try to cover portrait/landscape differences.
|
||||||
|
// zh-CN: 尝试覆盖横竖屏差异.
|
||||||
|
val isLandscape = res.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val candidates = when {
|
||||||
|
isLandscape -> 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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
.asSequence()
|
||||||
|
.map { res.queryDimen(it) }
|
||||||
|
.firstOrNull { it > 0 }
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get navigation bar thickness in pixels.
|
||||||
|
*
|
||||||
|
* Strategy and semantics:
|
||||||
|
* 1. WindowInsets / WindowMetrics.
|
||||||
|
* - Uses Type.navigationBars() as the primary source.
|
||||||
|
* - If ignoreVisibility is true, uses "ignoring visibility" insets to get a stable thickness even when bars are hidden.
|
||||||
|
* - If ignoreVisibility is false, uses "visible" insets to reflect current visibility.
|
||||||
|
* 2. Computed (display diff), optional.
|
||||||
|
* - Enabled only when withComputed is true, and only on pre-Android R or when window is null.
|
||||||
|
* - Computes system UI insets by (real metrics - usable metrics), then derives a navigation bar thickness.
|
||||||
|
* 3. Internal dimen, optional.
|
||||||
|
* - Reads internal android dimen resources as an estimate, may not reflect runtime visibility.
|
||||||
|
* 4. Gesture insets, optional.
|
||||||
|
* - Only used when ignoreVisibility is true.
|
||||||
|
* - Uses mandatorySystemGestures().bottom, then systemGestures().bottom as the last resort.
|
||||||
|
*
|
||||||
|
* Important notes:
|
||||||
|
* - In landscape, the navigation bar may be on the left or right side. This API returns the maximum thickness of bottom, left, and right.
|
||||||
|
* - If ignoreVisibility is false, this API is "visibility-pure": it will not fall back to computed, dimen, or gesture insets. If the navigation bar is not visible or cannot be determined, it returns 0.
|
||||||
|
*
|
||||||
|
* zh-CN: 获取导航栏厚度, 单位为 px.
|
||||||
|
*
|
||||||
|
* 策略与语义说明:
|
||||||
|
* 1. WindowInsets / WindowMetrics.
|
||||||
|
* - 以 Type.navigationBars() 为主来源.
|
||||||
|
* - ignoreVisibility=true 时使用 "忽略可见性" 的 insets, 即使系统栏被隐藏也尽量返回稳定厚度.
|
||||||
|
* - ignoreVisibility=false 时使用 "可见" 的 insets, 反映当前可见状态.
|
||||||
|
* 2. computed(显示差值法), 可选.
|
||||||
|
* - 仅在 withComputed=true 且 pre-Android R 或 window==null 时启用.
|
||||||
|
* - 通过 real metrics 与 usable metrics 的差值计算系统 UI insets, 再推导导航栏厚度.
|
||||||
|
* 3. 内部 dimen, 可选.
|
||||||
|
* - 读取 android 内部 dimen 作为估值, 可能不反映运行时可见性.
|
||||||
|
* 4. 手势 insets, 可选.
|
||||||
|
* - 仅在 ignoreVisibility=true 时启用.
|
||||||
|
* - 依次尝试 mandatorySystemGestures().bottom 与 systemGestures().bottom 作为最终兜底.
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 横屏时导航栏可能位于左右侧, 本 API 返回 bottom, left, right 三者的最大厚度.
|
||||||
|
* - ignoreVisibility=false 时该 API 语义更 "纯粹": 不会回退到 computed, dimen 或 gestures, 如果无法确定或不可见则返回 0.
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getNavigationBarHeight(context: Context, withFallback: Boolean = true): Int {
|
fun getNavigationBarHeight(
|
||||||
|
context: Context,
|
||||||
|
withComputed: Boolean = true,
|
||||||
|
withDimen: Boolean = true,
|
||||||
|
ignoreVisibility: Boolean = true,
|
||||||
|
): Int {
|
||||||
val window = getWindowForContext(context)
|
val window = getWindowForContext(context)
|
||||||
|
|
||||||
// [1] Status bar height solution based on WindowInsets.
|
// [1] WindowInsets / WindowMetrics.
|
||||||
// zh-CN: 基于 WindowInsets 的状态栏高度方案.
|
// zh-CN: 通过 WindowInsets / WindowMetrics 获取导航栏厚度.
|
||||||
|
val thicknessFromNavBars = window?.let {
|
||||||
val maxInsets = window?.decorView?.let { decor ->
|
val nav = it.getInsetsCompat(WindowInsetsCompat.Type.navigationBars(), ignoreVisibility)
|
||||||
ViewCompat.getRootWindowInsets(decor)
|
maxOf(nav.bottom, nav.left, nav.right)
|
||||||
?.getInsets(WindowInsetsCompat.Type.navigationBars())
|
} ?: run {
|
||||||
?.let { insets ->
|
// R+ fallback: WindowMetrics (no Activity/Window needed).
|
||||||
// Compatible with horizontal screen navigation bars: take the maximum value of bottom/left/right.
|
// zh-CN: R+ 降级方案: WindowMetrics (无需 Activity/Window).
|
||||||
// zh-CN: 兼容横屏左右侧导航栏: 取 bottom/left/right 最大值.
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
maxOf(insets.bottom, insets.left, insets.right)
|
val nav = getInsetsFromWindowMetrics(context, WindowInsetsCompat.Type.navigationBars(), ignoreVisibility)
|
||||||
|
maxOf(nav.bottom, nav.left, nav.right)
|
||||||
|
} else 0
|
||||||
}
|
}
|
||||||
} ?: 0
|
if (thicknessFromNavBars > 0) return thicknessFromNavBars
|
||||||
if (maxInsets > 0) return maxInsets
|
|
||||||
|
|
||||||
// [2] Internal dimen solution, only as an estimate.
|
// [Pure visibility semantics] If ignoreVisibility is false, do not fall back to computed/dimen/gestures.
|
||||||
// zh-CN: 内部 dimen 方案, 仅作为估值.
|
// zh-CN: 语义纯粹化. ignoreVisibility=false 时不回退到 computed/dimen/gestures, 直接返回 0.
|
||||||
|
if (!ignoreVisibility) return 0
|
||||||
|
|
||||||
if (withFallback) return getNavigationBarHeightByDimen(context)
|
// [2] Computed (display diff), only on pre-R or when window is null.
|
||||||
|
// zh-CN: 显示差值法, 仅在 pre-R 或 window==null 时启用.
|
||||||
|
if (withComputed && (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || window == null)) {
|
||||||
|
val computed = getNavBarHeightByDisplayDiff(context)
|
||||||
|
if (computed > 0) return computed
|
||||||
|
}
|
||||||
|
|
||||||
// [3] Finally return 0, no constant forced.
|
// [3] Internal dimen (estimate).
|
||||||
|
// zh-CN: 内部 dimen 估值.
|
||||||
|
if (withDimen) {
|
||||||
|
val dimen = getNavigationBarHeightByDimen(context)
|
||||||
|
if (dimen > 0) return dimen
|
||||||
|
}
|
||||||
|
|
||||||
|
// [4] Mandatory gesture insets (bottom only).
|
||||||
|
// zh-CN: 强制手势区域, 只取 bottom 以避免左右返回手势边缘导致语义失真.
|
||||||
|
val bottomFromMandatory = window
|
||||||
|
?.getInsetsCompat(WindowInsetsCompat.Type.mandatorySystemGestures(), true)?.bottom
|
||||||
|
?: run {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
getInsetsFromWindowMetrics(context, WindowInsetsCompat.Type.mandatorySystemGestures(), true).bottom
|
||||||
|
} else 0
|
||||||
|
}
|
||||||
|
if (bottomFromMandatory > 0) return bottomFromMandatory
|
||||||
|
|
||||||
|
// [5] Gesture insets (bottom only).
|
||||||
|
// zh-CN: 普通手势区域, 同样只取 bottom.
|
||||||
|
val bottomFromGestures = window
|
||||||
|
?.getInsetsCompat(WindowInsetsCompat.Type.systemGestures(), true)?.bottom
|
||||||
|
?: run {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
getInsetsFromWindowMetrics(context, WindowInsetsCompat.Type.systemGestures(), true).bottom
|
||||||
|
} else 0
|
||||||
|
}
|
||||||
|
if (bottomFromGestures > 0) return bottomFromGestures
|
||||||
|
|
||||||
|
// [6] Finally return 0, no constant forced.
|
||||||
// zh-CN: 最终返回 0, 不强制指定常量.
|
// zh-CN: 最终返回 0, 不强制指定常量.
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the navigation bar thickness (px) by the difference of display metrics on pre-Android R.
|
||||||
|
*
|
||||||
|
* Heuristic:
|
||||||
|
* - Compute system UI insets by (real metrics - usable metrics).
|
||||||
|
* - Estimate the status bar height using internal dimen only (to avoid requiring WindowInsets),
|
||||||
|
* then treat the remaining vertical inset as a "bottom navigation bar" candidate.
|
||||||
|
* - Also treat the horizontal inset as a "side navigation bar" candidate (common in landscape).
|
||||||
|
* - Return the maximum of the two candidates, with a reasonable upper bound check.
|
||||||
|
*
|
||||||
|
* zh-CN: 通过 DisplayMetrics 差值在 Android R 以下估算导航栏厚度 (单位: px).
|
||||||
|
*
|
||||||
|
* 经验规则 (heuristic):
|
||||||
|
* - 使用 (real metrics - usable metrics) 计算系统 UI 的 inset.
|
||||||
|
* - 仅通过内部 dimen 估算状态栏高度 (避免依赖 WindowInsets),
|
||||||
|
* 再把剩余的垂直 inset 作为 "底部导航栏" 的候选值.
|
||||||
|
* - 同时把水平 inset 作为 "侧边导航栏" 的候选值 (横屏更常见).
|
||||||
|
* - 取两者最大值, 并做合理上限校验后返回.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun getNavBarHeightByDisplayDiff(context: Context): Int {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return 0
|
||||||
|
|
||||||
|
val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager ?: return 0
|
||||||
|
val display = wm.defaultDisplay ?: return 0
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Estimate status bar height by dimen only, to avoid requiring WindowInsets.
|
||||||
|
// zh-CN: 仅使用 dimen 估算状态栏高度, 避免依赖 WindowInsets.
|
||||||
|
val statusBar = getStatusBarHeightByDimen(context).coerceAtLeast(0)
|
||||||
|
|
||||||
|
val bottomGuess = (systemUiHeightInset - statusBar).coerceAtLeast(0)
|
||||||
|
val sideGuess = systemUiWidthInset.coerceAtLeast(0)
|
||||||
|
|
||||||
|
// In landscape, nav bar may be on the side; in portrait, more likely on the bottom.
|
||||||
|
// zh-CN: 横屏导航栏更可能在侧边, 竖屏更可能在底部.
|
||||||
|
val computed = maxOf(bottomGuess, sideGuess)
|
||||||
|
|
||||||
|
// Prevent abnormal values.
|
||||||
|
// zh-CN: 防止异常值.
|
||||||
|
val maxReasonable = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
96f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
|
return if (computed in 1..maxReasonable) computed else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||||
|
private fun getNavigationBarHeightByDimen(context: Context, withComputedOnPreR: Boolean = true): Int {
|
||||||
|
val res = context.resources
|
||||||
|
|
||||||
|
// [1] Check if system declares to show navigation bar.
|
||||||
|
// zh-CN: 检查系统是否声明显示导航栏.
|
||||||
|
|
||||||
|
val showNavDeclared: Boolean? = runCatching {
|
||||||
|
val id = res.getIdentifier("config_showNavigationBar", "bool", "android")
|
||||||
|
if (id > 0) res.getBoolean(id) else null
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
// [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 candidates = when {
|
||||||
|
isLandscape -> listOf(
|
||||||
|
"navigation_bar_width",
|
||||||
|
"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 = candidates.asSequence().map { res.queryDimen(it) }.firstOrNull { it > 0 } ?: 0
|
||||||
|
|
||||||
|
if (showNavDeclared != false && 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)
|
||||||
|
val bottomGuess = (systemUiHeightInset - statusBar).coerceAtLeast(0)
|
||||||
|
val sideGuess = 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(bottomGuess, sideGuess)
|
||||||
|
|
||||||
|
// Prevent abnormal values (navigation bar usually not exceeding 96dp).
|
||||||
|
// zh-CN: 防止异常值 (导航栏通常不超过 96dp).
|
||||||
|
val maxReasonable = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, res.displayMetrics).toInt()
|
||||||
|
val computedOk = computed in 1..maxReasonable
|
||||||
|
|
||||||
|
// If system explicitly says "no nav bar", only accept computed when it strongly indicates otherwise.
|
||||||
|
// zh-CN: 如果系统明确声明 "无导航栏", 则仅在计算值强烈指示存在导航栏时才接受该计算值.
|
||||||
|
return when {
|
||||||
|
showNavDeclared == false -> if (computedOk) computed else 0
|
||||||
|
computedOk -> computed
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||||
|
private fun Resources.queryDimen(name: String): Int = runCatching {
|
||||||
|
val id = getIdentifier(name, "dimen", "android")
|
||||||
|
if (id > 0) getDimensionPixelSize(id) else 0
|
||||||
|
}.getOrDefault(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 wInset = (real.widthPixels - usable.widthPixels).coerceAtLeast(0)
|
||||||
|
val hInset = (real.heightPixels - usable.heightPixels).coerceAtLeast(0)
|
||||||
|
|
||||||
|
return Pair(wInset, hInset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Window.getInsetsCompat(typeMask: Int, ignoreVisibility: Boolean): Insets {
|
||||||
|
val decor = decorView
|
||||||
|
|
||||||
|
val compat = ViewCompat.getRootWindowInsets(decor)
|
||||||
|
?: decor.rootWindowInsets?.let { WindowInsetsCompat.toWindowInsetsCompat(it, decor) }
|
||||||
|
?: return Insets.NONE
|
||||||
|
|
||||||
|
return if (ignoreVisibility) {
|
||||||
|
compat.getInsetsIgnoringVisibility(typeMask)
|
||||||
|
} else {
|
||||||
|
compat.getInsets(typeMask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun getInsetsFromWindowMetrics(context: Context, typeMask: Int, ignoreVisibility: Boolean): Insets {
|
||||||
|
val wm = context.getSystemService(WindowManager::class.java) ?: return Insets.NONE
|
||||||
|
val wi = wm.currentWindowMetrics.windowInsets
|
||||||
|
val compat = WindowInsetsCompat.toWindowInsetsCompat(wi)
|
||||||
|
return if (ignoreVisibility) compat.getInsetsIgnoringVisibility(typeMask) else compat.getInsets(typeMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWindowForContext(context: Context): Window? = when (context) {
|
||||||
|
is Activity -> context.window
|
||||||
|
is ContextWrapper -> getWindowForContext(context.baseContext)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -468,12 +883,6 @@ object ViewUtils {
|
|||||||
return WindowInsetsControllerCompat(activity.window, activity.window.decorView)
|
return WindowInsetsControllerCompat(activity.window, activity.window.decorView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWindowForContext(context: Context): Window? = when (context) {
|
|
||||||
is Activity -> context.window
|
|
||||||
is ContextWrapper -> getWindowForContext(context.baseContext)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onConfigurationChangedForNightMode(configuration: Configuration) {
|
fun onConfigurationChangedForNightMode(configuration: Configuration) {
|
||||||
if (isAutoNightModeEnabled) {
|
if (isAutoNightModeEnabled) {
|
||||||
val isNightModeYes = isNightModeYes(configuration)
|
val isNightModeYes = isNightModeYes(configuration)
|
||||||
@@ -662,12 +1071,12 @@ object ViewUtils {
|
|||||||
borderColor: Int = Color.TRANSPARENT,
|
borderColor: Int = Color.TRANSPARENT,
|
||||||
): Drawable {
|
): Drawable {
|
||||||
val src = this
|
val src = this
|
||||||
val bmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
val bmp = createBitmap(sizePx, sizePx)
|
||||||
val canvas = Canvas(bmp)
|
val canvas = Canvas(bmp)
|
||||||
|
|
||||||
// First stretch/center draw the source drawable to sizePx * sizePx bitmap.
|
// First stretch/center draw the source drawable to sizePx * sizePx bitmap.
|
||||||
// zh-CN: 先把源 drawable 拉伸/居中绘制到 sizePx * sizePx 的位图上.
|
// zh-CN: 先把源 drawable 拉伸/居中绘制到 sizePx * sizePx 的位图上.
|
||||||
val tmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
val tmp = createBitmap(sizePx, sizePx)
|
||||||
Canvas(tmp).apply {
|
Canvas(tmp).apply {
|
||||||
val w = src.intrinsicWidth.takeIf { it > 0 } ?: sizePx
|
val w = src.intrinsicWidth.takeIf { it > 0 } ?: sizePx
|
||||||
val h = src.intrinsicHeight.takeIf { it > 0 } ?: sizePx
|
val h = src.intrinsicHeight.takeIf { it > 0 } ?: sizePx
|
||||||
@@ -701,8 +1110,9 @@ object ViewUtils {
|
|||||||
canvas.drawCircle(radius, radius, radius - borderWidthPx / 2f, stroke)
|
canvas.drawCircle(radius, radius, radius - borderWidthPx / 2f, stroke)
|
||||||
}
|
}
|
||||||
|
|
||||||
return BitmapDrawable(context.resources, bmp)
|
return bmp.toDrawable(context.resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setSearchViewColorsByColorLuminance(context: Context, searchView: SearchView, aimColor: Int) {
|
fun setSearchViewColorsByColorLuminance(context: Context, searchView: SearchView, aimColor: Int) {
|
||||||
searchView.setColorsByColorLuminance(context, aimColor)
|
searchView.setColorsByColorLuminance(context, aimColor)
|
||||||
@@ -827,145 +1237,6 @@ 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) {
|
enum class MODE(val key: String) {
|
||||||
|
|
||||||
DAY(key(R.string.key_night_mode_always_off)),
|
DAY(key(R.string.key_night_mode_always_off)),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Tue Dec 30 16:16:09 CST 2025
|
#Wed Dec 31 17:28:38 CST 2025
|
||||||
BUILD_TIME=1767082569045
|
BUILD_TIME=1767173318339
|
||||||
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=3565
|
VERSION_BUILD=3571
|
||||||
VERSION_NAME=6.7.0 Alpha13
|
VERSION_NAME=6.7.0 Alpha13
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user