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

前言

  • 在 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 的过程》

目录

Android | 说说从 android:text 到 TextView 的过程_第1张图片

1. 属性概述

1.1 属性的本质

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

1.2 如何定义属性?

定义属性需要用到标签,需要定义 属性名属性值类型,格式上可以分为以下 2 种:

格式 1 :

1.1 先定义属性名和属性值类型



    1.2 引用上面定义的属性
    


格式 2:


    一步到位
    

  • 格式 1:分为两步,先定义属性名和属性值类型,然后在引用;
  • 格式 2:一步到位,直接指定属性名和属性值类型。

1.3 属性的命名空间

使用属性时,需要指定属性的命名空间,命名空间用于区分属性定义的位置。目前一共有 4 种 命名空间:

Android | 说说从 android:text 到 TextView 的过程_第2张图片
  • 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,其中可以看到一些我们熟知的属性:







你也可以在 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、自定义 —— appxmlns: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 样式的作用

使用样式可以 复用属性值,避免定义重复的属性值,便于项目维护

随着业务功能的叠加,项目中肯定会存在一些通用的,可以复用的样式。例如在很多位置会出现的标签样式:

Android | 说说从 android:text 到 TextView 的过程_第3张图片

观察可以发现,这些标签虽然颜色不一样,但是也是有共同之处:圆角、边线宽度、字体大小、内边距。如果不使用样式,那么这些相同的属性都需要在每处标签重复声明。

此时,假设 UI 需要修改全部标签的内边距,那么就需要修改每一处便签的属性值,那就很繁琐了。而使用样式的话,就可以将重复的属性 收拢 到一份样式上,当需要修改样式时,只需要修改一个文件,类似这样:


2.3 在 xml 中使用样式

使用样式时,需要用到style="",类似这样:


关于这两句属性是如何生效的,我后文再说。

2.4 样式的注意事项

  • 样式不在多层级传递

样式只有在使用它的 View 上才起作用,而在它的子 View 上样式是无效的。举个例子,假设 ViewGroup 有三个按钮,若设置 MyStyle 样式到此 ViewGroup 上,此时,仅这个 ViewGroup 有效,而对三个按钮来说是无效的。

Android | 说说从 android:text 到 TextView 的过程_第4张图片

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 将单独使用新的主题。

Android | 说说从 android:text 到 TextView 的过程_第5张图片
  • 勿使用 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 中引用的样式属性

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

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

你可能感兴趣的:(Android | 说说从 android:text 到 TextView 的过程)