Android | 说说从 android:text 到 TextView 的过程(主题&样式)

简介: Android | 说说从 android:text 到 TextView 的过程(主题&样式)

前言


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


相关文章



目录

image.png


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 种 命名空间:

image.png

  • 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//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 样式的作用


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

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

image.png


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

此时,假设 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 有效,而对三个按钮来说是无效的。


image.png

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

image.png

  • 勿使用 Application Context 加载资源

Application 是 ContextWrapper 的子类,因此Application Context 不保留任何主题相关信息,在 manifest 中设置的主题仅用作未明确设置主题背景的 Activity 的默认选择。切勿使用 Application Context 加载可使用的资源。


4. 问题回归


现在,我们回过头来讨论 从 android:text 到 TextView 的过程。其实,这说的是如何将android:text属性值解析到 TextView 上。这个过程就是 LayoutInflater 布局解析的过程,我之前专门写过一篇文章探讨布局解析的核心过程:《Android | 带你探究 LayoutInflater 布局解析原理》,核心过程如下图:


image.png

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中为我们声明相同的数组,类似这样:


<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% 时,返回值为 base*50% ;当设置的属性值为 50%p 时,返回值为 pbase*50%


6. 总结


  • 应试建议
  • 应理解样式和主题的区别,两者截然不同:样式是一组可复用的 View 属性集合,而主题是一组命名的资源集合。
  • 应掌握属性来源优先级顺序:View > Style > Default Style > Theme
目录
相关文章
|
1月前
|
Android开发 开发者
Android UI设计: 请解释Activity的Theme是什么,如何更改应用程序的主题?
Android UI设计: 请解释Activity的Theme是什么,如何更改应用程序的主题?
74 1
|
14天前
|
XML IDE 开发工具
13. 【Android教程】文本框 TextView
13. 【Android教程】文本框 TextView
10 2
|
24天前
|
XML 搜索推荐 Java
Android TextView的字体设置
【5月更文挑战第13天】
|
1月前
|
算法 Android开发
Compose - Text 详解,2024年Android社招面试题精选
Compose - Text 详解,2024年Android社招面试题精选
Compose - Text 详解,2024年Android社招面试题精选
|
1月前
|
Android开发
android TextView HTML 的效果
android TextView HTML 的效果
16 2
|
1月前
|
XML Android开发 数据格式
Android下自定义Button样式
Android下自定义Button样式
18 3
|
1月前
|
XML Java Android开发
Android控件之基础控件——进度条类的view——TextView、Checkbox复选控件、RadioButton单选控件、ToggleButton开关、SeekBar拖动条、menu、弹窗
Android控件之基础控件——进度条类的view——TextView、Checkbox复选控件、RadioButton单选控件、ToggleButton开关、SeekBar拖动条、menu、弹窗
|
1月前
|
Android开发
Android开发小技巧:怎样在 textview 前面加上一个小图标。
Android开发小技巧:怎样在 textview 前面加上一个小图标。
22 0
|
10月前
|
前端开发 Android开发
前端项目实战壹-安卓4.4/6.0无法安装非原生项目无法渲染样式
前端项目实战壹-安卓4.4/6.0无法安装非原生项目无法渲染样式
51 0
|
11月前
|
XML Java Android开发