属性,样式,主题以及实践(attr, style, theme)(未完)

0. 前言

最近写Android app时要美化外观了,但是发现自己对attr,style,theme这几个概念理解的比较模糊,不知道哪些应该定义在styles.xml中,哪些应该定义在theme中,从而不知道好的实践是什么,因此也写不出清晰,分离的代码。

Google了一些资源,现总结如下。

1. 属性,样式,主题(attr, style, theme)

attr

每种View都有属性,不管是自定义view,还是内置的view, 都要定义一些属性,对于自定义的view,官方文档中说明定义属性的方法。
在项目的attrs.xml文件中,添加

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>

其中一个良好的规范是declare-styleable的name一般定义为View的名字,但既然是规范而不是必须就说明也可以不这样,至于为什么,下文会讲到。

对于内置的view,其实和自定义view也一样,只不过Google已经帮你定义好了。那么其属性的定义必定也是类似于以上这种形式。

那么相关的代码在哪里呢?

就在SDK下的attrs.xml中,在这个文件中搜索view的名称就能找到可用的属性了(当然也可在官方文档中找到其支持的属性)

还有一个关键的地方,以上的讲解给人一种属性必定属于某一view的印象,其实属性是可以不属于任何view的。我们可以这样定义。

<resources>
    <attr name="customattr" format="boolean"/>
</resources>

和前面对比,是不是觉得在<declare-styleable>下定义就属于某一view,在<resources>下定义就不属于某一view?

其实不是这样的,属性是有一个全局空间的,无论是在<declare-styleable>下,还是在<resources>下定义的都属于这个全局空间,比如说,对于以上两种情况定义的属性,我们可以这样R.attrs.customattrR.attrs.showText来引用,可见与其是否在<declare-styleable>没有关系。

既然属性都在一个全局空间中,那么就不允许同名不同类型的属性定义,如以下是错误的。

<resources>
    <attr name="customattr" format="boolean"/>
    <attr name="customattr" format="string"/>
</resources>

好,到这里,是不是有了很多疑问。如

  1. 什么情况下属性要定义在<declare-styleable>下,什么情况下不需要
  2. <declare-styleable>及其name值是做什么用的?为什么在其下定义属性就和view关联上了?

先说第二个问题。

要想理解第二个问题,关键的一个地方是理解obtainStyledAttributes函数。而该函数一般是在view的构造函数中调用的。继续来看官方文档中的例子。

public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0);

   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}

注意obtainStyledAttributes的参数R.styleable.PieChart,R说明是一个资源,styleable是资源类型,PieChart是资源的名称,
styleable是由<declare-styleable>生成的,而PieChart就是对应<declare-styleable>的name属性。所以说<declare-styleable>的name可以是任意值,只要在obtainStyledAttributes传入相应的R.styleable.xxx就行了,但是无意义的值将造成代码的难读。

同时,从以上代码也可知道<declare-styleable>起的作用主要是为了方便将一组属性组织到一起,方便在view的构造函数中获得其属性值。

至于第1个问题,在主题一节会讲到。

style

前面已经说到如何定义属性,但是属性要有值啊,属性的值在哪里指定的呢?

最简单的方法在Layout文件中定义,像这样:

<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textColor="#00FF00"
    android:typeface="monospace"
    android:text="@string/hello" />

但是这样不太好,我们知道CSS就是将外观样式从HTML中分离出来,同样的思想用到这里,可以这样。

<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

<resources>
    <style name="CodeFont" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">fill_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">#00FF00</item>
        <item name="android:typeface">monospace</item>
    </style>
</resources>

从上可以看出,样式为就是为属性进行赋值。

现在来谈一谈内置view的样式。

我们平时使用一个Button,一个TextView,一个ImageView,很多属性值并没有指定,但依然可以显示出某种样式,既然有样式就一定是为相应属性赋值了,那么在哪里呢?

定义的地方在SDK的styles.xml中,该文件中定义了很多style,其名称表明了style的样式,这也是一个好的命令规范。

其实,影响一个view的style并非只有这一个文件,我们知道,影响一个界面外观的还有一个因素,那就是主题因素。至于主题和样式的关系,以及两者是如何影响到一个view的外观的,在讲解了主题之后,通过分析一个view的源码来说明这个问题。

theme

Android系统已经内置了很多主题,这些主题的定义是在SDK的themes.xml文件中,打开该文件,内容如下

<resources>

    <style name="Theme">

        <item name="isLightTheme">false</item>
        <item name="colorForeground">@color/bright_foreground_dark</item>
        <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item>
        <item name="colorBackground">@color/background_dark</item>
        <item name="colorBackgroundFloating">?attr/colorBackground</item>
        <item name="colorBackgroundCacheHint">?attr/colorBackground</item>

        ………………

我们发现,主题实际上也是一种style资源,其代码结构与styles.xml中的一样,那么为什么要弄两个文件,themes.xml与styles.xml有什么不同呢?

虽然两者都是style,但关注的面不一样,styles.xml关注局部,而themes.xml关注整体风格。

什么是局部,什么是整体?看个例子就知道了。从styles.xml和themes.xml中各抽取一个。

styles.xml

<style name="Widget.Button.Transparent">
    <item name="background">@drawable/btn_default_transparent</item>
    <item name="textAppearance">?attr/textAppearanceSmall</item>
    <item name="textColor">@color/white</item>
</style>

从名字中可以看出,这是一个透明的Button,如果为某个Button设为该style,则该Button就是透明的。这里只影响到一个Button。

themes.xml

<style name="Theme">
    <item name="buttonStyle">@style/Widget.Button</item>

如果应用该主题,则会影响到该应用的所以Button样式。

这里就是整体与局部的区别。

我们知道,无论是style还是theme,都是为了属性赋值,以上面两段代码为例,第一段会影响到button的background, textAppearance,textColor属性,而对于themes.xml中的buttonStyle,查询attrs.xml,可以看出button并没有这个属性(因为Button继承自TextView以及TextView继承View,所以也要在这两个View中找)。那么这个button不支持的属性是如何应用到该Button中呢?

我们注意到该属性的类型是一个引用类型,系统会解析这个引用,最终会将Widget.Button样式应用到该Button中,而该样式中的属性都是Button支持的,所以没有问题。至于具体细节,在下一节中结合具体代码讲一下。

那么,在平时开发应用程序时,如何利用好style.xml和themes.xml呢?

由上讨论可知,我们可以利用style.xml定义一些局部的样式,将外观从layout文件中分离,定义style中,把注意力集中在某一个view上面。

而在写themes.xml中,把注意力放在整个app的风格上面。为一些带有全局性含义的属性赋值,如

  • colorPrimary
  • buttonStyle
  • textSize

等等。

2. 分析源码,一个例子

以上分别介绍了attr, style, theme,下面以Button这个view为例讲一下系统是如果将theme中定义的样式指定到某一局部Button的。

为应用指定内置的主题

android:theme="@android:style/Theme"

看一下这个主题和Button相关的style,从themes.xml中得到如下信息

<item name="buttonStyle">@style/Widget.Button</item>

现在转到Button的构造函数,假如以以下方式生成一个Button

Button button = new Button(this);

该构造函数源码如下

Button(Context context) {
    this(context, null);
}

public Button(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.buttonStyle);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

可知最后调用的是父类的构造函数,而Button的父类是TextView,传入的参数分别如下

context = thix
attrs = null
defStyleAttr = com.android.internal.R.attr.buttonStyle
defStyleRes = 0

现在转到TextView的构造函数(以下省略了很多,只包含有关内容)

public TextView(
            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);


        //得到当前应用的主题,theme中保存的就是主题的信息,这里
        //就是内置的Theme主题信息
        final Resources.Theme theme = context.getTheme();

        a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);

        a = theme.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);

        a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

}

以上调用了obtainStyledAttributes函数三次,该函数的作用是获得属性的值。

其中第二个参数指明了要获得哪些属性的值。

第一个参数,第三个参数和第四个参数是值的三种来源,优先级为

第一个参数 > 第二个参数 > 第三个参数

第一个参数为一个键值对集合,如果要获得值的属性在第一个参数中,则使用第一个参数。

如果第一个参数为空,就使用第三个参数,第三个参数就是当前主题中的一个属性,该属性是一个引用,引用style中的某一个具体的style.

如果第三个参数为0,就使用第四个参数,该参数直接指定一个style resource,从该resource中取得所需要属性的值。

在本例中,第一个参数为空,所以使用的是第三个参数,而第三个参数就是从主题中获得属性的值,这里是buttonStyle属性,而该属性又引用的styles.xml中的Widget.Button样式,所以最终使用在Button上的就是该样式了。

以上就是主题所定义的style如何应用到View的大概过程。

3. 好的实践

  1. 利用style.xml定义一些局部的样式,将外观从layout文件中分离,定义style中,把注意力集中在某一个view上面。

    而在写themes.xml中,把注意力放在整个app的风格上面。为一些带有全局性含义的属性赋值,如

    • colorPrimary
    • buttonStyle
    • textSize

    等等。

  2. color.xml不代表样式,只表示调色板

  3. dimens.xml和color一样

  4. 为了保持风格的一致性,优先使用主题中定义的风格,使用方式如下

    对于内置的主题

    ?android:attr/xxx

    对于自定义主题

    ?attr/xxx

    以上可以使应用的风格和谐统一

4. Q&A

  1. 如果想继承某个主题并修改某些属性,如何知道有哪些属性可以修改

    A: 从源文件中找,如themes.xml

  2. 如何知道内置了哪些style

    A: 源文件styles.xml, 或官方文档 R.style

  3. 如何知道一个View有哪些属性可用

    A: 源文件attrs.xml中搜索view名称,或参考该View的官方文档

  4. 查看某主题的外观效果

    A: 可以Theme Editor中查看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容