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去定义整个应用的样式,而且这种配置还有继承能力,果然是万事尽在配配配呀。
利用配置文件去配置组件的样式是现在十分常用的一种方式,其易用性、易读性、易扩展性和易维护性不言而喻,但这种方式的背后定是一套十分完善的框架体系。身为程序猿的我被它散发的无穷魅力所吸引,对其研究的必然会是困难重重,愿我能常保现在的求知欲,一步一个脚印地探索下去。