这篇文章是 Google 官方文档 Custom Components 的翻译。
由于打算开始好好学习自定义 View 了,就打算边学边写点文章记录自己的学习过程。不知道从哪里下手,干脆看看官方的文档吧,于是就想着翻译出来了。不过我是个英文菜鸡,翻译什么的更不要提了。翻译的很差劲,不少地方实在编不出来的,只好寻求了 Google 翻译 的帮助,说实话,比我自己翻译的强多了。
注意: 下文涉及到的 demo 我已经找不到了,不知道是我的操作方法不对还是由于年代久远已经被 Google 下架了。
以下是正文。
Android 为开发者提供了许多复杂且强大的组件化模型,而这些又是基于两个基础的布局类:View 与 ViewGroup。首先,Android 提供了许多 View 和 ViewGroup 的子类,它们被称作部件和布局,你可以直接使用它们。
你可以用到的部件包括:Button、TextView、EditText、ListView、CheckBox、RadioButton、Gallery、Spinner 等,还有更多专用的部件比如:AutoCompleteTextView、ImageSwitcher 与 TextSwitcher 。
可以用到的布局包括:LinearLayout、FrameLayout、RelativeLayout 等。如果你想了解更多,请查看 Common Layout Objects 。
如果这些系统提供的部件或者布局无法满足你的要求,你可以创建自己需要的 View 的子类。如果你仅仅是需要在当前的部件或者布局的基础上做一些小小的调整,你仅仅需要写一个继承自它们的类并重写一些方法。
创建你自己的子类可以让你完全控制屏幕元素的显示与行为。为了给出一些方便你控制自定义 View 的方法,你不妨参考下面的一些例子:
你可以创建一个完全自定义呈现的 View 类型,例如使用 2D 图形呈现一个“音量控制”旋钮,用它实现模拟电子控制的功能。
你可以把一组 View 组件组合在一个新的组件中,也许就像是去做个组合框(弹出列表与自由文本的组合),双窗体选择器控制(每个列表都有左右两个窗体,你可以重新分配列表中的项)。
你可以重写一个 EditText 组件在屏幕上表现的方式(Notepad Tutorial 创建了一个线型的记事本页,效果很不错)。
你可以捕获“按下屏幕”等事件然后按照自己的意愿来处理(如游戏)。
以下各节说明如何创建自定义 View,并在你的应用中使用它们。要获取更多的详细相关信息,请参考 View 类。
在你开始创建自己的自定义组件之前,你需要知道以下大致的概述:
让你自己的类继承自 View 或者它的子类。
重写一些父类的方法。重写的方法一般是以 on 开头的,比如 onDraw()、onMeasure() 与 onKeyDown() 。这和你重写 Activity 或者 ListActivity 的生命周期和其他一些功能的 on... 方法是很相似的。
使用你的新拓展类。一旦创建完成,你就可以用它去代替基于它的视图。
提示:拓展类可以定义成 activity 的内部类并使用它们,因为这样可以很容易地控制它们,但并不是必须的(也许你更想在应用中创建一个 public 的类以便更广泛的使用)。
完全自定义组件可以去创建你希望显示的视图组件。也许是一个图形 VU 表,看起来像一个旧的模拟量表,或一个长长的文本视图,弹跳球沿着这些词移动,所以你可以和一个卡拉 OK 机一起唱歌(以上这一句话来自 Google 翻译,因为我真的翻译不来 = =)。总之,无论你怎么组合内置组件,一些你想要的东西它们始终是无法实现的。
幸运的是,你可以很简单地创建你想要的组件,但也会受到一些限制,比如你的想象力、屏幕的尺寸大小或者处理器的性能(记住将来运行你的应用的机器性能远比桌面工作站要低得多)。
创建一个完全自定义组件:
你要继承的最普通的视图当然就是 View 了,所以你的第一步总是要从继承 View 类开始。
为了使用 XML 文件中的属性与参数,你需要提供一个构造方法,并且你也可以自定义属性与参数(比如 VU 表的尺寸与颜色、箭头的宽度和阻尼等)。
你当然也可以实现自己的事件监听器、属性访问器和修饰符以及更多的复杂行为。
如果你想让你的自定义组件看起来漂亮一些,你就得重写 onMeasure() 与 onDraw() 。它们都有默认的行为,onDraw() 默认什么也不做,onMeasure() 则会默认设置一个 100 x 100 的尺寸,但这肯定不是你想要的。
需要的话,你也可以重写其它的 on... 方法。
onDraw() 方法给你提供了一个 Canvas(画布) 从而可以实现任何你想要的东西:2D 图形、标准或自定义的组件、样式文本或者其它你能想到的东西。
提示: 它不支持 3D 图形。如果你想使用 3D 图形,就要继承 SurfaceView 而不是 View,并且在子线程进行绘制。可以在 GLSurfaceViewActivity 的示例中查看详情。
onMeasure() 还是有一点小复杂的。在你的组件与父容器之间的连接上,onMeasure() 扮演着重要的角色。onMeasure() 需要被重写的目的是高效准确地计算所包含部分的尺寸。由于父布局的限制要求使它看起来稍微复杂一些(它们传递给 onMeasure() 方法),并且一旦宽度和高度被计算出来,就需要去调用 setMeasuredDimension() 方法来测量宽度和高度。如果你在 onMeasure() 里面忘记了调用这个方法,运行的时候就会抛出异常。
在高水平上,onMeasure() 的实现看起来像下面这样:
重写的 onMeasure() 方法伴随着宽度和高度测量规范被调用(widthMeasureSpec 与 heightMeasureSpec 这两个参数都是代表尺寸的整型数据),这些规范应该被当做一种需求对待,这种需求是你对应该要测量的宽度和高度的限制。有关这些规范可能需要的限制类型的完整参考,请参见 View.onMeasure(int,int)中的参考文档(这个参考文档也很好地解释了整个测量操作)。
你的组件的 onMeasure() 方法需要去计算一个将来要提供给自己的宽度和长度的度量。它应该尽量保持在规定范围之内,尽管它可以选择超过它们(在这种情况下,父方法可以选择去做些什么,包括裁剪、滚动、抛异常或者以不同的测量规范重新运行 onMeasure() 方法)。
一旦宽度和高度被计算好了,setMeasuredDimension(int width, int height) 就肯定会被调用去做计算测量。如果不这么做将导致引发异常。
下面是 framework 会调用的其他一些方法的简单总结:
分类方法描述Creation构造方法有一种构造方法是在 view 在代码中被创建的时候调用,还有一类是在 view 从布局文件加载的时候调用。如果是后者的话,一定要解析并应用布局文件中定义的属性。onFinishInflate()当一个视图和它的所有子视图从 XML 文件加载完毕后调用LayoutonMeasure(int, int)确定一个视图和它的所有子视图的尺寸需求的时候调用onLayout(boolean, int, int, int, int)当一个视图去给他的子视图分配大小和位置的时候调用onSizeChanged(int, int, int, int)当视图的尺寸大小发生变化时调用DrawingonDraw(Canvas)当视图渲染内容的时候调用Event processingonKeyDown(int, KeyEvent)一个新的按键事件发生时调用onKeyUp(int, KeyEvent)一个按键事件结束时调用onTrackballEvent(MotionEvent)轨迹移动事件发生时调用onTouchEvent(MotionEvent)触摸屏幕事件发生时调用FocusonFocusChanged(boolean, int, Rect)视图获得或失去焦点时调用。onWindowFocusChanged(boolean)当包含一个视图的窗口获得或失去焦点时调用AttachingonAttachedToWindow()当视图附加到窗口时调用onDetachedFromWindow()当视图从窗口分离时调用onWindowVisibilityChanged(int)当包含一个视图的窗口的可见性发生变化时调用API Demos 中的 CustomView 示例提供了自定义视图的示例。 自定义视图在 LabelView 类中定义。
LabelView 示例演示了自定义组件的许多不同方面:
为了实现一个完全自定义组件,需要继承自 View 类。
参数化的构造方法,用来获取视图的参数(在 XML 文件中定义的参数)。一些被传递给了 View 的父类,但更重要的是,有一些被定义和用于 LabelView 的自定义属性。
你也会希望能看到一个标签组件的标准 public 方法,比如 setText()、setTextSize()、setTextColor() 等等。
重写的 onMeasure() 方法决定并且渲染一个组件的尺寸。(请注意,在 LabelView 中真正起到作用的是 private 的 measureWidth() 方法。)
重写的 onDraw() 方法在给定的画布上绘制标签。
你可以在 custom_view_1.xml 文件中看到一些自定义 View – LabelView 的简单用法。特别的是,你可以看到两个混合的命名空间参数,android: 与 app: 。app: 的参数是 LabelView 可以辨识并且用来工作的自定义参数,并且在 R 资源文件定义类中的一个 styleable 内部类中被定义。
如果你不想创造一个完全的自定义组件,而是更倾向于把由已存在的控件组成的可复用组件放在一起,那么创造一个复合组件(或复合控件)就刚好合适。简而言之,它把一系列的控件(或视图)聚合成一个有逻辑的组,从而可以像单个组件一样被对待。例如,一个组合框可以看做是一个单行 EditText 与旁边的一个带有列表框的按钮组合而成。如果你按下按钮选择了列表中的某些东西,它就会被填入到 EditText 中,但是用户也可以按照自己的意愿直接在 EditText 中输入内容。
在 Android 中,实际上还有两个其他视图可以实现这种功能:Spinner 与 AutoCompleteTextView,但不管怎么说,Combo Box 的概念是很好理解的。
创建一个复合组件的步骤:
通常的起点是某种布局,因此创建一个扩展布局的类。 也许在组合框的情况下,我们可能会使用一个水平方向的 LinearLayout。 请记住,其他布局可以嵌套在内部,因此复合组件可以是任意复杂和结构化的。 请注意,就像使用 Activity 一样,您可以使用声明式(基于 XML)的方法来创建包含的组件,也可以使用编程方式将其嵌套在代码中。
在新类的构造方法中,使用父类预期的任何参数,并将其传递给父类的构造方法。 然后,你可以设置其他视图以在新组件中使用;这是你将要创建 EditText 和 PopupList 的位置。 请注意,你还可以将自己的属性和参数引入可以由构造方法提取和使用的 XML 文件中。
你还可以为包含的视图发生的事件创建监听方法,例如,当在一个列表中进行选择的时候,列表项的点击监听方法就可以更新 EditText 中的内容。
你还可以使用访问器和修饰符创建自己的属性,例如,允许最初在组件中给 EditText 设置初始值并且在需要的时候可以查询它的内容。
在拓展布局的情况下,由于布局有默认的行为并且工作正常,所以你没有必要去重写 onDraw() 与onMeasure() 方法。然而,你需要的话当然也可以重写。
你也可以重写其它的 on... 方法,比如 onKeyDown(),以便在按下某个按钮时从组合框的弹出列表中选择某些默认值。
总结一下,使用 Layout 作为自定义控件的基础有很多优点,包括:
你可以使用声明性 XML 文件指定布局,就像 activity 的用法一样,或者你可以以编程方式创建视图,并通过你的代码将其嵌入到布局中。
onDraw() 与 onMeasure() 方法(再加上大部分其它 on... 的方法)一般都是合适的行为,你也许没有必要去重写它们。
最后,你可以非常快速地构建任意复杂的复合视图,并重新使用它们,就像它们是单个组件一样。
复合控件的示例
在 SDK 附带的 API Demos 项目中,有两个列表示例 – Views/Lists 下面的示例 4 与示例 6 演示了一个 SpeechView,它继承自 LinearLayout,创建了一个显示语音引号的组件。示例代码中的相应类是 List4.java 和 List6.java。
有一个更容易的选择是创建一个在某些特定情况下有用的自定义视图。 如果有一个已经非常类似于你想要的组件,你可以简单地扩展该组件,并且只是重写你想要改变的行为。 你可以使用完全自定义的组件执行所有操作,但是从 View 层次结构中的更专门的类开始,你还可以轻易获取大量可能完全符合你的需求的行为。
例如,在 SDK 的示例中包含了一个 NotePad application 。它展示了 Android 平台的许多特征,其中包括继承自一个 EditText 视图去实现一个线型的记事本。它并不是一个完美的示例,它的 API 可能会自早期预览版本中发生改变,但它确实说明了原理。
如果你还没有这么做,将 NotePad 示例导入到 Android Studio 中(或只是看看提供的链接中的源码)。着重看 NoteEditor.java 文件中的 MyEditText 的定义。
一些要点如下
1.定义
该类定义如下:
public static class MyEditText extends EditText
它被定义成 NoteEditor 活动的内部类,但由于它是 public 的,所以需要的话,在 NoteEditor 的外部类中可以通过 NoteEditor.MyEditText 的方式访问它。
它是 static 的,这意味着它不会产生所谓的允许父类访问数据的“合成方法”,这反过来意味着它真的表现为一个单独的类,而不是与 NoteEditor 强烈相关的东西。 如果不需要从外部类访问它的状态,使生成的类保持较小,并允许其从其他类轻松使用,则这是创建内部类的更为清晰的方法。
它继承自 EditText,这是我们在这种情况下选择的视图。当我们完成之后,新的类就可以代替普通的 EditText 视图。
2.类的初始化
一如以往,父类方法首先被调用。 此外,这不是一个默认的构造方法,而是一个参数化的构造方法。 EditText 通过 XML 布局文件中的属性被创建,因此,我们的构造方法需要将它们传递给父类构造方法。
3.重写方法
在这个示例中,只有一个方法被重写了:onDraw() – 但是当你创建自己的自定义组件时,可能很容易需要重写其它的方法。
在 NotePad 的示例中,重写 onDraw() 方法允许我们在 EditText 的视图画布(画布被传递给了重写的 onDraw() 方法)上绘制蓝色的线条。super.onDraw() 方法在方法结束之间被调用了。父类的方法应该被调用,但是在我们的示例里,我们在绘制完成我们想要包括的行之后再调用。
4.使用自定义组件
我们现在已经创建好了自定义组件,但我们该如何使用它呢?在 NotePad 的示例中,自定义组件在声明好的布局中直接被使用,具体请看 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 文件中,自定义组件像普通视图一样被创建,并且使用完整的包名指定类。还要注意,我们定义的内部类是使用 NoteEditor$MyEditText 符号来引用的,这是在 Java 编程语言中引用内部类的一种标准方法。如果你的自定义视图组件不是被定义成了内部类,那么也可以用 XML 的元素名称来声明 View 组件,并且把 class 属性排除掉。比如:
<com.android.notepad.MyEditText id="@+id/note" ... />注意现在 MeEditText 已经是一个单独的类文件。当类嵌套在 NoteEditor 类中时,此方法将无法正常工作。
定义中的其他属性和参数是传递给自定义组件构造方法的属性和参数,然后传递给 EditText 的构造方法,因此它们与 EditText 视图使用的参数相同。 请注意,你也可以添加自己的参数,以后将再次介绍。这就是自定义组件的一切了。诚然,这是一个简单的例子,但这就是要点 – 创建自定义组件并没有你想象的那么复杂。
更加复杂的组件需要重写更多的 on... 方法,并引进一些自己的帮助方法,大大地定制其属性和行为。唯一的限制是你的想象力和你需要组件去做什么。