Android 中View的四个构造函数

I often see confusion around Android View constructors. Why are there four of them? What does each parameter do? Which constructors do I need to implement?

我经常对View的构造函数感到很困惑,为什么会有四个?每个参数表示什么?我应该需要实现那些构造函数?

If you just want quick, practical advice, here's a few good guidelines:
如果你只想知道快速,并且实用的建议。如下

  • Use View(Context) for creating Views in code.
    Override View(Context, AttributeSet) when inflating Views from XML.
    在代码中使用View(Context)来创建View,如果要用在XML文件中就需要重写 View(Context, AttributeSet)构造函数。
  • Ignore the rest because you probably won't need them.
    忽略其他两个构造函数,你可能不需要他们。

For those still with me - let's dive in.

Constructor parameters
At most, there can be four constructor parameters. A brief summary:
基本上,存在四个构造函数参数,一个简洁的总结

  1. Context - Used all over the place in Views.
    上下文
  2. AttributeSet - The XML attributes (when inflating from XML).
    XML属性
  3. int defStyleAttr - A default style to apply to the View (defined in the theme).
    一个默认的风格应用到View,在theme中定义。
  4. int defStyleResource - A default style to apply to the View, if defStyleAttr is unused.
    一个默认的风格应用到View,如果defStyleAttr没有使用。

Besides the Context, the other parameters are only used to configure the initial state of the View based on XML attributes (from layout, styles and themes).

除了Context,其他参数都是通过XML来配置View的初始状态。(来自于layout,styles,themes)

Attributes
Let's start by talking about how you define valid XML attributes. Here's a basic ImageView in XML:

<ImageView  
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@drawable/icon"
  />

Have you ever wondered where layout_width, layout_height and src come from? It's not out of thin air; you actually declare these attributes explicitly as something the system should handle via <declare-styleable>

你是否好奇layout_width, layout_height and src从哪里来,它并不是无中生有;它在系统的<declare-styleable中声明了

For example, here's where src is defined:

<declare-styleable name="ImageView">  
  <!-- Sets a drawable as the content of this ImageView. -->
  <attr name="src" format="reference|color" />

  <!-- ...snipped for brevity... -->

</declare-styleable>  

Each declare-styleable generates one R.styleable.[name] plus an additional R.styleable.[name]_[attribute] for each attribute. For example, the above generates R.styleable.ImageView and R.styleable.ImageView_src.

每一个declare-styleable都会生成一个R.styleable.[name],节点里面的每个属性(attribute)都会生成一个R.styleable.[name]_[attribute]的资源,例如,上面代码,会生成一个R.styleable.ImageViewR.styleable.ImageView_src

What are these resources? The base R.styleable.[name] is an array of all the attribute resources, which the system uses to lookup attribute values. Each R.styleable.[name]_[attribute] is just an index into that array, so that you can retrieve all the attributes at once, then lookup each value individually.

这些资源是什么?R.styleable.[name]是个数组,里面包含了所有的属性资源,系统用它来寻找属性所对应的值(也就是资源)。每一个R.styleable.[name]_[attribute] 只是上面数组里面的索引(index),通过它就可以获得所有属性的值。

If you think of it like a cursor, you can consider R.styleable.[name] as list of columns to query and each R.styleable.[name]_[attribute] as a column index.

你可以把它想象成一个cursor,把R.styleable.[name]当成要查询字段的集合,R.styleable.[name]_[attribute]就是这些字段的索引。

For more on declare-styleable, here's the official documentation on creating your own.

想要了解更多有关declare-styleable 可以去查看官方文档

AttributeSet

The XML we wrote above is given to the View as an AttributeSet.

Usually you don't access AttributeSet directly, but instead use Theme.obtainStyledAttributes(). That's because the raw attributes often need to resolve references and apply styles. For example, if you define style=@style/MyStyle in your XML, this method resolves MyStyle and adds its attributes to the mix. In the end, obtainStyledAttributes() returns a TypedArray which you can use to access the attributes.

通常你不会直接去访问AttributeSet,而是通过使用Theme.obtainStyledAttributes()方法去访问。这是因为这些attributes需要去解析引用和应用样式。例如,如果你在你的XML中定义了style=@style/MyStyle,这个方法可以解析MyStyle并且把它的attributes添加到属性集合中。最后obtainStyledAttributes() 返回一个TypedArray,通过TypedArray可以访问这些attributes。

Greatly simplified, the process looks like this:
通常来说,处理过程如下:

public ImageView(Context context, AttributeSet attrs) {  
  TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ImageView, 0, 0);
  Drawable src = ta.getDrawable(R.styleable.ImageView_src);
  setImageDrawable(src);
  ta.recycle();
}

In this case, we're passing two parameters to obtainStyledAttributes(). The first is AttributeSet attrs, the attributes from XML. The second is the array R.styleable.ImageView, which tells the method which attributes we want to extract (and in what order).

在这里,我们传了两个参数给obtainStyledAttributes()函数,第一参数就是AttributeSet attrs,是所有来源于XML的attributes。第二个参数就是R.styleable.ImageView数组,它告诉了这个方法那些attributes我们想要去提取(比如src属性,而不是sra其他属性)。

With the TypedArray we get back, we can now access the individual attributes. We need to use R.styleable.ImageView_src so that we correctly index the attribute in the array.
通过使用返回的TypedArray对象,我们可以去访问这些独立的attributes。我们需要使用R.styleable.ImageView_src作为index来正确的提取该属性(src属性)所对应的值(图片的引用)
(Recycling TypedArrays is important, too, so I left it in the sample above.)
回收TypedArrays是非常重要的,所以我在上面的代码中写上了。

Normally you extract multiple attributes at once. Indeed, the actual ImageView implementation is far more complex than what's shown above (since ImageView itself has many more attributes it cares about).
通常你会一次性提取多个attributes。确实,实际上ImageView的实现比上面代码要复杂的多。

Theme Attributes

A sidenote, for completeness: The AttributeSet is not the only place we got our values from when using obtainStyledAttributes() in the last section. Attributes can also exist in the theme.

AttributeSet并不是obtainStyledAttributes()获取值得唯一来源,Attributes(属性)也可以来源于Theme。

This rarely plays a role for View inflation because your theme shouldn't be setting attributes like src, but it can play a role if you use obtainStyledAttributes() for retrieving theme attributes (which is useful but is outside the scope of this article).
如果你打算从theme中获取的属性来初始化View,那么这种情况是很少见的,毕竟我们通常不会在theme中设置src这样的属性,但是如果你是想使用obtainStyledAttributes()来检索theme的属性,它确实有点作用。不过这不在本文的讨论范围内。

Default Style Attribute

You may have noticed that I used 0 for the last two parameters in obtainStyledAttributes(). They are actually two resource references - defStyleAttr and defStyleRes. I'm going to focus on the first one here.

你可能已经注意到在obtainStyledAttributes()函数中我使用了0来作为后面两个参数的值。这两个参数实际上是两个资源的引用--defStyleAttr 和 defStyleRes。后面我将重点放在第一个参数(defStyleAttr )上。

defStyleAttr is, by far, the most confusing parameter for obtainStyledAttributes(). According to the documentation it is:

defStyleAttr是obtainStyledAttributes()函数中最让人感到困惑的参数。下面是官方文档的说明。

An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the TypedArray.

Whew, that's a mouthful. In plain English, it's a way to be able to define a base style for all Views of a certain type. For example, you can set textViewStyle in your theme if you want to modify all your app's TextViews at once. If this didn't exist, you'd have to manually style every TextView instead.

它是一种可以让同一类型的所有View具有基本的样式。例如,你如果想要一次性的改变你App中所有TextView的样式,你可以在theme中设置textViewStyle属性。如果theme没有设置的话,你就必须手动的改变每个TextView。

Let's walk through how it actually works, using TextView as an example.

First, it's an attribute (in this case, R.attr.textViewStyle). Here's where the Android platform defines textViewStyle:
首先,textViewStyle是一个属性,对应索引就是 R.attr.textViewStyle, 下面就是textViewStyle属性的定义。

<resources>  
  <declare-styleable name="Theme">

    <!-- ...snip... -->

    <!-- Default TextView style. -->
    <attr name="textViewStyle" format="reference" />

    <!-- ...etc... -->

  </declare-styleable>
</resource>  

Again, we're using declare-styleable, but this time to define attributes that can exist in the theme. Here, we're saying that textViewStyle is a reference - that is, its value is just a reference to a resource. In this case, it should be a reference to a style.

再一次我们使用了declare-styleable,但是这次我们定义的属性可以在Theme中使用。在这里textViewStyle值的类型是一个引用类型。所以它对应的值就是资源的引用。本文中它引用了一个style。

Next we have to set textViewStyle in the current theme. The default Android theme looks like this:
接下来我们在当前theme中使用textViewStyle属性,默认的Android主题类似下面的代码:

 <resources>  
  <style name="Theme">
    <!-- ...snip... -->

    <item name="textViewStyle">@style/Widget.TextView</item>

    <!-- ...etc... -->

  </style>
</resource>  

Then the theme has to be set for your Application or Activity, typically via the manifest:
然后要按如下的方式在manifest中设置给你的应用或者Activity。

  <activity  
  android:name=".MyActivity"
  android:theme="@style/Theme"
  />

Now we can use it in obtainStyledAttributes():
现在我们可以在obtainStyledAttributes()方法中使用它了:

 TypedArray ta = theme.obtainStyledAttributes(attrs, R.styleable.TextView, R.attr.textViewStyle, 0);  

The end result is that any attributes not defined by the AttributeSet are filled in with the style that textViewStyle references.
结果就是那些没有在AttributeSet中定义的属性,都会通过textViewStyle引用来填充这些属性的值。

Phew! Unless you're being hardcore, you don't need to know all these implementation details. It's mostly there so that that the Android framework can let you define base styles for various Views in your theme.
具体细节不用了解的太清楚。大多数情况下,Android框架可以让你在主题中为各种View定义基本样式。

Default Style Resource

defStyleRes is much simpler than its sibling. It is just a style resource (i.e. @style/Widget.TextView). No complex indirection through the theme.

这个就更简单了,它就是一个style资源。

The attributes from the style in defStyleRes are applied only if defStyleAttr is undefined (either as 0 or it isn't set in the theme).

defStyleRes所引用的style中的属性,只有在defStyleAttr没有用到的情况下才会被使用。

Precedence 优先级

We've now got a bunch of ways to derive the value for an attribute via obtainStyledAttributes(). Here's their order of precedence, from highest to lowest:
现在通过obtainStyledAttributes()方法我们有几种方式去获取属性的值,那么这些的途径的优先级从高到低如下排列

  1. Any value defined in the AttributeSet.
  2. The style resource defined in the AttributeSet (i.e. style=@style/blah).
  3. The default style attribute specified by defStyleAttr.
  4. The default style resource specified by defStyleResource (if there was no defStyleAttr).
    Values in the theme.

In other words, any attributes you set directly in XML will be used first.
But there are all sorts of other places those attributes can be retrieved from if you don't set them yourself.
也就是首先使用你在XML文件中设置的属性。其次才从其他地方获取那些没有XML中设置的属性的值

View constructors
This article was supposed to be about View constructors, right?

There are four of them total, each one adding a parameter:

View(Context)

View(Context, AttributeSet)

View(Context, AttributeSet, defStyleAttr)

View(Context, AttributeSet, defStyleAttr, defStyleRes)
An important note: the last one was added in API 21, so you unless you've got minSdkVersion 21, you should avoid it for now. (If you want to use defStyleRes just call obtainStyledAttributes() yourself since it's always been supported.)

最后一个构造函数是在API21的时候才被添加进去。所以除非你的minSdkVersion为21,否则不要使用它。

They cascade, so if you call one, you end up calling them all (via super). The cascading also means you only need to override the constructors you use. Generally, this means that you only need to implement the first two (one for code constructor, the other for XML inflation).

他们是串联的。所以你可以只在带两个参数的构造函数中实现逻辑,在另外一个构造函数中使用this()来调用带两个参数的构造函数即可。

I usually setup my custom Views like so:
我通常会按如下方式来使用。


SomeView(Context context) {  
  this(context, null);
}

SomeView(Context context, AttributeSet attrs) {  
  // Call super() so that the View sets itself up properly
  super(context, attrs);

  // ...Setup View and handle all attributes here...
}

Within the two-arg constructor you can use obtainStyledAttributes() any way you want. A quick way to implement a default style is to just provide defStyleRes to it; that way you don't need to go through the pain in the butt that is defStyleAttr (which is more of a framework tool and not usually necessary for a single app).
你可以在带两个参数的构造函数中以你想要的方式来使用obtainStyledAttributes() 函数,一种快速的方法就是提供一个默认的defStyleRes来这个函数,这样我们就可以避免使用defStyleAttr(他需要在Theme声明属性的值)所带来的痛苦。

Anyways, I hope this helps not only your understanding of View constructors but also how attributes are retrieved during View construction!

推荐阅读更多精彩内容