View 绘制体系知识梳理(8) - obtainStyledA
一、基本概念
1.1 资源
Android
使用xml
文件来描述各种资源,包括字符串、颜色、主题、布局等等。资源分为两个部分,及 值 和 属性。
1.1.1 属性
在App
开发的过程中,如果需要为自定义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
则是其内部的属性,在R.java
文件中declare-styleable
对应一个int[]
数组。
需要注意的是:
- 如果
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
提供了一组接口可以根据attr.xml
中已有的名称获取相应的值。
- 操作特定属性
- 操作通用属性
- 获取特定类型的值
1.2.2 TypedArray
TypedArray
是对AttributeSet
数据类的某种抽象,对于下面在控件的xml
自定义的属性而言:
browser:image_border_width="@dimen/light_app_icon_border_width"
如果采用AttributeSet
的方法,那么仅仅可以获取@dimen/light_app_icon_border_width
。
如果想要获取light_app_icon_border_width
对应的值,那么可以通过context
的obtainStyledAttributes
将AttributeSet
作为参数构造TypedArray
对象,从而直接获取TypeArray
的值。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundedCornerImageView, defStyle, 0);
二、实例讲解
下面我们来看以下,如何在控件中使用自定义的属性,实现的核心就是obtainStyledAttributes
函数。
2.1 直接在 View 的 xml 中指定具体的属性
这是最直观也最常见的方式,我们需要做的有以下三步:
- 在
res/values/attr.xml
中定义属性名称,为了方便管理,我们一般都会将一个控件中的所有自定义属性通过<declare-styleable>
标签包裹起来,里面的每一个<attr>
代表一个自定义属性。 - 在
layout
布局中,指定对应的自定义属性的属性值。 - 在
View
的构造方法中,通过obtainStyledAttributes
得到TypedArray
对象,obtainStyledAttributes
的第一个参数传入构造函数中AttributeSet
对象,它包含了该控件在xml
中声明的属性及其值,第二个参数就是我们在第一步中定义的styleable
。得到该TypedArray
对象后,再通过getXXX
获得对应的属性值,从而实现通过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 运行结果
2.2 在 View 的 xml 中声明 style,通过 style 间接指定属性
2.1
中直接指定的方式,优点是 简单且直观,但是也有它的局限性,就是每次使用该控件的时候都需要对属性进行定义,假如我们有 多个相同的控件要使用相同的样式 时,就可以采用指定相同@style
的方式进行复用。
对于自定义的控件的Java
代码,和2.1
的方式是相同的,区别在于在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 运行结果
2.3 在主题中指定某个 attr 的 style,并将该 attr 作为第三个参数传给 obtainStyledAttributes
除了在xml
中通过@style
的方式进行复用,还可以采用定义主题的方式,这样我们就可以不必在每个xml
中都指定@style
,系统会自动去主题当中寻找对应的属性值,实现这种方案需要做以下三步:
- 在
res/values/attr.xml
中声明一个新的AttrThemeStyle
属性,其类型为reference
。 - 在
res/values/style.xml
中定义一个新的style
,命名为AttrUseThemeStyle
,它包含了自定义控件的属性,将该AttrUseThemeStyle
在应用的主题AppTheme
中,指定给第一步定义的AttrThemeStyle
属性。 - 在自定义控件中,将
R.attr.AttrThemeStyle
作为obtainStyledAttributes
方法的第三个参数。
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 作为第三个参数传给 obtainStyledAttributes"</item>
</style>
</resources>
2.3.2 运行结果
2.4 通过 obtainStyledAttributes 的第四个参数直接传入一个 style,该 style 中的 item 指定字符串
最后一种方法最简单(给所有用到这个控件的地方都设置一个默认值),做法就是定义一个新的style
,在该style
中指定控件的自定义属性,作为obtainStyledAttributes
的第四个参数。
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 的第四个参数直接传入一个 style,该 style 中的 item 指定字符串"</item>
</style>
</resources>
2.4.2 运行结果
2.5 小结
对于以上四种处理方式,如果 重复指定了同一个属性的不同值,那么将会按照优先级的顺序进行匹配,优先级按照2.1-2.4
的顺序依次递减,最终以高优先级指定的值为准。
也就是说,如果通过2.1
的方式直接指定了attrTvName
,那么即使通过2.2
的方式在style
中指定attrTvName
的属性,也会以2.1
为准,使用下来我们可以发现,它的粒度其实是依次增大的:
- 单一
xml
的属性 - 多个
xml
复用@style
- 应用或者
Activity
的主题复用@style
- 全局默认
@style
三、夜间模式
下面,通过一个项目中的例子来介绍一下obtainStyledAttributes
的应用,其最终效果就是通过Attr
的方式来实现不重启的夜间模式切换,项目的地址为 AttrDemo。
该方案的原理就是给控件声明一个属性dayNightAttr
,该属性指向了一个style
,该style
中又包含两个新的attr
,分别指向白天和夜间模式的style
,在切换的时候,通过遍历对应模式的style
中的属性,将其属性值应用到对应的控件当中。
3.1 使用方法
首先来看一下使用的方法,首先在控件中对dayNightAttr
进行指定:
接下来为
NightModeTextViewStyle
定义两个style
,分别指向白天和夜间模式:
style 的定义
NightModeTextView
实现了INightMode
接口,当需要切换的时候,调用该接口传入当前的主题即可
切换方式
3.2 实现原理
首先看一下支持夜间模式的控件的实现:
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);
}
}
}
第一个关键的函数为DayNightHelper.getDayNightStyleId
,它会解析我们在3.1
中定义的dayNightAttr
的属性值,得到白天和夜间模式主题的ID
,这里用到的方式就是我们前面看到的obtainStyledAttributes
方法。
第二个关键的函数是applyTextView
,当得到了对应style
的ID
后,我们会去遍历它下面的所有attr
,然后与反射获得的ID
对比,找到对应的属性,然后调用View
对应的方法去设置。
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;
}
}
}