Android | 说说从 android:text 到 TextView 的过程

前言

  • 在 Android UI 开发中,经常需要用到 属性,例如使用android:text设置文本框的文案,使用android:src设置图片。那么,android:text是如何设置到 TextView 上的呢?
  • 其实这个问题主要还是考察应试者对于源码(包括:LayoutInflater 布局解析、Style/Theme 系统 等)的熟悉度,在这篇文章里,我将跟你一起探讨。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

相关文章


目录


1. 属性概述

1.1 属性的本质

属性 (View Attributes) 本质上是一个键值对关系,即:属性名 => 属性值。

1.2 如何定义属性?

定义属性需要用到<declare-styleable>标签,需要定义 属性名属性值类型,格式上可以分为以下 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、工具 —— toolsxmlns:tools="http://schemas.android.com/tools"

只在 Android Studio 中生效,运行时不生效。比如以下代码,背景色在编辑器的预览窗口显示白色,但是在运行时显示黑色:

tools:background="@android:color/white"
android:background="@android:color/black"
  • 2、原生 —— androidxmlns: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/<Android API 28 Platform>/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、自定义 —— appxmlns: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.textColorandroid.R.attr.layout_width,表示我们感兴趣的属性。当我们将 mAttr 用于Context#obtainStyledAttributes(),则只会解析出我们感兴趣的属性来。

输出:

-16777216 ,即:Color.BLACK => 这个值来自于 ?attr/colorPrimary 引用的主题属性
-2 ,即:WRAP_CONTENT => 这个值来自于 @style/smallTagStyle 中引用的样式属性

需要注意的是,大多数情况下并不需要在代码中硬编码,而是使用<declare-styleable>标签。编译器会自动在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>
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

参考资料

推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341