安卓风格:主题叠加

评论 4 浏览 0 2020-02-27

在本系列关于 Android 造型的前几篇文章中,我们’已经了解了样式和主题之间的区别,谈到了使用主题和主题属性的好处,并强调了一些要使用的常用属性

今天我们将重点讨论实际使用主题,它们如何应用于您的应用,以及对您如何构建它们的影响。

适用范围

在以前的文章中,我们说过

A Theme is accessed as a property of a Context and can be obtained from any object which is or has a context e.g. Activity, View or ViewGroup. These objects exist in a tree, where an Activity contains ViewGroups which contain Views etc. Specifying a theme at any level of this tree cascades to descendent nodes e.g. setting a theme on a ViewGroup applies to all the Views 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),你可以用它们来将你的应用程序的某个部分的颜色从浅色翻到深色。

根据定义,主题覆盖并不指定一些东西,不应该孤立地使用,例如作为你活动的主题。事实上,你可以考虑在你的应用程序中使用两种 "类型 "的主题。

  1. “完整” 主题。这些主题指定了你对一个屏幕所需要的一切。它们继承自另一个“full”主题,如Theme.MaterialComponents,并应被用于为Activity提供主题。
  2. 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,从而分配新的ThemeResources实例。它还引入了更多需要解决的样式指示层次。要警惕随意使用主题,特别是在重复的情况下,如RecyclerView项目布局或配置文件,以监测其影响。

根据实际情况使用

我们说过,一个Theme与一个Context相关联&mdash;这意味着如果你’在代码中使用Context来检索资源,那么要注意你使用了正确的Context。例如,在你的代码中的某个地方,你可能会检索到一个Drawable

someView.background = AppCompatResources.getDrawable(requireContext(), R.drawable.foo)

如果drawable引用了一个主题属性(所有drawable都可以从API 21+做起,并且VectorDrawables 可以从API 14+通过Jetpack做),那么你应该确保你使用正确的Context来加载Drawable。如果你不这样做,你可能会在试图将一个主题应用到一个子层级上时感到沮丧,并想知道为什么你的Drawable不尊重它。例如,如果你使用FragmentActivity’的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 the Application Context to load themable resources

这也是为什么我们为Activity指定了一个“完整”的主题,并将这些主题结构化,以便从任何应用范围的主题中延伸出来&mdash;<activity>’的主题并没有叠加在<application>’的主题之上。

建立起来的

希望这篇文章已经解释了主题是如何在树中覆盖祖先的,以及这种行为在为我们的应用程序设计风格时如何有用。使用android:theme标签来为你的布局的各个部分设定主题,并使用主题叠加来只调整你需要的属性。要注意使用正确的主题和上下文来加载资源,并警惕应用程序的上下文!

最后更新2022-11-02
4 个评论
#1 Jeremiah Zucker 2020-05-14

很好的文章,尼克!你提到要警惕自由创建ContextThemeWrappers的成本。你提到要对自由创建ContextThemeWrappers的成本感到警惕。我自己也会去测试,但你有没有这方面的资料?我很想看看一些具体的例子,看看它对性能的影响有多大(以及多快)。谢谢!

Nick Butcher 2020-05-19

抱歉,我没有任何公开的例子可以指出(只看到过内部的例子)。请分享您的基准发现,我很乐意指出它们。

#2 Alexey Ershov 2020-02-27

Nick,感谢你的精彩文章!它们让我对主题有了更清晰的认识。它们使我对主题有了更多的了解)

然而,有一件事有点令人困惑。应用程序中有些组件的生命周期比 Fragment 实例更长,比如 ViewModels。我有时会在ViewModels中使用资源,由于生命周期的关系,它总是App级的资源。现在看来这是个不好的做法,因为我们应该在Fragment的ViewModel中拥有Fragment级别的资源,以便正确地主题化它们,而我们不能。当然,保持ViewModel不受Android SDK类的影响是比较干净的,但有时似乎是一种矫枉过正。在这个问题上你有什么建议?谢谢!

#3 Denis Stanishevskiy 2020-03-04
inherit a default theme.

给我们一个内部资源的链接是不太公平的:)

Nick Butcher 2020-03-06

噢,对不起!修复了该链接。

#4 Rkreddy 2020-02-27

嗨,我有一个关于主题背景的问题。我在安卓系统中运行lint工具。我不止一次收到过绘区域的lint警告。我想修复它们。他们说背景是由布局绘制的,会过度绘制主题背景。但警告中提到的主题不是来自主项目,而是定义的依赖模块,这些模块没有用于任何活动。如果我把提到的主题的windowbackground改为null,那么警告就消失了。我的问题是,为什么它显示其他主题,而这些主题在整个应用程序中没有使用。我怎样才能为应用程序中的所有布局解决这个问题,这些布局有不同的背景。

标签