安卓系统的造型:主题与风格

评论 9 浏览 0 2020-02-05

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的组件上指定一个主题,例如ActivityViews/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的对象中获得,例如ActivityViewViewGroup。这些对象存在于一棵树中,其中一个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种样式。由于样式是特定于视图类型的(ButtonSwitch等),您需要为您应用程序中的每个视图类型创建这些排列组合。

没有主题的小工具/风格的爆炸性排列组合

如果我们使用样式主题,我们就可以把通过主题改变的部分隔离为主题属性,所以我们只需要为每个视图类型定义一个样式。在上面的例子中,我们可以定义4个主题,每个主题都为colorPrimary主题属性提供不同的值,然后这些样式参考并自动反映主题的正确值。

这种方法可能看起来更复杂,因为你需要考虑样式和主题的互动,但它的好处是隔离了每个主题的变化部分。因此,如果你的应用程序从蓝色重新命名为橙色,你只需要在一个地方改变,而不是散布在你的样式中。它还有助于防止风格的扩散。理想情况下,你只需为每个视图类型设置少量的样式。如果你不利用主题化的优势,你的styles.xml文件很容易失控,并爆发出类似样式的不同变化,这将成为一个令人头痛的维护问题。

在下一篇文章中,我们将探讨一些常见的主题属性,以及如何创建你自己的主题。

Android造型。常见的主题属性 在这一系列关于Android风格的文章中,我们看了主题和风格的区别,以及…

最后更新2022-11-02
9 个评论
#1 Oleksandr Kucherenko 2020-02-21

我的刷新/理解安卓风格/主题的快速骗局清单

p.s.可以在文章中免费使用。

#2 Vasya Drobushkov 2020-02-05

遗憾的是,这是整个Android主题化的一个主要缺点(以及以编程方式创建视图和通过膨胀创建视图之间的差异),无论你的样式和主题组织得多么好,这都会使它变得不那么可用。

Owais Idris 2021-03-01

是的,我在Kitkat中使用了SceneAPI的介绍。我所有的功能都是基于MVVM的。当我的状态发生变化时,我会反应性地进入一个场景。但所有这些都需要一定量的LayoutInflator,现在我被卡住了。

#3 Robert Mirabelle 2021-01-27

首先,非常感谢你花时间来创建这篇文章,这对你有帮助。

我真正的问题是这个。安卓团队是否能解决绝对可怕的样式、主题和属性的荒地,使基本的样式设计也变得完全神秘?根据我的统计,一个属性,`textAppearanceSubtitle1`被不少于20个不同的材料组件所共享。如果我胆敢改变这个样式,我几乎可以保证会破坏我喜欢的其他二十一个组件的样式。然而,我们被积极鼓励这样做。难道真的是这样吗,每次我想改变一个主题属性时,我将永远被迫手动搜索该属性的每一个引用?这真是让人抓狂。

#4 AndroidDeveloperLB 2021-08-09

为什么没有关于每个UI组件的造型的明确文档呢?

例如,我没有看到任何关于如何对日期/时间选择器对话框进行样式设计的提法。

https://developer.android.com/guide/topics/ui/controls/pickers

#5 Sogbey, Daniel 2021-08-07

很好的文章

#6 Xurf Org 2020-07-12

我正在学习udacity课程,来到这里。在我看来,主题属性访问器"?attr/attrName "可以缩短为"?attrName",省略关键字attr。是这样吗?

#7 Miroslav Kacera 2020-04-29

感谢您提供的精彩的Android造型文章Nick Butcher。不过我有一个问题。

根据您之前文章中的样式优先顺序(https://medium.com/androiddevelopers/whats-your-text-s-appearance-f3a1729192d),xml中定义在视图上的所有内容都应该“覆盖所有内容”。

根据我的经验,与此相关的注意事项相当多,而且一点也不明显。例如,为元素设置海拔高度,几乎每次都是令人不快的经历。

以按钮为例,有默认的stateListAnimator将你的海拔值重写成蓝色。这里有一个关于这个话题的堆栈溢出的讨论 https://stackoverflow.com/a/27112143/1900854

我在设置AppBarLayout海拔方面有类似的经验,可能还有更多。你能不能推荐一些与设置标高有关的东西,或者指出一些文章/资源(如果有)?

谢谢您!

#8 anup kunwar 2020-04-04

我将在不同的资源文件中使用不同的颜色值,例如values和values-dark分别用于light和dark主题。我是否遗漏了一些理解,它们只提供语义价值。

#9 Sangam Pandey 2020-02-07

Nick Butcher 顺便说一下,非常好的文章。如果能有更多的背景资料或样本或细节参考,那就更好了。因为我想开发一个样本应用程序,供人们参考,并比较他们在正确做事的情况下能节省多少时间。

Nick Butcher 2020-02-07

材料样本遵循这些最佳做法:https://github.com/material-components/material-components-android-examples

你希望有什么额外的背景?

标签