[译]控件属性的各种声明方式的比较

1,612 阅读9分钟

理解如何声明具有特定风格的TextView

TextView提供了各种各样的属性,和不同的方式去声明它们。我们可以在xml中直接定义View的属性,也可以通过style的方式去设置TextView的属性,还可以通过设置themeandroid:textAppearance的方式来设置一个控件的属性。所以,我们究竟该在什么样的情境下使用它们?如果我们混合使用,会发生什么?

本文概述了设置TextView的属性的各种方法,查看了这些方法它们的范围和优先级,以及何时应该使用何种技术。

1.结论

你应该把整篇文章读完,这里是结论

注意这里提到的各种设置属性方式的优先级顺序。如果对某个TextView声明了属性但是没有看到预期的结果,可能是你应用的样式被一个比它更高优先级的样式所覆盖

下图是样式的优先级:

关于TextView样式的一些建议

  • 可以在主题theme中使用textViewStyle来为每一个TextView指定默认的样式
  • 可以设置一些TextAppearance的样式,然后在view中直接使用
  • 也可以为TextAppearance不支持的属性,创建style,然后xml或代码中使用
  • 直接在View中指定一些非公共的属性

2.直接指定属性和利用style

尽管可以直接在xml布局中指定TextView的属性,但这种方法很容易犯错并且冗余,因为你不得不为应用中的所有TextView添加一些相同的属性。有没有一个简便的方法呢?

我们可以利用style来为TextView指定样式。这提高了代码的重复利用率并且易于更改。

所以,建议如果有相同的样式应用于多个视图,那么可以为TextView创建style。

在View上设置style,幕后的情况是什么?写过自定义View的同学有可能见过对context.obtainStyledAttributes(AttributeSet, int[], int, int)的调用。 这就是Android视图系统将layout布局中指定的属性传递给View的方式。 本质上,可以将AttributeSet参数视为在layout中指定的XML参数的映射。 如果此AttributeSet指定一种style,则首先读取该style,然后在此之上应用在xml中直接指定的属性。 这样,我们得出了第一个优先权规则。

View > Style

相较于style,直接在View中指定的属性,比如textColor,优先级会更高。需要注意的一点是,当你在xml中同时指定style和具体的属性的时候,style中定义的其他的属性并不会失效。

虽然style非常有用,但确实有其局限性。其中之一是,只能将一种style应用于一个View(不同于CSS,可以在其中应用多个类)。但是,TextView有其窍门,它提供了TextAppearance属性,其功能类似于style。如果通过TextAppearance提供文本style,则style属性可以自由用于其他样式,这听起来很有用。让我们仔细看看什么是TextAppearance及其工作方式。

TextAppearance

关于TextAppearance没有什么神奇的地方。以下代码来自TextView的源码

TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
TypedArray appearance = null;
int ap = a.getResourceId(com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
a.recycle();
if (ap != -1) {
  appearance = theme.obtainStyledAttributes(ap, com.android.internal.R.styleable.TextAppearance);
}
if (appearance != null) {
  readTextAppearance(context, appearance, attributes, false);
  appearance.recycle();
}
// a little later
a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
readTextAppearance(context, a, attributes, true);

我们可以看到,在TextView源码内部,它首先看你是否在xml中指定了android:textAppearance属性。如果是,那么加载这个style然后应用其中的所有属性。之后,它将加载在View中声明的style和所有声明的属性,因此,我们得到了我们的第二个优先级规则。

View > Style > TextAppearance

因为android:textAppearance这条属性会被最先检查,所以定义在view中的style和所有直接定义在View的属性将会覆盖textAppearance

还有一点要注意的是,TextAppearance仅仅支持TextView所提供的属性中的一部分。主要是因为这一行代码

obtainStyledAttributes(ap, android.R.styleable.TextAppearance);

我们之前看过了obtainStyledAttributes的4个参数的重载,这个2个参数的重载略有不同。 而是依据xml或代码中声明的textAppearance的style(由第一个id参数标识),并将其过滤为仅显示在第二个参数style所声明的属性。 这样,android.R.styleable.TextAppearance定义了TextAppearance所能够表示的所有属性的范围。 具体查看android.R.styleable.TextAppearance,我们可以看到TextAppearance支持TextView支持的许多属性,但不是全部。

以下是,TextAppearance支持的属性

<attr name="textColor" />
<attr name="textSize" />
<attr name="textStyle" />
<attr name="typeface" />
<attr name="fontFamily" />
<attr name="textColorHighlight" />
<attr name="textColorHint" />
<attr name="textColorLink" />
<attr name="textAllCaps" format="boolean" />
<attr name="shadowColor" format="color" />
<attr name="shadowDx" format="float" />
<attr name="shadowDy" format="float" />
<attr name="shadowRadius" format="float" />
<attr name="elegantTextHeight" format="boolean" />
<attr name="letterSpacing" format="float" />
<attr name="fontFeatureSettings" format="string" />

3.默认Style

之前在查看Android视图系统如何解析属性(context.obtainStyledAttributes)时,我们实际上简化了一些事情。 实际上View内部会将调用theme.obtainStyledAttributes来指定默认的style和主题。查看参考后,我们优化了View属性的优先级,并指定了另外两种添加属性的位置:View的默认style和theme。

先让我们看一下默认的style。 什么是默认的style? 为了回答这个问题,这里借用Button的一些属性来做一个说明,TextView也是类似的。 将<Button>放到布局中时,它看起来像这样。

为什么会是这样的样式呢?让我们看一下Button的源码

public class Button extends TextView {
  public Button(Context context) {
    this(context, null);
  }
  public Button(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.buttonStyle);
  }
  public Button(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
  }
  public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }
}

可以点击这里查看Button的源码。

那么问题来了,Button的background的属性,大写文本,ripple的属性等都是从哪里来的呢?我们可以看到在拥有两个参数的构造函数中,最后一个参数,它指定叫com.android.internal.R.attr.buttonStyle的defaultStyleAttr。这是默认样式,从本质上讲是一个间接点,允许你指定默认使用的样式。它不直接指向样式,而是让您指向主题中的一个,在解析属性时将对其进行检查。这正是您通常从中继承的所有主题所做的事情,以便为常见的小部件提供默认的外观。例如,以Material主题为例,它定义了<item name =“ buttonStyle”>@style/Widget.Material.Light.Button</item>,正是这种样式提供了所有属性,theme.obtainStyledAttributes将提供这些属性。如果您未指定其他任何内容。

让我们回到TextView,TextView还提供了一种默认样式:textViewStyle。 如果要对应用程序中的每个TextView应用某种样式,则可以非常方便。 假设我们想要始终将默认行距倍数设置为1.2。 那么可以通过设置style或者TextAppearance来做到这一点。

然而一个更好的方法可能是为应用程序中的所有TextView指定自己的默认样式。 您可以通过为自己的textViewStyle设置样式(从platfom或MaterialComponents / AppCompat的默认样式继承而来)来实现。

<style name="Theme.MyApp"
  parent="@style/Theme.MaterialComponents.Light">
  ...
  <item name="android:textViewStyle">@style/Widget.MyApp.TextView</item></style>
<style name="Widget.MyApp.TextView"
  parent="@android:style/Widget.Material.TextView">
  <item name="android:textAppearance">@style/TextAppearance.MyApp.Body</item>
  <item name="android:lineSpacingMultiplier">@dimen/text_line_spacing</item>
</style>

因此把默认的Style考虑在内,我们的优先规则变为:

View > Style > Default Style > TextAppearance

需要注意的一点是,尽管默认的style覆盖了TextAppearance,但它是会被之后的比如style和直接在View中定义的属性所覆盖。

默认的style可以非常方便。

如果你的自定义View继承了一个控件并且你没有指定你自己的默认的style,请确保在构造函数中使用父类的默认样式(不要只传递0)。 例如,如果你要扩展AppCompatTextView并编写自己的两个参数的构造函数,请确保将android.R.attr.textViewStyle作为defaultStyleAttr传递,否则你将失去父级的行为。

4.Theme

之前提到过,有一种提供style信息的方法。我们可以看到,在TextView源码中, theme.obtainStyledAttributes将会提供theme中指定的一些属性。 也就是说,我们可以在theme中设置诸如android:textColor之类的属性。 通常,将theme属性和style属性混合使用是一个比较糟糕的想法,也就是说,通常不要将那些你在View中直接定义的属性设置在theme或者style中(反之亦然),但是有一些罕见的例外情况。

例如,如果你尝试在整个app中更改字体。 你可以使用上面的任何一种技术,但是在任何地方手动设置style/textAppearance都是重复的,并且容易出错,默认的style仅在窗口小部件级别有效; 如果是继承TextView的子类,则可能会覆盖此行为,例如一个自定义View继承了Button,并且定义了自己的android:buttonStyle,但不会选择您自定义的android:textViewStyle。 因此,你可以在theme中指定字体:

<style name="Theme.MyApp"
  parent="@style/Theme.MaterialComponents.Light">
  ...
  <item name="android:fontFamily">@font/space_mono</item>
</style>

现在,除非有更高优先级内容指定了View的fontFamily这个属性,我们将在所有的地方看到View的字体都是我们想要的字体

View > Style > Default Style > Theme > TextAppearance

需要注意的是,在上面的通过theme全局设定字体的例子中,你可能希望Toolbar将会是我们指定的字体,因为在Toolbar内部包含了一个TextView。 但是,Toolbar这个类本身定义了一个默认的style,该style包含一个titleTextAppearance,在其内部指定了一个属性叫android:fontFamily,并直接在标题TextView上设置此style,从而覆盖了我们在Theme级别设置的字体。 Theme级别的style很有用,但很容易被覆盖,因此需要检查是否和我们想要得到的效果一致。

为了完整起见,我们把通过代码的方式手动设置style和span也囊括进来。因此,我们的优先级变成了这样:

Span > 代码中手动设置 > xml中直接设置View的属性 > Style > Default Style > Theme > TextAppearance

5.总结

虽然有各种各样的方式可以设置一个控件的属性,但是了解这些方式之间的差异和局限可以让我们找到最适合当前需求的方式,同时也可以减少我们的重复代码,降低出错的可能性。

这篇文章是对国外的文章的翻译,如果有什么翻译的不好或者不正确的地方欢迎评论指正

原文:What’s your text’s appearance?