Button源碼解析(API 26)
概述
說到Button,實在是再熟悉不過了,那就追追它的源碼吧。
啥啥啥?代碼這麼簡單,4個構造函數和2個重寫函數就完啦?Button的秘密究竟在哪裡,别着急,且聽細細道來。
預熱
總結一下Button的特性吧:
- 可以設定文字,是以繼承TextView
- 有Focused,Pressed,Normal等狀态,對應不同狀态呈現不同UI,這個View裡面都有,是以間接繼承View
- 可以監聽點選事件,這個也是View裡的,是以間接繼承View
這樣看,實作一個Button好像很簡單嘛(的确很簡單),繼承個TextView,然後對應不同狀态設定不同的顯示UI就好了,再簡單點就用個selector,so esay。快去源碼看看是不是這麼回事吧,what?比我說的還簡單,核心代碼就一句話。然而真的這麼簡單嗎?簡單的東西用起來很爽,但是把問題簡單化卻很難,這背後的智慧才是我想要的,那麼就來看看吧。
源碼跟蹤
以下代碼取自Android API-26。這裡僅列出主要代碼(其實重要的就那麼一句話):
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, );
}
public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
前3個構造函數都會去調用第4個構造函數,自定過View的小夥伴對這些構造函數肯定不會陌生,還不太懂的就再聽我唠叨一下吧。
我們自定義一個View的同時也會自定義它的一些屬性,屬性的定義一般是放在
<resource>
下的
<declare-styleable>
标簽中的,但屬性值又從哪裡擷取呢?一般有這麼幾種途徑:
- 布局檔案中定義,比如在
中定義android:text=”hahaha”,這些屬性值會在構造Button時傳遞給attrs參數(構造函數中的第二個參數)<Button>
- 主題中定義,我們可以在AndroidManifest中指定主題,在構造Button時傳遞給defStyleAttr參數(構造函數中的第三個參數)
- 樣式資源中定義,一般在
标簽下的<resource>
中定義樣式,在構造Button時傳遞給defStyleRes參數(構造函數的第四個參數)<style>
是以當我們在代碼中new Button(context)時,會調用第1個構造函數,傳遞到第4個構造函數時,attrs為null,defStyleAttr為com.android.internal.R.attr.buttonStyle,deyStyleRes為0.
當我們在布局檔案中定義Button時,會調用第2個構造函數,此時attrs為從布局檔案中擷取的值,defStyleAttr為com.android.internal.R.attr.buttonStyle,deyStyleRes為0.
com.android.internal.R.attr.buttonStyle非常重要,Button的預設UI效果必定是在這裡面指定的,它的定義可以在Android SDK目錄下的platforms/android-xx/data/res/values/attrs.xml中找到,具體内容一會再詳細說明。
先看Button的第4個構造函數,它會調用父類(TextView)的構造函數,TextView的代碼這裡就不多做介紹了,直接劃重點:
...
TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
...
這裡會通過obtainStyleAttributes方法擷取對應的屬性值,這個方法很重要,得說一下。
前面說過,Button的屬性值可以在好幾個地方指定,但是如何擷取呢,這就得靠obtainStyleAttributes方法了。它有好幾個重載方法,這裡直接看4個參數的,代碼如下:
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}
這個函數用來擷取指定屬性集的值,什麼意思呢?比方說,我們想知道View的width,height和background,這些值的定義可以在布局檔案中(對應參數set),主題中(對應參數defStyleAttr)或樣式資源中(對應參數defStyleRes)。而attrs則表示想要擷取哪些屬性的值,如[width, height, background]。
總結一下:
- set,布局檔案中的屬性值
- attrs,指明想要擷取哪些屬性的值
- defStyleAttr,主題中的屬性值
- defStyleRes,樣式資源中的屬性值
加入同一個屬性值在多個地方被定義了咋辦?優先級set
>
defStyleAttr,當defStyleAttr為0時,defStyleRes才有效。
是以再來看前面的代碼,把TextView中的obainStyledAttributes的參數替換一下:
...
TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TextViewAppearance, com.android.internal.R.attr.buttonStyle, );
...
這樣就清楚了,TextView擷取了TextViewAppearance指定的屬性的值,這些值從attrs和buttonStyle中查找。TextViewAppearance必定定義了一個屬性集合,而這個集合裡的屬性顧名思義,必定和TextView的外觀相關,如text color,typeface,size等等。
為了驗證想法,來做一個小實驗吧。
在預設主題上添點東西:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="buttonStyle">@style/MyButtonStyle</item>
</style>
<style name="MyButtonStyle">
<item name="android:background">@color/colorPrimary</item>
</style>
</resources>
把buttonStyle的風格指定為MyButtonStyle,在代碼中或者布局檔案中定義一個button看看是什麼樣子:
看吧,一點button的樣子也沒有了,點選也沒有UI的變換了。那麼Android預設主題中的Button樣式是怎樣定義的呢?從AppTheme沿着parent路徑往前追吧,能被繞的暈死,不管了,我的直覺告訴我在platforms/android-xx/data/res/values/styles.xml檔案中搜尋Button肯定能找到,果不其然(看來還是有點程式猿直覺的)。
看代碼:
<style name="Widget.Button">
<item name="background">@drawable/btn_default</item>
<item name="focusable">true</item>
<item name="clickable">true</item>
<item name="textAppearance">?attr/textAppearanceSmallInverse</item>
<item name="textColor">@color/primary_text_light</item>
<item name="gravity">center_vertical|center_horizontal</item>
</style>
這些應該就是Button的預設屬性值了,都是些熟悉的屬性,看看background吧,是個drawable,去drawable目錄找找看,有了:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/btn_default_normal" />
<item android:state_window_focused="false" android:state_enabled="false"
android:drawable="@drawable/btn_default_normal_disable" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_default_pressed" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/btn_default_selected" />
<item android:state_enabled="true"
android:drawable="@drawable/btn_default_normal" />
<item android:state_focused="true"
android:drawable="@drawable/btn_default_normal_disable_focused" />
<item
android:drawable="@drawable/btn_default_normal_disable" />
</selector>
哈哈,還是用了個selector嘛,到此Android提供的預設Button元件就扒完了。嗯,用了主題的方式去設定Button的預設樣式。
總結
配置檔案被玩出花了,我們不僅能夠通過布局檔案去初始化View的樣式,還能通過Theme去定義整個應用的樣式,而且這種配置還有繼承能力,果然是萬事盡在配配配呀。
利用配置檔案去配置元件的樣式是現在十分常用的一種方式,其易用性、易讀性、易擴充性和易維護性不言而喻,但這種方式的背後定是一套十分完善的架構體系。身為程式猿的我被它散發的無窮魅力所吸引,對其研究的必然會是困難重重,願我能常保現在的求知欲,一步一個腳印地探索下去。