Android Style和自定义属性

佐手、 提交于 2019-12-05 12:39:35

1. Android Style & Theme

1.1 基本概念

Styles and themes on Android allow you to separate the details of your app design from the UI structure and behavior, similar to stylesheets in web design.

Android上的style和theme允许你将应用中设计的详细细节与UI结构和行为进行分离,类似于web设计中的样式表。

1.1.1 Style(样式)

A style is a collection of attributes that specify the appearance for a single View. A style can specify attributes such as font color, font size, background color, and much more.

Style是一些可以指定单个View外观的属性集合,一个style可以指定一些属性比如字体颜色,字体大小,背景颜色等等。

1.1.2 Theme(主题)

A theme is a type of style that’s applied to an entire app, activity, or view hierarchy—not just an individual view. When you apply your style as a theme, every view in the app or activity applies each style attribute that it supports. Themes can also apply styles to non-view elements, such as the status bar and window background.

Theme是一种应用于整个app,activity或者view层次的style,而不仅仅是单独的view。当你应用你的style作为theme时,每一个app或者activity中的view将应用它支持的每个style属性。theme还可以将style应用于非view元素,例如状态栏和窗口背景。

Style和theme定义在res/values/文件夹下,通常命名为styles.xml(也可以根据需求命名为其他文件名)。

1.2 创建和应用style

打开res/values/styles.xml文件

  1. 添加一个以独特ID命名的<style>元素
  2. 为每个你想要定义的style属性添加<item>元素

举个栗子:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="GreenText" parent="TextAppearance.AppCompat">
        <item name="android:textColor">#00FF00</item>
    </style>
</resources>

使用方法如下:

<TextView
    ...
    style="@style/GreenText"
    ... />

每个style中定义的view所支持的属性会被应用到view中。view会忽略掉它所不支持的属性。

注意:
只有添加style属性的元素才会接受这些属性,任何子view不会应用这些style。如果你想要子视图继承style,取而代之的是使用android:theme属性的style。

1.3 扩展和自定义style

为了保持与平台UI的兼容性,在创建自己style的时候,通常继承自framework或support库中已经存在的style。如果要继承style,需要在style中指定parent属性。之后,你可以覆盖继承style的属性并添加一个新的。
举个栗子:

<style name="GreenText" parent="@android:style/TextAppearance">
    <item name="android:textColor">#00FF00</item>
</style>

然而,通常应用的核心style是从Android Support库中继承的。support库中的style通过针对每个版本中可用的UI属性进行了优化,提供了与Android 4.0(API 14)和更高版本的兼容性。Support库中的style通常有一个与framework中的style相似的名称,但是包含“AppCompat”。例如:

<style name="GreenText" parent="TextAppearance.AppCompat">
    <item name="android:textColor">#00FF00</item>
</style>

你也可以通过点符号拓展style名称来继承style(平台的style除外),代替使用parent属性。也就是说,将你想要继承的style名称作为前缀,以点号分隔。当你继承自自己的style,而非其他库时,通常这么做。例如下面的例子,继承自GreenText并增加了文本大小。

<style name="GreenText.Large">
    <item name="android:textSize">22dp</item>
</style>

你可以不断继承style很多次只要你愿意链接更多的名字。例如为GreenText增加一个大写style

<style name="GreenText.Large.AllCaps">
    <item name="android:textAllCaps">true</item>
</style>

注意:
如果你使用点符号来拓展style,并且使用了parent属性,这时parent属性会覆盖掉所有你继承自点符号的属性。
例如定义一个BlueText style

<style name="BlueText" parent="TextAppearance.AppCompat">
    <item name="android:textColor">#0000FF</item>
</style>

修改GreenText.Large.AllCaps属性继承自BlueText

<style name="GreenText.Large.AllCaps" parent="BlueText">
    <item name="android:textAllCaps">true</item>
</style>

在View类文件的"XML attributes"表(例如TextView的"XML attributes"表)中,你可以找到以<item>标签定义的各种属性。所有的view都支持基类view中的"XML attributes",大部分view都可以添加自己特殊的属性。

1.4 创建和应用theme

创建theme的方法与创建style的方法相同,不同的地方在于如何应用。应用style是在view中定义一个style属性,应用theme是在AndroidManifest.xml中的<application>或者<activity>标签中添加android:theme属性。
例如,以下是将support库中采用材料设计的"dark"theme应用到整个app。

<manifest ... >
    <application android:theme="@style/Theme.AppCompat" ... >
    </application>
</manifest>

这段是将"Light"theme应用到activity上。

<manifest ... >
    <application ... >
        <activity android:theme="@style/Theme.AppCompat.Light" ... >
        </activity>
    </application>
</manifest>

现在app或者activity中的每个view都应用了给定theme的style。如果一个view仅支持style中定义的部分属性,它会应用这些属性并舍弃那些它不支持的。
从Android 5.0(API 21)和Android Support Library v22.1开始,你也可以在布局中给view定义android:theme属性。它会修改当前view及子view的theme,这对于改变界面特定部分的theme非常有用。
但通常我们会自定义一个theme应用到我们的app中。最好的方法是拓展support库中的style并覆写某些属性。我们稍后会讲到。

1.5 扩展和自定义theme

当你使用Android Studio创建一个工程的时候,它会对你的应用使用一个默认的材料设计theme,定义在了项目的styles.xml文件中。这个theme继承自Support库中的theme并且包含了被用作关键UI元素的颜色属性的覆盖,例如应用栏(app bar)和浮动动作按钮(floating action button)。
如下是style.xml文件中的定义:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

应该注意到,style值引用自其他颜色资源文件,定义在工程中res/values/colors.xml文件中,你可以通过修改这个文件来修改颜色。在开始修改颜色之前,可以使用Material Color Tool来预览颜色,这个工具可以帮助你从材料调色板中选择颜色并且预览它在app中的显示效果。
你可以覆盖theme中你想要修改的style属性,例如,你可以修改activity背景颜色:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="android:windowBackground">@color/activityBackground</item>
</style>

你可以参考R.styleable.Theme来查看可以修改的theme属性。当对布局中的view添加style时,可以查看view类引用中的“XML attributes”表。在R.styleable.Theme中列出的一些theme属性适用于activity中的窗口,而非布局中的view。例如,windowBackground修改了窗口背景,windowEnterTransition定义了activity的入场动画。
Android Support Library也提供了其他属性,你可以使用它们自定义theme。
注意:
从Android Support Library中继承的theme修改属性时不需要加"android:"前缀。这个前缀只用于继承自Android framework的theme。

1.6 添加指定版本的style

如果你想要在Andorid新版本上添加theme属性。你可以将它们添加到你的theme中,并保持与旧版本的兼容性。你需要的是保存在其他包含资源版本限定符的values文件夹下的styles.xml文件。例如:

res/values/styles.xml        # themes for all versions
res/values-v21/styles.xml    # themes for API level 21+ only

由于values/styles.xml文件中的styles对全版本可见,所以定义在res/values-v21/styles.xml中的style可以继承自他们。你可以在style文件中定义一个base开头的theme并在特定版本的styles中拓展它,这样可以避免重复的style定义。
例如,在Android 5.0(API 21)或者更高版本发布了窗口转变的属性(windowActivityTransitions),如果你想要使用这个属性,你可以在res/values/styles.xml这样定义:

<resources>
    <!-- base set of styles that apply to all versions -->
    <style name="BaseAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/primaryColor</item>
        <item name="colorPrimaryDark">@color/primaryTextColor</item>
        <item name="colorAccent">@color/secondaryColor</item>
    </style>

    <!-- declare the theme name that's actually applied in the manifest file -->
    <style name="AppTheme" parent="BaseAppTheme" />
</resources>

在res/values-v21/styles.xml中这样定义:

<resources>
    <!-- extend the base theme to add styles available only with API level 21+ -->
    <style name="AppTheme" parent="BaseAppTheme">
        <item name="android:windowActivityTransitions">true</item>
        <item name="android:windowEnterTransition">@android:transition/slide_right</item>
        <item name="android:windowExitTransition">@android:transition/slide_left</item>
    </style>
</resources>

这时你可以在manifest文件中使用AppTheme这个theme了。

1.7 Style层级

Android提供了多样的方式去设置Android App中的属性,例如你可以在layout中直接设置属性,也可以对view设置style,也可以对layout应用theme,甚至可以在代码中设置。
当针对你的应用选择何种style时,需要注意style的层级。一般地,你应当尽可能地保持style与theme的一致。如果你在不同的地方定义了相同的属性,下面的列表会决定哪个属性会最终被应用。列表的优先级由高到低。

  1. Applying character- or paragraph-level styling via text spans to TextView-derived classes
    使用text spans将字符或段落级的style应用于TextView及其派生类
  2. Applying attributes programmatically
    在程序中应用属性
  3. Applying individual attributes directly to a View
    将单个属性直接应用于view
  4. Applying a style to a View
    将一个style应用于view
  5. Default styling
    默认的style
  6. Applying a theme to a collection of Views, an activity, or your entire app
    将theme应用于一个view集,一个activity或者整个应用
  7. Applying certain View-specific styling, such as setting a TextAppearance on a TextView
    应用特定于view的style,例如对TextView设置TextAppearance属性

关于这七种方式更详细的介绍,我们在第三节具体讲解。

注意:
如果你正在设计你的app但是并没有看到你期望的效果,可能是因为其他的style覆盖了你的修改。例如如果你将一个theme应用于你的app,同时将一个style应用于单个view,对于这个view,style属性会覆盖任何匹配到的theme属性。注意,任何没有被style覆盖的theme属性仍然会被使用。

1.7.1 TextAppearance

style的一个限制是只能对view定义一个style。然而对于TextView,你可以再定义一个TextAppearance属性,这个属性类似于style,如下例所示:

<TextView
    ...
    android:textAppearance="@android:style/TextAppearance.Material.Headline"
    android:text="This text is styled via textAppearance!" />

TextAppearance允许您定义特定于文本的style,同时保留view的style供其他用途使用。然而,注意,如果你直接在view中或style中定义了任何文本属性,这些属性值将会覆盖TextAppearance值。
例如对TextView添加

android:textColor="@color/colorPrimary"

则该文本颜色会覆盖掉TextAppearance中定义的文本颜色。

TextAppearance支持TextView所提供的style属性子集。一些一般的TextView属性没有被包含,比如:lines,lineHeight,breakStrategy,hyphenationFrequency等。TextAppearance工作在字符级而不是段落级,所以不支持影响全局的属性。

2 Android 自定义属性

2.1 创建自定义属性步骤

  1. 创建class CustomView
public class CustomView extends TextView {

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}
  1. 在res/value/attrs.xml中添加自定义属性
<declare-styleable name="CustomView">
    <attr name="Custom_Text" format="string"/>
    <attr name="Custom_TextSize" format="dimension"/>
    <attr name="Custom_TextColor" format="color"/>
</declare-styleable>
  1. 布局文件中使用自定义属性
<android.support.constraint.ConstraintLayout
    ...
    xmlns:CustomView="http://schemas.android.com/apk/res-auto"
    ...>

    <com.example.demo.demostyle.CustomView
        ...
        CustomView:Custom_Text="Test"
        CustomView:Custom_TextSize="@dimen/text_size"
        CustomView:Custom_TextColor="#FF4081"
        .../>

</android.support.constraint.ConstraintLayout>
  1. 在CustomView的构造方法中通过TypedArray获取自定义的属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0);
mText = typedArray.getString(R.styleable.CustomView_Custom_Text);
mTextSize = typedArray.getDimension(R.styleable.CustomView_Custom_TextSize, DEFAULT_TEXT_SIZE);
mTextColor = typedArray.getColor(R.styleable.CustomView_Custom_TextColor, DEFAULT_TEXT_COLOR);
typedArray.recycle();
this.setText(mText);
this.setTextSize(mTextSize);
this.setTextColor(mTextColor);

2.2 AttributeSet

AttributeSet是一个接口,里面提供了诸多获取属性的方法。它的实现类是android.content.res.XmlBlock.Parser,该类实现自XmlResourceParser,而XmlResourceParser又继承自AttributeSet。通常我们在代码中是不会直接使用该接口的。

官方API描述文档如下:

A collection of attributes, as found associated with a tag in an XML document. Often you will not want to use this interface directly, instead passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) Resources.Theme.obtainStyledAttributes()} which will take care of parsing the attributes for you. In particular, the Resources API will convert resource references (attribute values such as “@string/my_label” in the original XML) to the desired type for you; if you use AttributeSet directly then you will need to manually check for resource references (with {@link #getAttributeResourceValue(int, int)}) and do the resource lookup yourself if needed. Direct use of AttributeSet also prevents the application of themes and styles when retrieving attribute values.
This interface provides an efficient mechanism for retrieving data from compiled XML files, which can be retrieved for a particular XmlPullParser through {@link Xml#asAttributeSet Xml.asAttributeSet()}. Normally this will return an implementation of the interface that works on top of a generic XmlPullParser, however it is more useful in conjunction with compiled XML resources:
XmlPullParser parser = resources.getXml(myResource);
AttributeSet attributes = Xml.asAttributeSet(parser);
The implementation returned here, unlike using the implementation on top of a generic XmlPullParser, is highly optimized by retrieving pre-computed information that was generated by aapt when compiling your resources. For example, the {@link #getAttributeFloatValue(int, float)} method returns a floating point number previous stored in the compiled resource instead of parsing at runtime the string originally in the XML file.
This interface also provides additional information contained in the compiled XML resource that is not available in a normal XML file, such as {@link #getAttributeNameResource(int)} which returns the resource identifier associated with a particular XML attribute name.
@see XmlPullParser

翻译:AttributeSet是一个与XML文档中的标签相关联的属性集合。通常不会直接使用这个接口,而是将它传递给android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int),obtainStyledAttributes会负责解析属性。特别是,Resources API会将资源引用(源XML中定义的"@string/my_label"等属性值)转化为所需类型;如果直接使用AttributeSet则需要手动检查资源索引(使用getAttributeResourceValue(int, int))并且在需要时从资源中自行查找。直接使用AttributeSet还会在检索属性值时阻止应用theme和style。
这个接口提供了一个从编译的XML文件中检索数据的有效机制,通过Xml.asAttributeSet()为特定的XmlPullParser检索数据。通常,这会返回一个在通用XmlPullParser之上工作的接口实现,然而,它与编译的XML资源结合起来更有用:

XmlPullParser parser = resources.getXml(myResource);
AttributeSet attributes = Xml.asAttributeSet(parser);

与在通用XmlPullParser之上使用实现不同,此处返回的AttributeSet实现通过检索在编译资源时被aapt生成的预先计算的信息而得到高度优化。例如,getAttributeFloatValue(int, float)方法返回一个预先存储在编译资源中的浮点数值而不是在运行时解析XML文件中的源字符串。
此接口还提供了编译的XML资源中包含的其他信息,这些信息在普通XML文件中是不可用的,例如 getAttributeNameResource(int) 返回与特定XML属性名称相关联的资源ID。

在构造方法中添加如下代码:

int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
  String attrName = attrs.getAttributeName(i);
  String attrVal = attrs.getAttributeValue(i);
  Log.e(TAG, "name : " + attrName + ", value : " + attrVal);
}

打印Log如下:

01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : textAppearance, value : @16974327
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_width, value : -2
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_height, value : -2
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_constraintBottom_toBottomOf, value : 0
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_constraintLeft_toLeftOf, value : 0
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_constraintRight_toRightOf, value : 0
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : layout_constraintTop_toTopOf, value : 0
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : Custom_Text, value : Test
01-30 10:23:31.690 29910 29910 E ZXN_CustomView: name : Custom_TextSize, value : @2131165282
01-30 10:23:31.691 29910 29910 E ZXN_CustomView: name : Custom_TextColor, value : #ffff4081

2.3 TypedArray

从上面的Log中,心细的同学已经发现了一个问题,布局中直接赋值的属性可以直接获取,但是通过引用而得到的是"@+数字符"的ID,很不方便我们使用。此时,需要一个简便的工具,来解析AttributeSet中的属性。TypedArray便应运而生。TypedArray中定义了很多通过索引获取属性值的方法。
官方API描述文档如下:

Container for an array of values that were retrieved with {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} or {@link Resources#obtainAttributes}. Be sure to call {@link #recycle} when done with them.
The indices used to retrieve values from this structure correspond to the positions of the attributes given to obtainStyledAttributes.

翻译:TypedArray是使用Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)或者Resources#obtainAttributes检索出来的值数组的容器。当使用完时必须调用recycle。 用于从此结构检索值的索引对应于赋予obtainStyledAttributes的属性的位置。

2.4 declare-styleable

declare-styleable从字面意思看是可声明样式。它是定义在XML文件中的标签。用来声明一些属性的合集。declare-styleable是非必须定义的,我们在自定义属性时可以不使用。不过为了方便,一般都会采用。
不使用declare-styleable,需要如下修改:
attrs.xml

<resources>
    <attr name="Custom_Attr_Text" format="string"/>
    <attr name="Custom_Attr_TextSize" format="dimension"/>
    <attr name="Custom_Attr_TextColor" format="color"/>
</resources>

布局文件:

<android.support.constraint.ConstraintLayout
    ...>

    <com.example.demo.demostyle.CustomView
        ...
        app:Custom_Attr_Text="AttrTest"
        app:Custom_Attr_TextSize="@dimen/text_size"
        app:Custom_Attr_TextColor="#FF4081"
        .../>
</android.support.constraint.ConstraintLayout>

CustomView.java

private int[] custom_attrs = {R.attr.Custom_Attr_Text, R.attr.Custom_Attr_TextSize, R.attr.Custom_Attr_TextColor};
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ...
        TypedArray typedArray = context.obtainStyledAttributes(attrs, custom_attrs);
        mText = typedArray.getString(0);
        mTextSize = typedArray.getDimension(1, DEFAULT_TEXT_SIZE);
        mTextColor = typedArray.getColor(2, DEFAULT_TEXT_COLOR);
        typedArray.recycle();

        this.setText(mText);
        this.setTextSize(mTextSize);
        this.setTextColor(mTextColor);
        ...
    }

2.5 obtainStyledAttributes

obtainStyledAttributes函数总共有四个,分别为

  • Resources#obtainAttributes(AttributeSet set, int[] attrs)
  • Resources#Theme#obtainStyledAttributes(int[] attrs)
  • Resources#Theme#obtainStyledAttributes(int resId,int[] attrs)
  • Resources#Theme#obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)

2.5.1 obtainAttributes(AttributeSet set, int[] attrs)

/**
 * Retrieve a set of basic attribute values from an AttributeSet, not performing styling of them using a theme and/or style resources.
 * 从AttributeSet检索一组基本属性值,而不是使用theme和/或style资源执行它们的style。
 *
 * @param set The current attribute values to retrieve.
 *            当前需要检索的属性集合
 *
 * @param attrs The specific attributes to be retrieved. These attribute IDs must be sorted in ascending order.
 *              指定所要检索的属性组,这些属性ID必须按照升序排列
 *
 * @return Returns a TypedArray holding an array of the attribute values.
 *         返回一个包含属性值数组的TypedArray。
 *
 * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when done with it.
 * 确保在使用完后调用TypedArray.recycle()。
 */
public TypedArray obtainAttributes(AttributeSet set, @StyleableRes int[] attrs) {
    int len = attrs.length;
    TypedArray array = TypedArray.obtain(this, len);
    XmlBlock.Parser parser = (XmlBlock.Parser)set;
    mResourcesImpl.getAssets().retrieveAttributes(parser, attrs, array.mData, array.mIndices);
    array.mXml = parser;
    return array;
}

2.5.2 obtainStyledAttributes(int[] attrs)

/**
 * Return a TypedArray holding the values defined by <var>Theme</var> which are listed in <var>attrs</var>.
 * 返回一个TypedArray,其中包含由attrs中列出的Theme定义的值。
 *
 * @param attrs The desired attributes. These attribute IDs must be sorted in ascending order.
 *              所需的属性。 必须按升序对这些属性ID进行排序。
 *
 * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
 *                           如果ID不存在,抛出NotFoundException异常。
 *
 * @return Returns a TypedArray holding an array of the attribute values.
 *         返回一个包含属性值数组的TypedArray。
 *
 * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when done with it.
 * 确保在使用完后调用TypedArray.recycle()。
 */
public TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) {
    return mThemeImpl.obtainStyledAttributes(this, null, attrs, 0, 0);
}

2.5.3 obtainStyledAttributes(int resId,int[] attrs)

/**
 * Return a TypedArray holding the values defined by the style resource <var>resid</var> which are listed in <var>attrs</var>.
 * 返回一个TypedArray,其中包含由attrs中列出的style资源resid定义的值。
 *
 * @param resId The desired style resource.
 *              所需的style资源。
 *
 * @param attrs The desired attributes in the style. These attribute IDs must be sorted in ascending order.
 *              所需style中的属性。 必须按升序对这些属性ID进行排序。
 *
 * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
 *                           如果ID不存在,抛出NotFoundException异常。
 *
 * @return Returns a TypedArray holding an array of the attribute values.
 *         返回一个包含属性值数组的TypedArray。
 *
 * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when done with it.
 * 确保在使用完后调用TypedArray.recycle()。
 */
public TypedArray obtainStyledAttributes(@StyleRes int resId, @StyleableRes int[] attrs) throws NotFoundException {
    return mThemeImpl.obtainStyledAttributes(this, null, attrs, 0, resId);
}

2.5.4 obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)

/**
 * Return a TypedArray holding the attribute values in <var>set</var> that are listed in <var>attrs</var>.  In addition, if the given AttributeSet specifies a style class (through the "style" attribute), that style will be applied on top of the base attributes it defines.
 * 返回一个TypedArray,其中包含"attrs"中列出的"set"中的属性值(即obtainStyledAttributes会按照attrs中列出的属性值去set中进行查找,将结果写入TypedArray并返回)。另外,如果给定的AttributeSet定义了一个style类(通过"style"属性),则style将会被应用于它定义的基础属性之上。
 *
 * <p>When determining the final value of a particular attribute, there are four inputs that come into play:</p>
 * 确定特定属性的最终值时,有四个输入起作用:
 *
 * <ol>
 *     <li> Any attribute values in the given AttributeSet.
 *          给定AttributeSet中的任何属性值
 *     <li> The style resource specified in the AttributeSet (named "style").
 *          AttributeSet中定义的style资源(命名为"style")
 *     <li> The default style specified by <var>defStyleAttr</var> and <var>defStyleRes</var>
 *          "defStyleAttr"和"defStyleRes"定义的默认style
 *     <li> The base values in this theme.
 *          Theme中的默认值
 * </ol>
 *
 * <p>Each of these inputs is considered in-order, with the first listed taking precedence over the following ones.  In other words, if in the AttributeSet you have supplied <code>&lt;Button textColor="#ff000000"&gt;</code>, then the button's text will <em>always</em> be black, regardless of what is specified in any of the styles.
 * 这些输入中的每一个都需要考虑顺序,首先列出的输入优先于下一个输入。换句话说,如果在AttributeSet中你已经定义了Button textColor="#ff000000",那么按钮的文本将始终是黑色,无论你在任何style中再指定。
 *
 * @param set The base set of attribute values. May be null.
 *            属性值的基类,可能为空。
 *
 * @param attrs The desired attributes to be retrieved. These attribute IDs must be sorted in ascending order.
 *              要检索的所需属性。 必须按升序对这些属性ID进行排序。
 *
 * @param defStyleAttr An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the TypedArray. Can be 0 to not look for defaults.
 *                     当前theme中的一个属性。它包含了为TypedArray提供默认值的style资源的引用。可以为0以查找默认值。
 *
 * @param defStyleRes A resource identifier of a style resource that supplies default values for the TypedArray, used only if defStyleAttr is 0 or can not be found in the theme.  Can be 0 to not look for defaults.
 *                    为TypedArray提供默认值的style资源的资源ID,只有在defStyleAttr为0或者在Theme中找不到时才会被使用。可以为0以查找默认值。
 *
 * @return Returns a TypedArray holding an array of the attribute values.
 * 返回一个包含属性值数组的TypedArray。
 *
 * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} when done with it.
 * 确保在使用完后调用TypedArray.recycle()。
 */
public TypedArray obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
    return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}

注:
关于attrs,文档中要求ID按照升序排列,意思是在R.java文件中生成的ID必须按照升序排列。
例如,在Attrs.xml文件中定义的declare-styleable

<declare-styleable name="CustomViewStyle">
    <attr name="Custom_TextSize" format="dimension"/>
    <attr name="Custom_TextColor" format="color"/>
    <attr name="Custom_Text" format="string"/>
</declare-styleable>

查看编译后的R.java文件

...
public static final int Custom_Text=0x7f0100d0;
public static final int Custom_TextColor=0x7f0100cf;
public static final int Custom_TextSize=0x7f0100ce;
...
public static final int[] CustomViewStyle = {0x7f0100ce, 0x7f0100cf, 0x7f0100d0};
...

3 几种赋值属性方法

在第一章中,提到了七种Style层级,也即七种赋值属性的方法,分别如下:

  1. Applying character- or paragraph-level styling via text spans to TextView-derived classes
    使用text spans将字符或段落级的style应用于TextView及其派生类
  2. Applying attributes programmatically
    在程序中应用属性
  3. Applying individual attributes directly to a View
    将单个属性直接应用于view
  4. Applying a style to a View
    将一个style应用于view
  5. Default styling
    默认的style
  6. Applying a theme to a collection of Views, an activity, or your entire app
    将theme应用于一个view集,一个activity或者整个应用
  7. Applying certain View-specific styling, such as setting a TextAppearance on a TextView
    应用特定于view的style,例如对TextView设置TextAppearance属性

以上七种方式中,加粗字体的为我们需要详细讨论的,其他的由于使用场景有限或非重点,暂不讨论。
对于第五种方法,可以细分为使用defStyleAttr和defStyleRes来设置默认的style,所以以上方法可以进一步划分为如下方法:

  1. 将单个属性直接应用于view
  2. 将一个style应用于view
  3. 通过defStyleAttr设置默认style
  4. 通过defStyleRes设置默认style
  5. 将theme应用于一个view集,一个activity或者整个应用

在讲解这几种方式之前,我们先在attrs.xml中定义CustomViewStyle,并且自定义View:CustomView

<resources>
    <declare-styleable name="CustomViewStyle">
        <attr name="Custom_Text" format="string"/>
        <attr name="Custom_TextSize" format="dimension"/>
        <attr name="Custom_TextColor" format="color"/>
    </declare-styleable>
</resources>
public class CustomView extends TextView {

    private static final int DEFAULT_TEXT_SIZE = 20;
    private static final int DEFAULT_TEXT_COLOR = Color.BLUE;

    private String mText;
    private float mTextSize;
    private int mTextColor;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);
        mText = typedArray.getString(R.styleable.CustomViewStyle_Custom_Text);
        if (!TextUtils.isEmpty(mText)) {
            this.setText(mText);
        }
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.CustomViewStyle_Custom_TextSize, DEFAULT_TEXT_SIZE);
        this.setTextSize(mTextSize);
        mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_Custom_TextColor, DEFAULT_TEXT_COLOR);
        this.setTextColor(mTextColor);
        typedArray.recycle();
    }
}

3.1 将单个属性直接应用于view

在布局文件中直接对属性进行赋值,可以为Android提供系统属性,也可以为自定义属性。

<com.example.demo.demostyle.CustomView
    ...
    app:Custom_Text="Test"
    app:Custom_TextSize="30sp"
    app:Custom_TextColor="#00FF00"
    android:text="Test"
    android:textSize="30sp"
    android:textColor="#00FF00"
    .../>

3.2 将一个style应用于view

为了方便管理,我们常把某一些特定的属性抽取出来汇聚到一个style文件中进行管理。从而使一些控件有相同的风格。

  1. 在styles.xml文件中定义一个特定的style
<resources>
    ...
    <style name="SpecialTextStyle">
        <item name="Custom_Text">"SpecialTextStyle"</item>
        <item name="Custom_TextSize">10sp</item>
        <item name="Custom_TextColor">#FF00FF</item>
        <item name="android:text">"SpecialTextStyle"</item>
        <item name="android:textSize">10sp</item>
        <item name="android:textColor">#FF00FF</item>
        <item name="android:textAllCaps">true</item>
    </style>
    ...
</resources>
  1. 将定义的style赋值给自定义View
<android.support.constraint.ConstraintLayout
    ...>
    <com.example.demo.demostyle.CustomView
        ...
        style="@style/SpecialTextStyle"
        .../>
    <TextView
        ...
        style="@style/SpecialTextStyle"
        .../>
    <Button
        ...
        style="@style/SpecialTextStyle"
        .../>
</android.support.constraint.ConstraintLayout>

此时,这几个控件便具有了相同的文本,文本大小,文本颜色以及文本大写

3.3 通过defStyleAttr设置默认style

  1. 在attrs.xml中定义默认style和默认属性
<resources>
    <style name="default_text_style">
        <item name="Custom_Text">"Default Text Style"</item>
        <item name="Custom_TextSize">40sp</item>
        <item name="Custom_TextColor">#00FFFF</item>
    </style>

    <attr name="defaultTextViewStyle" format="reference"/>
</resources>
  1. 在styles.xml文件中自定义一个theme,并且给defaultTextViewStyle属性赋予我们自定义的默认style的引用
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="defaultTextViewStyle">@style/default_text_style</item>
</style>
  1. 在AndroidManifest.xml文件中将我们自定义的theme应用在application或activity上
<application
    ...
    android:theme="@style/AppTheme">
    ...
</application>
  1. 在CustomView.java文件中修改构造函数,将obtainStyledAttributes函数中的defStyleAttr参数修改为R.attr.defaultTextViewStyle
public class CustomView extends TextView {
    ...
    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ...
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, R.attr.defaultTextViewStyle, 0);
        ...
    }
}

通过以上几个步骤,便将我们定义的defaultTextViewStyle应用到CustomView中。

3.4 通过defStyleRes设置默认style

在第三种的基础上,假如我们没有设置defStyleAttr,或者设置为Theme中查到不到的style,第四个参数defStyleRes就起作用了。
在CustomView.java文件中修改构造函数,将obtainStyledAttributes函数中的defStyleAttr修改为0,defStyleRes修改为R.style.default_text_style

public class CustomView extends TextView {
    ...
    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ...
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, 0, R.style.default_text_style);
        ...
    }
}

此时,CustomView中将会应用default_text_style作为默认style。

3.5 将theme应用于一个view集,一个activity或者整个应用

在Theme中添加一些属性,并将Theme设置到Application或Activity上,使得Application或Activity内的空间默认属性为定义的属性,可以理解为一个全局默认属性

  1. 在styles.xml文件中自定义一个theme,并添加一些属性
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        ...
        <item name="Custom_Text">"Theme Default Text"</item>
        <item name="Custom_TextSize">30sp</item>
        <item name="Custom_TextColor">#F06F58</item>
        <item name="android:text">"Theme Default Text"</item>
        <item name="android:textSize">30sp</item>
        <item name="android:textColor">#F06F58</item>
        ...
    </style>
</resources>
  1. 在AndroidManifest.xml文件中将我们自定义的theme应用在application或activity上
<application
    ...
    android:theme="@style/AppTheme">
    ...
</application>

这样当你布局中的视图未设置这几个属性时,将默认使用Theme中定义的属性。

注意:经过验证,以上几个方式的优先级由高到低

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!