前言
- 在 Android UI 開(kāi)發(fā)中,經(jīng)常需要用到 屬性,例如使用
android:text
設(shè)置文本框的文案爪瓜,使用android:src
設(shè)置圖片。那么翁都,android:text
是如何設(shè)置到 TextView 上的呢税娜? - 其實(shí)這個(gè)問(wèn)題主要還是考察應(yīng)試者對(duì)于源碼(包括:LayoutInflater 布局解析概行、Style/Theme 系統(tǒng) 等)的熟悉度,在這篇文章里堡称,我將跟你一起探討桐臊。另外巫俺,文末的應(yīng)試建議也不要錯(cuò)過(guò)哦却嗡,如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注坪它,這真的對(duì)我非常重要否灾。
相關(guān)文章
- 《Android | 一個(gè)進(jìn)程有多少個(gè) Context 對(duì)象(答對(duì)的不多)》
- 《Android | 帶你探究 LayoutInflater 布局解析原理》
- 《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎惩阶?》
- 《Android | 說(shuō)說(shuō)從 android:text 到 TextView 的過(guò)程》
目錄
1. 屬性概述
1.1 屬性的本質(zhì)
屬性 (View Attributes) 本質(zhì)上是一個(gè)鍵值對(duì)關(guān)系冬筒,即:屬性名 => 屬性值。
1.2 如何定義屬性?
定義屬性需要用到<declare-styleable>
標(biāo)簽,需要定義 屬性名 與 屬性值類型,格式上可以分為以下 2 種:
格式 1 :
1.1 先定義屬性名和屬性值類型
<attr name="textColor" format="reference|color"/>
<declare-styleable name="TextView">
1.2 引用上面定義的屬性
<attr name="textColor" />
</declare-styleable>
格式 2:
<declare-styleable name="TextView">
一步到位
<attr name="text" format="string" localization="suggested" />
</declare-styleable>
- 格式 1:分為兩步,先定義屬性名和屬性值類型焕阿,然后在引用毅桃;
- 格式 2:一步到位莺掠,直接指定屬性名和屬性值類型。
1.3 屬性的命名空間
使用屬性時(shí),需要指定屬性的命名空間,命名空間用于區(qū)分屬性定義的位置匙铡。目前一共有 4 種 命名空間:
-
1具帮、工具 —— tools:
xmlns:tools="http://schemas.android.com/tools"
只在 Android Studio 中生效匪凡,運(yùn)行時(shí)不生效。比如以下代碼,背景色在編輯器的預(yù)覽窗口顯示白色玉控,但是在運(yùn)行時(shí)顯示黑色:
tools:background="@android:color/white"
android:background="@android:color/black"
-
2、原生 —— android:
xmlns:android="http://schemas.android.com/apk/res/android"
原生框架中attrs
定義的屬性,例如,我們找到 Android P 定義的屬性 attrs.xml,其中可以看到一些我們熟知的屬性:
<!-- 文本顏色 -->
<attr name="textColor" format="reference|color"/>
<!-- 高亮文本顏色 -->
<attr name="textColorHighlight" format="reference|color" />
<!-- 高亮文本顏色 -->
<attr name="textColorHint" format="reference|color" />
你也可以在 SDK 中找到這個(gè)文件,有兩種方法:
文件夾:sdk/platform/android-28/data/res/values/attrs.xml
Android Studio(切換到 project 視圖):
External Libraries/<Android API 28 Platform>/res/values/attrs.xml
(你在這里看到的版本號(hào)是在app/build.gradle中的
compileSdkVersion
設(shè)置的)
- 3、AppCompat 兼容庫(kù) —— 無(wú)需命名空間
Support 庫(kù) 或 AndroidX 庫(kù)中定義的屬性,比如:
<attr format="color" name="colorAccent"/>
你也可以在 Android Studio 中找到這個(gè)文件:
- Android Studio(切換到 project 視圖):
External Libraries/Gradle:com.android.support:appcompat-v7:[版本號(hào)]@aar/res/values/values.xml
-
4而叼、自定義 —— app:
xmlns:app="http://schemas.android.com/apk/res-auto"
用排除法液荸,剩下的屬性就是自定義屬性了绊困。包括 項(xiàng)目中自定義 的屬性與 依賴庫(kù)中自定義 的屬性,比如ConstraintLayout
中自定義的屬性:
<attr format="reference|enum" name="layout_constraintBottom_toBottomOf">
<enum name="parent" value="0"/>
</attr>
你也可以在 Android Studio 中找到這個(gè)文件:
- Android Studio(切換到 project 視圖):
External Libraries/Gradle:com.android.support:constraint:constraint-layout:[版本號(hào)]@aar/res/values/values.xml
2. 樣式概述
需要注意的是:雖然樣式和主題長(zhǎng)得很像然遏,雖然兩者截然不同姨裸!
2.1 樣式的本質(zhì)
樣式(Style)是一組鍵值對(duì)的集合,本質(zhì)上是一組可復(fù)用的 View 屬性集合,代表一種類型的 Widget揖闸。類似這樣:
<style name="BaseTextViewStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:includeFontPadding">false</item>
</style>
2.2 樣式的作用
使用樣式可以 復(fù)用屬性值贮泞,避免定義重復(fù)的屬性值,便于項(xiàng)目維護(hù)。
隨著業(yè)務(wù)功能的疊加俯萎,項(xiàng)目中肯定會(huì)存在一些通用的,可以復(fù)用的樣式虱咧。例如在很多位置會(huì)出現(xiàn)的標(biāo)簽樣式:
觀察可以發(fā)現(xiàn)绘沉,這些標(biāo)簽雖然顏色不一樣喻喳,但是也是有共同之處:圓角谦去、邊線寬度翔怎、字體大小、內(nèi)邊距。如果不使用樣式,那么這些相同的屬性都需要在每處標(biāo)簽重復(fù)聲明。
此時(shí),假設(shè) UI 需要修改全部標(biāo)簽的內(nèi)邊距兼都,那么就需要修改每一處便簽的屬性值瓦胎,那就很繁瑣了芬萍。而使用樣式的話,就可以將重復(fù)的屬性 收攏 到一份樣式上搔啊,當(dāng)需要修改樣式時(shí)柬祠,只需要修改一個(gè)文件负芋,類似這樣:
<style name="smallTagStyle" parent="BaseTextViewStyle">
<item name="android:paddingTop">3dp</item>
<item name="android:paddingBottom">3dp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:textSize">10sp</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
2.3 在 xml 中使用樣式
使用樣式時(shí)漫蛔,需要用到style=""
,類似這樣:
<TextView
android:text="標(biāo)簽"
style="@style/smallTagStyle"/>
關(guān)于這兩句屬性是如何生效的旧蛾,我后文再說(shuō)莽龟。
2.4 樣式的注意事項(xiàng)
- 樣式不在多層級(jí)傳遞
樣式只有在使用它的 View 上才起作用,而在它的子 View 上樣式是無(wú)效的锨天。舉個(gè)例子毯盈,假設(shè) ViewGroup 有三個(gè)按鈕,若設(shè)置 MyStyle 樣式到此 ViewGroup 上病袄,此時(shí)搂赋,僅這個(gè) ViewGroup 有效,而對(duì)三個(gè)按鈕來(lái)說(shuō)是無(wú)效的益缠。
3. 主題概述
3.1 主題的本質(zhì)
與樣式相同的是脑奠,主題(Theme)也是一組鍵值對(duì)的集合,但是它們的本質(zhì)截然不同幅慌。樣式的本質(zhì)是一組可復(fù)用的 View 屬性集合宋欺,而主題是 一組可引用的命名資源集合。類似這樣:
<style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="dialogTheme">@style/customDialog</item>
</style>
3.2 主題的作用
主題背景定義了一組可以在多處引用的資源集合胰伍,這些資源可以在樣式齿诞、布局文件、代碼等位置使用骂租。使用主題掌挚,可以方便全局替換屬性的值。
舉個(gè)例子菩咨,首先你可以定義一套深色主題和一套淺色主題:
<style name="BlackTheme" parent="AppBaseTheme">
<item name="colorPrimary">@color/black</item>
</style>
<style name="WhiteTheme" parent="AppBaseTheme">
<item name="colorPrimary">@color/white</item>
</style>
然后,你在需要主題化的地方引用它,類似這樣:
<ViewGroup …
android:background="?attr/colorPrimary">
此時(shí)抽米,如果應(yīng)用了 BlackTheme 特占,那么 ViewGroup 的背景就是黑色;反之云茸,如果引用了 WhiteTheme是目,那么 ViewGroup 的背景就是白色。
在 xml 中使用主題屬性标捺,需要用到?
懊纳,表示獲得此主題中的語(yǔ)義屬性代表的值。我把所有格式都總結(jié)在這里:
格式 | 描述 |
---|---|
android:background="?attr/colorAccent " |
/ |
android:background="?colorAccent " |
("?attr/colorAccent" 的縮寫(xiě)) |
android:background="?android:attr/colorAccent " |
(屬性的命名空間為 android) |
android:background="?android:colorAccent " |
("?android:attr/colorAccent") |
3.3 在 xml 中使用主題
在 xml 中使用主題亡容,需要用到android:theme
嗤疯,類似這樣:
1. 應(yīng)用層
<application …
android:theme="@style/BlackTheme ">
2. Activity 層
<activity …
android:theme="@style/BlackTheme "/>
3. View 層
<ConstraintLayout …
android:theme="@style/BlackTheme ">
需要注意的是,android:theme
本質(zhì)上也是用到 ContextThemeWrapper 來(lái)使用主題的闺兢,這在我之前寫(xiě)過(guò)的兩篇文章里說(shuō)過(guò):《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎茂缚?》、《Android | 帶你探究 LayoutInflater 布局解析原理》屋谭。這里我簡(jiǎn)單復(fù)述一下:
LayoutInflater.java
private static final int[] ATTRS_THEME = new int[] {
com.android.internal.R.attr.theme
};
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
構(gòu)造 ContextThemeWrapper
context = new ContextThemeWrapper(context, themeResId);
}
- 1脚囊、LayoutInflater 在進(jìn)行布局解析時(shí),需要根據(jù) xml 實(shí)例化 View桐磁;
- 2悔耘、在解析流程中,會(huì)判斷 View 是否使用了
android:theme
我擂; - 3衬以、如果使用,則使用 ContextThemeWrapper 包裝 Context扶踊,并將包裝類用于子 View 的實(shí)例化過(guò)程泄鹏。
3.4 在代碼中使用主題
在代碼中使用主題,需要用到ContextThemeWrapper & Theme
秧耗,它們都提供了設(shè)置主題資源的方法:
ContextThemeWrapper.java
@Override
public void setTheme(int resid) {
if (mThemeResource != resid) {
mThemeResource = resid;
最終調(diào)用的是 Theme#applyStyle(...)
initializeTheme();
}
}
Theme.java
public void applyStyle(int resId, boolean force) {
mThemeImpl.applyStyle(resId, force);
}
當(dāng)構(gòu)造新的 ContextThemeWrapper 之后备籽,它會(huì)分配新的主題 (Theme) 和資源 (Resources) 實(shí)例。那么分井,最終主題是在哪里生效的呢车猬,我在 第 4 節(jié) 說(shuō)。
3.5 主題的注意事項(xiàng)
- 主題會(huì)在多層級(jí)傳遞
與樣式不同的是尺锚,主題對(duì)于更低層級(jí)也是有效的珠闰。舉個(gè)例子,假設(shè) Activity 設(shè)置 BlackTheme瘫辩,那么對(duì)于 Activity 上的所有 View 是有效的伏嗜。此時(shí)坛悉,如果其中 View 單獨(dú)指定了 android:theme,那么此 View 將單獨(dú)使用新的主題承绸。
- 勿使用 Application Context 加載資源
Application 是 ContextWrapper 的子類裸影,因此Application Context 不保留任何主題相關(guān)信息,在 manifest 中設(shè)置的主題僅用作未明確設(shè)置主題背景的 Activity 的默認(rèn)選擇军熏。切勿使用 Application Context 加載可使用的資源轩猩。
4. 問(wèn)題回歸
現(xiàn)在,我們回過(guò)頭來(lái)討論 從 android:text 到 TextView 的過(guò)程荡澎。其實(shí)均践,這說(shuō)的是如何將android:text
屬性值解析到 TextView 上。這個(gè)過(guò)程就是 LayoutInflater 布局解析的過(guò)程摩幔,我之前專門寫(xiě)過(guò)一篇文章探討布局解析的核心過(guò)程:《Android | 帶你探究 LayoutInflater 布局解析原理》彤委,核心過(guò)程如下圖:
4.1 AttributeSet
在前面的文章里,我們已經(jīng)知道 LayoutInflater 通過(guò)反射的方式實(shí)例化 View热鞍。其中的參數(shù)args
分別是 Context & AttributeSet:
- Context:上下文葫慎,有可能是包裝類 ContextThemeWrapper
- AttributeSet:屬性列表,xml 中 View聲明的屬性都會(huì)解析到這個(gè)對(duì)象上薇宠。
LayoutInflater.java
final View view = constructor.newInstance(args);
舉個(gè)例子偷办,假設(shè)有布局文件,我們嘗試輸出 LayoutInflater 實(shí)例化 View 時(shí)傳入的 AttributeSet:
<...MyTextView
android:text="標(biāo)簽"
android:theme="@style/BlackTheme"
android:textColor="?colorPrimary"
style="@style/smallTagStyle"/>
MyTextView.java
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
總共有 4 個(gè)屬性
for (int index = 0; index < attrs.getAttributeCount(); index++) {
System.out.println(attrs.getAttributeName(index) + " = " + attrs.getAttributeValue(index));
}
}
AttributeSet.java
返回屬性名稱字符串(不包括命名空間)
public String getAttributeValue(int index);
返回屬性值字符串
public String getAttributeValue(int index);
輸出如下:
theme = @2131558563
textColor = ?2130837590
text = 標(biāo)簽
style = @2131558752
可以看到澄港,AttributeSet 里只包含了在 xml 中直接聲明的屬性椒涯,對(duì)于引用類型的屬性,AttributeSet 只是記錄了資源 ID回梧,并不會(huì)把它拆解開(kāi)來(lái)废岂。
4.2 TypedArray
想要取到真實(shí)的屬性值,需要用到 TypeArray狱意,另外還需要一個(gè) int 數(shù)組(其中湖苞,int 值是屬性 ID)。類似這樣:
private static final int[] mAttr = {android.R.attr.textColor, android.R.attr.layout_width};
private static final int ATTR_ANDROID_TEXTCOLOR = 0;
private static final int ATTR_ANDROID_LAYOUT_WIDTH = 1;
1. 從 AttributeSet 中加載屬性
TypedArray a = context.obtainStyledAttributes(attrs, mAttr);
for (int index = 0; index < a.getIndexCount(); index++) {
2. 解析每個(gè)屬性
switch (index) {
case ATTR_ANDROID_TEXTCOLOR:
System.out.println("attributes : " + a.getColor(index, Color.RED));
break;
case ATTR_ANDROID_LAYOUT_WIDTH:
System.out.println("attributes : " + a.getInt(index, 0));
break;
}
}
在這里详囤,mAttr 數(shù)組是兩個(gè) int 值财骨,分別是android.R.attr.textColor
和android.R.attr.layout_width
,表示我們感興趣的屬性藏姐。當(dāng)我們將 mAttr 用于Context#obtainStyledAttributes()
隆箩,則只會(huì)解析出我們感興趣的屬性來(lái)。
輸出:
-16777216 羔杨,即:Color.BLACK => 這個(gè)值來(lái)自于 ?attr/colorPrimary 引用的主題屬性
-2 捌臊,即:WRAP_CONTENT => 這個(gè)值來(lái)自于 @style/smallTagStyle 中引用的樣式屬性
需要注意的是,大多數(shù)情況下并不需要在代碼中硬編碼兜材,而是使用<declare-styleable>
標(biāo)簽理澎。編譯器會(huì)自動(dòng)在R.java
中為我們聲明相同的數(shù)組逞力,類似這樣:
<declare-styleable name="MyTextView">
<attr name="android:textColor" />
<attr name="android:layout_width" />
</declare-styleable>
R.java
public static final int[] MyTextView={ 相當(dāng)于 mAttr
0x01010098, 0x010100f4
};
public static final int MyTextView_android_textColor=0; 相當(dāng)于 ATTR_ANDROID_TEXTCOLOR
public static final int MyTextView_android_layout_width=1; 相當(dāng)于 ATTR_ANDROID_LAYOUT_WIDTH
提示: 使用
R.styleable.
設(shè)計(jì)的優(yōu)點(diǎn)是:避免解析不需要的屬性。
4.3 Context#obtainStyledAttributes() 取值順序
現(xiàn)在矾端,我們來(lái)討論obtainStyledAttributes()
解析屬性值的優(yōu)先級(jí)順序掏击,總共分為以下幾個(gè)順序。當(dāng)在越優(yōu)先的級(jí)別找到屬性時(shí)秩铆,優(yōu)先返回該處的屬性值:View > Style > Default Style > Theme。
- View
指 xml 直接指定的屬性灯变,類似這樣:
<TextView
...
android:textColor="@color/black"/>
- Style
指 xml 在樣式中指定的屬性殴玛,類似這樣:
<TextView
...
android:textColor="@style/colorTag"/>
<style name="colorTag">
<item name="android:textColor">@color/black</item>
- Default Style
指在 View 構(gòu)造函數(shù)中指定的樣式,它是構(gòu)造方法的第 3 個(gè)參數(shù)添祸,類似于 TextView 這樣:
public AppCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public AppCompatTextView(Context context, AttributeSet attrs, @AttrRes int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
...
}
其中滚粟,android.R.attr.textViewStyle
表示引用主題中的textViewStyle
屬性,這個(gè)值在主題資源中指定的是一個(gè)樣式資源:
<item name="android:textViewStyle">@style/Widget.AppCompat.TextView</item>
提示: 從
@AttrRes
可以看出刃泌,defStyleAttr
一定要引用主題屬性凡壤。
- Default Style Resource
指在 View 構(gòu)造函數(shù)中指定的樣式資源耙替,它是構(gòu)造方法的第 3 個(gè)參數(shù):
public View(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
}
提示: 從
@StyleRes
可以看出俗扇,defStyleRes
一定要引用樣式資源滞谢。
- Theme
如果以上層級(jí)全部無(wú)法匹配到屬性到忽,那么就會(huì)使用主題中的主題屬性滓走,類似這樣:
<style name="AppTheme" parent="...">
...
<item name="android:textColor">@color/black</item>
</style>
5. 屬性值類型
前文提到,定義屬性需要指定:屬性名 與 屬性值類型涛漂,屬性值類型可以分為資源類與特殊類
5.1 資源類
屬性值類型 | 描述 | TypedArray |
---|---|---|
fraction | 百分?jǐn)?shù) | getFraction(...) |
float | 浮點(diǎn)數(shù) | getFloat(...) |
boolean | 布爾值 | getBoolean(...) |
color | 顏色值 | getColor(...) |
string | 字符串 | getString(...) |
dimension | 尺寸值 | getDimensionPixelOffset(…) getDimensionPixelSize(...) getDimension(...) |
integer | 整數(shù)值 | getInt(...) getInteger(...) |
5.2 特殊類
屬性值類型 | 描述 | TypedArray |
---|---|---|
flag | 標(biāo)志位 | getInt(...) |
enum | 枚舉值 | getInt(…)等 |
reference | 資源引用 | getDrawable(...)等 |
fraction 比較難理解,這里舉例解釋下:
- 1音比、屬性定義
<declare-styleable name="RotateDrawable">
// ...
<attr name="pivotX" format="float|fraction" />
<attr name="pivotY" format="float|fraction" />
<attr name="drawable" />
</declare-styleable>
- 設(shè)置屬性值
<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/fifth">
</animated-rotate>
- 應(yīng)用(RotateDrawable)
if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
// 取出對(duì)應(yīng)的TypedValue
final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX);
// 判斷屬性值是float還是fraction
state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
// 取出最終的值
state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
}
可以看到俱笛,pivotX 支持 float 和 fraction 兩種類型,因此需要通過(guò)TypedValue#type
判斷屬性值的類型贩幻,分別調(diào)用TypedValue#getFraction()
與TypedValue#getFloat()
坎穿。
getFraction(float base,float pbase)
的兩個(gè)參數(shù)為基數(shù)玷犹,最終的返回值是 基數(shù)*百分?jǐn)?shù)幻妓。舉個(gè)例子,當(dāng)設(shè)置的屬性值為 50% 時(shí),返回值為 base50%* ;當(dāng)設(shè)置的屬性值為 50%p 時(shí),返回值為 pbase*50%眶诈。
6. 總結(jié)
- 應(yīng)試建議
- 應(yīng)理解樣式和主題的區(qū)別氧敢,兩者截然不同:樣式是一組可復(fù)用的 View 屬性集合梅掠,而主題是一組命名的資源集合。
- 應(yīng)掌握屬性來(lái)源優(yōu)先級(jí)順序:View > Style > Default Style > Theme
參考資料
- 《Android 樣式系統(tǒng) | 主題背景和樣式》 —— Android Developers
- 《What’s your text’s appearance?》 —— Nick Butcher(Google) 著
- 《Style resource》 — Android Developers
- 《Styles and Themes》 — Android Developers
- 《Creating a Custom View Class》 — Android Developers
- 《Best Practices for Themes and Styles》 — Android Dev Summit '18
- 《Android themes & styles demystified》 — Google I/O 2016
- 《Android 編程權(quán)威指南》[美]Bill Phillips, Chris Stewart, Kristin Marsicano 著
推薦閱讀
- 密碼學(xué) | Base64是加密算法嗎?
- 算法面試題 | 回溯算法解題框架
- 算法面試題 | 鏈表問(wèn)題總結(jié)
- Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎偏陪?
- Android | 面試必問(wèn)的 Handler勺馆,你確定不看看?
- 計(jì)算機(jī)組成原理 | Unicode 和 UTF-8是什么關(guān)系赛糟?
- 計(jì)算機(jī)組成原理 | 為什么浮點(diǎn)數(shù)運(yùn)算不精確派任?(阿里筆試)
- 計(jì)算機(jī)網(wǎng)絡(luò) | 圖解 DNS & HTTPDNS 原理