Android LayerDrawable 与 Drawable.Callback 深度剖析:回调调用链、View 背景切换与一个经典 Bug 的修复
文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载本篇技术指南以 issue-24/Android LayerDrawable 和 Drawable.Callback.md 译文为骨架结合 Android 框架的Drawable.Callback机制与仓库内其他 Drawable 实践系统讲解LayerDrawable的多层结构、重绘回调的传递链路以及先设置 View 背景、再组装 LayerDrawable这一错误顺序引发的经典动画失效 Bug。读完本文你将能理解 Drawable 的 callback 归属关系能独立排查Drawable 更新了但界面不刷新这类问题并掌握正确的组装顺序与工程规避手段。一、先看现象一个卡住的 ActionBar 背景动画原文作者要展示的 Bug 场景非常具体在一个使用 ActionBar 的界面中开发者打算用绿色纯色层 一张重复图案图标层合成一个LayerDrawable作为 ActionBar 的背景并用ValueAnimator不断修改图标层的透明度来实现淡入淡出动画。结果发现动画数值在跑但 ActionBar 背景纹丝不动。如果你不了解LayerDrawable的机制这类问题很难定位——因为报错、崩溃、异常日志统统没有只是界面不刷新。所有线索都藏在Drawable与其宿主View 或外层容器 Drawable之间的Drawable.Callback联系里。二、LayerDrawable 是什么多层 Drawable 的容器LayerDrawable是一个特殊的Drawable它内部持有一个Drawable数组数组中每一个Drawable都是视图中的一层。绘制时各层按数组索引顺序叠加绘制索引越大越靠上层。在 XML 中它通常写作layer-list?xml version1.0 encodingutf-8? layer-list xmlns:androidhttp://schemas.android.com/apk/res/android !-- 底层纯色 -- item android:drawablecolor/colorPrimary / !-- 上层图标可通过 left/top/right/bottom 或 width/height 控制位置与尺寸 -- item android:drawabledrawable/ic_launcher android:top16dp android:left16dp / /layer-list在代码中则通过构造函数传入Drawable[]数组创建这正是原文 Bug 示例采用的方式。LayerDrawable的常用能力还包括getNumberOfLayers()、setLayerInset(index, l, t, r, b)、setDrawableByLayerId(id, drawable)等可动态调整每一层的缩进或替换某层内容。值得补充的一点是LayerDrawable这类容器型 Drawable在兼容包中还有特殊价值。仓库 issue-44/Android-Support-Library-23.2.md 中提到在 Android Lollipop 之前的设备上直接引用矢量图会失败但矢量图被StateListDrawable、InsetDrawable、LayerDrawable、LevelListDrawable、RotateDrawable等 drawable 容器间接引用时兼容包可以正常加载——也就是说LayerDrawable也是绕过旧版本矢量图兼容限制的常见手法之一。三、Drawable.Callback 与 invalidateSelf()重绘请求从哪发起Drawable.Callback是 Drawable 与其宿主之间的重绘契约。接口定义了三个方法invalidateDrawable(Drawable who)宿主收到子 Drawable 的重绘请求scheduleDrawable(Drawable who, Runnable what, long when)宿主安排一个延迟执行的动作动画帧调度会用到unscheduleDrawable(Drawable who, Runnable what)取消已安排的动作。Drawable自身在状态变化如setAlpha()、setLevel()、setColorFilter()需要重绘时会调用invalidateSelf()。原文给出了这段核心实现public void invalidateSelf() { /* 获取注册的Callback实例如果无则返回null。 */ final Callback callback getCallback(); if (callback ! null) { callback.invalidateDrawable(this); } }要点invalidateSelf()只做一件事——把我需要重绘的消息转交给注册的Callback。如果getCallback()返回null那么这次重绘请求就静默丢失Drawable 内部数据变了界面却永远不会刷新。这正是本文要讲的 Bug 的根源。四、Callback 调用链从内层 Drawable 一路传到 View我们知道View实现了Drawable.Callback接口因此一个被设置为 View 背景的 Drawable其 callback 通常指向该 View当 View 的重绘被安排进下一帧时ViewRootImpl会执行 measure/layout/draw 流程完成实际刷新。如果背景是一个LayerDrawable情况会变成一条层层传递的调用链。原文明确指出在LayerDrawable中每一层 Drawable 都会把LayerDrawable注册为自己的Drawable.Callback从而允许内层 Drawable 在需要重绘时通知LayerDrawable。于是当 View 背景是LayerDrawable时内层 Drawable如图标层调用 setAlpha()/setLevel() 等 → invalidateSelf() │ ▼ getCallback() → LayerDrawable L │ L.invalidateDrawable(内层 Drawable) ▼ L 更新自身状态并调用 invalidateSelf() → 通知自己的 Callback │ ▼ getCallback() → View V │ V.invalidateDrawable(L) ▼ View 被标记为需要重绘 → ViewRootImpl 安排下一帧 measure/layout/draw链条可以理解为内层 → LayerDrawable → View → ViewRootImpl。链条上任何一环的 callback 被破坏重绘信号就会在这一环断掉。五、setBackgroundDrawable()更换背景时无条件清空旧 callback理解了调用链再来看 View 一侧的源码。在View.setBackgroundDrawable(Drawable background)中有这么一段if (mBackground ! null) { mBackground.setCallback(null); unscheduleDrawable(mBackground); } … if (background ! null) { background.setCallback(this); }结论非常明确当 View 改变背景时会无条件将原背景如果原背景是 Drawable 的话的Drawable.Callback设置为null同时unscheduleDrawable(mBackground)会取消旧背景上尚未执行的所有动画调度。这里有一个需要留意的历史细节setBackgroundDrawable(Drawable)是早期的 API后来被setBackground(Drawable)取代后者在较新的框架版本内部同样是走相同的背景替换逻辑但从更换背景会切断旧背景 callback这一行为上讲两者一致。这是框架层面的硬性行为不是某个版本特有的怪癖。六、经典 Bug 复现四步让 ActionBar 背景动画失效把前面三节的知识串起来原文给出了一个可以精确复现 Bug 的步骤把DrawableA设置成ViewV的背景。此时A 的 callback 指向 V将A作为一层放进LayerDrawableL。此时A 的 callback 指向 L为V设置另一个背景也就是把L设上去。在这一步V 会把原背景此时仍是 A的 callback 强制置为 null破坏了 A 与 L 之间的联系Bug 出现再更新DrawableA如修改透明度、levelL 不会收到任何通知界面不刷新。关键在第 3 步虽然此刻 View 的新背景已经是LayerDrawableL但 View 执行清空动作针对的是当前记录的旧背景 mBackground——也就是刚才被设上去的 A。于是 A 与 L 之间刚建立起来的 callback 联系转瞬之间就被 View 的换背景清理动作打断了。原文配套的 ActionBar 示例代码如下两个按钮分别触发正常与失效两条路径Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn1 (Button) findViewById(R.id.button1); btn1.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { // 1. 将 launcherIconDrawable.callback 赋值给 actionBar actionBar.setBackgroundDrawable(launcherIconDrawable); animateActionBarWorking(); } }); Button btn2 (Button) findViewById(R.id.button2); btn2.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { // 1. 将 launcherIconDrawable.callback 赋值给 actionBar actionBar.setBackgroundDrawable(launcherIconDrawable); animateActionBarNotWorking(); } }); actionBar getSupportActionBar(); launcherIconDrawable getResources().getDrawable(R.drawable.launcher_repeat); colorLayer new ColorDrawable(Color.rgb(0, 255, 0)); actionBar.setBackgroundDrawable(colorLayer); } /* 这个函数运行后 ActionBar 不会得到更新。 */ private void animateActionBarNotWorking() { Drawable[] layers new Drawable[] { colorLayer, launcherIconDrawable }; LayerDrawable layerDrawable new LayerDrawable(layers); actionBar.setBackgroundDrawable(layerDrawable); ValueAnimator valueAnimator ValueAnimator.ofInt(0, 255); valueAnimator.setDuration(1000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Override public void onAnimationUpdate(ValueAnimator animation) { // 4. 更新 launcherIconDrawable 不会触发 actionBar 背景的更新 // 因为此时 launcherIconDrawable.callback 已经是 null launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue()); } }); valueAnimator.start(); } /* 由于先移除了 launcherIconDrawable 与 ActionBar 的联系 这个函数运行后会让 ActionBar 得到更新。 */ private void animateActionBarWorking() { actionBar.setBackgroundDrawable(null); animateActionBarNotWorking(); }逐步推演两条路径NotWorking 路径btn2先把launcherIconDrawable设为背景A.callback V→ 构造LayerDrawableA.callback L→actionBar.setBackgroundDrawable(layerDrawable)触发清空旧背景 callback旧背景正是 A于是A.callback 变为 null。动画每帧setAlpha()都调用invalidateSelf()但getCallback()为 null重绘请求丢失界面冻结。Working 路径btn1animateActionBarWorking()先执行actionBar.setBackgroundDrawable(null)主动把 A 与 V 的联系切断随后再走animateActionBarNotWorking()的流程——此时旧背景已经清空setBackgroundDrawable(layerDrawable)不会再误伤 A新背景 L 设置成功后L 会重新把自己注册为各层含 A的 callback链条 A → L → V 完好动画正常生效。原文作者还给出了一个可以自行下载运行的完整示例工程位于原作者的开源仓库 blog-android-source-code 的 LayerDrawableCallback 目录上述代码即为该示例的核心部分。七、修复方案与工程实践建议1. 牢记正确顺序先在 View 上设置好或清空背景再构造 / 组装LayerDrawable。用一句话总结LayerDrawable的组装时机必须排在把新背景赋给 View之后避免 View 的换背景清理动作切断内部子层的 callback。2. 用 getCallback() 做现场诊断排查Drawable 更新了但界面不刷新问题时可以在更新前打印一下目标 Drawable 的 callbackLog.d(CallbackDebug, drawable.callback launcherIconDrawable.getCallback()); // 期望输出某个 LayerDrawable且该 LayerDrawable 的 callback 又是对应的 View // 若输出 null则说明链条已断重绘请求无法送达3. 不跨宿主复用 Drawable一个 Drawable 同一时间只能有一个 callback。把一个已经挂在 View 上的 Drawable 再塞进另一个容器或反过来把容器内的 Drawable 直接设为 View 背景都会造成 callback 归属混乱。要么先解除旧关系置 null要么干脆不复用。4. 优先用 XML 声明减少代码顺序负担像纯色底 图标层这类固定叠加效果直接写在res/drawable/xxx.xml的layer-list中由框架统一解析、统一设置 callback天然避免先设背景再组 LayerDrawable这种错误顺序。只有当各层需要运行时动态创建、频繁替换时才走代码构造此时务必遵守第 1 点的顺序约束。5. 动画调度同样会被清理注意setBackgroundDrawable里还有一行unscheduleDrawable(mBackground)换背景不仅清 callback还会取消旧背景上排队中的scheduleDrawable调度如帧动画、延迟动作。因此涉及动画的背景替换同样要在替换后重新启动调度不能假设动画会自动继续。八、延伸阅读本仓库中相关的 Drawable 实践LayerDrawable只是 Android Drawable 体系的一环仓库中还有多篇与之互补的译文值得对照阅读issue-26/Tinting drawables.md自定义BitmapDrawable子类结合LightingColorFilter实现按主题着色并介绍了用StateListDrawable复用同一张图做多状态变色与Drawable 容器思想一脉相承issue-17/Android中的帧动画.mdAnimationDrawable与Animation-list的使用展示另一种通过Drawable驱动 UI 动画的方式其scheduleDrawable调度正是上节unscheduleDrawable清理的对象issue-44/Android-Support-Library-23.2.md说明LayerDrawable等容器 Drawable 在旧版本上间接加载矢量图的兼容技巧。结语LayerDrawable与Drawable.Callback的这套机制本质是 Android 对谁负责重绘的职责划分invalidateSelf()只负责发出请求真正的刷新由注册的 callback最终落到View驱动。View 换背景时无条件清空旧背景 callback是框架的既定行为理解了这一点原文中的四步 Bug 就一目了然——它不是一个框架缺陷而是Drawable 复用 组装顺序错误叠加的结果。记住正确的顺序再配合getCallback()做诊断这类静默失效问题就不再神秘。赞分享文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载相关推荐Raspberry Pi OS 与 Linux 调度器scheduler_tick、schedule 与上下文切换的深入剖析Raspberry Pi OS 与 Linux 调度器scheduler_tick、schedule 与上下文切换的深入剖析 本篇技术指南以 Linux v4示例工程WPF UI 主题与外观系统深度解析主题切换、强调色同步与窗口背景效果WPF UI 主题与外观系统深度解析主题切换、强调色同步与窗口背景效果 导读 本文基于 docs/architecture/cross cutting/theUI组件桌面应用pandas 1.1.2 版本发布详解回归修复、Bug 修复与 factorize API 调整全解析pandas 1.1.2 版本发布详解回归修复、Bug 修复与 factorize API 调整全解析 导读 本文基于 pandas 官方变更日志 doc/s数据分析数据科学数据处理上一篇使用 lego 与 NS1 完成 DNS-01 挑战配置、凭据与源码实现解析下一篇Opslane开发者指南如何扩展和自定义你的多会话管理器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

第132篇ContentProvider:跨进程数据共享的标准入口

第132篇ContentProvider:跨进程数据共享的标准入口

先把结论放在前面:ContentProvider 是四大组件里唯一"不是给界面用的",它本质是跨进程的表结构接口 + 数据通知机制。两件事必须立住:① 它的调用不经过 Binder 代理的常规路径——每次 query 都是一次完整的事务,因此计数严格;② 它的实例在应用进程启动时就被…

2026/10/10 11:34:49 阅读更多 →
把 OpenStock 塞进 Docker Compose:自托管行情监控平台的完整实战路线

把 OpenStock 塞进 Docker Compose:自托管行情监控平台的完整实战路线

把 OpenStock 塞进 Docker Compose:自托管行情监控平台的完整实战路线 【免费下载链接】OpenStock OpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insi…

2026/10/10 11:34:49 阅读更多 →
ruflo Hive Mind 集群状态监控实战:hive-mind-status 命令与 hive-mind_status 工具全解析

ruflo Hive Mind 集群状态监控实战:hive-mind-status 命令与 hive-mind_status 工具全解析

人工智能AI Agent多智能体Agent 编排Agent 记忆工具调用代码智能体MCP 服务 【免费下载链接】ruflo 🌊 The original agent harness. Deploy intelligent multi-player swarms, coordinate autonomous workflows, and build conversational AI systems. Features ad…

2026/10/10 11:33:48 阅读更多 →

最新新闻

Rust trait 完全指南:从基础语法到零成本抽象工程实践

Rust trait 完全指南:从基础语法到零成本抽象工程实践

1. Rust 的 trait:行为契约,不只是接口在 Rust 里,trait 是个绕不过去的核心概念。我可以直接说:如果你理解了 trait,你就理解了 Rust 设计哲学的一半。剩下的一半是所有权和生命周期。我第一次接触 trait 的时候&…

2026/10/10 12:19:56 阅读更多 →
DeepSeek解读Windows电池报告:笔记本健康度查询与更换判断

DeepSeek解读Windows电池报告:笔记本健康度查询与更换判断

没用过几台Windows笔记本的人,可能不会意识到电池健康度这个数字有多重要。等你真正带着笔记本出门开会,结果屏幕亮度调到最低、WiFi都关了,还是眼睁睁看着电量从100%掉到20%,那时候才明白“健康度”三个字有多值钱。我最近一直在…

2026/10/10 12:19:56 阅读更多 →
Spring Boot冷链生鲜系统:从订单到温度的全程可溯设计

Spring Boot冷链生鲜系统:从订单到温度的全程可溯设计

1. 为谁做、做什么:重新审视冷链生鲜系统的问题边界做系统之前,我习惯先问一句:这个项目到底要解决谁的什么问题。Spring Boot冷链运输生鲜销售系统这个选题,听起来是一个电商系统加一个物流系统拼在一起,但真正落地后…

2026/10/10 12:19:56 阅读更多 →
AgentEarth 使用心得:用 TaoToken 统一 Key 打通 AI Agent 碎片化工具链

AgentEarth 使用心得:用 TaoToken 统一 Key 打通 AI Agent 碎片化工具链

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 12:19:56 阅读更多 →
贝叶斯稀疏表示实战:MATLAB R2018上的ARD变分推断与不确定性量化

贝叶斯稀疏表示实战:MATLAB R2018上的ARD变分推断与不确定性量化

做稀疏表示这行当的人,一开始接触的套路基本都是OMP、MP这类贪心算法,或者Lasso这种凸优化。解是解出来了,但有个问题一直让人不踏实:你拿到的只是一组稀疏系数,至于这系数靠不靠谱、哪些位置是数据真正支撑的、哪些只…

2026/10/10 12:19:55 阅读更多 →
安卓模拟器MUMU+tcpdump抓包Socket协议流量

安卓模拟器MUMU+tcpdump抓包Socket协议流量

fiddler抓包只捕获http/https流量,走Socket层的流量抓不到。要通过Wireshark来抓。 但Wireshark抓到的数据量有点大(浏览器/微信等流量全部一起抓),虽然Wireshark可以通过ip和端口过滤,通过ip过滤又容易漏掉。 所以考虑在mumu上安装tcpdump…

2026/10/10 12:18:55 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →