# 自定义组件
*Android提供了一套复杂而强大的组件化模型来建立你自己的UI。其依赖于基本的布局类:View和ViewGroup。首先,这个平台提供了各种各样的View和ViewGroup的子类——被称作窗口小部件(widgets)和布局(layout),你可以自由的使用它们来创建你自己的UI。*
部分可用的窗口小部件如下:`Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner`和专用的`AutoCompleteTextView, ImageSwitcher`, 和`TextSwitcher`等。
在布局中可得到的有:`LinearLayout, FrameLayout, RelativeLayout`等。更多示例请参阅[通用布局对象](https://developer.android.com/guide/topics/ui/declaring-layout.html#CommonLayouts)。
如果这些预置的widgets和layouts中没有符合你的需求的,你可以创建自己的View子类。如果你只需要对现有的widgets和layouts进行很小的调整,你可以简单的继承那个widget或layout,然后重写它的某些方法。
系统为你提供了精确的控制屏幕上呈现元素外观和功能的能力来创建你自己的View子类。为了给出一个通过自定义View你可以做些什么的引子,下面是自定义View可以做些什么的例子。
- 你可以完全创建一个自定义绘制的View类型,例如用2D 绘图绘制一个像模拟电子器件的“音量控制”旋钮。
- 你可以把一组View组件组合到一个新的组件当中,就像制作一个组合框(弹出列表和自由文本域的组合),一个双窗口选择器(左右两个窗口都具有一个列表,在每个列表中你可以重新赋值其中的每一条内容)等等
- 你可以覆写EditText组件在屏幕上呈现的方式(记事本教程使用此功能)。
- 你可以捕获其他事件,例如按键按下的事件,并且自定义处理方式(就像游戏一样)
下面的部分介绍如何创建自定义的view并且将它应用到应用中。详细信息请看[View.class](https://developer.android.com/reference/android/view/View.html)
##1.基本方法##
这里是开始创建你的自定义View组件所需要知道的高级概述:
1. 你自己的View需要继承一个View类或其子类。
2. 重写父类中的一些方法。父类中可以重写的方法都已`on...`开头,就像`onDraw()`, `onMeasure()`, 和`onKeyDown()`等等。这些和 Activity 或 ListActivity中的`on...`很像。
3. 使用你的新扩展类。一旦完成,你的扩展类可以用于代替你的父类的位置。
> **注意**:扩展类可以以内部类的方式定义在使用它的Activity中。这是很有用处的,因为Activity可以控制他们的访问权限,但这不是必须的(因为或许你想创建一个使用更广泛的公共View应用在你的应用中)。
## 2.完全自定义组件##
完全自定义组件可以被用来绘制你想显示的任何样子的组件。或许让一个图形VU表看起来像一个旧的模拟量表。或许是一个很长的文字View,有一个弹跳球沿着这些词移动,所以你可以随着这个KTV机器一起唱歌。无论哪种原因,你想要做某些无论内置组件如何组合也不能实现的工作,可以采用这种方式定义组件。
幸运的是,你可以轻松的以你喜欢的方式创建出一个组件,能限制你的只有你的想象力、屏幕的大小、和可得到的处理的能力(记住,在最后你的应用或许不得不运行在一些比你的机器处理能力差的机器上)。
创建一个完全自定义的组件所需如下:
1. 你可以扩展的最通用的View,不出所料的是View类,所以通常首先扩展它来创建新的超级组件。
2. 你需要提供一个可以从XML中获取属性和参数的构造器,你也可以指定自己的属性和参数。
3. 你或许会也想要在你的组件中创建自己的事件监听器,修改和访问属性,和更多更复杂的行为。
4. 如果你想要组件展示一些东西,几乎大多数情况你要重写 `onMeasure()`和`onDraw()`方法 ,尽管他们都有默认的行为,默认的`onDraw()`方法是空方法,默认的`onMeasure()`会将大小设置为100*100 这可能不是你想要的。
5. 其他`on...`方法也可以根据需要进行重写。
## 3.重写`onDraw()` 和 `onMeasure()`##
` onDraw()`提供给你一个画布(Canvas)你可以在上面实现你想做的任何事:2D绘图、其他的标准或自定义的组件,字体样式,或者任何你可以想到的。
> **注意:**这不适用于3D绘图。如果要使用3D图形,则必须扩展SurfaceView 而不是View,并从单独的线程绘制。有关详细信息,请参阅GLSurfaceViewActivity示例。
>
`onMeasure()`是你的组件与其容器之间的渲染的关键部分。`onMeasure()`应该被重写以有效和准确地报告其所含部件的测量结果。 `onMeasure()`视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。在`setMeasuredDimension()`方法调用之后,我们才能使用`getMeasuredWidth()`和`getMeasuredHeight()`来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。如果你无法从覆盖`onMeasure()`方法调用此方法,则结果将在测量时间内出现异常。
> **注意:**上面这一段是从郭霖的文章上截取出来的[链接](http://blog.csdn.net/guolin_blog/article/details/16330267)。与原文不相符。
`onMeasure()`方法实现步骤如下:
1. 重写`onMeasure()`方法,调用它来测量组件的长度与宽度(他们都是以int整数来表示大小)对于你创作的组件来说,自定义他们的长宽是必须的。更全面的参考,请参见参考文档`View.onMeasure(int, int)`(参考文档很好的解释了整个测量操作的过程)。
2. 你的组件的onMeasure()方法应该能计算出一个所要呈现出的长宽的大小。它应该满足传入的规格参数的要求,虽然它可以选择超出他们(在这种情况下,父控件可以选择他们的处理方法,包括裁剪、滚动,抛出异常,或者根据不同的测量规则重新调用`onMeasuer()`方法,)
3. 一旦控件的宽度和长度被计算出来,必须调用`setMeasuredDimension(int width, int height)`将计算值传递给它,如果失败将会抛出异常。
以下是一些View的框架提供的回调方法
| 类别 | 方法 | 描述 |
| ------ | ---------------------------------------- | ---------------------------------------- |
| 创建 | 构造方法 | 当一个View从代码中被创建的时候会调用一种构造器,当一个View从布局文件中引入时会调用另一种构造方法,第二种形式可以解析一些布局文件中定义的属性。 |
| | [onFinishInflate()](https://developer.android.com/reference/android/view/View.html#onFinishInflate()) | 当View和它的子View全部从XML中获取之后调用 |
| Layout | [onMeasure(int, int)](https://developer.android.com/reference/android/view/View.html#onMeasure(int, int)) | 调用这个方法来确定它和它的子View的所需要的大小 |
| | [onLayout(boolean, int, int, int, int)](https://developer.android.com/reference/android/view/View.html#onLayout(boolean, int, int, int, int)) | 当这个View需要为它的子View赋值大小和位置的时候调用 |
| | [onSizeChanged(int, int, int, int)](https://developer.android.com/reference/android/view/View.html#onSizeChanged(int, int, int, int)) | 当View的大小改变时调用 |
| 绘制 | [onDraw(Canvas)](https://developer.android.com/reference/android/view/View.html#onDraw(android.graphics.Canvas)) | 当一个View需要绘制它自己时调用 |
| 事件处理 | [onKeyDown(int, KeyEvent)](https://developer.android.com/reference/android/view/View.html#onKeyDown(int, android.view.KeyEvent)) | 当一个新的键盘事件发生时调用 |
| | [onKeyUp(int, KeyEvent)](https://developer.android.com/reference/android/view/View.html#onKeyUp(int, android.view.KeyEvent)) | 当键盘抬起事件发生时调用 |
| | [onTrackballEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onTrackballEvent(android.view.MotionEvent)) | 当轨迹球移动事件发生时调用 |
| | [onTouchEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onTouchEvent(android.view.MotionEvent)) | 当触摸屏幕的事件发生时被调用 |
| 焦点事件 | [onFocusChanged(boolean, int, Rect)](https://developer.android.com/reference/android/view/View.html#onFocusChanged(boolean, int, android.graphics.Rect)) | 当其获得或失去焦点时调用 |
| | [onWindowFocusChanged(boolean)](https://developer.android.com/reference/android/view/View.html#onWindowFocusChanged(boolean)) | 当包含这个View的窗口获得或失去焦点时调用 |
| 绑定事件 | [onAttachedToWindow()](https://developer.android.com/reference/android/view/View.html#onAttachedToWindow()) | 当View绑定到窗口上时调用 |
| | [onDetachedFromWindow()](https://developer.android.com/reference/android/view/View.html#onDetachedFromWindow()) | 当View和窗口分离解绑时调用 |
| | [onWindowVisibilityChanged(int)](https://developer.android.com/reference/android/view/View.html#onWindowVisibilityChanged(int)) | 当窗口的可见性发生变化时调用 |
## 4.复合控件##
如果你不想创建一个完全自定义的组件,而是希望将 由一组现有的控件组成的可重用的组 件组合在一起。创建一个复合控件或许会满足你的需求。简单的来说,他将一些原生控件汇集在一起,形成一个item的逻辑组,这个item可以被当作一个单一事情看来对待。例如,一个组合框可以被看作由一行 EditText和一个相邻的附加了一个PopupList的button按钮组和而成的,如果你点击这个按钮,选择了list中的一些数据,就会填充EditText字段,但如果用户喜欢,用户也可以直接将其输入到EditText中。
在Android中,实际上还有两个其他View可以实现:Spinner 和 AutoCompleteTextView,但不管他们是如何实现的,组合框的概念是一个容易理解的例子。
创建一个复合组件需要:
1. 通常的起点是某种布局,因此创建一个继承于布局的类。也许在组合框的例子中,我们可能会使用一个水平方向的LinearLayout。请记住,其他布局可以嵌套在内部,因此复合组件可以是任意复杂和结构化的。请注意,就像使用Activity一样,你可以使用声明式(基于XML)的方法来创建包含的组件,也可以使用编程方式将其嵌套在代码中。
2. 在新建类的构造方法中,首先将父类所需要的所有参数提交到父类的构造方法中去。然后,你就可以设置其他的View组件使用到你的新的组件当中,这里就是你应该创建EditText 文本域和PopupList的地方。注意,你还可以从XML中将自己的属性和参数引入,通过构造函数提取。
3. 你还可以为控件内部包含的View创建事件监听器。例如,通过列表选项的Click Listener侦听器来更新EditText的内容。
4. 您还可以使用访问器和修饰符创建自己的属性,例如,允许一开始在组件中设置EditText值,并在需要时查询其内容。
5. 在继承布局的情况下,你不需要重写 onDraw()和onMeasure()方法,因为布局自己的默认行为会工作的很好,然而,如果你需要的话还是可以重写他们。
6. 你或许会重写其他on...方法,例如 onKeyDown(),当按下某个键时,可能从组合框的弹出列表中选择某些默认值。
总而言之,使用“布局”作为自定义控件的基础具有许多优点,包括:
- 你可以使用声明XML文件的方式指定布局,也可以以编程方式创建视图,并通过代码将其嵌入到布局中。
- onDraw()与onMeasure()方法(和其他on...方法)有适合的行为,所以你不必重写它们。
- 最后,你可以非常快速地构建任意复杂的复合控件,并重新使用它们,就像它们是单个组件一样。
## 5.修改一个现有view的类别##
在某些特殊的情况下,这里有一个更加简单创建自定义View的方法,如果这里有一个和你想要的组件很相似的组件,你可以简单的继承它,仅仅重写你想改变的地方。通过一个完全自定义的组件,你可以做任何你想做的事情,但是从View的层次结构中继承一个功能更专一的类,可以让你免费获取大量可能完全符合你需要的行为。
例如,SDK中包含了一个[记事本应用](https://developer.android.com/samples/index.html)的例子,这个例子从很多方面展示了对Android平台的利用,就像扩展了EditText View来实现了一个内置的记事本。这或许不是一个很完美的例子,但是它确实展示了这一观念。
如果你还没有这样做,导入记事本的例子到你的工作空间中,特别的,请注意[NoteEditor.java](https://developer.android.com/samples/index.html)文件中的MyEditText的定义。
一些关键点记录如下:
**1.定义**
类的定义如下
`publicstatic class MyEditText extends EditText`
- 它以一个内部类的方式定义在NoteEditor 活动中,但是它是public 的所以它可以以NoteEditor.MyEditText方式被其他外部类使用。
- 它是静态(static)的,这意味着
- 它继承自 EditText,一个我们选择用来自定义一些情况的View。当我们完成后,这个View就可以用来替代一个普通的 EditText view。
**2.类初始化**
一般情况下父类的构造方法先被调用。此外,这不是默认的构造器,而是一个参数化的构造函数。当EditText从XML文件中引入时,通过这些参数来创建它,因此,我们的构造器也需要将他们传递到父构造器当中去。
**3.重写方法**
在这个例子中,只重写了 `onDraw()`方法,但是其他项目中可能还需要重写其他方法。
对于这个记事本的例子来说,重写 `onDraw()`方法允许我们在EditText的画布上中绘制蓝色线,其父类的`onDraw()`方法在子类的方法结束前调用。父类的方法应当被调用,但是在这个例子中,我们在绘制完成蓝线后才调用的它。
**4.使用自定义组件**
现在我们已经有了自定义的组件,但是现在我们该怎样来使用它?在这个记事本的例子中,自定义的组件直接在声明布局文件中使用,详情请在 res/layout文件夹下查看note_editor.xml
```
<view
class="com.android.notepad.NoteEditor$MyEditText"
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" />
```
- XML中生命自定义组件的与普通的View一样,并且以用完整的包名指定类。还要注意,我们定义的内部类要通过NoteEditor$MyEditText符号来引用, 这是Java编程语言中一种标准的引用内部类的方法。
如果你自定义的组件没有定义为内部类,就可以按如下声明。
```
<com.android.notepad.MyEditText
id="@+id/note"
... />
```
> **注意**:MyEditText不是一个单独的文件,他是嵌入在 NoteEditor 类内部的,上面这种方法将会不起作用。
- 声明的其他属性和参数等将通过组件的构造函数传递进去,然后传递到 EditText的构造函数中。所以,这里有很多参数是EditText使用的。请注意,你也可以添加自己的参数,下面将再次介绍。
这就是它的一切。诚然,这是一个简单的例子,但是记住一点---创建一个组件的难度取决于你的需求。
一个更加复杂的组件可能会重写更多`on...`方法,并且引入一些自己的方法,定义他们的参数和行为。但请记住,唯一的限制是你的组件的功能的,就是你的想象力。