我們都知道,在Android中要使用一個View,一般會有兩種方式:
- 在XML文件中配置;
- 直接在代碼中new一個View的對象。
我們今天討論的內容就是圍繞著View的構造方法的梦谜。
1、實例
首先我們先來看一個例子。
新建一個工程唁桩,layout文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="(Context, AttributeSet)" />
</LinearLayout>
Activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.three_button_layout);
Button btn1 = new Button(this);
btn1.setText("(Context)");
Button btn2 = new Button(this, null, 0);
btn2.setText("(Context, AttributeSet, int)");
LinearLayout layout = (LinearLayout) findViewById(R.id.layout);
layout.addView(btn1);
layout.addView(btn2);
}
在layout文件中有一個Button闭树,然后在代碼中new了兩個Button,并且添加到layout文件中荒澡,顯示結果如下:很顯然报辱,前面兩個Button樣式是一樣的,并且默認可以點擊单山,第3個Button就有點奇怪了碍现,而且還無法點擊。為什么會出現這種現象呢米奸?這就是這篇文章要說明的問題了昼接。
View的構造函數
要想理解上面的問題,我們必須先得了解View的構造函數悴晰。默認情況下慢睡,View有3個構造函數,函數原型如下:
/**
* 在Code中實例化一個View就會調用這個構造函數
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public View(Context context);
/**
* 在xml中定義會調用這個構造函數
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file
*/
public View(Context context, AttributeSet attrs);
public View(Context context, AttributeSet attrs, int defStyle);
- 如果要在代碼中new一個View對象铡溪,我們一般會使用第一個構造函數漂辐。
- 如果是在XML文件中聲明的View,系統會默認調用第二個構造函數棕硫。
- 而對于第三個構造函數髓涯,我們在自己的代碼中一般都沒有去調用它。
在上面的例子中哈扮,btn2這個Button正是采用的第三種構造方法創(chuàng)建出來的纬纪,結果導致了很奇怪的結果。既然是用Button做的例子灶泵,我們來看下Button的源碼(Button的源碼可以說是所有Android自帶控件中最簡單的了吧):
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 defStyle) {
super(context, attrs, defStyle);
}
}
我們可以看到育八,整個類中僅僅只有3個構造方法对途,但是它繼承自TextView赦邻,所以它的各種方法都是在TextView中實現的。然而实檀,我們平時看到的TextView和Button還是有很多地方不同的惶洲,那是什么地方導致的這些差異呢?
顯然膳犹,除了第二個構造方法中的com.android.internal.R.attr.buttonStyle恬吕,不可能有其他地方來區(qū)分TextView和Button了。而這里第二個構造方法調用了第三個構造方法须床,第三個構造比第二個構造方法多了一個int類型的參數铐料。這就是關鍵所在了。
View構造方法中的第三個參數。
我們來看一下第三個構造方法的官方文檔注釋:
Perform inflation from XML and apply a class-specific base style. This constructor of View allows subclasses to use their own base style when they are inflating. For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyle; this allows the theme's button style to modify all of the base view attributes (in particular its background) as well as the Button class's attributes.
對第三個參數的解釋是:
An attribute in the current theme that contains a reference to a style resource to apply to this view. If 0, no default style will be applied.
它的大概意思就是钠惩,給View提供一個基本的style柒凉,如果我們沒有對View設置某些屬性,就使用這個style中的屬性篓跛。
繼續(xù)用Button來分析膝捞。
通過Button第3個構造方法的調用,我們來到TextView的構造方法中愧沟,當中有一句關鍵代碼:
TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
接下來蔬咬,我們分析一下obtainStyledAttributes方法。
obtainStyledAttributes
跟蹤該方法沐寺,發(fā)現最終調用的是Resources.Theme類中的obtainStyledAttributes()方法林艘,該方法里面主要是通過調用一個native方法來拿到控件的屬性,放到TypedArray中混坞。
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}
我們來仔細閱讀一下obtainStyledAttributes()方法的官方文檔
- set:在XML中明確寫出了的屬性集合北启。(比如android:layout_width、android:text="@string/hello_world"這些)
- attrs:需要在上面的set集合中查詢哪些內容拔第。如果是自定義View咕村,一般會把自定義的屬性寫在declare-styleable中,代表我們想查詢這些自定義的屬性值蚊俺。
- defStyleAttr:這是一個定義在attrs.xml文件中的attribute懈涛。這個值起作用需要兩個條件:1. 值不為0;2. 在Theme中使用了(出現即可)泳猬。
- defStyleRes:這是在styles.xml文件中定義的一個style批钠。只有當defStyleAttr沒有起作用,才會使用到這個值得封。
這還是一個比較模糊的概念埋心,我們來看看系統里面是怎么使用這些值的。
首先找到frameworks\base\core\res\res\values目錄下的attrs.xml忙上、styles.xml拷呆、themes.xml三個文件,打開疫粥。
既然Button的構造方法中使用到了com.android.internal.R.attr.buttonStyle茬斧,我們就來看看這個attr。該attr位于attrs.xml中:
<attr name="buttonStyle" format="reference" />
只是簡單的定義了一個attr梗逮。
然后在哪里用到了它呢项秉?看到themes.xml文件下,有這樣一個style:
<style name="Theme">
...
<item name="buttonStyle">@android:style/Widget.Button</item>
...
</style>
在這里用到了buttonStyle屬性慷彤,它指向另外一個style娄蔼,這個style在styles.xml文件下:
<style name="Widget.Button">
<item name="android:background">@android:drawable/btn_default</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
<item name="android:textAppearance">?android:attr/textAppearanceSmallInverse</item>
<item name="android:textColor">@android:color/primary_text_light</item>
<item name="android:gravity">center_vertical|center_horizontal</item>
</style>
我們可以看到怖喻,這里面的屬性都是用來配置Button的。如果在XML文件中沒有給Button配置背景岁诉、內容的位置等屬性罢防,就會默認使用這里的屬性。當然這是在使用了defStyleAttr的情況才會出現的唉侄,這也解釋了文章開頭的例子中的奇怪現象了咒吐。
千萬不要以為這樣就萬事大吉了,現在我們只是定義好了這些屬性属划,并沒有使用到它恬叹。那在哪里使用到的呢?注意上面的themes.xml中的那個style的名稱為Theme同眯,而在我們自己的工程中绽昼,在配置menifest文件的時候,給application或者activity設置的主題android:theme一般都是這個style的子類须蜗,所以也就這樣使用到了defStyleAttr定義的屬性了硅确。至于是如何拿到這些屬性的,我想是在obtainStyledAttributes()方法中處理的明肮,這里不需要過多追究菱农。
還有一個defStyleRes參數,我們可以發(fā)現在TextView柿估、ImageView等控件中循未,這個值傳的都是0,也就是不使用它秫舌。它的作用就像是一個替補的妖,當defStyleAttr不起作用的時候它就上場,因為它也是一個style足陨,這個參數是怎么起作用的在下面的實例中有提到嫂粟。
實例
上面的都是理論,我們接下來用一個例子來實踐一下墨缘。
首先創(chuàng)建一個attrs.xml文件:(如果還不會自定義View屬性的星虹,請參考
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="attr1" format="string" />
<attr name="attr2" format="string" />
<attr name="attr3" format="string" />
<attr name="attr4" format="string" />
<attr name="attr5" format="string" />
<attr name="attr6" format="string" />
</declare-styleable>
<attr name="customViewStyle" format="reference" />
</resources>
注意,這里即使將customViewStyle屬性寫在declare-styleable里飒房,最終效果也一樣搁凸。
定義style媚值。
首先定義我們的defStyleAttr屬性(在本項目中是customViewStyle屬性)需要用到的style(位于styles.xml文件中):
<style name="custom_view_style">
<item name="attr3">attr3 from custom_view_style</item>
<item name="attr4">attr4 from custom_view_style</item>
</style>
然后定義一個在xml布局文件中需要用到的style(位于styles.xml文件中):
<style name="xml_style">
<item name="attr2">attr2 from xml_style</item>
<item name="attr3">attr3 from xml_style</item>
</style>
自定義一個簡單的View:
public class CustomView extends View {
static final String LOG_TAG = "CustomView";
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.customViewStyle);
}
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);
Log.d(LOG_TAG, "attr1 => " + array.getString(R.styleable.CustomView_attr1));
Log.d(LOG_TAG, "attr2 => " + array.getString(R.styleable.CustomView_attr2));
Log.d(LOG_TAG, "attr3 => " + array.getString(R.styleable.CustomView_attr3));
Log.d(LOG_TAG, "attr4 => " + array.getString(R.styleable.CustomView_attr4));
Log.d(LOG_TAG, "attr5 => " + array.getString(R.styleable.CustomView_attr5));
Log.d(LOG_TAG, "attr6 => " + array.getString(R.styleable.CustomView_attr6));
}
}
注意這里用到了R.attr.customViewStyle狠毯。為了使它生效,需要在當初工程的theme中設置它的值(位于styles.xml文件中):
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="customViewStyle">@style/custom_view_style</item>
</style>
這里就用到了我們上面定義的custom_view_style這個style褥芒。
分析:
- attr1只在xml布局文件中設置嚼松,所以值為attr1 from xml嫡良。
- attr2在xml布局文件和xml style中都設置了,取值為布局文件中設置的值献酗,所以為attr2 from xml寝受。
- attr3沒有在xml布局文件中設置,但是在xml style和defStyleAttr定義的style中設置了罕偎,取xml style中的值很澄,所以值為attr3 from xml_style。
- attr4只在defStyleAttr定義的style中設置了颜及,所以值為attr4 from custom_view_style甩苛。
- attr5和attr6沒有在任何地方設置值,所以為null俏站。
這也證實了前面所得出的順序是正確的讯蒲。
我們再來測試一下defStyleRes這個參數,它是一個style肄扎,所以添加一個style(位于styles.xml文件中):
<style name="default_view_style">
<item name="attr4">attr4 from default_view_style</item>
<item name="attr5">attr5 from default_view_style</item>
</style>
然后還需要修改CustomView中的第16行墨林,為下面一行:
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, R.style.default_view_style);
運行結果:咦,為什么結果和上面一樣呢犯祠?
我們看到官方文檔中對obtainStyledAttributes()方法的defStyleRes參數解釋是這樣的:
A resource identifier of a style resource that supplies default values for the TypedArray, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
也就是說旭等,當defStyleAttr這個參數定義為0(即不使用這個參數),或者是在theme中找不到defStyleAttr這個屬性時(即使在theme中的配置是這樣的:<item name="defStyleAttr">@null</item>衡载,也代表找到了defStyleAttr屬性辆雾,defStyleRes參數也不會生效),defStyleRes參數才會生效月劈。
所以我們修改CustomView為下面內容(或者是去掉theme中對customViewStyle的使用):
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, 0, R.style.default_view_style);
運行結果:由于defStyleAttr已經失效度迂,所以attr4和attr5都是從default_view_style中獲取到的值。
我們知道猜揪,在theme所在的style中也可以設置屬性惭墓,如下:
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="customViewStyle">@style/custom_view_style</item>
<item name="attr5">attr5 from AppTheme</item>
<item name="attr6">attr6 from AppTheme</item>
</style>
運行結果:attr1~attr4不用說了。
attr5在default style和theme下都定義了而姐,取default style下的值腊凶,所以為attr5 from default_view-style。
attr6只在theme下定義了拴念,所以取值為attr6 from AppTheme钧萍。
注意,如果將CustomView中重新改成下面的內容(即使customViewStyle生效):
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);
這時政鼠,default style是失效了的风瘦,那么在theme中設置的值會不會生效呢?
看運行結果:attr5在default style和theme下都定義了公般,但default style失效了万搔,這里并沒有因為customViewStyle是有效的而忽略theme中設置的值胡桨,所以為attr5 from AppTheme。
attr6只在theme下定義了瞬雹,同樣沒有因為customViewStyle是有效的而忽略theme中設置的值昧谊,所以取值為attr6 from AppTheme。
這里和default style的取值形式有一點點不同酗捌。
總結
View中的屬性有多處地方可以設置值呢诬,這個優(yōu)先級是:
- 1、直接在XML布局文件中設置的值優(yōu)先級最高胖缤,如果這里設置了值馅巷,就不會去取其他地方的值了。
- 2草姻、XML布局文件中有一個叫“style”的屬性钓猬,它指向一個style,在這個style中設置的屬性值優(yōu)先級次之撩独。
- 3敞曹、如果上面兩個地方都沒有設置值,那么就會根據View帶三個參數的構造方法中的第三個參數attribute指向的style設置值综膀,前提是這個attribute的值不為0澳迫。
- 4、如果上面的attribute設置為0了剧劝,我們就根據obtainStyledAttributes()方法中的最后一個參數指向的style來設置值橄登。
- 5、如果仍然沒有設置到值讥此,就會用theme中直接設置的屬性值拢锹,而不會去管第3步和第4步中是否設置了值。
必須要注意:要想讓View構造方法的第三個參數生效萄喳,必須讓它出現在我們自己的Application或者Activity的android:theme所指向的style中卒稳。設置Activity的theme一樣可以。