基于布局类View和ViewGroup的基本功能,Android为创建自己的UI界面提供了先进和强大的定制化模式。首先,平台包含了各种预置的View和ViewGroup子类---Widget和layout,可以使用它们来构造自己的UI界面。
部分的可以利用的widget包括:Button、TextView、EditText、ListView、CheckBox、RadioButton、Gallery、Spinner、以及比较特殊用途的AutoCompleteTextView、ImageSwitcher和TextSwitcher。
其中可利用的布局是:LinearLayout、FrameLayout、RelativeLayout以及其他的布局。更多的例子请看“共通布局对象”。http://developer.android.com/guide/topics/ui/layout-objects.html
如果遇到了没有预置的widget或layout的需求,可以创建自己的View子类。如果只需要对既存的widget或layout进行小的调整,那么只需简单的继承widget或layout,并且重写它们的方法。
创建自己的View子类,以便能够精准的控制屏幕元素的外观和功能。以下是用定制View对象来实现这种控制想法的一些例子:
1. 创建一个完全的定制化渲染的View类型,如用类似模拟电子控制的2D图形来渲染的音量控制按钮。
2. 把一组View组件组合成一个新的单一组件,制作一些像ComboBox(一个下拉列表和文本输入域的组合)、双面板选择器(左右两个列表面板,右边的列表面板中的项目与左边列表面板中的一个项目相关联)等组件。
3. 重写一个EditText组件在屏幕上的渲染的方法。
4. 捕获一些像按键一样的事件,并在某些定制的方法中处理它们(如游戏)。
基本方法
以下是创建自定义View组件需要要了解基本概要:
1. 自定义的View类要继承一个既存的View类或其子类;
2. 在子类重写父类的一些方法。要覆写的父类方法是用‘on’开头的,例如,onDraw()、onMeasure()和onKeyDown()等,这有点类似于重写Activity或ListActivity的生存周期回调的on…事件。
3. 使用新的扩展类,一旦完成,新扩展的类就能被用于替换基本的View对象。
提示:扩展类能够作为使用它们的Acticity的内部类来定义。这样对控制对它们的访问是有益的,当然可以创建一个新的公共的View类,这样就可以在应用程序范围内来使用。
完全定制化的组件
完全定制化的组件能够用于创建你所期望的显示效果的图形化组件。可以是看上去像旧的模拟仪表的图形化VU仪表,或者是一个长的歌词视图,有一个跳动的球沿着歌词移动,以便跟着这卡拉OK机歌唱,这两种情况,无论如何组织内置的组件都无法满足要求。
幸运的是,能够使用任意自己喜欢的方法来创建组件的外观和行为,唯一的限制就是你的想象力、屏幕的尺寸和可利用的处理能力(因为应用程序最终可能运行在比桌面工作站处理能力要弱的设备上)。
以下是创建完全定制组件的步骤:
1. 毋庸置疑,能够扩展的最通用的视图是View类,因此通常是继承这个View类来创建自己的新的组件;
2. 提供一个能够从XML中获取属性和参数的构造器,并且也能够使用自己属性和参数(如VU仪表的颜色和范围,指针的宽度和阻尼等);
3. 创建组件中可能的事件监听器、属性访问器和修饰符以及尽可能准确的行为等;
4. 覆写onMeasure()回调方法,如果想要组件显示一些东西,也要覆写onDraw()回调。虽然它们都有默认的行为,onDraw()回调默认什么也不做,onMeasure()方法默认的要设置组件的尺寸为100x100;
5. 覆写其他的需要on…方法。
扩展onDraw()和onMeasure()
onDraw()方法会把能够实现的任何想要的东西放到一个Canvas对象上,如2D图形、标准或定制的组件、样式化的文本、或其他任何能够想到的东西。
注意:View类不能使用3D图形。如果要使用3D图形,必须继承SurfaceView类,而不是View类,并且要在一个独立的线程中描画。
onMeasure()方法有点复杂,它是组件和它的容器之间的渲染约束的关键部分。覆写onMeasure(),以便准确高效的报告组件被包含部分的尺寸。由于来自父容器限制的要求,使得尺寸的测量有些复杂,并且组件的尺寸一旦被计算完成,就要调用setMeasureDimension()方法来保存测量的宽度和高度。如果在onMeasure()方法中调用setMeasureDimension()方法失败,这个结果在测量时将是一个异常的值。
在上层看,实现onMeasure()方法的步骤如下:
1. 要用父容器的宽度和高度的计量规格来调用被覆写的onMensure()方法(widthMeasureSpec和heightMeasureSpec参数都是代表了尺寸的整数),这两个参数应该作为生成组件的宽度和高度的约束要求。对于这些规格约束类型的完整说明可以在View类说明的View.onMeasure(int,int)方法中找到。
2. 组件的onMeasure()方法应该计算用于渲染组件所需的尺寸(宽度和高度)。组件应该尽量保留在被传入的规格范围内,尽管它能够选择超出规格范围(在这种情况下,父容器能够选择做的事情包括:裁剪、滚动、抛出异常、或者要求onMeasure()方法用不同的尺寸规格再试)。
3. 一旦组件的宽度和高度被计算完成,就必须调用setMeasuredDimension(int width, int height)方法来保存计算结果。不这样做就会抛出一个异常。
下表是framework调用View类的其他标准方法:
分类 | 方法 | 说明 |
Creation | Constructors | 构造器的调用有两种类型:1.在代码中创建View对象;2.用布局文件填充View对象。第二种类型应该解析和应用布局文件中的任何属性定义。 |
onFinishInflate() | View对象和它的所有子对象都用XML填充完之后,调用这个方法。 | |
Layout | onMeasure(int, int) | 调用这个方法决定View对象及其所有子对象的尺寸要求。 |
onLayout(boolean,int,int,int,int) | 当View对象给它的所有子对象分配尺寸和位置时,调用这个方法。 | |
onSizeChanged(int,int,int,int) | 当View对象的尺寸发生改变时,调用这个方法。 | |
Drawing | onDraw(Canvas) | 当View对象渲染它的内容时,调用这个方法。 |
Event | onKeyDown(int,KeyEvent) | 当一个键的按下事件发生时,调用这个方法 |
onKeyUp(int,KeyEvent) | 当一个键弹起事件发生时,调用这个方法 | |
onTrackballEvent(MotionEvent) | 当鼠标轨迹球滚动事件发生时,调用这个方法。 | |
onTouchEvent(MotionEvent) | 当触屏事件发生时,调用这个方法。 | |
Focus | onFocusChanged(boolean,int,Rect) | 当View对象获取或失去焦点时,调用这个方法。 |
onWindowFocusChanged(boolean) | 当包含View对象的窗口获得或失去焦点时,调用这个方法。 | |
Attaching | onAttachedToWindow() | 当View对象被绑定到一个窗口时,调用这个方法。 |
onDetachedFromWindow() | 当View对象被从它的窗口中分离的时候,调用这个方法。 | |
onWindowVisibilityChanged(int) | 当包含View对象的窗口的可见性发生改变时,调用这个方法。 | |
|
|
|
定制View的例子
在API Demos中提供了一个定制的View对象的例子:CustomView。这个定制的View定义在LabelView类中。
LabelView示例展示了很多定制组件的不同特征:
1. 继承View类的完全定制化的组件;
2. 参数化的带有View填充参数(在XML中定义的参数)方式构造View对象。有一些填充参数使用通过这个View的父类传递过来的,还有一些用于labelView对象而定义的定制的属性;
3. 你所期望看到的标准的公共类型的方法,如setText()、setTextSize()、setTextColor()等等;
4. 一个重写的onMeasure()方法,它决定和设置了组件的渲染尺寸。(注意:在LabelView类中,实际的工作是由一个私有的measureWidth()方法来做的。)
5. 一个重写的onDraw()方法,它在提供的Canvas上描画标签。
从这个示例的custom_view_1.xml中,能够看到一些LabelView定制View的用法。实际上,可以看到android:命名空间参数和定制的app:命名空间的组合。这些app:参数是LabelView类所承认的并用于工作的一些定制化的属性,并且这些参数在示例的R资源定义类的styleable内部类中被定义。
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class CrossView extends View {
private float mRotation;
private Paint mPaint;
public CrossView( Context context, AttributeSet attrs ) {//AttributeSet对象在实例化时被系统传给了视图
super( context, attrs );
//start 创建paint对象
mPaint = new Paint();
mPaint.setAntiAlias( true );
mPaint.setColor( 0xFFFFFFFF );
//end 创建paint对象
//使用obtainStyledAttributes方法来创建一个TypedArray,这是访问存储于AttributeSet中的值的一个方便类,这类执行内部缓存,
//所以当你结束使用它之后随时调用回收函数。注意:需同时使用<declare-styleable>名称和<arr>名称的访问自定义属性
TypedArray arr = getContext().obtainStyledAttributes( attrs,R.styleable.cross );
int color = arr.getColor( R.styleable.cross_android_color, Color.WHITE );
float rotation = arr.getFloat( R.styleable.cross_rotation, 0f );
//remember to call this when finished
arr.recycle();
setColor(color);
setRotation(rotation);
}
private void setRotation( float rotation ) {
// TODO Auto-generated method stub
mRotation = rotation;
}
private void setColor( int color ) {
// TODO Auto-generated method stub
mPaint.setColor( color );
}
//重写onMeasure方法
@Override
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) {
// TODO Auto-generated method stub
super.onMeasure( widthMeasureSpec, heightMeasureSpec );
//需要使用计算好的宽和高的值作为该方法的实参
setMeasuredDimension( calculateMeasure(widthMeasureSpec), calculateMeasure(heightMeasureSpec) );
}
float[] mPoints = {0.5f,0f,0.5f,1f,0f,0.5f,1f,0.5f};
@Override
protected void onDraw( Canvas canvas ) {
// TODO Auto-generated method stub
super.onDraw( canvas );
canvas.save();//所有的在画布上绘图的调用都应当受对应的sava()和restore()的约束
int scale = getWidth();
canvas.scale( scale, scale );
canvas.rotate( mRotation );
canvas.drawLines( mPoints, mPaint );//绘制十字的两条线
canvas.restore();//所有的在画布上绘图的调用都应当受对应的sava()和restore()的约束
}
private static final int DEFAULT_SIZE = 100;//默认的试图尺寸
//实现计算测量值的代码
private int calculateMeasure(int measureSpec){
int result = ( int ) ( DEFAULT_SIZE*getResources().getDisplayMetrics().density );
int specMode = MeasureSpec.getMode( measureSpec );//在MeasureSpec中检索模式
int specSize = MeasureSpec.getSize( measureSpec );//在MeasureSpec中检索尺寸
//基于模式选择尺寸
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else if(specMode == MeasureSpec.AT_MOST){
result = Math.min( result, specSize );
}
return result;
}
}
(二)自定义属性
<?xml version="1.0" encoding="utf-8"?>
<!-- 该文件被放置在res/values/目录下 -->
<resources>
<!-- 声明属性 -->
<declare-styleable name="cross">
<attr name="android:color"/>
<attr name="rotation" format="string"/>
</declare-styleable>
<!--
<attr name="test" format="string"/>
<declare-styleable name="foo">
<attr name="test"/>
</declare-styleable>
<declare-styleable name="bar">
<attr name="test"/>
</declare-styleable> -->
</resources>
(三)在XML中使用自定义View
<?xml version="1.0" encoding="utf-8"?>
<!-- 要使用在XML中的自定义属性,首先必须为视图声明命名空间 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:example="http://schemas.android.com/apk/res/com.example"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<!-- 添加CrossView -->
<com.example.CrossView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<com.example.CrossView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
example.rotation="30"
android:color="#0000FF"/>
/>
<com.example.CrossView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
example.rotation="40"
android:color="#FFFF00"
/>
</LinearLayout>