Android中如何利用attrs和styles定义控件
一直有个问题就是,Android中是如何通过布局文件,就能实现控件效果的不同呢?比如在布局文件中,我设置了一个TextView,给它设置了 textColor,它就能够改变这个TextView的文本的颜色。这是如何做到的呢?我们分3个部分来看这个问题1.attrs.xml 2.styles.xml 3.看组件的源码。
1.attrs.xml:
我们知道Android的源码中有attrs.xml这个文件,这个文件实际上定义了所有的控件的属性,就是我们在布局文件中设置的各类属性
你可以找到attrs.xml这个文件,打开它,全选,右键->Show In->OutLine。可以看到整个文件的解构
下面是两个截图:
我们大概可以看出里面是Android中的各种属性的声明,比如textStyle这个属性是这样定义的:
<!-- Default text typeface style. --> <attr name="textStyle"> <flag name="normal" value="0" /> <flag name="bold" value="1" /> <flag name="italic" value="2" /> </attr>
那么现在你知道,我们在写android:textStyle的时候为什么会出现normal,bold和italic这3个东西了吧,就是定义在这个地方。
再看看textColor:
<!-- Color of text (usually same as colorForeground). --> <attr name="textColor" format="reference|color" />
format的意思是说:这个textColor可以以两种方式设置,要么是关联一个值,要么是直接设置一个颜色的RGB值,这个不难理解,因为我们可以平时也这样做过。
也就是说我们平时在布局文件中所使用的各类控件的属性都定义在这里面,那么这个文件,除了定义这些属性外还定义了各种具体的组件,比如TextView,Button,SeekBar等所具有的各种特有的属性
比如SeekBar:
<declare-styleable name="SeekBar"> <!-- Draws the thumb on a seekbar. --> <attr name="thumb" format="reference" /> <!-- An offset for the thumb that allows it to extend out of the range of the track. --> <attr name="thumbOffset" format="dimension" /> </declare-styleable>
也许你会问SeekBar的background,等属性怎么没有看到?这是因为Android中几乎所有的组件都是从View中继承下来 的,SeekBar自然也不例外,而background这个属性几乎每个控件都有,因此被定义到了View中,你可以在declare- styleable:View中找到它。
总结下,也就是说attrs.xml这个文件定义了布局文件中的各种属性attr:***,以及每种控件特有的属性declare-styleable:***
2.styles.xml:
刚才的attrs.xml定义的是组件的属性,现在要说的style则是针对这些属性所设置的值,一些默认的值。
这个是SeekBar的样式,我们可以看到,这里面设置了一个SeekBar的默认的样式,即为attrs.xml文件中的各种属性设置初始值
<style name="Widget.SeekBar"> <item name="android:indeterminateOnly">false</item> <item name="android:progressDrawable">@android:drawable/progress_horizontal</item> <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item> <item name="android:minHeight">20dip</item> <item name="android:maxHeight">20dip</item> <item name="android:thumb">@android:drawable/seek_thumb</item> <item name="android:thumbOffset">8dip</item> <item name="android:focusable">true</item> </style>
这个是Button的样式:
<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>
有了属性和值,但是这些东西是如何关联到一起的呢?它们如何被android的framework层所识别呢?
3.组件的源码
我们看下TextView的源码:
public TextView(Context context) { this(context, null); }//这个构造器用来给用户调用,比如new TextView(this); public TextView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.textViewStyle); } public TextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle);//为用户自定义的TextView设置默认的style mText = ""; //设置画笔 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.density = getResources().getDisplayMetrics().density; mTextPaint.setCompatibilityScaling( getResources().getCompatibilityInfo().applicationScale); mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mHighlightPaint.setCompatibilityScaling( getResources().getCompatibilityInfo().applicationScale); mMovement = getDefaultMovementMethod(); mTransformation = null; //attrs中包含了这个TextView控件在布局文件中定义的属性,比如android:background,android:layout_width等 //com.android.internal.R.styleable.TextView中包含了TextView中的针对attrs中的属性的默认的值 //也就是说这个地方能够将布局文件中设置的属性获取出来,保存到一个TypeArray中,为这个控件初始化各个属性 TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyle, 0); int textColorHighlight = 0; ColorStateList textColor = null; ColorStateList textColorHint = null; ColorStateList textColorLink = null; int textSize = 15; int typefaceIndex = -1; int styleIndex = -1; /* * Look the appearance up without checking first if it exists because * almost every TextView has one and it greatly simplifies the logic * to be able to parse the appearance first and then let specific tags * for this View override it. */ TypedArray appearance = null; //TextView_textAppearance不太了解为什么要这样做?难道是为了设置TextView的一些默认的属性? int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1); if (ap != -1) { appearance = context.obtainStyledAttributes(ap, com.android.internal.R.styleable. TextAppearance); } if (appearance != null) { int n = appearance.getIndexCount(); for (int i = 0; i < n; i++) { int attr = appearance.getIndex(i); switch (attr) { case com.android.internal.R.styleable.TextAppearance_textColorHighlight: textColorHighlight = appearance.getColor(attr, textColorHighlight); break; case com.android.internal.R.styleable.TextAppearance_textColor: textColor = appearance.getColorStateList(attr); break; case com.android.internal.R.styleable.TextAppearance_textColorHint: textColorHint = appearance.getColorStateList(attr); break; case com.android.internal.R.styleable.TextAppearance_textColorLink: textColorLink = appearance.getColorStateList(attr); break; case com.android.internal.R.styleable.TextAppearance_textSize: textSize = appearance.getDimensionPixelSize(attr, textSize); break; case com.android.internal.R.styleable.TextAppearance_typeface: typefaceIndex = appearance.getInt(attr, -1); break; case com.android.internal.R.styleable.TextAppearance_textStyle: styleIndex = appearance.getInt(attr, -1); break; } } appearance.recycle(); } //各类属性 boolean editable = getDefaultEditable(); CharSequence inputMethod = null; int numeric = 0; CharSequence digits = null; boolean phone = false; boolean autotext = false; int autocap = -1; int buffertype = 0; boolean selectallonfocus = false; Drawable drawableLeft = null, drawableTop = null, drawableRight = null, drawableBottom = null; int drawablePadding = 0; int ellipsize = -1; boolean singleLine = false; int maxlength = -1; CharSequence text = ""; CharSequence hint = null; int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean password = false; int inputType = EditorInfo.TYPE_NULL; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); //通过switch语句将用户设置的,以及默认的属性读取出来并初始化 switch (attr) { case com.android.internal.R.styleable.TextView_editable: editable = a.getBoolean(attr, editable); break; case com.android.internal.R.styleable.TextView_inputMethod: inputMethod = a.getText(attr); break; case com.android.internal.R.styleable.TextView_numeric: numeric = a.getInt(attr, numeric); break; //更多的case语句... case com.android.internal.R.styleable.TextView_textSize: textSize = a.getDimensionPixelSize(attr, textSize);//设置当前用户所设置的字体大小 break; case com.android.internal.R.styleable.TextView_typeface: typefaceIndex = a.getInt(attr, typefaceIndex); break; //更多的case语句... }
通过上面的代码大概可以知道,每个组件基本都有3个构造器,其中只传递一个Context上下文的那个构造器一般用来在java代码中实例化使用。
比如你可以
TextView tv = new TextView(context);
来实例化一个组件。
最终调用的是第3个构造器
public TextView(Context context, AttributeSet attrs, int defStyle)
在这个构造器中为你设置了默认的属性attrs和值styles。关键不在这里,而是后面通过使用下面的代码
TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
来将属性和值获取出来,放到一个TypeArray中,然后再利用一个switch语句将里面的值取出来。再利用这些值来初始化各个属性。这个View最终利用这些属性将这个控件绘制出来。
如果你在布局文件中定义的一个View的话,那么你定义的值,会被传递给构造器中的attrs和styles。也是利用同样的方式来获取出你定义的值,并根据你定义的值来绘制你想要的控件。
再比如其实Button和EditText都是继承自TextView。看上去两个控件似乎差异很大,其实不然。Button的源码其实相比TextView变化的只是style而已:
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); } }
再看看EditText:
public class EditText extends TextView { public EditText(Context context) { this(context, null); } public EditText(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.editTextStyle); } public EditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected boolean getDefaultEditable() { return true; } @Override protected MovementMethod getDefaultMovementMethod() { return ArrowKeyMovementMethod.getInstance(); } @Override public Editable getText() { return (Editable) super.getText(); } @Override public void setText(CharSequence text, BufferType type) { super.setText(text, BufferType.EDITABLE); } /** * Convenience for {@link Selection#setSelection(Spannable, int, int)}. */ public void setSelection(int start, int stop) { Selection.setSelection(getText(), start, stop); } /** * Convenience for {@link Selection#setSelection(Spannable, int)}. */ public void setSelection(int index) { Selection.setSelection(getText(), index); } /** * Convenience for {@link Selection#selectAll}. */ public void selectAll() { Selection.selectAll(getText()); } /** * Convenience for {@link Selection#extendSelection}. */ public void extendSelection(int index) { Selection.extendSelection(getText(), index); } @Override public void setEllipsize(TextUtils.TruncateAt ellipsis) { if (ellipsis == TextUtils.TruncateAt.MARQUEE) { throw new IllegalArgumentException("EditText cannot use the ellipsize mode " + "TextUtils.TruncateAt.MARQUEE"); } super.setEllipsize(ellipsis); } }
不知道你是不是和我一样感到意外呢?
不得不说这种方式非常的好。最大程度地利用了继承,并且可以让控件之间的属性可以很方便的被开发者使用。也利用以后的扩展,实际上,不同的style就可以得到不同的UI,这也是MVC的一种体现。
比如用户想自定义某个控件,只要覆盖父类的style就可以很轻松的实现,可以参考我的一篇博文,就是使用style自定义ProgressBar。
Android中的主题theme也是使用的style。当用户在Activity中设置一个style的时候那么会影响到整个Activity,如果为Application设置style的话,则会影响所有的Activity,所以,如果你在开发一个应用的时候
可以考虑将应用的Activity的背景颜色等一类的属性放到一个style中去,在Application中调用,这种做法会比较方便。
themes.xml:
<!-- Variant of the default (dark) theme with no title bar --> <style name="Theme.NoTitleBar"> <item name="android:windowNoTitle">true</item> </style> <!-- Variant of the default (dark) theme that has no title bar and fills the entire screen --> <style name="Theme.NoTitleBar.Fullscreen"> <item name="android:windowFullscreen">true</item> <item name="android:windowContentOverlay">@null</item> </style>
我们平时使用的主题实际上就定义在这个文件中。也是一个style。