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


http://www.jianshu.com/p/3c4dc799e6db

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中查看



文/lingnanlu(简书作者)
原文链接:http://www.jianshu.com/p/3c4dc799e6db
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: