一、从源码中浅析Android中怎么利用attrs和styles定义控件
1.attrs.xml:
我们知道Android的源码中有attrs.xml这个文件,这个文件实际上定义了所有的控件的属性,就是我们在布局文件中设置的各类属性
你可以找到attrs.xml这个文件,打开它,全选,右键->Show In->OutLine。可以看到整个文件的解构
我们大概可以看出里面是Android中的各种属性的声明,比如textStyle这个属性是这样定义的:
Java代码
<!-- 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:
Java代码
<!-- Color of text(usually same as colorForeground).-->
<attr name="textColor" format="reference|color"/>
format的意思是说:这个textColor可以以两种方式设置,要么是关联一个值,要么是直接设置一个颜色的RGB值,这个不难理解,因为我们可以平时也这样做过。
也就是说我们平时在布局文件中所使用的各类控件的属性都定义在这里面,那么这个文件,除了定义这些属性外还定义了各种具体的组件,比如TextView,Button,SeekBar等所具有的各种特有的属性
比如SeekBar:
Java代码
<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文件中的各种属性设置初始值
Java代码
<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的样式:
Java代码
<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的源码:
Java代码
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代码中实例化使用。
比如你可以
Java代码
TextView tv= new TextView(context);
来实例化一个组件。
最终调用的是第3个构造器
Java代码
public TextView(Context context,
AttributeSet attrs,
int defStyle)
在这个构造器中为你设置了默认的属性attrs和值styles。关键不在这里,而是后面通过使用下面的代码
Java代码
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而已:
二、Android 中 declare-styleable 和 style 的不同
我们注意到上文中的CodeFont的定义,有没有发现item里面的name都是android开头?因为这些属性都是在android中预先设定好的,所以我们可以随意用。但是如果我们想用自己定义的属性名呢?这时候styleable的作用就出现了。我们只需要把attr的定义包围在styleable里面,这样定义的属性名就可以在style里面用。示例如下(示例来自上文给出的stackoverflow链接):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="attrib1" format="string"/>
<declare-styleable name="blahblah">
<attr name="attrib2" format="string"/>
</declare-styleable>
在attrs.xml里面定义了两个attr,attrib1是普通的,attrib2包围在declare-styleable标签中;
<com.custom.ViewClass attrib1="xyz" attrib2="abc"/>
我们可以在layout/someactivity.xml里直接使用这些attr;
<style name="customstyle" parent="@android:style/Widget.TextView">
<item name="attrib2">text value</item>
<!-- customize other, standard attributes too:-->
<item name="android:textColor">@color/white</item>
</style>
在styles.xml中,我们就能用attrib2。(原网站这里写成了attrib1,怀疑是笔误。)
后来我验证过attrib1也能使用在style里面(我真的不确定,逻辑上应该不能才对,但是编译就是通过了。。。),那么这里就必须说明attr包不包含在styleable里面的另一个主要区别了,stackoverflow中是这么说的:
三、android怎样动态设置toolbar背景
首先使用 Toolbar来代替ActionBar
,这样我们就能够把ActionBar嵌入到我们的View体系中,然后我们"禁用"系统的status bar,由 DrawerLayout
来处理status bar,最后抽屉部分往上移,或者裁剪掉status bar那一部分。
控制Status bar
在你的values-v21里面添加新的主题,并设置一下属性:
values-v21/themes.xml
<style name="AppTheme">
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
这里解释一下:
windowDrawsSystemBarBackgrounds,将它设置为true,系统将在你的window里面绘制status
bar,默认为 TRUE
,之所以要写出来是因为你的theme有可能是继承过来的,确保为true。(在这里小插曲一下,因调试时,总以为注释了这段代码就以为是false,程
序员思维害苦了我。另外从命名来看,Android把它称为system bar,可能是为了与能被我们处理的status bar区分开而做的改变。)
statusBarColor设置为透明是因为我们不再需要系统的status bar,因为我们无法控制它的位置,后面我们将交由 DrawerLayout来处理。
使用DrawerLayout
首先,你的布局文件应该是和这个类似的:
<android.support.v4.widget.DrawerLayout
xmlns:android="url"
android:id="@+id/my_drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- Your normal content view-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- We use a Toolbar so that our drawer can be displayed
in front of the action bar-->
<android.support.v7.widget.Toolbar
android:id="@+id/my_awesome_toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"/>
<!-- The rest of your content view-->
</LinearLayout>
<!-- The navigation drawer-->
<ScrimInsetsFrameLayout xmlns:android="rul"
xmlns:app="url"
android:layout_width="304dp"
android:layout_height="match_parent"
android:layout_gravity="left"
android:background="@android:color/white"
android:elevation="10dp"
android:fitsSystemWindows="true"
app:insetForeground="#4000">
<!-- Your drawer content-->
</ScrimInsetsFrameLayout>
</android.support.v4.widget.DrawerLayout>
在这里布局里面我们用到了一个的开源类 ScrimInsetsFrameLayout,它的主要作用就是利用 fitsSystemWindows
的回调方法 fitSystemWindows(Rect insets)来获取status bar的大小,然后调整画布已达到去掉status
bar的效果,所以我们需要在ScrimInsetsFrameLayout下设置 fitsSystemWindows
为true。当然你也可以不使用这个类,而改用 layout_marginTop属性来达到效果。
insetForeground这个属性是ScrimInsetsFrameLayout自带的,表示插入区域的前景色,我们设置为带透明的黑色#4000。别忘了使用这个属性需要添加如下代码到attrs.xml里:
values/attrs.xml
<declare-styleable name="ScrimInsetsView">
<attr name="insetForeground" format="reference|color"/>
</declare-styleable>
自此,我们已经实现了将DrawerLayout抽屉的那一部分显示在 Toolbar和systembar(为了和下面的status
bar区分,我们称为system bar)之间了,可是system bar的颜色被我们设置了透明,所以我们接下来要改变status
bar的颜色。
改变Status bar的颜色
你可能已经注意到刚才的布局里面 DrawerLayout的 fitsSystemWindows属性设置了为true,这是因为我们要在代码里面使用了 DrawerLayout设置status bar颜色的方法:
//在这里我们获取了主题暗色,并设置了status bar的颜色
TypedValue typedValue= new TypedValue();
getTheme().resolveAttribute(R.attr.colorPrimaryDark, typedValue, true);
int color= typedValue.data;
//注意setStatusBarBackgroundColor方法需要你将fitsSystemWindows设置为true才会生效
DrawerLayout drawerLayout=(DrawerLayout) findViewById(R.id.my_drawer_layout);
drawerLayout.setStatusBarBackgroundColor(color);
使用ToolBar来代替ActionBar
在代码里面这样设置:
Toolbar toolbar=(Toolbar) findViewById(R.id.my_awesome_toolbar);
setSupportActionBar(toolbar);
文章分享到这里,希望我们关于declare-styleable使用的内容能够给您带来一些新的认识和思考。如果您还有其他问题,欢迎继续探索我们的网站或者与我们交流,我们将尽力为您提供满意的答案。