Android的framework中提供了大量的视图类,如Button,TextView,ListView等,如果要使用这些视图类,需要在res/layout目录下对应的xml文件写上需要的控件,并且可以设置相应的属性值,就能控制布局;但有时候你的app有特定的需求,但android提供的控件都不能支持其需求功能时,就需要创建自定义的view;
1.继承一个android已有的View
所有定义在framework里的view控件都是继承自View这个类,自定义的View也可以直接继承View,或者你可以继承已经存在的View子类,比如Button,这样能更节省时间;为了让Android Developer Tools能和你的view控件交互,你至少要提供一个构造方法,该构造方法需有Context和AttributeSet作为参数。这个构造方法能让layout editor创建和编辑你自定义View的实例;
public class LabelView extends View {
private Paint mTextPaint;
private String mText;
private int mAscent;
/**
* Constructor. This version is only needed if you will be instantiating
* the object manually (not from a layout XML file).
* @param context
*/
public LabelView(Context context) {
super(context);
initLabelView();
}
/**
* Construct object, initializing with any attributes we understand from a
* layout file. These attributes are defined in
* SDK/assets/res/any/classes.xml.
*/
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
}
2. 自定义属性
当你在添加android自有的view控件到界面时,通常都是在xml文件中通过属性来控制view的特性和行为,而自定义的view也可以同通过xml来添加和改变特性,自定义view要能在xml中受控制,就需要:
- 在资源文件res/values/attrs.xml中的一个<declare-styleable>元素中添加属性;
- 在xml文件中指定自定义view的属性值;
- 在运行代码中返回在xml指定的属性值;
- 把返回的属性值应用到view控件中;
这个部分主要说明了怎样定义自定义属性并为自定义属性指定赋值,下个部分将会说明要怎样获得属性值并使用获得的属性值;为了定义属性值,就要添加一个<declare-styleable>元素到你的工程中,通常的做法都是把该元素定义在res/values/attrs.xml文件下,这就是apidemos的一个例子:
<resources>
<declare-styleable name="LabelView">
<attr name="text" format="string" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
</declare-styleable>
<resources>
定义了这些属性后,就可以像android自有的view控件一样在layout中的xml文件中添加自定义view,唯一的不同是自定义属性属于不同的命名空间,android自有的view控件的命名空间是
http://schemas.android.com/apk/res/android,而自定义view的命名空间为:http://schemas.android.com/apk/res/[your package name]以下代码展示了怎样使用自定义属性:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.android.apis.view.LabelView
android:background="@drawable/red"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:text="Red"/>
<com.example.android.apis.view.LabelView
android:background="@drawable/blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:text="Blue" app:textSize="20dp"/>
<com.example.android.apis.view.LabelView
android:background="@drawable/green"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:text="Green" app:textColor="#ffffffff" />
</LinearLayout>
为了避免重复岑长的URI命名空间,这个例子用了xmlns指示器,这个指示器指派custom作为
http://schemas.android.com/apk/res/com.example.android.apis.view.LabelView的别名,当然你也可以选择任何别名来指示该命名空间;
注意到添加到layout中自定义view的xml标签名,可以看到标签名是自定义view这个类的全名,如果自定义view是一个子类的话,在标签命中也必须展现它的外部类名,举个例子,如果LableView有一个子类叫LableButton,如果要为这个内部类指定属性值,则标签名为:com.example.android.apis.view.LabelView&LableButton.
3.应用自定义属性
当一个view在layout中被创建后,在该view的xml标签中所有属性值就会被读取出来打包成一个资源bundle,这个资源bundle为AttributeSet类型,然后作为view构造方法的一个参数穿进去;虽然你可以直接从读取出来的AttributeSet获得各个属性值,但这样做会有一些缺点:
(1).属性值的资源引用不能被解析;
(2).Styles没有被应用;
相反,应该把AttributeSet传进obtainStyledAttributes()方法,这个方法会返回一个TypedArray数组,这个数组已经被间接引用并styled。android资源编译器做了大量工作才让调用obtainStyledAttributes()看起来很简单,对于在资源目录下的每个<declare-styleable>,系统产生的R文件定义了该属性数组里所有属性值的id和关于该数组里所有属性值的索引常量。所以就可以通过这些常量和id从TypedArray获得具体的属性值:
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LabelView);
CharSequence s = a.getString(R.styleable.LabelView_text);
if (s != null) {
setText(s.toString());
}
// Retrieve the color(s) to be used for this view and apply them.
// Note, if you only care about supporting a single color, that you
// can instead call a.getColor() and pass that to setTextColor().
setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));
int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
if (textSize > 0) {
setTextSize(textSize);
}
a.recycle();
}
注意:TypedArray是一个共享资源,在每次使用完后必须回收,即每次使用完后要调用recycle()方法;
4. 添加事件和外观
属性是一个很有用的方法来控制view的行为和外观,但这些属性只有这个view被初始化后才能被读出来,为了提供动态的读写操作,应该为每个自定义属性值提供一对getter和setter方法,以下代码展示了属性mText的setter方法:
/**
* Sets the text to display in this label
* @param text The text to display. This will be drawn as one line.
*/
public void setText(String text) {
mText = text;
requestLayout();
invalidate();
}
注意到mText调用了invalidata()和requestLayout()方法,这些调用方法很关键,能确保view视图更稳定展现出来。在对能对view的外观造成影响的属性改变之后,就需要让该view无效化,因为这样能通知系统这个view之后可以再被重画。同样,当一个view的大小和外观变化后,也需要向系统请求一个新的layout.如果没有调用上述两个方法,将会出现很多隐藏的bug。自定义view应该要支持时间监听器来处理交互事件。在写自定义view时很容易忘记暴露出属性和时间的借口,尤其只有一个app要调用这个view时,一定要花一些时间来定义自定义view的属性和时间接口,这样才能让之后的维护更容易。一个公认的规则就是:把能够改变view外观和行为的属性暴露出来。