如何正确的使用 Android 中的 themes 和 styles

本文深入探讨Android中styles和themes的使用方法,解析其内部机制,包括继承、文本样式定义及自定义方法。

如何正确的使用 Android 中的 themes 和 styles

译自:How to avoid headaches with Android themes and styles

Android 确实是一个非常棒的开发平台。它拥有非常棒的框架(Activities, Intents…),简洁的概念(Java 和 XML 的关联,可缩放的9切图…),清晰的文档以及可以使设计自己的 app 成为一种乐趣的辅助工具。它非常容易学习,当你写出第一个 HelloWorld 程序之后不久你就会觉得自己拥有设置非常酷的 app 的能力。这时候你已经体会到酷的应用设计会让酷的功能更加吸引人,从人吸引大量酷的用户。

但是,如果你深入到设计的过程中,你会涉及到那些,正如 Android 文档中自己说的“文档编写不是很好”的部分:styles 和 themes。确实,

“然而,R.style 部分的文档并没有被很好的编档,它并没有完整的描述 styles,所以查看 styles 和 themes 部分的源码会让你更好的了解那些 style 属性的意义。”

(http://developer.android.com/guide/topics/ui/themes.html#Platformstyles)

在研究 styles 和 themes 的概念的时候,我们会发现一些像是 theme 属性/attribute(R.attr),styleable 特性/property(R.styleable),text appearance/文本样式,theme inheritance/继承 这些概念。如果你没有将所有这些的细节全部记在脑子里的话,它们会让你非常头疼。我最新在试图以 Holo 为模板设计自定义的 AlertDialogs,而最终这些概念就出现在了我的面前,让我意识到这不是一件容易的事情。我会尝试从头开始捋清这些东西。

这篇文章呈现的所有信息都是对以下内容的总结:

Android “styles and themes” API guide http://developer.android.com/guide/topics/ui/themes.html

Android R.style reference http://developer.android.com/reference/android/R.style.html

Android R.attr reference http://developer.android.com/reference/android/R.attr.html

Android R.styleable reference http://developer.android.com/reference/android/R.styleable.html

Android source for default themes.xml https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/res/res/values/themes.xml

Android source for default styles.xml https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/res/res/values/styles.xml

这篇文章的目标读者包括 Android 初级和高级开发者。你已经搞定 Android XML 文件(resources,layouts,values…),并试图理解什么是 R.attr 和 textAppearance 了吗?或者是你正需要进行设计并想知道最简单的自定义你的 App 的方法,使用自己的 theme 而不是标准的。那么,这篇文章非常适合你。

Android 中的 themes 和 styles 是什么?

概述

让我们从最基础的 theme 和 style 的比较开始。

比较项themesstyles
定义在一个资源文件内部,通常是 themes.xml。
使用<style>标签,以 theme. 为命名前缀。
在一个资源文件内部,通常是 styles.xml。
使用 <style> 标签。
使用在 Android Manifest 中,
使用 <application android:theme="@style/Theme.LE_Theme"> 来定义整个 Application 的 theme
或者使用 <activity android:theme="@style/Theme.LE_Theme"> 为特定的 activity 指定 theme。
在任何布局文件中
<AnyWidget style="@style/LE_style">
AnyWidget 可以是 Button,TextView,LinearLayout,ScrollView等等。
Android 中的默认值
(API level 19)
General themes
theme
theme.Black
theme.Light
theme.DeviceDefault
theme.DeviceDefault.Light
Theme.Holo
Theme.Holo.Light
Sub-themes
theme.NoTitleBar

theme.Black.NoTitleBar

theme.Light.NoTitleBar

theme.DeviceDefault.NoActionBar

theme.DeviceDefault.Light.NoActionBar

Theme.Holo.NoActionBar

Theme.Holo.Light.NoActionBar
theme-related widget styles
Widget.Button
Widget.Button.Small

Widget.DeviceDefault.Button
Widget.DeviceDefault.Button.Small

Widget.DeviceDefault.Light.Button
Widget.DeviceDefault.Light.Button.Small

Widget.Holo.Button
Widget.Holo.Button.Small

Widget.Holo.Light.Button
Widget.Holo.Light.Button.Small

theme-related property styles
TextAppearance.Small
TextAppearance.Small.Inverse

TextAppearance.DeviceDefault.Small
TextAppearance.DeviceDefault.Small.Inverse

TextAppearance.Holo.Small
TextAppearance.Holo.Small.Inverse

Standalone styles
MediaButton
MediaButton.Play
MediaButton.Next

我们可以从表中看出:

  • 一个 theme 实际上就是一个 style,唯一的不同是他们在哪里被使用:你在 Android Manifest 中为 app 或者 activity 设置 theme,你在布局文件中为 widget 设置 style。
  • style 比 theme 更多(styles.xml 文件比 themes.xml 文件大)。这是因为一个 theme 的定义本质上就是一些对这个 theme 会使用的 style 的引用。这意味着 Theme.Holo 引用并使用:
    • Widget.Holo.Button
    • Widget.Holo.Button.Small
    • TextAppearance.Holo.Small
    • TextAppearance.Holo.Small.Inverse
  • theme 可以被分为 general themesub-theme。例如,当你为自己的应用设计一个 theme 的时候,你会去创建一个 Theme.LE_Theme。但是可以想象的到,你的 app 有可能在某些地方会用到没有 ActionBar
    的特殊 activity。所以你应该为这个 activity 再创建一个 sub-theme Theme.LE_Theme.NoActionBar。那么你的 Android Manifest 文件会像是这样:
<application android:theme="@style/Theme.LE_Theme">
    <activity android:name="MainActivity" />
    <activity android:name="SpecialActivity" android:theme="@style/Theme.LE_Theme.NoActionBar" />
</application>
  • 你的所有 activity ,包括 MainActivity,都会使用那个在 application 下面定义的 theme,除了那些重写了应用 theme 的 activity,像 SpecialActivity。
  • style 可以被分割为3类:
    • theme-related widget style:这些 style 的名字已经明确的表明了这个 widget 被定义为什么样子。当你读到 Widget.Holo.Light.Button.Small 这个 style 的定义时,你就已经知道它定义了 Holo theme 中 light 版本的小 button。
    • theme-related property style:这些定义了那些不只是绑定到一个 widget 的特性。例如:TextAppearance.Small 定义了 小文本 的大小和颜色。但是这个显示效果并没有指定给任何一个 widget。我们一会儿会看到 widget 是如何引用它需要的文本显示效果的。
    • standalone styles:这些是不常见的,这些 style 会一直有效,不论实用的 theme 是什么。在上面的表格中,MediaButton,你可以想象一个 MediaButton 在所有的 theme 中看起来总是一样的。

继承

style/theme 的其中一个有趣的特性是,你可以继承一个已经存在的 style/theme,但是方法是很微妙的。

方案1:使用显式的 parrent 属性

这是最常见的方法,而你可能已经学过了。如果你想要定义一个名叫 Child 的 style 继承一个名叫 Parent 的 style 的所有特性(property),你只需要这样写:

<style name="Child" parent="@style/Parent">

然后你可以在 Child 中定义一些特性(property)来重写 Parent 的特性,并且/或者添加一些新的特性。当然,你也可以继承 Android 的默认 style/theme。例如,你可以像这样定义自己应用的 theme:

<style name="Theme.LE_Theme" parent="@android:style/Theme.Holo">
方案2:使用隐式的 style 名字

有另一个方式来继承 style/theme,那就是隐式继承。与设置一个 parent 属性不同,隐式继承做的只是在命名的时候将 style/theme 想要继承的父 style/theme的名字加一个“.”作为前缀。例如:

<style name="Parent.Child">

但是使用这种方法需要注意:

  • 父 style/theme 需要存在:如果Theme.LE_Theme不存在的话,Theme.LE_Theme.NoActionBar 会报错。
  • 父 style/theme 不可以是 Android 的默认 style/theme。这意味着你不能创建 <style name="Theme.Holo.LE_Theme> 因为它的隐式父 theme 是 Theme.Holo,而 Theme.Holo 并不是你的 theme。但是你可以创建 <style name="Theme.Holo.LE_Theme parent="@android:style/Theme.Holo"> 因为它是显式继承。
  • 当你在 java 中引用一个 style/theme 的时候,所有的点“.”都要用下划线“_”代替。所以上面的 theme 在 java 中引用的写法就应该是 R.style.Theme_Holo_LE_Theme。

theme 中都指定了些什么?

现在我们知道了 theme 和 style 之间的区别,我们知道要在哪些地方使用它们,我们知道如何正常的或者通过继承定义和命名一个 theme。而且我们知道每一个 theme/style 都包含一些特性(property)或者属性(attribute)。但是这些属性都是什么呢?

从源码中学习

首先,让我们聚焦在 theme 上。
一个 theme 可以定义的所有属性的完整列表在这里。一共有超过218个属性。Android 源码允许我们允许我们查看 Android 默认 theme 的定义,所以我们把 HOLO 作为例子,它被定义在 themes.xml中(2014年6月的版本)。

<style name="Theme.Holo">
    <!-- Text styles -->
    <item name="textAppearance">@android:style/TextAppearance.Holo</item>
    <item name="textAppearanceInverse">@android:style/TextAppearance.Holo.Inverse</item>
    <item name="textAppearanceLarge">@android:style/TextAppearance.Holo.Large</item>
    <item name="textAppearanceLargeInverse">@android:style/TextAppearance.Holo.Large.Inverse</item>

    <item name="textColorPrimary">@android:color/primary_text_holo_dark</item>
    <item name="textColorSecondary">@android:color/secondary_text_holo_dark</item>
    <item name="textColorPrimaryInverse">@android:color/primary_text_holo_light</item>
    <item name="textColorSecondaryInverse">@android:color/secondary_text_holo_light</item>

    <item name="editTextColor">?android:attr/textColorPrimary</item>
    <item name="editTextBackground">@android:drawable/edit_text_holo_dark</item>

    <!-- Window attributes -->
    <item name="windowFullscreen">false</item>
    <item name="windowTitleStyle">@android:style/WindowTitle.Holo</item>
    <item name="windowTitleSize">25dip</item>

    <!-- Widget styles -->
    <item name="listViewStyle">@android:style/Widget.Holo.ListView</item>
    <item name="scrollViewStyle">@android:style/Widget.Holo.ScrollView</item>
    <item name="textViewStyle">@android:style/Widget.Holo.TextView</item>
</style>

(这个文件来自于 Android 源码,因此它是内部的(internal),它的属性不使用 android: 前缀。但是当你定义自己的 theme 的时候必须使用。)

这只是整个 Holo theme 定义的一小段片段,但是所有重要的东西都在这里了:

  • 让我们从最后开始。第22行到第24行是对 widget style 的引用。它很容易理解:那是你用来定义你的 theme 中所有的 ListView 将会使用某个 style ,所有的 TextView 将会使用另一个 style 等等。所有常见的控件(widget)都有类似的属性。
  • 第17行到第19行是窗口(window)自身相关的属性,典型的情况是,当你开始设计一个特别的 activity、dialog、透明窗口等的时候,你就会碰到它们。
  • 第3行到第14行定义了不同类型的文本的显示样式(appearance):尺寸(小/中/大),重要程度(primary/secondary/tertiary)以及颜色版本(正常/反转),你的 theme 中所有可能的文本显示样式都在这里被定义。
  • 这里最有趣的是这些属性的值。当然,你可以硬编码那些 dimension 值、boolean 值等等,或者你也可以选择引用 drawable 或者是 color。但是,通常你会想要在你的 theme 中引用 style。两种方式可以实现:
    • 直接引用一个 style
      xml
      <item name="scrollViewStyle">@android:style/Widget.Holo.ScrollView</item>

      这非常的简单:你只是简单的说明了你的所有ScrollView都使用 Widget.Holo.ScrollView 这个 style。
    • 使用相同的 style 作为另一个属性
      xml
      <item name="textColorPrimary">@android:color/primary_text_holo_dark</item>
      <item name="editTextColor">?android:attr/textColorPrimary</item>

      textColorPrimary 是直接引用,但是 editTextColor 只是简单的说明“使用这个 theme 中 textColorPrimary 属性所定义的 style”。

theme 属性

这个部分非常重要,或者说是非常核心。那些通过 ?android:attr/ 来定义的属性都是 theme 属性。它们各自指向当前 theme 中定义的一个属性。android: 前缀可能会产生误导,但是它仅仅代表着那个 theme 属性的名字是定义在 Android 中的,但是它在当前 theme 中锁定义的值将会被获取到

假设你有一个自定义的布局文件,你在其中这样定义了一个TextView:

<TextView android:textAppearance="?android:attr/textAppearanceLarge" />

如果你在一个使用 theme A 的 activity 中显示这个布局文件,那么定义在

<style name="A"><item name="android:textAppearance">@style/LE_STYLE_A</item></style>

中的 LE_STYLE_A style 将会被使用。

如果你在一个使用 theme B 的 activity 中显示同样的布局文件,那么定义在

<style name="B"><item name="android:textAppearance">@style/LE_STYLE_B</item></style>

中的 LE_STYLE_B style 将会被使用。

一个 style 中都指定了一些什么?或者说 theme 属性的力量

我们知道一个 theme 如何通过引用 style 来指定自己的控件(widget)/文本(text)等等的显示效果。现在让让我们看看另一边。这些 style 是怎么定义的?这里有一个 Holo style 的定义,来自于styles.xml(2014年6月的版本)。

<style name="TextAppearance.Holo" parent="TextAppearance">
</style>

<style name="TextAppearance">
    <item name="android:textColor">?textColorPrimary</item>
    <item name="android:textColorHighlight">?textColorHighlight</item>
    <item name="android:textColorHint">?textColorHint</item>
    <item name="android:textColorLink">?textColorLink</item>
    <item name="android:textSize">16sp</item>
    <item name="android:textStyle">normal</item>
</style>

<style name="Widget.Holo.TextView" parent="Widget.TextView">
</style>

<style name="Widget.TextView">
    <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
    <item name="android:textSelectHandleLeft">?android:attr/textSelectHandleLeft</item>
    <item name="android:textSelectHandleRight">?android:attr/textSelectHandleRight</item>
    <item name="android:textSelectHandle">?android:attr/textSelectHandle</item>
    <item name="android:textEditPasteWindowLayout">?android:attr/textEditPasteWindowLayout</item>
    <item name="android:textEditNoPasteWindowLayout">?android:attr/textEditNoPasteWindowLayout</item>
    <item name="android:textEditSidePasteWindowLayout">?android:attr/textEditSidePasteWindowLayout</item>
    <item name="android:textEditSideNoPasteWindowLayout">?android:attr/textEditSideNoPasteWindowLayout</item>
    <item name="android:textEditSuggestionItemLayout">?android:attr/textEditSuggestionItemLayout</item>
    <item name="android:textCursorDrawable">?android:attr/textCursorDrawable</item>
</style>

正如你所看到的,这些 Holo style 只是简单的使用它们父 style,并没有重写任何属性。而默认的 style 只是定义了对 theme 属性的引用(textColorPrimary、textAppearanceSmall…)。而这正是展现 theme 属性的力量的地方!你可以看到默认的TextView控件没有硬编码它的颜色,文字大小和其他属性。它完全依赖 theme 属性,这意味着每个 TextView 都会被展示它的 theme 所定制!

现在,让我们来实践一下。假设你想要创建一个自定义的 theme,在这个 theme 中你只改变一件事:把小文本的大小设为25sp 而不是默认的14sp(这是为你眼神不好的奶奶设计的),其他的全部基于 DeviceDefault theme。你要做的所有事情就是:

在 granny_themes.xml 中:

<style name="theme.Granny" parent="@android:style/theme.DeviceDefault">
    <item name="android:textAppearanceSmall">@style/TextAppearance.Granny.Small</item>
</style>

在 granny_styles.xml 中:

<style name="TextAppearance.Granny.Small" parent="@android:style/TextAppearance.DeviceDefault.Small">
    <item name="android:textSize">25sp</item>
</style>

在你的 Manifest 中设置上 theme.Granny,然后就完成了!所有使用“小文本”显示方式的控件(包括默认的 TextView 都会显示 25sp 。不需要对这些控件的 style 做任何改变。你也不需要在在布局文件中硬编码一个 text size。

同样,你也可以一同样的方式改变你的 ActionBar。是需要在你的 theme 中通过 android:actionBarstyle 引用你自定义的 style 即可。

总结

根据我的经验,需要大量的事件才能够理解什么是 文本样式(text appearance)、什么是 theme 属性、怎样继承 style以及更多的时间才能学会如何恰当的使用那些概念。然而,它的益处也是非常大的:你不必再花费时间去创建孤立的 style。相反,你可以一次性的深入定制你的 app。

作为最后的话,让我分享一些小技巧:

  • 永远使你的自定义 style/theme 继承自 DeviceDefault theme 而不是 Holo。从 API level 14 之后,DeviceDefault 作为默认的 theme,并且使用 DeviceDefault 可以保证在 HTC、Samsung、Sony、Nexus 等设备中都有最好的显示效果。
  • 使用 Android 命名惯例 Theme.LE_Theme.Light、Widget.LE_THEME.TextView 等等。这会让你的代码更加易读和易维护。
  • Eclipse 文本补全对于 theme 和 style 属性来说非常糟糕。想要知道你可以给 theme、TextView style 或者 TextAppearance style 设置什么属性,你需要查看 R.styleable 的文档或者直接查看你想要在 XML 文件中自定义的控件的文档(例如 ScrollView 的文档)。

以上就是全部内容。现在开始把玩尺寸(size)、颜色(color)以及其他的 style 来控制你的 app 的样子和感觉吧!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值