如何通过Android drawable level-list XML实现动态电量或信号图标变化?
- 内容介绍
- 相关推荐
本文共计1237个文字,预计阅读时间需要5分钟。
这个不是用来响应按钮点击或触摸变色的,而是依赖 `android:level` 动态切换子项——比如电量从 0 到 100,你给每个区间配一个 `item`。系统根据 `level` 自动选择匹配的 `drawable`。`state-list` 只认状态(如 `android:state_pressed`),不认数值,硬编码的 `level` 会完全没有响应。
常见错误现象:ImageView 死活不换图,或者只显示第一个 item;debug 时发现 setImageLevel() 调了但 UI 没变——大概率是 XML 里没写 android:maxLevel,或 Java/Kotlin 里传的 level 超出范围(0–10000 是合法范围,但你的 item 只定义了 0–4)。
- 每个
item的android:maxLevel和android:minLevel必须覆盖连续整数区间,比如 0–20、21–40、41–60…不能留空档,也不能重叠 -
level-list根节点的android:constantSize="true"推荐加上,否则每次 level 变化时,intrinsic size 可能抖动(尤其嵌套在TextView的 compound drawable 里) - Android 旧版本(animateLevelChange() 在所有机型上都丝滑
XML 里怎么写 level 区间才不踩坑
别按百分比写死 0–25、26–50…实际用电池 API 拿到的是整数(如 Intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)),而 level-list 的 level 值默认是 0–10000 映射,所以更稳妥的做法是把原始值放大 100 倍再设,让区间对齐整数边界。
示例:电量图标分 5 档(0%、20%、40%、60%、80%+),对应 level 值 0、2000、4000、6000、8000:
<level-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/ic_battery_0" android:minLevel="0" android:maxLevel="1999" /> <item android:drawable="@drawable/ic_battery_20" android:minLevel="2000" android:maxLevel="3999" /> <item android:drawable="@drawable/ic_battery_40" android:minLevel="4000" android:maxLevel="5999" /> <item android:drawable="@drawable/ic_battery_60" android:minLevel="6000" android:maxLevel="7999" /> <item android:drawable="@drawable/ic_battery_80" android:minLevel="8000" android:maxLevel="10000" /> </level-list>
- 所有
minLevel/maxLevel必须是整数,小数会被截断(写 20.5 等于写 20) - 最后一个 item 的
maxLevel建议设为 10000,避免 level 略超预期(比如 101%)导致 fallback 到第一个 item - 资源名别带中文或空格,
@drawable/ic_battery_100%这种会编译失败
Java/Kotlin 里怎么安全设置 level
直接调 setImageLevel() 不行——如果 ImageView 的 drawable 不是 level-list 类型,会静默失败;而且 level 值没做校验,传负数或超大数可能触发异常或显示错乱。
正确做法:先取 drawable,强转成 LevelListDrawable,再 setLevel,并确保在主线程执行:
val drawable = imageView.drawable if (drawable is LevelListDrawable) { val level = (batteryPercent * 100).coerceAtMost(10000) drawable.level = level }
- 必须检查类型,
imageView.getDrawable()返回的是Drawable,不是所有 drawable 都支持 level - level 值建议用
coerceAtMost(10000)或clamp(0, 10000)限制范围,别信外部数据源 - 如果用在
RecyclerView里,记得在onBindViewHolder中每次都重新 setLevel,因为 ViewHolder 复用会导致 level 残留
信号强度场景下 level-list 的特殊处理
信号强度(TelephonyManager.getSignalStrength())返回的是 ASU 值(通常 0–31),和电量不同,它没有标准百分比映射。硬套 0–31 到 level 0–10000 会导致图标切换生硬——比如 ASU=12 和 ASU=13 在视觉上几乎没差别,但 level 差 323,可能跨两个 item。
更合理的做法是做离散映射:把 ASU 分成 4–5 档(差、弱、中、强、满),每档对应一个固定 level 值(如 0、2500、5000、7500、10000),再查表转换:
private fun asuToLevel(asu: Int): Int = when (asu) { in 0..5 -> 0 in 6..12 -> 2500 in 13..18 -> 5000 in 19..25 -> 7500 else -> 10000 }
- 别用线性缩放,ASU 和实际信号质量不是正比关系
- 注意不同制式(LTE/5G)ASU 范围不同,有些设备 LTE 下 ASU 最高是 97,得适配
- 如果用
SignalStrength的getLevel()(返回 0–4),反而更简单——直接乘 2500 就行,但要注意这个 level 是系统估算值,不一定准
level-list 看似简单,真正难的是 level 值的来源是否稳定、区间划分是否符合人眼感知、以及复用场景下的状态清理——这些地方一漏,图标就卡在某一级不动了。
本文共计1237个文字,预计阅读时间需要5分钟。
这个不是用来响应按钮点击或触摸变色的,而是依赖 `android:level` 动态切换子项——比如电量从 0 到 100,你给每个区间配一个 `item`。系统根据 `level` 自动选择匹配的 `drawable`。`state-list` 只认状态(如 `android:state_pressed`),不认数值,硬编码的 `level` 会完全没有响应。
常见错误现象:ImageView 死活不换图,或者只显示第一个 item;debug 时发现 setImageLevel() 调了但 UI 没变——大概率是 XML 里没写 android:maxLevel,或 Java/Kotlin 里传的 level 超出范围(0–10000 是合法范围,但你的 item 只定义了 0–4)。
- 每个
item的android:maxLevel和android:minLevel必须覆盖连续整数区间,比如 0–20、21–40、41–60…不能留空档,也不能重叠 -
level-list根节点的android:constantSize="true"推荐加上,否则每次 level 变化时,intrinsic size 可能抖动(尤其嵌套在TextView的 compound drawable 里) - Android 旧版本(animateLevelChange() 在所有机型上都丝滑
XML 里怎么写 level 区间才不踩坑
别按百分比写死 0–25、26–50…实际用电池 API 拿到的是整数(如 Intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)),而 level-list 的 level 值默认是 0–10000 映射,所以更稳妥的做法是把原始值放大 100 倍再设,让区间对齐整数边界。
示例:电量图标分 5 档(0%、20%、40%、60%、80%+),对应 level 值 0、2000、4000、6000、8000:
<level-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/ic_battery_0" android:minLevel="0" android:maxLevel="1999" /> <item android:drawable="@drawable/ic_battery_20" android:minLevel="2000" android:maxLevel="3999" /> <item android:drawable="@drawable/ic_battery_40" android:minLevel="4000" android:maxLevel="5999" /> <item android:drawable="@drawable/ic_battery_60" android:minLevel="6000" android:maxLevel="7999" /> <item android:drawable="@drawable/ic_battery_80" android:minLevel="8000" android:maxLevel="10000" /> </level-list>
- 所有
minLevel/maxLevel必须是整数,小数会被截断(写 20.5 等于写 20) - 最后一个 item 的
maxLevel建议设为 10000,避免 level 略超预期(比如 101%)导致 fallback 到第一个 item - 资源名别带中文或空格,
@drawable/ic_battery_100%这种会编译失败
Java/Kotlin 里怎么安全设置 level
直接调 setImageLevel() 不行——如果 ImageView 的 drawable 不是 level-list 类型,会静默失败;而且 level 值没做校验,传负数或超大数可能触发异常或显示错乱。
正确做法:先取 drawable,强转成 LevelListDrawable,再 setLevel,并确保在主线程执行:
val drawable = imageView.drawable if (drawable is LevelListDrawable) { val level = (batteryPercent * 100).coerceAtMost(10000) drawable.level = level }
- 必须检查类型,
imageView.getDrawable()返回的是Drawable,不是所有 drawable 都支持 level - level 值建议用
coerceAtMost(10000)或clamp(0, 10000)限制范围,别信外部数据源 - 如果用在
RecyclerView里,记得在onBindViewHolder中每次都重新 setLevel,因为 ViewHolder 复用会导致 level 残留
信号强度场景下 level-list 的特殊处理
信号强度(TelephonyManager.getSignalStrength())返回的是 ASU 值(通常 0–31),和电量不同,它没有标准百分比映射。硬套 0–31 到 level 0–10000 会导致图标切换生硬——比如 ASU=12 和 ASU=13 在视觉上几乎没差别,但 level 差 323,可能跨两个 item。
更合理的做法是做离散映射:把 ASU 分成 4–5 档(差、弱、中、强、满),每档对应一个固定 level 值(如 0、2500、5000、7500、10000),再查表转换:
private fun asuToLevel(asu: Int): Int = when (asu) { in 0..5 -> 0 in 6..12 -> 2500 in 13..18 -> 5000 in 19..25 -> 7500 else -> 10000 }
- 别用线性缩放,ASU 和实际信号质量不是正比关系
- 注意不同制式(LTE/5G)ASU 范围不同,有些设备 LTE 下 ASU 最高是 97,得适配
- 如果用
SignalStrength的getLevel()(返回 0–4),反而更简单——直接乘 2500 就行,但要注意这个 level 是系统估算值,不一定准
level-list 看似简单,真正难的是 level 值的来源是否稳定、区间划分是否符合人眼感知、以及复用场景下的状态清理——这些地方一漏,图标就卡在某一级不动了。

