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.customattr
和R.attrs.showText
来引用,可见与其是否在<declare-styleable>没有关系。
既然属性都在一个全局空间中,那么就不允许同名不同类型的属性定义,如以下是错误的。
<resources>
<attr name="customattr" format="boolean"/>
<attr name="customattr" format="string"/>
</resources>
好,到这里,是不是有了很多疑问。如
- 什么情况下属性要定义在<declare-styleable>下,什么情况下不需要
- <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. 好的实践
-
利用style.xml定义一些局部的样式,将外观从layout文件中分离,定义style中,把注意力集中在某一个view上面。
而在写themes.xml中,把注意力放在整个app的风格上面。为一些带有全局性含义的属性赋值,如
- colorPrimary
- buttonStyle
- textSize
等等。
color.xml不代表样式,只表示调色板
dimens.xml和color一样
-
为了保持风格的一致性,优先使用主题中定义的风格,使用方式如下
对于内置的主题
?android:attr/xxx
对于自定义主题
?attr/xxx
以上可以使应用的风格和谐统一
4. Q&A
-
如果想继承某个主题并修改某些属性,如何知道有哪些属性可以修改
A: 从源文件中找,如themes.xml
-
如何知道内置了哪些style
A: 源文件styles.xml, 或官方文档 R.style
-
如何知道一个View有哪些属性可用
A: 源文件attrs.xml中搜索view名称,或参考该View的官方文档
-
查看某主题的外观效果
A: 可以Theme Editor中查看