创建自定义组件
Android提供了一个复杂且强大的自定义组件模型来创建自定义UI,基于基本的布局类:View和ViewGroup。平台包含了许多预先创建好的View和ViewGroup子类——分别被称为小部件和布局——你可以用它们来构建UI。
其中一些小部件包括Button、TextView、EditView、ListView、CheckBox、RadioButton、Gallery、Spinner,和具有更多特殊用途的AutoCompleteTextView、ImageSwitcher和TextSwitcher。
可用的布局有LinearLayout、FrameLayout、RelativeLayout和其他。更多例子请看“常用布局对象”。
如果预先创建好的小部件或布局都不能满足你的需要,你可以创建自己的View子类。如果你只需要对已经存在的小部件或布局做些小调整,那你可以简单创建他们的子类并覆写他们的方法。
创建自己的View子类在屏幕元素的外观和功能上给你精确的控制。要了解如何控制自定义view,这里有一些例子来说名你通过它能做什么:
l
你可以创建一个完全自定义渲染的View类型,例如一个用2D图形渲染的“音量控制”旋钮,它类似于模拟电子控制旋钮。
l
你可以把一族View组合在一起成为一个新的单独的组件,也许像下拉列表框(ComboBox),一个双面板选择器控制(左边面板的不同选择会一起右边面板列表内容的相应变化),等等。
l
你可以重写被渲染在屏幕上的EditText组件的方式,“Notepad Tutorial”使用这个方法效果很好,创建了一个有线纹的便签页。
l
你可以捕捉其他事件,如按下按键,并用自定义的方法去处理他们(例如游戏)。
下文解析了如何创建自定义View和在应用程序里使用他们。了解更多细节请看View类。
基本方法
下面是高度概括你创建自定义View组件所需要知道的事情:
1.继承一个已存在的View类或者其子类。
2.覆写父类的一些方法。要重写的方法是以“on”开头的,例如,onDraw()、onMeasure()和onKeyDown()。这类似于你重写Activity或ListActivity中的on...事件来控制生命周期或其他功能性的钩子。
3.使用你新的继承类。一旦完成,你的新继承类就可以用来代替你继承的View父类。
技巧:继承来可以在使用它们的Activity内定义为内部类。这样他可以访问activity中的所有变量和方法。但这不是必须的,你可能想创建一个新的公共View来在应用程序范围内使用。
完全自定义组件
完全自定义组件可以用来创建你想要的任何图形组件。也许是一个像旧式模拟计量表一样的图形音量单位表,或者是一个想卡拉OK字幕一样的文本。这些是内置的组件无法提供的,即使是把现有的组件组合起来也不行。
幸运的是,你可以很容易的创建你想要的任何形状和行为的组件,只限于你的想象、屏幕的大小和处理能力(记住最终的应用程序试运行比桌面工作站性能弱得多的设备上的)。
要创建一个完全自定义组件:
1.
你可以继承的最一般的view当然是View类,所以你通常由继承他来开始创建一的新的超级组件。
2.
你可以提供一个构造器来从XML文件获取属性和参数,你还可以有自定义的属性和参数(也许是颜色和大小范围,或者阻尼和针头的宽度等等)。
3.
你可能需要在你的组件类中创建自定义的时间监听器,属性访问器和修改器,以及其他更复杂的行为。
4.
你还要重写onMeasure(),并且如果你想组件显示某些东西有可能需要重写onDraw()。默认的,onDraw()方法不做任何事,onMeasure()方法设置一个100×100的大小——可不是你想要的大小。
5.
其他on...方法根据需要可能要重写。
扩展onDraw()和onMeasure()
onDraw()方法给你传递一个Canvas,在他上面你可以实现任何你想要的东西:2D图形、其他标准的或自定义的组件,样式文字或其他你能想到的。
注意:这里不支持3D图形。如果你想使用3D图形,必须继承SurfaceView而不是View,并且在一个单独的线程中进行绘图。参看GLSurfaceViewActivity例子。
onMeasrue()稍微常用一点。onMeasure()是你的组件和所在他的容器之间相邻边界的渲染约定。onMeasure()应该被重写以便高效并准确的报告他做包含的部件的尺寸。??该方法稍微有些复杂,因为受到父容器的限制的要求(父容器被传递到onMeasure()方法里),并且受到调用setMeasureDimension()方法计算宽和高的要求??如果你没有从一个覆写的onMeasure()方法里调用setMeasureDimension()方法,结果有可能与期望的不一致。
总的来看看,实现onMeasure()方法看起来像是这样的:
1.
重写的onMeasure()方法被调用需要宽和高的尺寸说明(widthMeasureSpec和heightMeasureSpec参数,两者都是代表大小的整型规范数据),这两者是你应该给出的宽和高尺寸的限制要求。全面的参考请看View.onMeasure(int,int)下的参考(该参考文档也不是能很清楚的解析这种尺寸的操作)。
2.
你的组件的onMeasure()方法应该计算出要渲染组件的宽和高。他应该尽可能地在传递进来的尺寸之内,尽管可以选择超出该尺寸(这种情况下,副容器可以选择怎样做,包括剪裁、滚动,抛出异常或者要求onMeasure()方法再计算一遍,也许使用不同的尺寸规格)。
3.
一旦计算出宽和高,就必须用计算出来的尺寸来调用setMeasureDimension(int width, int
height)方法。如果这步失败将会抛出异常。
以下是其他一些标准方法的概括,框架在view上调用这些方法:
Category
Methods
Description
Creation
Constructors
There is a form of the constructor that are called
when the view is created from code and a form that is called when
the view is inflated from a layout file. The second form should
parse and apply any attributes defined in the layout
file.
Called after a view and all of its children has
been inflated from XML.
Layout
Called to determine the size requirements for this
view and all of its children.
Called when this view should assign a size and
position to all of its children.
Called when the size of this view has
changed.
Drawing
Called when the view should render its
content.
Event processing
Called when a new key event occurs.
Called when a key up event occurs.
Called when a trackball motion event
occurs.
Called when a touch screen motion event
occurs.
Focus
Called when the view gains or loses
focus.
Called when the window containing the view gains
or loses focus.
Attaching
Called when the view is attached to a
window.
Called when the view is detached from its
window.
Called when the visibility of the window
containing the view has changed.
自定义View的例子
API Demos里的自定义View例子提供一个自定义View的示例。自定义View被定义在LableView类里。
LabelView例子展示了自定义组件的一些不同的方面:
l
继承View类来完全自定义组件
l
参数化的构造方法获取iew inflation参数(参数定义在XML文件中)。一些参数传递给了View父类,但更重要的是为LabelView定义了一些自定义属性并供其使用。
l
设置label组件的标准公共方法,如setText()、setTextSize()、setTextColor()等等。
l
覆写的onMeasure()方法,决定并设置组件渲染的大小。(注意在LabelView中,真正的工作是由一个私有的measureWidth()方法完成的。)
l
覆写的onDraw()方法,在提供的canvas上绘制label。
你从custom_view_1.xml中看到LabelView自定义View的使用例子。尤其可以看见android:namespace参数和自定义app:namespace参数两者相混合。app:参数是自定义的,只有LabelView认识并且能使用,并且被定义在一个样式内部类里。
复合控制(复合组件)
如果你不想创建完全自定义组件,只是想把已存在的可重用组件组合在一起,那么创建一个复合组件(或者叫复合控制)就在合适不过了。简单来说,这样把几个现有的组件融合到一个逻辑组合里面可以封装成一个新的组件。例如,一个Combo Box组件可以看作是是一个EditText和一个带有弹出列表的Button组件的混合体。如果你点击按钮为列表选择一项,被选择项就会填充到EditText中,但是用户仍然能够想EditText中直接输入一些东西。
在Android中,其实还有其他的两个View类可以做到类似的效果:Spinner和AutoCompleteTextView,,但是Combo Box作为一个例子更容易让人理解。
要创建一个复合组件:
1.
通常以布局开始,创建一个Layout类的派生类。也许在Combo box我们会选择水平方向的LinearLayout作为父类。记住,其他的Layout类是可以嵌套到里面的,因此混合组件可以是任何组件的混合。注意,正如Activity一样,你既可以使用外部XML文件来声明你的组件,也可以嵌套在代码中。
2.
在新的混合组件的构造函数中,首先,调用所有的父类的构造函数,传入对应的参数。然后可以在你新的混合组件设置的其他的一些view,在哪创建EditText组件,又在哪创建PopupList组件。注意:你同时也可以在XML文件中引入一些自己的属性和参数,这些属性和参数也可以被你的混合组件所使用。
3.
你也可以创建时间监听器去监听新组件中View类触发的事件,例如,对List选项单击事件的监听,你在该事件发生后更新你EditText的内容。
4.
你可能创建自己的一些属性,带有访问和修改方法。例如,允许设置EditText初始值并且提供访问它的方法。
5.
在Layout的派生类中,你没有必要去覆写onDraw()和onMeasure()方法,因为Layout会有比较好的默认处理。但是,如果你觉得有必要你也可以覆写它。
6.
你也可能覆写一些on...方法,例如通过onKeyDown()的覆写,你可以通过按某个键去选择列表中的对应的值。
总之,把Layout类作为复合组件的基类有许多优点,包括:
l
像activity一样,你可以通过XML文件去声明你的新组件,或者你也可以在代码中嵌套。
l
onDraw()方法和onMeasure()方法(还有其他大部分on...方法)已经做得很好了,所以你不需要覆写。
l
最后,你可以很快的创建你的混合组件,并且可以像单一组件那样进行重用。
复合组件的例子
在API
Demos项目中,有两个List类的例子——Example 4和Example 6,里面的SpeechView组件是从LinearLayout类派生过来,实现显示演讲显示功能,对应的原代码是List4.java和List6.java。
修改已有View类型
在某些情况下,你可能有更简单的方法去创建你的组件。如果你应经有了一个非常类似的组件,你所要做的只是简单的从这个组件派生出你的组件,覆写其中一些有必要修改的方法。通过完全自定义组件的方法你也可以同样的实现,但通过从特定View类派生产生新的组件,你可以简单获取一些已经存在的处理机制,这些很可能是你所想要的,而没有必要从头开始。
例如,在SDK中有一个NotePad的例子(NotePad application )。该例子演示了很多使用Android平台的细节,例如你会学到从EditView派生出能够自动换行的记事本。这还不是一个完美的例子,因为相比早期的版本来说,这些API已经改变了很多,但它确实说明了一些问题。
如果你还未查看该程序,现在你就可以在Eclipse中导入记事本例程(或仅通过提供的链接查看相应的源代码)。特别是查看NoteEditor.java 中的MyEditText的定义。
下面有几点要注意的地方:
1.声明(The Definition)
这个类是通过下面一行代码来定义的:
public static class MyEditText extends
EditText
l
它是定义在NoteEditor activity类里面的,但是它是共有的(public),因此如果有必要,它可以通过NoteEditor.MyEditText从NoteEditor外面来调用。
l
它是static类(静态类),意味着不会出现所谓的通过父类访问数据的“虚态方法”(“synthetic methods”),
这样就使该类成为一个可以不严重依赖NoteEditor的单独类。对于不需要从外部类访问的内部类的创建,这是一个很清晰地思路,保证所产生的类很小,并且允许它可以被其他的类方便的调用。
l
它是EditText类的扩展,它是我们选择的用来自定义的父类。当我们完成以后,新的类就可以作为一个普通的EditText来使用。
2.类的初始化
一般来说,父类是首先调用的。进一步来说,这不是一个默认的构造函数,而是一个带参数的构造函数。因为EditText是使用从XML布局文件提取出来的参数进行创建,因此我们的构造函数也要取出参数并且将这些参数传递给父类。
3.覆写的方法
在本例中,仅对onDraw()一个方法进行覆写。但你可以很容易地为你的定制组件覆写其他需要的方法。
对于记事本例子来说,通过覆写onDraw()方法我们可以在EidtView的画布(canvas)上绘制蓝色的线条(canvas类是通过重写的onDraw()方法传递)。该函数快要结束时要调用super.onDraw()函数。父类的该方法应该被调用,但是在这个例子里面,我们是在划好了蓝线之后调用的。
4.使用自定义组件
现在,我们已经有自己定制的组件了,但是应该怎样使用它呢?在记事本例子中,定制的组件直接在预定义的布局文件中使用,让我们看一看res/layout目录中的note_editor.xml文件。
id="@+id/note" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:drawable/empty" android:padding="10dip" android:scrollbars="vertical" android:fadingEdge="vertical" />
l
该自定义组件在XML中是作为一个一般的View类来创建的,并且是通过全路径包来描述的。注意这里内部类是通过NoteEditor$MyEditText来表示的,这是Java编程中引用内部类的标准方法。
如果你的自定义view组件不是作为内部类定义的,那么你还可以通过XML元素的名字并且去掉class属性来声明该View组件,例如:
注意,MyEditText类现在是一个独立的类文件。当类被嵌套在NoteEditor类里时,这种方法就不再起作用了。
l
在定义中的其他属性和参数将传递给定制组件的构造函数,然后才传到EditText构造函数中,因此这些参数也是你使用EditText组件的参数。注意,这里你也可以增加你自己的参数,我们将在下面讨论这个问题。
这就是你全部需要做的,诚然这是一个简单的例子。但问题的关键是:你的需求有多复杂,那么你的自定义组件就有多么复杂。
一个更为复杂的组件可能需要重载更多的on系列函数,并且还要很多特有的函数来充分实现自定义组件的功能。唯一的限制就是你的想象力和你需要组件去执行什么工作。