Android View構造方法第三參數使用方法詳解

我們都知道,在Android中要使用一個View,一般會有兩種方式:

  1. 在XML文件中配置;
  2. 直接在代碼中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文件中,顯示結果如下:
Demo截圖顯示

很顯然,前面兩個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正是采用的第三種構造方法創建出來的,結果導致了很奇怪的結果。既然是用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,不可能有其他地方來區分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中的屬性。

繼續用Button來分析。

通過Button第3個構造方法的調用,我們來到TextView的構造方法中,當中有一句關鍵代碼:

TypedArray a =  context.obtainStyledAttributes(  
        attrs, com.android.internal.R.styleable.TextView, defStyle, 0);  

接下來,我們分析一下obtainStyledAttributes方法。

obtainStyledAttributes

跟蹤該方法,發現最終調用的是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()方法的官方文檔

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參數,我們可以發現在TextView、ImageView等控件中,這個值傳的都是0,也就是不使用它。它的作用就像是一個替補,當defStyleAttr不起作用的時候它就上場,因為它也是一個style,這個參數是怎么起作用的在下面的實例中有提到。

實例

上面的都是理論,我們接下來用一個例子來實踐一下。

首先創建一個attrs.xml文件:(如果還不會自定義View屬性的,請參考

Android 自定義View 之 自定義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中的屬性有多處地方可以設置值,這個優先級是:

  • 1、直接在XML布局文件中設置的值優先級最高,如果這里設置了值,就不會去取其他地方的值了。
  • 2、XML布局文件中有一個叫“style”的屬性,它指向一個style,在這個style中設置的屬性值優先級次之。
  • 3、如果上面兩個地方都沒有設置值,那么就會根據View帶三個參數的構造方法中的第三個參數attribute指向的style設置值,前提是這個attribute的值不為0。
  • 4、如果上面的attribute設置為0了,我們就根據obtainStyledAttributes()方法中的最后一個參數指向的style來設置值。
  • 5、如果仍然沒有設置到值,就會用theme中直接設置的屬性值,而不會去管第3步和第4步中是否設置了值。

必須要注意:要想讓View構造方法的第三個參數生效,必須讓它出現在我們自己的Application或者Activity的android:theme所指向的style中。設置Activity的theme一樣可以。

參考文章:

Android中style和theme的區別

View構造方法第三參數使用方法詳解

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容