文档教程知识库【免费下载链接】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),仅供参考