Android

Android 的事件处理

凡凡 · 8月20日 · 2019年 205次已读

1 Android 的事件处理

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

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

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

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

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

2 基于监听的事件处理

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

2.1 事件监听的处理模型

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

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

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

activity_main.xml

<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

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

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

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

<Button
    android:id="@+id/bn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    android:onClick="clickHandler"
    />

MainAcitivity.java

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 组件类,并重写该类的事件处理方法来实现。

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,如下:

<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。

0 条回应