Android 用户界面---定制组件(Custom Components)
基于布局类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>