前言

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

目录

image

1. 属性概述

1.1 属性的本质

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

1.2 如何定义属性?

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

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

1.3 属性的命名空间

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

image
  • 1、工具 —— toolsxmlns:tools="http://schemas.android.com/tools"

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

tools:background="@android:color/white"android:background="@android:color/black"
image.gif
  • 2、原生 —— androidxmlns:android="http://schemas.android.com/apk/res/android"

原生框架中attrs定义的属性,例如,我们找到 Android P 定义的属性 attrs.xml,其中可以看到一些我们熟知的属性:

image.gif

你也可以在 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 库中定义的属性,比如:

image.gif

你也可以在 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中自定义的属性:

        
image.gif

你也可以在 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。类似这样:

image.gif

2.2 样式的作用

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

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

image

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

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

image.gif

2.3 在 xml 中使用样式

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

image.gif

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

2.4 样式的注意事项

  • 样式不在多层级传递

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

image

3. 主题概述

3.1 主题的本质

与样式相同的是,主题(Theme)也是一组键值对的集合,但是它们的本质截然不同。样式的本质是一组可复用的 View 属性集合,而主题是 一组可引用的命名资源集合。类似这样:

image.gif

3.2 主题的作用

主题背景定义了一组可以在多处引用的资源集合,这些资源可以在样式、布局文件、代码等位置使用。使用主题,可以方便全局替换属性的值。

举个例子,首先你可以定义一套深色主题和一套浅色主题:

image.gif

然后,你在需要主题化的地方引用它,类似这样:

image.gif

此时,如果应用了 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 层
image.gif

需要注意的是,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);}
image.gif
  • 1、LayoutInflater 在进行布局解析时,需要根据 xml 实例化 View;
  • 2、在解析流程中,会判断 View 是否使用了android:theme
  • 3、如果使用,则使用 ContextThemeWrapper 包装 Context,并将包装类用于子 View 的实例化过程。

3.4 在代码中使用主题

在代码中使用主题,需要用到ContextThemeWrapper & Theme,它们都提供了设置主题资源的方法:

ContextThemeWrapper.java

@Overridepublic void setTheme(int resid) {    if (mThemeResource != resid) {        mThemeResource = resid;        最终调用的是 Theme#applyStyle(...)        initializeTheme();    }}
image.gif

Theme.java

public void applyStyle(int resId, boolean force) {    mThemeImpl.applyStyle(resId, force);}
image.gif

当构造新的 ContextThemeWrapper 之后,它会分配新的主题 (Theme) 和资源 (Resources) 实例。那么,最终主题是在哪里生效的呢,我在 第 4 节 说。

3.5 主题的注意事项

  • 主题会在多层级传递

与样式不同的是,主题对于更低层级也是有效的。举个例子,假设 Activity 设置 BlackTheme,那么对于 Activity 上的所有 View 是有效的。此时,如果其中 View 单独指定了 android:theme,那么此 View 将单独使用新的主题。

image
  • 勿使用 Application Context 加载资源

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


4. 问题回归

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

image

4.1 AttributeSet

在前面的文章里,我们已经知道 LayoutInflater 通过反射的方式实例化 View。其中的参数args分别是 Context & AttributeSet:

  • Context:上下文,有可能是包装类 ContextThemeWrapper
  • AttributeSet:属性列表,xml 中 View声明的属性都会解析到这个对象上。

LayoutInflater.java

final View view = constructor.newInstance(args);
image.gif

举个例子,假设有布局文件,我们尝试输出 LayoutInflater 实例化 View 时传入的 AttributeSet:

<...MyTextView    android:text="标签"    android:theme="@style/BlackTheme"    android:textColor="?colorPrimary"    style="@style/smallTagStyle"/>
image.gif

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));    }}
image.gif

AttributeSet.java

返回属性名称字符串(不包括命名空间)public String getAttributeValue(int index);返回属性值字符串public String getAttributeValue(int index);
image.gif

输出如下:

theme = @2131558563textColor = ?2130837590text = 标签style = @2131558752
image.gif

可以看到,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;    }}
image.gif

在这里,mAttr 数组是两个 int 值,分别是android.R.attr.textColorandroid.R.attr.layout_width,表示我们感兴趣的属性。当我们将 mAttr 用于Context#obtainStyledAttributes(),则只会解析出我们感兴趣的属性来。

输出:

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

需要注意的是,大多数情况下并不需要在代码中硬编码,而是使用标签。编译器会自动在R.java中为我们声明相同的数组,类似这样:

        
image.gif

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 
image.gif

提示: 使用R.styleable.设计的优点是:避免解析不需要的属性。

4.3 Context#obtainStyledAttributes() 取值顺序

现在,我们来讨论obtainStyledAttributes()解析属性值的优先级顺序,总共分为以下几个顺序。当在越优先的级别找到属性时,优先返回该处的属性值:View > Style > Default Style > Theme

  • View

指 xml 直接指定的属性,类似这样:

image.gif
  • Style

指 xml 在样式中指定的属性,类似这样:

复制代码
image.gif

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、属性定义
    // ...            
image.gif
  • 设置属性值
<?xml version="1.0" encoding="utf-8"?>
image.gif
  • 应用(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();}
image.gif

可以看到,pivotX 支持 float 和 fraction 两种类型,因此需要通过TypedValue#type判断属性值的类型,分别调用TypedValue#getFraction()TypedValue#getFloat()

getFraction(float base,float pbase)的两个参数为基数,最终的返回值是 基数百分数。举个例子,当设置的属性值为 50% 时,返回值为 base50% ;当设置的属性值为 50%p 时,返回值为 pbase50%*。


6. 总结

  • 应试建议
    • 应理解样式和主题的区别,两者截然不同:样式是一组可复用的 View 属性集合,而主题是一组命名的资源集合。
    • 应掌握属性来源优先级顺序:View > Style > Default Style > Theme

PS:关于我

image.png

本人是一个拥有6年开发经验的帅气Android攻城狮,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。

另外耗时两年整理收集的Android一线大厂面试完整考点PDF出炉,资料【完整版】已更新在我的【Github】,有面试需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

地址:【https://github.com/733gh/xiongfan】

更多相关文章

  1. Android高手进阶教程(四)之----Android(安卓)中自定义属性(attr.
  2. Android开发指南-框架主题-资源和资产
  3. Android(安卓)将软键盘的回车按钮改变成搜索按钮
  4. Android(安卓)Manifest.xml全局配置文件中根标签中package属性及
  5. Android(安卓)Property System | Android属性系统
  6. Android开发指南-框架主题-资源和资产
  7. Android(安卓)Layout布局文件里的android:layout_height等属性为
  8. Android(安卓)SystemProperties.get()和System.getProperty()的
  9. android 如何使用android:supportsRtl属性

随机推荐

  1. ScrollNumber 数字滚动
  2. Android热补丁动态修复技术(四):完善框架①
  3. Android 为CheckBoxPreference Preferenc
  4. Android 各种Context区别
  5. Android滚动文字效果
  6. android 按纽状态配置文件
  7. android --- 2g/3g流量问题实例
  8. android 几个快速编译image的指令
  9. Android:setLatestEventInfo问题
  10. Android ble setCharactersticNotificati