前言
- 在 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 先定义属性名和属性值类型 <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 属性的命名空间
使用属性时,需要指定属性的命名空间,命名空间用于区分属性定义的位置。目前一共有 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,其中可以看到一些我们熟知的属性:
<!-- 文本颜色 --> <attr name="textColor" format="reference|color"/> <!-- 高亮文本颜色 --> <attr name="textColorHighlight" format="reference|color" /> <!-- 高亮文本颜色 --> <attr name="textColorHint" format="reference|color" /> 复制代码
你也可以在 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 库中定义的属性,比如:
<attr format="color" name="colorAccent"/> 复制代码
你也可以在 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
中自定义的属性:
<attr format="reference|enum" name="layout_constraintBottom_toBottomOf"> <enum name="parent" value="0"/> </attr> 复制代码
你也可以在 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。类似这样:
<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 样式的作用
使用样式可以 复用属性值,避免定义重复的属性值,便于项目维护。
随着业务功能的叠加,项目中肯定会存在一些通用的,可以复用的样式。例如在很多位置会出现的标签样式:
观察可以发现,这些标签虽然颜色不一样,但是也是有共同之处:圆角、边线宽度、字体大小、内边距。如果不使用样式,那么这些相同的属性都需要在每处标签重复声明。
此时,假设 UI 需要修改全部标签的内边距,那么就需要修改每一处便签的属性值,那就很繁琐了。而使用样式的话,就可以将重复的属性 收拢 到一份样式上,当需要修改样式时,只需要修改一个文件,类似这样:
<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 中使用样式
使用样式时,需要用到style=""
,类似这样:
<TextView android:text="标签" style="@style/smallTagStyle"/> 复制代码
关于这两句属性是如何生效的,我后文再说。
2.4 样式的注意事项
- 样式不在多层级传递
样式只有在使用它的 View 上才起作用,而在它的子 View 上样式是无效的。举个例子,假设 ViewGroup 有三个按钮,若设置 MyStyle 样式到此 ViewGroup 上,此时,仅这个 ViewGroup 有效,而对三个按钮来说是无效的。
3. 主题概述
3.1 主题的本质
与样式相同的是,**主题(Theme)**也是一组键值对的集合,但是它们的本质截然不同。样式的本质是一组可复用的 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 主题的作用
主题背景定义了一组可以在多处引用的资源集合,这些资源可以在样式、布局文件、代码等位置使用。使用主题,可以方便全局替换属性的值。
举个例子,首先你可以定义一套深色主题和一套浅色主题:
<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"> 复制代码
此时,如果应用了 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. 应用层 <application … android:theme="@style/BlackTheme "> 2. Activity 层 <activity … android:theme="@style/BlackTheme "/> 3. View 层 <ConstraintLayout … android:theme="@style/BlackTheme "> 复制代码
需要注意的是,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
中为我们声明相同的数组,类似这样:
<declare-styleable name="MyTextView"> <attr name="android:textColor" /> <attr name="android:layout_width" /> </declare-styleable> 复制代码
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 直接指定的属性,类似这样:
<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 构造函数中指定的样式,它是构造方法的第 3 个参数,类似于 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
属性,这个值在主题资源中指定的是一个样式资源:
<item name="android:textViewStyle">@style/Widget.AppCompat.TextView</item> 复制代码
提示: 从
@AttrRes
可以看出,defStyleAttr
一定要引用主题属性。
- Default Style Resource
指在 View 构造函数中指定的样式资源,它是构造方法的第 3 个参数:
public View(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { } 复制代码
提示: 从
@StyleRes
可以看出,defStyleRes
一定要引用样式资源。
- Theme
如果以上层级全部无法匹配到属性,那么就会使用主题中的主题属性,类似这样:
<style name="AppTheme" parent="..."> ... <item name="android:textColor">@color/black</item> </style> 复制代码
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、属性定义
<declare-styleable name="RotateDrawable"> // ... <attr name="pivotX" format="float|fraction" /> <attr name="pivotY" format="float|fraction" /> <attr name="drawable" /> </declare-styleable> 复制代码
- 设置属性值
<?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> 复制代码
- 应用(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% 时,返回值为 base*50% ;当设置的属性值为 50%p 时,返回值为 pbase*50%。
6. 总结
- 应试建议
- 应理解样式和主题的区别,两者截然不同:样式是一组可复用的 View 属性集合,而主题是一组命名的资源集合。
- 应掌握属性来源优先级顺序:View > Style > Default Style > Theme