前言
- 在 Android UI 开发中,经常需要用到 属性,例如使用
android:text
设置文本框的文案,使用android:src
设置图片。那么,android:text
是如何设置到 TextView 上的呢? - 其实这个问题主要还是考察应试者对于源码(包括:LayoutInflater 布局解析、Style/Theme 系统 等)的熟悉度,在这篇文章里,我将跟你一起探讨。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
相关文章
- 《Android | 一个进程有多少个 Context 对象(答对的不多)》
- 《Android | 带你探究 LayoutInflater 布局解析原理》
- 《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?》
- 《Android | 说说从 android:text 到 TextView 的过程》
目录
1. 属性概述
1.1 属性的本质
属性 (View Attributes) 本质上是一个键值对关系,即:属性名 => 属性值。
1.2 如何定义属性?
定义属性需要用到
标签,需要定义 属性名 与 属性值类型,格式上可以分为以下 2 种:
格式 1 :
1.1 先定义属性名和属性值类型
1.2 引用上面定义的属性
格式 2:
一步到位
- 格式 1:分为两步,先定义属性名和属性值类型,然后在引用;
- 格式 2:一步到位,直接指定属性名和属性值类型。
1.3 属性的命名空间
使用属性时,需要指定属性的命名空间,命名空间用于区分属性定义的位置。目前一共有 4 种 命名空间:
- 1、工具 —— tools:
xmlns:tools="http://schemas.android.com/tools"
只在 Android Studio 中生效,运行时不生效。比如以下代码,背景色在编辑器的预览窗口显示白色,但是在运行时显示黑色:
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,其中可以看到一些我们熟知的属性:
你也可以在 SDK 中找到这个文件,有两种方法:
文件夹:sdk/platform/android-28/data/res/values/attrs.xml
Android Studio(切换到 project 视图):
External Libraries/
/res/values/attrs.xml (你在这里看到的版本号是在app/build.gradle中的
compileSdkVersion
设置的)
- 3、AppCompat 兼容库 —— 无需命名空间
Support 库 或 AndroidX 库中定义的属性,比如:
你也可以在 Android Studio 中找到这个文件:
- Android Studio(切换到 project 视图):
External Libraries/Gradle:com.android.support:appcompat-v7:[版本号]@aar/res/values/values.xml
- 4、自定义 —— app:
xmlns:app="http://schemas.android.com/apk/res-auto"
用排除法,剩下的属性就是自定义属性了。包括 项目中自定义 的属性与 依赖库中自定义 的属性,比如ConstraintLayout
中自定义的属性:
你也可以在 Android Studio 中找到这个文件:
- Android Studio(切换到 project 视图):
External Libraries/Gradle:com.android.support:constraint:constraint-layout:[版本号]@aar/res/values/values.xml
2. 样式概述
需要注意的是:虽然样式和主题长得很像,虽然两者截然不同!
2.1 样式的本质
样式(Style)是一组键值对的集合,本质上是一组可复用的 View 属性集合,代表一种类型的 Widget。类似这样:
2.2 样式的作用
使用样式可以 复用属性值,避免定义重复的属性值,便于项目维护。
随着业务功能的叠加,项目中肯定会存在一些通用的,可以复用的样式。例如在很多位置会出现的标签样式:
观察可以发现,这些标签虽然颜色不一样,但是也是有共同之处:圆角、边线宽度、字体大小、内边距。如果不使用样式,那么这些相同的属性都需要在每处标签重复声明。
此时,假设 UI 需要修改全部标签的内边距,那么就需要修改每一处便签的属性值,那就很繁琐了。而使用样式的话,就可以将重复的属性 收拢 到一份样式上,当需要修改样式时,只需要修改一个文件,类似这样:
2.3 在 xml 中使用样式
使用样式时,需要用到style=""
,类似这样:
关于这两句属性是如何生效的,我后文再说。
2.4 样式的注意事项
- 样式不在多层级传递
样式只有在使用它的 View 上才起作用,而在它的子 View 上样式是无效的。举个例子,假设 ViewGroup 有三个按钮,若设置 MyStyle 样式到此 ViewGroup 上,此时,仅这个 ViewGroup 有效,而对三个按钮来说是无效的。
3. 主题概述
3.1 主题的本质
与样式相同的是,主题(Theme)也是一组键值对的集合,但是它们的本质截然不同。样式的本质是一组可复用的 View 属性集合,而主题是 一组可引用的命名资源集合。类似这样:
3.2 主题的作用
主题背景定义了一组可以在多处引用的资源集合,这些资源可以在样式、布局文件、代码等位置使用。使用主题,可以方便全局替换属性的值。
举个例子,首先你可以定义一套深色主题和一套浅色主题:
然后,你在需要主题化的地方引用它,类似这样:
此时,如果应用了 BlackTheme ,那么 ViewGroup 的背景就是黑色;反之,如果引用了 WhiteTheme,那么 ViewGroup 的背景就是白色。
在 xml 中使用主题属性,需要用到?
,表示获得此主题中的语义属性代表的值。我把所有格式都总结在这里:
格式 | 描述 |
---|---|
android:background="?attr/colorAccent " |
/ |
android:background="?colorAccent " |
("?attr/colorAccent" 的缩写) |
android:background="?android:attr/colorAccent " |
(属性的命名空间为 android) |
android:background="?android:colorAccent " |
("?android:attr/colorAccent") |
3.3 在 xml 中使用主题
在 xml 中使用主题,需要用到android:theme
,类似这样:
1. 应用层
2. Activity 层
3. View 层
需要注意的是,android:theme
本质上也是用到 ContextThemeWrapper 来使用主题的,这在我之前写过的两篇文章里说过:《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?》、《Android | 带你探究 LayoutInflater 布局解析原理》。这里我简单复述一下:
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) {
构造 ContextThemeWrapper
context = new ContextThemeWrapper(context, themeResId);
}
- 1、LayoutInflater 在进行布局解析时,需要根据 xml 实例化 View;
- 2、在解析流程中,会判断 View 是否使用了
android:theme
; - 3、如果使用,则使用 ContextThemeWrapper 包装 Context,并将包装类用于子 View 的实例化过程。
3.4 在代码中使用主题
在代码中使用主题,需要用到ContextThemeWrapper & Theme
,它们都提供了设置主题资源的方法:
ContextThemeWrapper.java
@Override
public void setTheme(int resid) {
if (mThemeResource != resid) {
mThemeResource = resid;
最终调用的是 Theme#applyStyle(...)
initializeTheme();
}
}
Theme.java
public void applyStyle(int resId, boolean force) {
mThemeImpl.applyStyle(resId, force);
}
当构造新的 ContextThemeWrapper 之后,它会分配新的主题 (Theme) 和资源 (Resources) 实例。那么,最终主题是在哪里生效的呢,我在 第 4 节 说。
3.5 主题的注意事项
- 主题会在多层级传递
与样式不同的是,主题对于更低层级也是有效的。举个例子,假设 Activity 设置 BlackTheme,那么对于 Activity 上的所有 View 是有效的。此时,如果其中 View 单独指定了 android:theme,那么此 View 将单独使用新的主题。
- 勿使用 Application Context 加载资源
Application 是 ContextWrapper 的子类,因此Application Context 不保留任何主题相关信息,在 manifest 中设置的主题仅用作未明确设置主题背景的 Activity 的默认选择。切勿使用 Application Context 加载可使用的资源。
4. 问题回归
现在,我们回过头来讨论 从 android:text 到 TextView 的过程。其实,这说的是如何将android:text
属性值解析到 TextView 上。这个过程就是 LayoutInflater 布局解析的过程,我之前专门写过一篇文章探讨布局解析的核心过程:《Android | 带你探究 LayoutInflater 布局解析原理》,核心过程如下图:
4.1 AttributeSet
在前面的文章里,我们已经知道 LayoutInflater 通过反射的方式实例化 View。其中的参数args
分别是 Context & AttributeSet:
- Context:上下文,有可能是包装类 ContextThemeWrapper
- AttributeSet:属性列表,xml 中 View声明的属性都会解析到这个对象上。
LayoutInflater.java
final View view = constructor.newInstance(args);
举个例子,假设有布局文件,我们尝试输出 LayoutInflater 实例化 View 时传入的 AttributeSet:
<...MyTextView
android:text="标签"
android:theme="@style/BlackTheme"
android:textColor="?colorPrimary"
style="@style/smallTagStyle"/>
MyTextView.java
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
总共有 4 个属性
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 = 标签
style = @2131558752
可以看到,AttributeSet 里只包含了在 xml 中直接声明的属性,对于引用类型的属性,AttributeSet 只是记录了资源 ID,并不会把它拆解开来。
4.2 TypedArray
想要取到真实的属性值,需要用到 TypeArray,另外还需要一个 int 数组(其中,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. 解析每个属性
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 数组是两个 int 值,分别是android.R.attr.textColor
和android.R.attr.layout_width
,表示我们感兴趣的属性。当我们将 mAttr 用于Context#obtainStyledAttributes()
,则只会解析出我们感兴趣的属性来。
输出:
-16777216 ,即:Color.BLACK => 这个值来自于 ?attr/colorPrimary 引用的主题属性
-2 ,即:WRAP_CONTENT => 这个值来自于 @style/smallTagStyle 中引用的样式属性
需要注意的是,大多数情况下并不需要在代码中硬编码,而是使用
标签。编译器会自动在R.java
中为我们声明相同的数组,类似这样:
R.java
public static final int[] MyTextView={ 相当于 mAttr
0x01010098, 0x010100f4
};
public static final int MyTextView_android_textColor=0; 相当于 ATTR_ANDROID_TEXTCOLOR
public static final int MyTextView_android_layout_width=1; 相当于 ATTR_ANDROID_LAYOUT_WIDTH
提示: 使用
R.styleable.
设计的优点是:避免解析不需要的属性。
4.3 Context#obtainStyledAttributes() 取值顺序
现在,我们来讨论obtainStyledAttributes()
解析属性值的优先级顺序,总共分为以下几个顺序。当在越优先的级别找到属性时,优先返回该处的属性值:View > Style > Default Style > Theme。
- View
指 xml 直接指定的属性,类似这样:
- Style
指 xml 在样式中指定的属性,类似这样:
5. 属性值类型
前文提到,定义属性需要指定:属性名 与 属性值类型,属性值类型可以分为资源类与特殊类
5.1 资源类
属性值类型 | 描述 | TypedArray |
---|---|---|
fraction | 百分数 | getFraction(...) |
float | 浮点数 | getFloat(...) |
boolean | 布尔值 | getBoolean(...) |
color | 颜色值 | getColor(...) |
string | 字符串 | getString(...) |
dimension | 尺寸值 | getDimensionPixelOffset(…) getDimensionPixelSize(...) getDimension(...) |
integer | 整数值 | getInt(...) getInteger(...) |
5.2 特殊类
属性值类型 | 描述 | TypedArray |
---|---|---|
flag | 标志位 | getInt(...) |
enum | 枚举值 | getInt(…)等 |
reference | 资源引用 | getDrawable(...)等 |
fraction 比较难理解,这里举例解释下:
- 1、属性定义
// ...
- 设置属性值
- 应用(RotateDrawable)
if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
// 取出对应的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 两种类型,因此需要通过TypedValue#type
判断属性值的类型,分别调用TypedValue#getFraction()
与TypedValue#getFloat()
。
getFraction(float base,float pbase)
的两个参数为基数,最终的返回值是 基数*百分数。举个例子,当设置的属性值为 50% 时,返回值为 base50%* ;当设置的属性值为 50%p 时,返回值为 pbase*50%。
6. 总结
- 应试建议
- 应理解样式和主题的区别,两者截然不同:样式是一组可复用的 View 属性集合,而主题是一组命名的资源集合。
- 应掌握属性来源优先级顺序:View > Style > Default Style > Theme
参考资料
- 《Android 样式系统 | 主题背景和样式》 —— 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 编程权威指南》[美]Bill Phillips, Chris Stewart, Kristin Marsicano 著
推荐阅读
- 密码学 | Base64是加密算法吗?
- 算法面试题 | 回溯算法解题框架
- 算法面试题 | 链表问题总结
- Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?
- Android | 面试必问的 Handler,你确定不看看?
- 计算机组成原理 | Unicode 和 UTF-8是什么关系?
- 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)
- 计算机网络 | 图解 DNS & HTTPDNS 原理