Android开发经验谈Android技术知识Android开发

面试官:作为Android高级攻城狮,请你解释一下 androi

2020-10-31  本文已影响0人  码农的书柜

前言

目录

image

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

1.3 属性的命名空间

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

image

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

tools:background="@android:color/white"
android:background="@android:color/black"
image.gif

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

<!-- 文本颜色 -->
<attr name="textColor" format="reference|color"/>
<!-- 高亮文本颜色 -->
<attr name="textColorHighlight" format="reference|color" />
<!-- 高亮文本颜色 -->
<attr name="textColorHint" format="reference|color" />
image.gif

你也可以在 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设置的)

Support 库 或 AndroidX 库中定义的属性,比如:

<attr format="color" name="colorAccent"/>
image.gif

你也可以在 Android Studio 中找到这个文件:

  • Android Studio(切换到 project 视图):External Libraries/Gradle:com.android.support:appcompat-v7:[版本号]@aar/res/values/values.xml

用排除法,剩下的属性就是自定义属性了。包括 项目中自定义 的属性与 依赖库中自定义 的属性,比如ConstraintLayout中自定义的属性:

<attr format="reference|enum" name="layout_constraintBottom_toBottomOf">
        <enum name="parent" value="0"/>
</attr>
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。类似这样:

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

2.2 样式的作用

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

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

image

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

此时,假设 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>
image.gif

2.3 在 xml 中使用样式

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

<TextView
    android:text="标签"
    style="@style/smallTagStyle"/>
image.gif

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

2.4 样式的注意事项

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

image

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

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

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

<ViewGroup …
    android:background="?attr/colorPrimary">
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. 应用层
<application …
    android:theme="@style/BlackTheme ">

2. Activity 层
<activity …
    android:theme="@style/BlackTheme "/>

3. View 层
<ConstraintLayout …
    android:theme="@style/BlackTheme ">
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

3.4 在代码中使用主题

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

ContextThemeWrapper.java

@Override
public 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 是 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:

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 = @2131558563
textColor = ?2130837590
text = 标签
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

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

<declare-styleable name="MyTextView">
    <attr name="android:textColor" />
    <attr name="android:layout_width" />
</declare-styleable>
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

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

<TextView
    ...
    android:textColor="@color/black"/>
image.gif

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

<TextView
    ...
    android:textColor="@style/colorTag"/>

<style name="colorTag">
    <item name="android:textColor">@color/black</item>
image.gif

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

其中,android.R.attr.textViewStyle表示引用主题中的textViewStyle属性,这个值在主题资源中指定的是一个样式资源:

<item name="android:textViewStyle">@style/Widget.AppCompat.TextView</item>
image.gif

提示:@AttrRes可以看出,defStyleAttr 一定要引用主题属性。

指在 View 构造函数中指定的样式资源,它是构造方法的第 3 个参数:

public View(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
}
image.gif

提示:@StyleRes 可以看出,defStyleRes 一定要引用样式资源。

如果以上层级全部无法匹配到属性,那么就会使用主题中的主题属性,类似这样:

<style name="AppTheme" parent="...">
    ...
    <item name="android:textColor">@color/black</item>
</style>
复制代码
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 比较难理解,这里举例解释下:

<declare-styleable name="RotateDrawable">
    // ...
    <attr name="pivotX" format="float|fraction" />
    <attr name="pivotY" format="float|fraction" />
    <attr name="drawable" />
</declare-styleable>
image.gif
<?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>
image.gif
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. 总结

PS:关于我

image.png

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

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

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

上一篇 下一篇

猜你喜欢

热点阅读