安卓系统的造型:主题与风格
Android风格系统为指定你的应用程序’视觉设计 "提供了一种强大的方式,但它可能很容易被滥用。正确使用它可以使主题和样式更容易维护,使品牌更新不那么可怕,并使其直接支持黑暗模式。这是一系列文章中的第一篇,Chris Banes和我将着手揭开Android风格设计的神秘面纱,这样你就可以制作出时尚的应用程序,而不会把你的头发拉断。
在这第一篇文章中,我将看一下造型系统的构建块:主题和样式。
主题 != 风格
主题和样式都使用相同的<style>
语法,但作用非常不同。你可以把两者看作是键值存储,其中键是属性,值是资源。让我们分别看看。
什么是风格?
一个样式是一个视图属性值的集合。你可以把一个样式想象成一个Map<view attribute, resource>
。也就是说,键是所有的视图属性,即一个小组件声明的属性和你可能在布局文件中设置的属性。样式是特定于单一类型的小组件的,因为不同的小组件支持不同的属性集。
Styles are a collection of view attributes; specific to a single type of widget
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="Widget.Plaid.Button.InlineAction" parent="…">
<item name="android:gravity">center_horizontal</item>
<item name="android:textAppearance">@style/TextAppearance.CommentAuthor</item>
<item name="android:drawablePadding">@dimen/spacing_micro</item>
</style>
正如你所看到的,样式中的每个键都是你可以在一个布局中设置的东西。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<Button …
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.CommentAuthor"
android:drawablePadding="@dimen/spacing_micro"/>
将它们提取到一个样式中,使其易于在多个视图中重复使用和维护。
使用方法
样式是由布局中的单个视图使用的。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<Button …
style="@style/Widget.Plaid.Button.InlineAction"/>
视图只能应用一种样式--这与其他样式系统(如网络上的css)形成对比,后者的组件可以设置多个css类。
适用范围
一个应用于视图的样式只适用于那个视图,而不是它的任何子视图。例如,如果你有一个带有三个按钮的ViewGroup
,在ViewGroup
上设置InlineAction
样式不会将该样式应用到按钮上。样式提供的值与那些直接在布局中设置的值相结合(使用样式优先顺序解决)。
什么是一个主题?
主题是一个命名资源的集合,以后可以通过样式、布局等进行引用。它们为Android资源提供语义名称,这样你就可以在以后引用它们,例如,colorPrimary
是给定颜色的一个语义名称。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="Theme.Plaid" parent="…">
<item name="colorPrimary">@color/teal_500</item>
<item name="colorSecondary">@color/pink_200</item>
<item name="android:windowBackground">@color/white</item>
</style>
这些命名的资源被称为主题属性,所以一个主题就是Map<theme attribute, resource>
。主题属性与视图属性不同,因为它们不是特定于单个视图类型的属性,而是明显命名的指向更广泛适用于应用程序的值的指针。一个主题为这些命名的资源提供了具体的值。在上面的例子中,colorPrimary
属性指定了这个主题的主要颜色是茶色。通过用一个主题来抽象资源,我们可以在不同的主题中提供不同的具体值(如colorPrimary
=橙色)。
Themes are a collection of named resources, useful broadly across an app
一个主题类似于一个接口。对接口的编程允许你将公共合同与实现解耦,允许你提供不同的实现。主题起到了类似的作用;通过针对主题属性编写我们的布局和样式,我们可以在不同的主题下使用它们,提供不同的具体资源。
大体上等同于伪代码。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
interface ColorPalette {
@ColorInt val colorPrimary
@ColorInt val colorSecondary
}
class MyView(colors: ColorPalette) {
fab.backgroundTint = colors.colorPrimary
}
这使得你可以改变MyView
的呈现方式,而不必创建它的变体。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
val lightPalette = object : ColorPalette { … }
val darkPalette = object : ColorPalette { … }
val view = MyView(if (isDarkTheme) darkPalette else lightPalette)
使用方法
你可以在具有(或属于)Context
的组件上指定一个主题,例如Activity
或Views
/ViewGroup
。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<!-- AndroidManifest.xml -->
<application …
android:theme="@style/Theme.Plaid">
<activity …
android:theme="@style/Theme.Plaid.About"/>
<!-- layout/foo.xml -->
<ConstraintLayout …
android:theme="@style/Theme.Plaid.Foo">
你也可以在代码中设置一个主题,方法是用ContextThemeWrapper
包裹现有的Context
,然后你可以用它来膨胀一个布局等等。
主题的力量真正来自于你如何使用它们;你可以通过引用主题属性来建立更灵活的小工具。不同的主题在以后的时间里提供具体的值。例如,你可能希望在你的视图层次结构的某个部分设置一个背景颜色。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
android:background="?attr/colorSurface">
我们可以通过使用?attr/themeAttributeName
语法来委托给主题,而不是设置一个静态的颜色(#ffffff
或@color
资源)。这种语法意味着:向主题查询这个语义属性的值。这一层次的指示允许我们提供不同的行为(例如,在浅色和深色主题中提供不同的背景颜色),而不必创建多个布局或样式,这些布局或样式除了一些颜色变化之外,大部分是相同的。它隔离了主题内正在变化的元素。
Use the ?attr/themeAttributeName
syntax to query the theme for the value of this semantic attribute
适用范围
一个Theme
是作为一个Context
的属性被访问的,可以从任何是或有Context
的对象中获得,例如Activity
、View
或ViewGroup
。这些对象存在于一棵树中,其中一个Activity
包含ViewGroup
,而ViewGroup
又包含View
等等。在这棵树的任何一级指定一个主题都会级联到下级节点,例如,在ViewGroup
上设置一个主题会适用于其中所有的View
(与只适用于单个视图的样式不同)。
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
android:theme="@style/Theme.App.SomeTheme">
<! - SomeTheme also applies to all child views. -->
</ViewGroup>
这可能是非常有用的,比如说,如果你想在一个浅色的屏幕上有一个黑暗的主题部分。阅读更多关于这种行为的信息这里。
请注意,这种行为只在布局膨胀时适用。虽然Context
提供了一个setTheme
方法,或者Theme
提供了一个applyStyle
方法,但这些需要在之前膨胀时调用。在膨胀后设置一个新的主题或应用一个样式将不会更新现有的视图。
分别关注的问题
了解不同的职责以及样式和主题的相互作用,有助于保持你的造型资源更易于管理。
例如,假设您的应用程序有一个蓝色的主题,但一些专业屏幕会出现花哨的紫色外观和,您希望提供带有调整过的颜色的暗色主题。如果你试图只使用样式来实现这一点,你将不得不为专业/非专业和浅色/深色的排列组合创建4种样式。由于样式是特定于视图类型的(Button
、Switch
等),您需要为您应用程序中的每个视图类型创建这些排列组合。
没有主题的小工具/风格的爆炸性排列组合
如果我们使用样式和主题,我们就可以把通过主题改变的部分隔离为主题属性,所以我们只需要为每个视图类型定义一个样式。在上面的例子中,我们可以定义4个主题,每个主题都为colorPrimary
主题属性提供不同的值,然后这些样式参考并自动反映主题的正确值。
这种方法可能看起来更复杂,因为你需要考虑样式和主题的互动,但它的好处是隔离了每个主题的变化部分。因此,如果你的应用程序从蓝色重新命名为橙色,你只需要在一个地方改变,而不是散布在你的样式中。它还有助于防止风格的扩散。理想情况下,你只需为每个视图类型设置少量的样式。如果你不利用主题化的优势,你的styles.xml
文件很容易失控,并爆发出类似样式的不同变化,这将成为一个令人头痛的维护问题。
在下一篇文章中,我们将探讨一些常见的主题属性,以及如何创建你自己的主题。
遗憾的是,这是整个Android主题化的一个主要缺点(以及以编程方式创建视图和通过膨胀创建视图之间的差异),无论你的样式和主题组织得多么好,这都会使它变得不那么可用。
是的,我在Kitkat中使用了SceneAPI的介绍。我所有的功能都是基于MVVM的。当我的状态发生变化时,我会反应性地进入一个场景。但所有这些都需要一定量的LayoutInflator,现在我被卡住了。
首先,非常感谢你花时间来创建这篇文章,这对你有帮助。
我真正的问题是这个。安卓团队是否能解决绝对可怕的样式、主题和属性的荒地,使基本的样式设计也变得完全神秘?根据我的统计,一个属性,`textAppearanceSubtitle1`被不少于20个不同的材料组件所共享。如果我胆敢改变这个样式,我几乎可以保证会破坏我喜欢的其他二十一个组件的样式。然而,我们被积极鼓励这样做。难道真的是这样吗,每次我想改变一个主题属性时,我将永远被迫手动搜索该属性的每一个引用?这真是让人抓狂。
为什么没有关于每个UI组件的造型的明确文档呢?
例如,我没有看到任何关于如何对日期/时间选择器对话框进行样式设计的提法。
https://developer.android.com/guide/topics/ui/controls/pickers
很好的文章
我正在学习udacity课程,来到这里。在我看来,主题属性访问器"?attr/attrName "可以缩短为"?attrName",省略关键字attr。是这样吗?
感谢您提供的精彩的Android造型文章Nick Butcher。不过我有一个问题。
根据您之前文章中的样式优先顺序(https://medium.com/androiddevelopers/whats-your-text-s-appearance-f3a1729192d),xml中定义在视图上的所有内容都应该“覆盖所有内容”。
根据我的经验,与此相关的注意事项相当多,而且一点也不明显。例如,为元素设置海拔高度,几乎每次都是令人不快的经历。
以按钮为例,有默认的stateListAnimator将你的海拔值重写成蓝色。这里有一个关于这个话题的堆栈溢出的讨论 https://stackoverflow.com/a/27112143/1900854。
我在设置AppBarLayout海拔方面有类似的经验,可能还有更多。你能不能推荐一些与设置标高有关的东西,或者指出一些文章/资源(如果有)?
谢谢您!
我将在不同的资源文件中使用不同的颜色值,例如values和values-dark分别用于light和dark主题。我是否遗漏了一些理解,它们只提供语义价值。
Nick Butcher 顺便说一下,非常好的文章。如果能有更多的背景资料或样本或细节参考,那就更好了。因为我想开发一个样本应用程序,供人们参考,并比较他们在正确做事的情况下能节省多少时间。
材料样本遵循这些最佳做法:https://github.com/material-components/material-components-android-examples
你希望有什么额外的背景?
我的刷新/理解安卓风格/主题的快速骗局清单
p.s.可以在文章中免费使用。