Android自定义View快速入门
Android自定义View快速入门
1.前言:
Android 提供了一个复杂且强大的组件化模型,可帮助您根据基本布局类 View
和 ViewGroup
构建界面。首先,该平台包含各种预构建的 View 和 ViewGroup 子类,分别称为微件和布局,可供用来构建界面。
可用的部分微件包括 Button
、TextView
、EditText
、ListView
、CheckBox
、RadioButton
、Gallery
、Spinner
,以及具有特殊用途的 AutoCompleteTextView
、ImageSwitcher
和 TextSwitcher
。
可用布局包括 LinearLayout
、FrameLayout
、RelativeLayout
等。如需查看更多示例,请参阅常见的布局对象。
如果预构建的微件或布局都不能满足您的需求,您可以创建自己的 View 子类。如果您只需要对现有微件或布局进行细微调整,则只需将相应微件或布局子类化并替换其方法即可。
通过创建自己的 View 子类,您可以精确控制屏幕元素的外观和功能。为帮助您了解自定义视图可为您带来的掌控力,下面的一些示例说明了您可以执行的相关操作:
- 您可以创建一个完全采用自定义渲染方式的 View 类型,例如一个使用 2D 图形渲染的“音量控制”调节按钮,外形类似于传统的模拟式电子控制旋钮。
- 您可以将一组 View 组件合并为一个新的组件,比如可以制作 ComboBox(弹出式列表与自由输入文本字段的组合)、双窗格选择器控件(左右各一个窗格,分别列出了您可以将哪个项重新分配到哪个列表)等等。
- 您可以替换 EditText 组件在屏幕上的渲染方式(记事本教程使用此功能成功创建了带线条的记事本页面)。
- 您可以捕获其他事件(例如按键),并以某种自定义方式处理这些事件(例如在游戏中)。
2.基本方法
-
使用您自己的类扩展现有
View
类或子类。 -
替换父类中的某些方法。要替换的父类方法以“
on
”开头,例如onDraw()
、onMeasure()
和onKeyDown()
。这类似于您为生命周期和其他功能钩子替换的Activity
或ListActivity
中的on...
事件。 -
使用您的新扩展类。完成后,可以使用您的新扩展类来代替其所基于的视图。
3.完全自定义的组件
完全自定义的组件可用于创建外观完全如您所需的图形组件。这可能是外观类似于旧式模拟计量器的图形声量计,也可能是跟唱的文本视图(其中有个弹力球会随着歌词移动,以便您能够跟着卡拉 OK 设备一起唱歌)。不管是哪种情形,内置组件都无法满足您的需求,无论您以何种方式组合使用这些组件都无法实现目标。
幸好,您可以根据自己的喜好轻松创建您所能想象出的具有任意外观和行为方式的组件,只需考虑屏幕大小及可用处理能力(请注意,最终应用可能需要在处理能力比桌面工作站低得多的平台上运行)。
要创建完全自定义的组件,请执行以下操作:
- 毫无疑问,您可以扩展的最通用的视图是
View
,因此您通常需要先扩展此视图,以创建新的父组件。 - 您可以提供一个构造函数(从 XML 获取属性和参数),也可以使用您自己的此类属性和参数(可能是声量计的颜色和范围,也可能是指针的宽度和阻尼等)。
- 您可能需要创建自己的事件监听器、属性存取器和修饰符,以及在组件类中创建可能更为复杂的行为。
- 您几乎肯定需要替换
onMeasure()
;如果您希望组件显示某些内容,也可能需要替换onDraw()
。虽然两者都具有默认行为,但默认的onDraw()
不会执行任何操作,而默认的onMeasure()
始终会设置 100x100 的大小,这可能不是您所希望的。 - 也可根据需要替换其他
on...
方法。
4.实现扩展 onDraw()
和 onMeasure()
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)
方法。如果不执行此操作,则会导致系统抛出异常。
下面汇总了框架针对视图调用的一些其他标准方法:
类别 | 方法 | 说明 |
---|---|---|
创建 | 构造函数 | 包含在从代码创建视图时调用的构造函数形式和在从布局文件扩充视图时调用的构造函数形式。第二种形式的构造函数应解析并应用布局文件中定义的任何属性。 |
|
在视图及其所有子级都已从 XML 扩充之后调用。 | |
布局 |
|
调用以确定此视图及其所有子级的大小要求。 |
|
在此视图应为其所有子级分配大小和位置时调用。 | |
|
在此视图的大小发生变化时调用。 | |
绘图 |
|
在视图应渲染其内容时调用。 |
事件处理 |
|
在发生新的按键事件时调用。 |
|
在发生 key up 事件时调用。 | |
|
在发生轨迹球动作事件时调用。 | |
|
在发生触屏动作事件时调用。 | |
焦点 |
|
在视图获得或失去焦点时调用。 |
|
在包含视图的窗口获得或失去焦点时调用。 | |
附加 |
|
在视图附加到窗口时调用。 |
|
在视图与其窗口分离时调用。 | |
|
在包含视图的窗口的可见性发生变化时调用。 |
5.复合控件
如果您不想创建完全自定义的组件,而是希望整合包含一组现有控件的可再用组件,那么创建复合组件(或复合控件)可能就足够了。简而言之,这会将许多更原子的控件(或视图)整合到可被视为一件事的项的逻辑分组中。例如,可以将组合框视为一行 EditText 字段以及一个附加有 PopupList 的相邻按钮的组合。如果您按下该按钮并从列表中选择内容,则会填充 EditText 字段,但用户也可以根据需要直接在 EditText 中输入内容。
在 Android 中,实际上有另外两个 View 可用于执行此操作:Spinner
和 AutoCompleteTextView
。但无论如何,组合框的概念都是一个易于理解的示例。
如要创建复合组件,请执行以下操作:
- 通常从某种类型的 Layout 入手,因此请创建可扩展 Layout 的类。对于组合框,我们可以使用水平方向的 LinearLayout。请注意,其他布局可以嵌套在其中,因此复合组件可以任意复杂化和结构化。请注意,就像使用 Activity 一样,您可以使用声明式(基于 XML)方法来创建所包含的组件,也可以通过编程方式从代码中嵌套组件。
- 在新类的构造函数中,获取父类所需的任何参数,将它们先传递给父类构造函数。然后,您可以设置其他视图以在新组件中使用;您可以在其中创建 EditText 字段和 PopupList。请注意,您也可以将自己的属性和参数引入到 XML 中,以供构造函数提取和使用。
- 您还可以为包含的视图可能生成的事件创建监听器,例如,如果选择了列表,则可以为 List Item Click Listener 创建监听器方法以更新 EditText 的内容。
- 您还可以使用存取器和修饰符创建自己的属性,例如,允许一开始在组件中设置 EditText 值,并在需要时查询其内容。
- 如果要扩展 Layout,您无需替换
onDraw()
和onMeasure()
方法,因为布局的默认行为会正常发挥作用。不过,您仍然可以根据需要替换这些方法。 - 您可以替换其他
on...
方法(如onKeyDown()
),以在按下某个键时从组合框的弹出式列表中选择特定的默认值。
简而言之,从 Layout 着手开始使用自定义控件有很多优点,包括:
- 您可以像使用 Activity 屏幕一样使用声明式 XML 文件指定布局,也可以通过编程方式从代码中创建视图并将其嵌套在布局中。
onDraw()
和onMeasure()
方法(加上大多数其他on...
方法)可能具有适当的行为,因此您无需替换它们。- 最后,您可以非常快速地构建任意复杂化的复合视图,并像使用单个组件一样重复使用它们。
6.修改现有 View 类型
可以通过一个更简单的选项来创建自定义 View,这在某些情况下非常实用。如果已经有一个组件非常契合您的需要,则只需扩展该组件并只替换您希望更改的行为即可。您可以使用完全自定义的组件来完成所有操作,但是通过从视图层次结构中更专用的类着手,您还可以免费获得很多可能完全符合您需求的行为。
例如,记事本应用演示了使用 Android 平台的多个方面。其中包括扩展 EditText View 以使记事本带有线条。这并非理想示例,并且用于执行此操作的 API 可能会发生变化,但该示例确实演示了相关原则。
如果您尚未这样做,请将记事本示例导入 Android Studio(或者只是通过提供的链接查看源代码)。请特别注意 NoteEditor.java 文件中的 LinedEditText
的定义。
下面是此文件中的一些注意事项:
- 定义
此类使用以下行进行定义:
public static class LinedEditText extends EditText
LinedEditText
定义为NoteEditor
Activity 中的内部类,但它是公开类,因此可以作为NoteEditor.LinedEditText
从NoteEditor
类的外部访问(如果需要)。- 它是
static
,这意味着它不会生成允许其从父类访问数据的所谓“合成方法”,而这反过来意味着它的行为方式其实就像单独的类(而不是与NoteEditor
密切相关的类)。如果内部类不需要从外部类访问状态,那么这是创建内部类的更简洁的方法,可以使生成的类一直比较小,并允许其他类轻松使用。 - 它扩展了
EditText
,即我们在这种情况下选择自定义的 View。之后,新类将能够取代普通的EditText
视图。
- 类初始化
与往常一样,首先调用父类。此外,这不是默认构造函数,而是参数化构造函数。可以在 EditText 从 XML 布局文件扩充时使用这些参数创建它,因此,我们的构造函数需要同时获取这些参数并将其传递给父类构造函数。
- 替换方法
此示例仅替换一个
onDraw()
方法,但您可能需要在创建自己的自定义组件时替换其他方法。对于此示例,通过替换
onDraw()
方法,可以在EditText
视图画布(此画布将传入已替换的onDraw()
方法)上绘制蓝色线条。在该方法结束之前调用 super.onDraw() 方法。应该调用父类方法,并且在这种情况下,我们应在绘制要包含的行之后执行此操作。 - 使用自定义组件
现在我们有了自定义组件,但该如何使用它呢?在记事本示例中,可以在声明式布局中直接使用自定义组件,因此请查看 res/layout 文件夹中的
note_editor.xml
。<view xmlns:android="http://schemas.android.com/apk/res/android" class="com.example.android.notepad.NoteEditor$LinedEditText" android:id="@+id/note" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" android:padding="5dp" android:scrollbars="vertical" android:fadingEdge="vertical" android:gravity="top" android:textSize="22sp" android:capitalize="sentences" />
- 您可以将自定义组件创建为 XML 中的通用视图,并使用完整软件包指定类。另请注意,我们定义的内部类是使用
NoteEditor$LinedEditText
标记引用的,这是以 Java 编程语言引用内部类的标准方式。如果您的自定义 View 组件未定义为内部类,则您可以选择使用 XML 元素名称声明 View 组件,并排除
class
属性。例如:<com.example.android.notepad.LinedEditText id="@+id/note" ... />
请注意,
LinedEditText
类现在是一个单独的类文件。如果该类嵌套在NoteEditor
类中,则此方法将不起作用。 - 定义中的其他属性和参数传入自定义组件构造函数中,然后传递给 EditText 构造函数,因此它们与用于 EditText 视图的参数是一样的。请注意,您也可以添加自己的参数,我们将在下文中再次对此进行说明。
- 您可以将自定义组件创建为 XML 中的通用视图,并使用完整软件包指定类。另请注意,我们定义的内部类是使用
这方面要介绍的内容就是这些。不可否认,这是一个简单的例子,但重点在于,创建何种复杂程度的自定义组件完全取决于您的需要。
更复杂的组件可以替换更多的 on...
方法,并引入一些自己的辅助方法,从而充分地自定义其属性和行为。唯一的限制是您的想象力以及您希望组件执行的操作。
例:飞机大战view
package chenlong.chenlong.viewstudy.myview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; import java.util.ArrayList; import java.util.List; import androidx.annotation.Nullable; import chenlong.chenlong.viewstudy.MainActivity; public class flyer extends LinearLayout { public int fraction; Paint mPaint = new Paint(); Paint TPaint = new Paint(); Paint DPaint = new Paint(); aircraft aircraft; List<bullet> bullet_list = new ArrayList<bullet>(); List<aircraft> enemy_flyer_list = new ArrayList<aircraft>(); Thread T; int NUM = 0; int mw; int mh; int FPS = 60; int num_move = 0; public flyer(Context context) { this(context, null); // initP(); } public flyer(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); // initP(); } public flyer(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initP(); } protected void initP() { // fraction= MainActivity.fraction_history; aircraft = new aircraft(); System.out.println(aircraft.x); System.out.println(aircraft.y); mPaint.setColor(Color.BLACK);//画笔颜色 mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL);//画笔模式为填充 mPaint.setStrokeWidth(10f);//画笔宽度 TPaint.setColor(Color.BLACK);//画笔颜色 TPaint.setAntiAlias(true); TPaint.setStyle(Paint.Style.FILL);//画笔模式为填充 TPaint.setStrokeWidth(5);//画笔宽度 TPaint.setTextSize(50); DPaint.setColor(Color.RED);//画笔颜色 DPaint.setAntiAlias(true); DPaint.setStyle(Paint.Style.FILL);//画笔模式为填充 DPaint.setStrokeWidth(5);//画笔宽度 DPaint.setTextSize(50); //生成敌机 } int randint(int max, int min) { int ran2 = (int) (Math.random() * (max - min) + min); return ran2; } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); int X = (int) event.getX(); int Y = (int) event.getY(); Log.i("view", String.format("X:%d , Y:%d", X, Y)); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i("按下", String.format("X:%d , Y:%d", X, Y)); float l = (float) Math.pow(Math.pow((X - aircraft.x), 2) + Math.pow((Y - aircraft.y), 2), 0.5); System.out.println(l); if (l < 80) { System.out.println("发射"); bullet_list.add(new bullet(X, Y)); } break; case MotionEvent.ACTION_MOVE: Log.i("移动", String.format("X:%d , Y:%d", X, Y)); num_move += 1; if (num_move > 2) { num_move = 0; bullet_list.add(new bullet(X, Y)); } aircraft.move(X, Y); break; case MotionEvent.ACTION_UP: Log.i("抬起", String.format("X:%d , Y:%d", X, Y)); break; } invalidate(); return true; } float p2pline(int x1, int y1, int x2, int y2) { return (float) Math.pow(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2), 0.5); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); try { aircraft.draw_(canvas);//绘制飞机 for (bullet bu : bullet_list) {//绘制子弹 bu.draw_(canvas); } for (aircraft air : enemy_flyer_list) {//绘制敌机 canvas.drawCircle(air.x, air.y, 20, DPaint); } canvas.drawText(String.format("飞机坐标:X:%d , Y:%d,飞行子弹数量:%d", aircraft.x, aircraft.y, bullet_list.size()), 20, 80, TPaint); canvas.drawText(String.format("敌机数量:%d", enemy_flyer_list.size()), 20, 160, TPaint); canvas.drawText(String.format("分数:%d", fraction), 20, 220, TPaint); } catch (Exception e) { System.out.println(e); } } private void updata() { for (bullet bu : bullet_list) { bu.y -= 1; for (aircraft air : enemy_flyer_list) { float l = (float) Math.pow(Math.pow(bu.x - air.x, 2) + Math.pow(bu.y - air.y, 2), 0.5); if (l < 30) { System.out.println("击中"); fraction += 1; MainActivity.pref.edit().putInt("fraction", fraction).apply();//保存分数 enemy_flyer_list.remove(air);//移除敌机 bullet_list.remove(bu);//移除子弹 } if (enemy_flyer_list.size() < 10) { for (int o = 0; o < 20; o++) { enemy_flyer_list.add(new aircraft(randint(100, mw - 100), randint(100, mh / 2))); } } if (bu.y < 0) { bullet_list.remove(bu); } } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mw = w; mh = h; aircraft.x = mw / 2; aircraft.y = mh - 200; for (int o = 0; o < 30; o++) { enemy_flyer_list.add(new aircraft(randint(100, mw - 100), randint(100, mh / 2))); } T = new Thread() { public void run() { long oldtime = System.currentTimeMillis(); while (true) { NUM += 1; long newtime = System.currentTimeMillis(); if (NUM > 5000) { oldtime = newtime; try { updata(); NUM = 0; invalidate(); } catch (Exception e) { System.out.println(e); } } } } }; T.start(); invalidate(); System.out.println(String.format("W:%d , H:%d", mw, mh)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } class bullet { int x; int y; bullet(int x, int y) { this.x = x; this.y = y; } void draw_(Canvas canvas) { canvas.drawCircle(this.x, this.y, 5, mPaint); } } class aircraft { int x = mw / 2; int y = mh - 80; String name = "我方战斗机"; int health = 100; public aircraft() { this(mw / 2, mh - 80); } public aircraft(int x, int y) { this.x = x; this.y = y; } boolean move(int x, int y) { try { this.x = x; this.y = y; return true; } catch (Exception e) { return false; } } void fire() { bullet_list.add(new bullet(this.x, this.y)); } void draw_(Canvas canvas) { canvas.drawCircle(aircraft.x, aircraft.y, 50, mPaint); } } }