1 Android 的事件处理

不管是桌面应用还是手机应用程序,面对最多的就是用户,经常需要处理的就是用户动作 —— 也就是需要为用户动作提供响应,这种为用户动作提供响应的机制就是事件处理。Android 提供了两种方式的事件处理:

  • 基于回调的事件处理;
  • 基于监听器的事件处理;

对于 Android 基于监听的事件处理而言,主要做法就是为 Android 界面组件绑定特定的事件监听器。

对于 Android 基于回调的事件处理而言,主要做法就是 重写 Android 组件特定的回调方法,或 重写 Activity 的回调方法。Android 为绝大部分界面组件都提供了事件响应的回调方法,开发者只要重写它们就可以了。

那这两者有什么区别吗?一般来说,基于回调的事件处理可用于处理一些具有通用性的事件,并且代码会显得比较简洁。但是对于某些特定的事件,无法使用基于回调的事件处理,只能采用基于监听的事件处理。

2 基于监听的事件处理

基于监听的事件处理是一种更“面向对象”的事件处理,这种处理方式与 Java 的 AWT、Swing 的处理方式几乎完全相同。

2.1 事件监听的处理模型

在事件监听的处理模型中,主要涉及如下三类对象:

  • EventSource(事件源):事件发生的场所,通常就是各个组件;
  • Event(事件):事件封装了界面组件上发生的特定事情,如果程序需要获得界面组件上所发生的相关信息,一般通过 Event 对象来获得;
  • EventListener(事件监听器):负责监听事件源所发生的事件,并对各种事件做出相应的响应;

下面举一个简单的例子来说明:

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<EditText
android:id="@+id/txt"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:editable="false"
android:cursorVisible="false"
android:textSize="12pt"
/>

<Button
android:id="@+id/bn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单击我"
/>

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 获取应用程序中的 bn 按钮
Button bn = (Button)findViewById(R.id.bn);
// 为按钮绑定事件监听器
bn.setOnClickListener(new MyClickListener());
}

class MyClickListener implements View.OnClickListener
{
@Override
public void onClick(View arg0)
{
EditText txt = (EditText)findViewById(R.id.txt);
txt.setText("bn按钮被单击了!");
}
}
}

以下对上面代码作个简单分析(基于监听的事件处理模型的编程步骤):

  • 获取普通界面组件(事件源),也就是被监听的对象;
  • 实现事件监听器类,该监听器类是一个特殊的 Java 类,必须实现一个 XxxListener 接口;
  • 调用事件源的 setXxxListener 方法将事件监听器对象注册给普通组件;

开发者只需要关注实现事件监听器类,注册监听器也只要一行代码即可。

2.2 事件和事件监听器

如果事件源触发的事件足够简单、事件里封装的信息比较有限,那就无须封闭事件对象,将事件对象传入事件监听器。但对于键盘事件、触摸屏事件等,此时程序需要获取事件发生的详细信息:例如键盘事件需要获取是哪个键触发的事件;触摸屏事件需要获取事件发生的位置等,对于这种包含更多信息的事件,Android 同样会将事件信息封装成 XxxEvent 对象,并把该对象作为参数传入事件处理器。下面举一个简单飞机移动例子:

PlaneView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PlaneView extends View {

public float currentX;
public float currentY;
Bitmap plane;

public PlaneView(Context context) {
super(context);
// 定义飞机图片
plane = BitmapFactory.decodeResource(context.getResources(), R.drawable.plane);
setFocusable(true);
}

@Override
public void onDraw (Canvas canvas) {
super.onDraw(canvas);
// 创建画笔
Paint p = new Paint();
// 绘制飞机
canvas.drawBitmap(plane, currentX, currentY, p);
}
}

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class MainActivity extends AppCompatActivity {

// 定义飞机的移动速度
private int speed = 12;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 去掉窗口标题
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 全屏显示
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

// 创建 PlaneView 组件
final PlaneView planeView = new PlaneView(this);
setContentView(planeView);
planeView.setBackgroundResource(R.drawable.back);

// 获取窗口管理器
WindowManager windowManager = getWindowManager();
Display display = windowManager.getDefaultDisplay();

// 获取屏幕宽和高
int screenWidth = display.getWidth();
int screenHeight = display.getHeight();

// 设置飞机初始位置
planeView.currentX = screenWidth / 2;
planeView.currentY = screenHeight - 500;

// 为 draw 组件键盘事件绑定监听器
planeView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// 获取由哪个键触发的事件
switch (event.getKeyCode()) {
// 控制飞机下移
case KeyEvent.KEYCODE_DPAD_DOWN:
planeView.currentY += speed;
break;
// 控制飞机上移
case KeyEvent.KEYCODE_DPAD_UP:
planeView.currentY -= speed;
break;
// 控制飞机左移
case KeyEvent.KEYCODE_DPAD_LEFT:
planeView.currentX -= speed;
break;
// 控制飞机右移
case KeyEvent.KEYCODE_DPAD_RIGHT:
planeView.currentX += speed;
break;
}
// 通知 planeView 组件重绘
planeView.invalidate();
return true;
}
});
}
}

显示效果如下:

通过键盘上的上下左右键可控制飞机的移动。从以上可得到,在基于事件监听的处理模型中,事件监听器必须实现事件监听器接口,Android 为不同的界面组件 提供了不同的监听接口,这些接口通常以 内部类 的形式存在,在程序中实现事件监听器,通常有如下几种形式:

  • 内部类形式:将事件监听器类定义成当前类的内部类;
  • 外部类形式:将事件监听器类定义成一个外部类;
  • Activity 本身作为事件监听器类:让 Activity 本身实现监听器接口,并实现事件处理方法;
  • 匿名内部类形式:使用匿名内部类创建事件监听器对象;

2.3 内部类作为事件监听器类

使用内部类作为事件监听器类有两个优势:

  • 使用内部类可以在当前类中复用该监听器类;
  • 因为监听器类是外部类的内部类,所以可以自由访问外部类的所有界面组件;

例如上面第一个给出的例子是内部类作为事件监听器类。

2.4 外部类作为事件监听器类

使用外部类作为事件监听器类比较少见,主要是因为以下两个原因:

  • 事件监听器通常属于特定的 GUI 界面,定义成外部类不利于提高程序的内聚性;
  • 外部类形式的事件监听器不能自由访问创建 GUI 界面的类中的组件,编程不够灵活;

那什么情况下用外部类作为事件监听器类呢?答案是如果某个事件监听器确实需要被多个 GUI 界面所共享,而且主要是完成某种业务逻辑的实现,则可以考虑使用外部类的形式来定义事件监听器类。

2.5 Activity 本身作为事件监听器

这种形式使用 Activity 本身作为事件监听器类,可以直接在 Activity 类中定义事件处理器方法,这种形式非常简洁,但是有两个缺点:

  • 这种形式可能造成程序结构混乱,Activity 的主要职责应该是完成界面初始化工作,但此时还需包含事件处理器方法,从而引起混乱;
  • 如果 Activity 界面类需要实现监听器接口,让人感觉比较怪异;

当为某个组件添加该事件监听器对象时,直接使用 this 作为事件监听器对象即可。

2.6 匿名内部类作为事件监听器类

大部分时候,事件处理器都没有什么复用价值,因此大部分事件监听器只是临时使用一次,所以使用匿名内部类形式的事件监听器更合适。实际上,这种形式是目前使用最广泛的事件监听器形式。

2.7 直接绑定到标签

Android 还有种更简单的绑定事件监听器的方式,直接在界面布局文件中为指定标签绑定事件处理方法。对于很多 Android 标签而言,它们都支持 onClick、onLongClick 等属性,这种属性的属性值就是一个形如 xxx(View source) 的方法的方法名。

activity_main.xml

1
2
3
4
5
6
7
<Button
android:id="@+id/bn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:onClick="clickHandler"
/>

MainAcitivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void clickHandler(View source) {
EditText show = (EditText)findViewById(R.id.show);
show.setText("bn按钮被单击了!");
}
}

3 基于回调的事件处理

除了前面介绍的基于监听的事件处理模型之外,Android 还提供了一种基于回调的事件处理模型。从代码实现的角度来看,基于回调的事件处理模型更加简单。

3.1 回调机制与监听机制

如果说事件监听机制是一种委托式的事件处理,那么回调机制则恰好与之相反:对于基于回调的事件处理模型来说,事件源与事件监听器是统一的,或者说事件监听器完全消失了。当用户在 GUI 组件上激发某个事件时,组件自己特定的方法将会负责处理该事件。

为了使用回调机制类处理 GUI 组件上所发生的事件,我们需要为该组件提供对应的事件处理方法 —— 而 Java 又是一种静态语言,我们无法为某个对象动态地添加方法,因此只能继承 GUI 组件类,并重写该类的事件处理方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyButton extends Button {

public MyButton(Context context, AttributeSet set) {
super(context, set);
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
super.onKeyDown(keyCode, event);
Log.v("-fanfanblog.cn-", "the onKeyDown in MyButton");
// 返回 true,表明该事件不会向外扩散
return true;
}
}

接下来在布局文件中使用这个自定义的 View,如下:

1
2
3
4
5
<cn.fanfanblog.myapplication.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单击我"
/>

Java 程序无须为该按钮绑定事件监听器 —— 因为该按钮自己重写了 onKeyDown(int keyCode, KeyEvent event) 方法,这意味着该按钮将会自己处理相应的事件。

3.2 基于回调的事件传播

几乎所有基于回调的事件处理方法都有一个 boolean 类型的返回值,该返回值用于标识该处理方法是否能完全处理该事件:

  • 如果处理事件的回调方法返回 true,表明处理方法已完全处理该事件,该事件不会传播出去;
  • 如果处理事件的回调方法返回 false,表明处理方法并未完全处理该事件,该事件会传播出去;

对于基于回调的事件传播而言,某组件上所发生的事情不仅激发该组件上的回调方法,也会触发该组件所在 Activity 的回调方法 —— 只要事件能传播到该 Activity。

(本文完)

留言

2019-04-20

⬆︎TOP