安卓风格:主题叠加
在本系列关于 Android 造型的前几篇文章中,我们’已经了解了样式和主题之间的区别,谈到了使用主题和主题属性的好处,并强调了一些要使用的常用属性。
今天我们将重点讨论实际使用主题,它们如何应用于您的应用,以及对您如何构建它们的影响。
适用范围
在以前的文章中,我们说过。
ATheme
is accessed as a property of aContext
and can be obtained from any object which is or has a context e.g.Activity
,View
orViewGroup
. These objects exist in a tree, where anActivity
containsViewGroup
s which containView
s etc. Specifying a theme at any level of this tree cascades to descendent nodes e.g. setting a theme on aViewGroup
applies to all theView
s within it (in contrast to styles which only apply to a single view).
在这个树中的任何一级设置一个主题都不会替换当前有效的主题,而是覆盖它。考虑一下下面这个Button
,它是一个主题,但它的父级也指定了一个主题。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
android:theme="@style/Theme.App.Foo">
<Button …
android:theme="@style/Theme.App.Bar"/>
</ViewGroup>
如果一个属性在两个主题中都被指定,那么最本地的“胜出”,即Bar
中的属性将被应用到按钮中。任何在主题Foo
中指定但不在主题Bar
中指定的属性也将被应用到按钮上。
主题相互叠加
这可能看起来像一个矫揉造作的例子,但这种技术对于具有不同外观的应用程序的子部分的造型非常有用,例如,在其他浅色屏幕上的深色工具栏,或者这个屏幕(来自Owl 示例应用程序),它的主题主要是粉红色,但显示相关内容的底部部分的主题是蓝色的。
在一个粉红色主题的屏幕中,有一个蓝色的分节。
这可以通过在蓝色部分的根部设置一个主题来实现,并且它可以级联到其中的所有视图上。
重叠式的
由于主题覆盖了树中更高的任何主题,重要的是要考虑您的主题指定的什么,以确保它不会意外地替换您想要保留的属性。例如,您可能想要更改视图的背景颜色(通常由colorSurface
控制),但没有其他内容,即您希望保留当前主题的其余部分。为此,我们可以使用一种称为主题叠加的技术。
这些主题是为了,well,覆盖另一个主题。它们的范围尽可能地窄,也就是说,它们只定义(或继承)尽可能少的属性。事实上,主题叠加通常(但不总是)没有父级,例如。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="ThemeOverlay.MyApp.DarkSurface" parent="">
<item name="colorSurface">#121212</item>
</style>
Theme overlays are narrowly scoped themes, defining as few attributes as possible, designed to overlay another theme
按照惯例,我们以“ThemeOverlay”来命名这些东西。有许多方便的主题覆盖由MDC提供(和AppCompat),你可以用它们来将你的应用程序的某个部分的颜色从浅色翻到深色。
根据定义,主题覆盖并不指定一些东西,不应该孤立地使用,例如作为你活动的主题。事实上,你可以考虑在你的应用程序中使用两种 "类型 "的主题。
- “完整” 主题。这些主题指定了你对一个屏幕所需要的一切。它们继承自另一个“full”主题,如
Theme.MaterialComponents
,并应被用于为Activity
提供主题。 - Theme overlays。只打算应用over一个完整的主题,即不应该孤立地使用,因为很可能不会指定重要和必要的东西。
永恒的存在
总是有一个有效的主题,即使您没有在应用程序的任何地方指定一个主题,您也会继承一个默认的主题。因此,上面的例子是一种简化,你不应该在View
内使用完整的主题,而应该使用主题叠加。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
- android:theme="@style/Theme.App.Foo">
+ android:theme="@style/ThemeOverlay.App.Foo">
<Button …
- android:theme="@style/Theme.App.Bar"/>
+ android:theme="@style/ThemeOverlay.App.Bar"/>
</ViewGroup>
这些覆盖物不会孤立地存在,而是本身就被覆盖在包围的Activity
的主题上。
成本 : 效益
使用Themes是有运行时间成本的;每次你声明一个android:theme
,你就会创建一个新的ContextThemeWrapper
,从而分配新的Theme
和Resources
实例。它还引入了更多需要解决的样式指示层次。要警惕随意使用主题太,特别是在重复的情况下,如RecyclerView
项目布局或配置文件,以监测其影响。
根据实际情况使用
我们说过,一个Theme
与一个Context
相关联&mdash;这意味着如果你’在代码中使用Context
来检索资源,那么要注意你使用了正确的Context
。例如,在你的代码中的某个地方,你可能会检索到一个Drawable
。
someView.background = AppCompatResources.getDrawable(requireContext(), R.drawable.foo)
如果drawable引用了一个主题属性(所有drawable都可以从API 21+做起,并且VectorDrawable
s 可以从API 14+通过Jetpack做),那么你应该确保你使用正确的Context
来加载Drawable
。如果你不这样做,你可能会在试图将一个主题应用到一个子层级上时感到沮丧,并想知道为什么你的Drawable
不尊重它。例如,如果你使用Fragment
或Activity
’的Context
来加载Drawable
,这不会尊重树中较低位置的主题。相反,使用Context
最接近的地方来使用资源。
someView.background = AppCompatResources.getDrawable(someView.context, R.drawable.foo)
错误的应用
我们’已经谈到了存在于树中的主题和背景。Activity
> ViewGroup
> View
等。我们可能很想把这个心理模型扩展到包括Application
类,毕竟你可以在清单的<application>
标签上指定一个主题。Don’不要被这个所迷惑!!!
Application
Context
不会保留任何主题信息,你可以在清单中设置的主题只是作为任何没有明确设置主题的Activity
的退路。因此,你不应该使用Application
Context
来加载可能因主题而不同的资源(如可画性或颜色)或解决主题属性。
Never use theApplication
Context
to load themable resources
这也是为什么我们为Activity
指定了一个“完整”的主题,并将这些主题结构化,以便从任何应用范围的主题中延伸出来&mdash;<activity>
’的主题并没有叠加在<application>
’的主题之上。
建立起来的
希望这篇文章已经解释了主题是如何在树中覆盖祖先的,以及这种行为在为我们的应用程序设计风格时如何有用。使用android:theme
标签来为你的布局的各个部分设定主题,并使用主题叠加来只调整你需要的属性。要注意使用正确的主题和上下文来加载资源,并警惕应用程序的上下文!
抱歉,我没有任何公开的例子可以指出(只看到过内部的例子)。请分享您的基准发现,我很乐意指出它们。
Nick,感谢你的精彩文章!它们让我对主题有了更清晰的认识。它们使我对主题有了更多的了解)
然而,有一件事有点令人困惑。应用程序中有些组件的生命周期比 Fragment 实例更长,比如 ViewModels。我有时会在ViewModels中使用资源,由于生命周期的关系,它总是App级的资源。现在看来这是个不好的做法,因为我们应该在Fragment的ViewModel中拥有Fragment级别的资源,以便正确地主题化它们,而我们不能。当然,保持ViewModel不受Android SDK类的影响是比较干净的,但有时似乎是一种矫枉过正。在这个问题上你有什么建议?谢谢!
inherit a default theme.
给我们一个内部资源的链接是不太公平的:)
噢,对不起!修复了该链接。
嗨,我有一个关于主题背景的问题。我在安卓系统中运行lint工具。我不止一次收到过绘区域的lint警告。我想修复它们。他们说背景是由布局绘制的,会过度绘制主题背景。但警告中提到的主题不是来自主项目,而是定义的依赖模块,这些模块没有用于任何活动。如果我把提到的主题的windowbackground改为null,那么警告就消失了。我的问题是,为什么它显示其他主题,而这些主题在整个应用程序中没有使用。我怎样才能为应用程序中的所有布局解决这个问题,这些布局有不同的背景。
很好的文章,尼克!你提到要警惕自由创建ContextThemeWrappers的成本。你提到要对自由创建ContextThemeWrappers的成本感到警惕。我自己也会去测试,但你有没有这方面的资料?我很想看看一些具体的例子,看看它对性能的影响有多大(以及多快)。谢谢!