一、基本概念
1.1 資源
Android
使用xml
文件來描述各種資源熟嫩,包括字符串秦踪、顏色、主題邦危、布局等等洋侨。資源分為兩個部分,及 值 和 屬性倦蚪。
1.1.1 屬性
在App
開發(fā)的過程中希坚,如果需要為自定義View
聲明一個新的屬性,那么我們會在res/values/attr.xml
文件中進行定義陵且。
<resources>
<declare-styleable name="AttrTextView">
<attr name="attrTvName" format="string"/>
<attr name="attrTvColor" format="color"/>
</declare-styleable>
</resources>
其中declare-styleable
相當于一個屬性的集合裁僧,而attr
則是其內(nèi)部的屬性个束,在R.java
文件中declare-styleable
對應(yīng)一個int[]
數(shù)組。
需要注意的是:
- 如果
attr
后面僅有一個name:
聊疲,那么這就是 引用茬底,其聲明在別的styleable
中。 - 如果
attr
后面不僅有name
获洲,還有format
阱表,那就是 聲明,不能在別的styleable
中再次聲明贡珊。
1.1.2 值
常見的值存放在以下幾個位置:
- 字符串最爬、顏色、樣式门岔,主題等爱致,在
res/values/xxx.xml
下,文件名可以為strings/colors/styles/theme
等寒随。 -
drawable
糠悯,在res/drawable/xxx.xml
-
layout
,在res/layout/xxx.xml
對于值的類型分為兩種妻往,一種是基本類型互艾,例如integer
、string
讯泣、boolean
等忘朝;另一種是引用類型,例如reference
判帮。
1.2 資源解析
資源解析涉及到兩個類,AttributeSet
和TypedArray
溉箕。
1.2.1 AttributeSet
AttributeSet
是xml
文件解析時會返回的對象晦墙,它包含了 解析元素的所有屬性及屬性值,AttributeSet
提供了一組接口可以根據(jù)attr.xml
中已有的名稱獲取相應(yīng)的值肴茄。
- 操作特定屬性
- 操作通用屬性
- 獲取特定類型的值
1.2.2 TypedArray
TypedArray
是對AttributeSet
數(shù)據(jù)類的某種抽象晌畅,對于下面在控件的xml
自定義的屬性而言:
browser:image_border_width="@dimen/light_app_icon_border_width"
如果采用AttributeSet
的方法,那么僅僅可以獲取@dimen/light_app_icon_border_width
寡痰。
如果想要獲取light_app_icon_border_width
對應(yīng)的值抗楔,那么可以通過context
的obtainStyledAttributes
將AttributeSet
作為參數(shù)構(gòu)造TypedArray
對象,從而直接獲取TypeArray
的值拦坠。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundedCornerImageView, defStyle, 0);
二连躏、實例講解
下面我們來看以下,如何在控件中使用自定義的屬性贞滨,實現(xiàn)的核心就是obtainStyledAttributes
函數(shù)入热。
2.1 直接在 View 的 xml 中指定具體的屬性
這是最直觀也最常見的方式,我們需要做的有以下三步:
- 在
res/values/attr.xml
中定義屬性名稱,為了方便管理勺良,我們一般都會將一個控件中的所有自定義屬性通過<declare-styleable>
標簽包裹起來绰播,里面的每一個<attr>
代表一個自定義屬性。 - 在
layout
布局中尚困,指定對應(yīng)的自定義屬性的屬性值蠢箩。 - 在
View
的構(gòu)造方法中,通過obtainStyledAttributes
得到TypedArray
對象事甜,obtainStyledAttributes
的第一個參數(shù)傳入構(gòu)造函數(shù)中AttributeSet
對象谬泌,它包含了該控件在xml
中聲明的屬性及其值,第二個參數(shù)就是我們在第一步中定義的styleable
讳侨。得到該TypedArray
對象后呵萨,再通過getXXX
獲得對應(yīng)的屬性值,從而實現(xiàn)通過xml
自定義控件屬性的效果跨跨。
2.1.1 代碼
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AttrTextView">
<attr name="attrTvName" format="string"/>
<attr name="attrTvColor" format="color"/>
</declare-styleable>
</resources>
public class AttrTextView extends TextView {
public AttrTextView(Context context) {
super(context);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
}
private void initAttr(Context context, @Nullable AttributeSet attrs) {
//attrs 類型為 AttributeSet潮峦,其包含了在 xml 中指定的屬性和值。
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, 0);
if (typedArray != null) {
String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
setText(text);
setTextColor(color);
typedArray.recycle();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.demo.lizejun.attrdemo.AttrTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:attrTvColor="@color/attrColorXmlDirect"
app:attrTvName="直接在 View 的 xml 中指定具體的屬性"/> <!-- 直接通過屬性來指定 -->
</FrameLayout>
2.1.2 運行結(jié)果
2.2 在 View 的 xml 中聲明 style勇婴,通過 style 間接指定屬性
2.1
中直接指定的方式忱嘹,優(yōu)點是 簡單且直觀,但是也有它的局限性耕渴,就是每次使用該控件的時候都需要對屬性進行定義拘悦,假如我們有 多個相同的控件要使用相同的樣式 時,就可以采用指定相同@style
的方式進行復用橱脸。
對于自定義的控件的Java
代碼础米,和2.1
的方式是相同的,區(qū)別在于在xml
中不指定具體的屬性值添诉,而通過style="@style/xxx"
的方式屁桑,在style
中再指定屬性。
2.2.1 代碼
public class AttrTextView extends TextView {
public AttrTextView(Context context) {
super(context);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
}
private void initAttr(Context context, @Nullable AttributeSet attrs) {
//attrs 類型為 AttributeSet栏赴,其包含了在 xml 中指定的屬性和值蘑斧。
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, 0);
if (typedArray != null) {
String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
setText(text);
setTextColor(color);
typedArray.recycle();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.demo.lizejun.attrdemo.AttrTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:attrTvColor="@color/attrColorXmlDirect"
style="@style/AttrUseXmlStyle"/>
</FrameLayout>
<resources>
<style name="AttrUseXmlStyle">
<item name="attrTvName">"在 View 的 xml 中聲明 style,通過 style 間接指定屬性"</item>
</style>
</resources>
2.2.2 運行結(jié)果
2.3 在主題中指定某個 attr 的 style须眷,并將該 attr 作為第三個參數(shù)傳給 obtainStyledAttributes
除了在xml
中通過@style
的方式進行復用竖瘾,還可以采用定義主題的方式,這樣我們就可以不必在每個xml
中都指定@style
花颗,系統(tǒng)會自動去主題當中尋找對應(yīng)的屬性值捕传,實現(xiàn)這種方案需要做以下三步:
- 在
res/values/attr.xml
中聲明一個新的AttrThemeStyle
屬性,其類型為reference
扩劝。 - 在
res/values/style.xml
中定義一個新的style
乐横,命名為AttrUseThemeStyle
求橄,它包含了自定義控件的屬性,將該AttrUseThemeStyle
在應(yīng)用的主題AppTheme
中葡公,指定給第一步定義的AttrThemeStyle
屬性罐农。 - 在自定義控件中,將
R.attr.AttrThemeStyle
作為obtainStyledAttributes
方法的第三個參數(shù)催什。
2.3.1 代碼
public class AttrTextView extends TextView {
public AttrTextView(Context context) {
super(context);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
}
private void initAttr(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, R.attr.AttrThemeStyle, 0);
if (typedArray != null) {
String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
setText(text);
setTextColor(color);
typedArray.recycle();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="AttrThemeStyle" format="reference"/>
<declare-styleable name="AttrTextView">
<attr name="attrTvName" format="string"/>
<attr name="attrTvColor" format="color"/>
</declare-styleable>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="AttrThemeStyle">@style/AttrUseThemeStyle</item>
</style>
<style name="AttrUseThemeStyle">
<item name="attrTvName">"在主題中指定某個 attr 的 style涵亏,并將該 attr 作為第三個參數(shù)傳給 obtainStyledAttributes"</item>
</style>
</resources>
2.3.2 運行結(jié)果
2.4 通過 obtainStyledAttributes 的第四個參數(shù)直接傳入一個 style,該 style 中的 item 指定字符串
最后一種方法最簡單(給所有用到這個控件的地方都設(shè)置一個默認值)蒲凶,做法就是定義一個新的style
气筋,在該style
中指定控件的自定義屬性,作為obtainStyledAttributes
的第四個參數(shù)旋圆。
2.4.1 代碼
public class AttrTextView extends TextView {
public AttrTextView(Context context) {
super(context);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
}
public AttrTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
}
private void initAttr(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AttrTextView, 0, R.style.AttrUseDirectStyle);
if (typedArray != null) {
String text = typedArray.getString(R.styleable.AttrTextView_attrTvName);
int color = typedArray.getColor(R.styleable.AttrTextView_attrTvColor, 0);
setText(text);
setTextColor(color);
typedArray.recycle();
}
}
}
<resources>
<style name="AttrUseDirectStyle">
<item name="attrTvName">"通過 obtainStyledAttributes 的第四個參數(shù)直接傳入一個 style宠默,該 style 中的 item 指定字符串"</item>
</style>
</resources>
2.4.2 運行結(jié)果
2.5 小結(jié)
對于以上四種處理方式,如果 重復指定了同一個屬性的不同值灵巧,那么將會按照優(yōu)先級的順序進行匹配搀矫,優(yōu)先級按照2.1-2.4
的順序依次遞減,最終以高優(yōu)先級指定的值為準刻肄。
也就是說瓤球,如果通過2.1
的方式直接指定了attrTvName
,那么即使通過2.2
的方式在style
中指定attrTvName
的屬性敏弃,也會以2.1
為準卦羡,使用下來我們可以發(fā)現(xiàn),它的粒度其實是依次增大的:
- 單一
xml
的屬性 - 多個
xml
復用@style
- 應(yīng)用或者
Activity
的主題復用@style
- 全局默認
@style
三麦到、夜間模式
下面绿饵,通過一個項目中的例子來介紹一下obtainStyledAttributes
的應(yīng)用,其最終效果就是通過Attr
的方式來實現(xiàn)不重啟的夜間模式切換瓶颠,項目的地址為 AttrDemo蝴罪。
該方案的原理就是給控件聲明一個屬性dayNightAttr
,該屬性指向了一個style
步清,該style
中又包含兩個新的attr
,分別指向白天和夜間模式的style
虏肾,在切換的時候廓啊,通過遍歷對應(yīng)模式的style
中的屬性,將其屬性值應(yīng)用到對應(yīng)的控件當中封豪。
3.1 使用方法
首先來看一下使用的方法谴轮,首先在控件中對dayNightAttr
進行指定:
接下來為
NightModeTextViewStyle
定義兩個style
,分別指向白天和夜間模式:
NightModeTextView
實現(xiàn)了INightMode
接口吹埠,當需要切換的時候第步,調(diào)用該接口傳入當前的主題即可
3.2 實現(xiàn)原理
首先看一下支持夜間模式的控件的實現(xiàn):
public class NightModeTextView extends AppCompatTextView implements INightMode {
private HashMap<String, Integer> mTheme = new HashMap<>();
private String mCurTheme = null;
public NightModeTextView(Context context) {
super(context);
}
public NightModeTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context, attrs, R.attr.dayNightAttr);
}
public NightModeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
private void initView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
int[] dayNightResId = DayNightHelper.getDayNightStyleId(context, attrs, defStyleAttr);
if (dayNightResId[0] != 0) {
mTheme.put(DayNightHelper.THEME_DAY, dayNightResId[0]);
}
if (dayNightResId[1] != 0) {
mTheme.put(DayNightHelper.THEME_NIGHT, dayNightResId[1]);
}
applyTheme(DayNightHelper.getCurTheme(context));
}
@Override
public void applyTheme(String theme) {
if (TextUtils.equals(mCurTheme, theme)) {
return;
}
Integer styleId = mTheme.get(theme);
if (styleId != null) {
DayNightHelper.applyTextView(this, styleId);
}
}
}
第一個關(guān)鍵的函數(shù)為DayNightHelper.getDayNightStyleId
疮装,它會解析我們在3.1
中定義的dayNightAttr
的屬性值,得到白天和夜間模式主題的ID
粘都,這里用到的方式就是我們前面看到的obtainStyledAttributes
方法廓推。
第二個關(guān)鍵的函數(shù)是applyTextView
,當?shù)玫搅藢?yīng)style
的ID
后翩隧,我們會去遍歷它下面的所有attr
樊展,然后與反射獲得的ID
對比,找到對應(yīng)的屬性堆生,然后調(diào)用View
對應(yīng)的方法去設(shè)置专缠。
public class DayNightHelper {
public static final String SP_NAME = "sp";
public static final String DAY_NIGHT_THEME_KEY = "theme_day_night";
public static final String THEME_DAY = "theme_day";
public static final String THEME_NIGHT = "theme_night";
public static int[] getDayNightStyleId(Context context, @Nullable AttributeSet attrs, int defStyle) {
int dayStyleId = 0;
int nightStyleId = 0;
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThemeCommonStyleable, defStyle, 0);
if (a != null) {
int dayNightStyleId = a.getResourceId(R.styleable.ThemeCommonStyleable_dayNightAttr, 0);
if (dayNightStyleId != 0) {
TypedArray dayNightArray = context.getTheme().obtainStyledAttributes(dayNightStyleId, R.styleable.DayNightStyleable);
if (dayNightArray != null) {
dayStyleId = dayNightArray.getResourceId(R.styleable.DayNightStyleable_themeDayAttr, 0);
nightStyleId = dayNightArray.getResourceId(R.styleable.DayNightStyleable_themeNightAttr, 0);
dayNightArray.recycle();
}
}
a.recycle();
}
return new int[]{ dayStyleId, nightStyleId };
}
public static void applyTextView(TextView textView, int styleId) {
TypedArray a = textView.getContext().getTheme().obtainStyledAttributes(styleId,
ReflectHelper.Styleable.sTextView);
if (a != null) {
int indexCount = a.getIndexCount();
for (int i = 0; i < indexCount; i++) {
int attr = a.getIndex(i);
if (attr == ReflectHelper.Styleable.sTextViewSize) {
int textSize = a.getDimensionPixelSize(attr, 0);
if (textSize != 0) {
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
} else if (attr == ReflectHelper.Styleable.sTextViewColor) {
ColorStateList textColor = a.getColorStateList(attr);
textView.setTextColor(textColor);
} else if (attr == ReflectHelper.Styleable.sText) {
String text = a.getString(attr);
textView.setText(text);
}
}
a.recycle();
}
}
public static String getCurTheme(Context context) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
return sp.getString(DAY_NIGHT_THEME_KEY, THEME_DAY);
}
public static void switchTheme(Context context) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String curTheme = sp.getString(DAY_NIGHT_THEME_KEY, THEME_DAY);
String nextTheme;
if (TextUtils.equals(curTheme, THEME_DAY)) {
nextTheme = THEME_NIGHT;
} else {
nextTheme = THEME_DAY;
}
sp.edit().putString(DAY_NIGHT_THEME_KEY, nextTheme).apply();
}
}
最后看一下反射獲取ID
值的代碼:
public class ReflectHelper {
private static final String TAG = ReflectHelper.class.getSimpleName();
public static class Styleable {
private static final Class<?> CLASS_STYLEABLE = getStyleableClass();
public static int[] sTextView;
public static int sTextViewSize;
public static int sTextViewColor;
public static int sText;
static {
try {
sTextView = (int[]) CLASS_STYLEABLE.getField("TextView").get(null);
} catch (Exception e) {
Log.w(TAG, "", e);
}
try {
sTextViewSize = CLASS_STYLEABLE.getField("TextView_textSize").getInt(null);
} catch (Exception e) {
Log.w(TAG, "", e);
}
try {
sTextViewColor = CLASS_STYLEABLE.getField("TextView_textColor").getInt(null);
} catch (Exception e) {
Log.w(TAG, "", e);
}
try {
sText = CLASS_STYLEABLE.getField("TextView_text").getInt(null);
} catch (Exception e) {
Log.w(TAG, "", e);
}
}
private static Class<?> getStyleableClass() {
try {
Class<?> clz = Class.forName("com.android.internal.R$styleable");
return clz;
} catch (Exception e) {
Log.w(TAG, "", e);
}
return null;
}
}
}