第3章 Android事件机制

第3章 Android事件机制

目录

3.1 Android 事件处理概述
  • 3.1.1 事件处理概述
3.2 基于监听的事件处理
  • 3.2.1 监听的处理模型
  • 3.2.2 事件和事件监听器
    • 实例:控制飞机移动
  • 3.2.3 内部类作为事件监听器类
  • 3.2.4 外部类作为事件监听器类
  • 3.2.5 Activity 本身作为事件监听器类
  • 3.2.6 Lambda 表达式作为事件监听器类
  • 3.2.7 直接绑定到标签
3.3 基于回调的事件处理
  • 3.3.1 回调机制与监听机制
  • 3.3.2 基于回调的事件传播
3.4 响应系统设置的事件
  • 3.4.1 Configuration 类简介
    • 实例:获取系统设备状态
  • 3.4.2 重写 onConfigurationChanged 方法响应系统设置更改
    • 实例:监听屏幕方向的改变
3.5 Handler 消息传递机制
  • 3.5.1 Handler 类简介
    • 实例:自动播放动画
  • 3.5.2 Handler、Looper、MessageQueue 的工作原理
    • 实例:使用新线程计算质数
3.6 异步任务 (AsyncTask)
  • 实例:使用异步任务执行下载
3.7 本章小结

本章要点

  • 事件处理概述与Android事件处理
  • 基于监听的事件处理模型
  • 事件与事件监听器接口
  • 实现事件监听器的方式
  • 基于回调的事件处理模型
  • 基于回调的事件传播
  • 常见的事件回调方法
  • 响应系统设置事件
  • 重写onConfigurationChanged方法响应系统设置更改
  • Handler类的功能与用法
  • 使用Handler更新程序界面
  • HandlerLooperMessageQueue工作原理
  • 异步任务的功能与用法

与界面编程紧密相关的知识就是事件处理。当用户在程序界面上执行各种操作时,应用程序必须为用户动作提供响应,这种响应需要通过事件处理来完成。因此,本章知识与上一章的内容衔接得非常紧密。实际上,我们在介绍上一章示例时已经使用过Android的事件处理了。

Android提供了两种事件处理方式:基于回调的事件处理和基于监听的事件处理。熟悉传统图形界面编程的读者可能对基于回调的事件处理较为熟悉;而熟悉AWT/Swing开发方式的读者可能对基于监听的事件处理较为熟悉。Android系统充分利用了这两种事件处理方式的优点,允许开发者采用自己熟悉的方式来为用户操作提供响应。

本章将会详细介绍Android事件处理的各种实现细节。学完本章内容之后,再结合上一章的内容,读者将能够开发出界面友好、人机交互良好的Android应用。

3.1 Android 事件处理概述

无论是桌面应用还是手机应用程序,最常面对的就是用户操作,需要频繁处理的便是用户动作。为用户动作提供响应的机制即是事件处理。Android 提供了两套强大的事件处理机制:

  1. 基于监听的事件处理
  2. 基于回调的事件处理
基于监听的事件处理

基于监听的事件处理,主要做法是为 Android 界面组件绑定特定的事件监听器。我们在上一章已经见到了大量这样的事件处理示例。具体来说,Android 还允许在界面布局文件中为 UI 组件的 android:onClick 属性指定事件监听方法。通过这种方式指定事件监听方法时,开发者需要在 Activity 中定义该事件监听方法(该方法必须有一个 View 类型的形参,该形参代表被单击的 UI 组件)。当用户单击该 UI 组件时,系统将会激发 android:onClick 属性所指定的方法。

示例:

<Button
    android:id="@+id/myButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click Me"
    android:onClick="handleClick" />
public void handleClick(View view) {
    // 处理点击事件的逻辑
}
基于回调的事件处理

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

示例:

public class MyActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button myButton = findViewById(R.id.myButton);
        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 处理点击事件的逻辑
            }
        });
    }
}

一般来说,基于回调的事件处理可用于处理一些具有通用性的事件,代码会显得比较简洁。但对于某些特定的事件,可能无法使用基于回调的事件处理,只能采用基于监听的事件处理。

总结:Android 提供了两种主要的事件处理机制,开发者可以根据具体的需求选择适合的事件处理方式,灵活应用这两种机制能够有效提升应用的用户体验。

3.2 基于监听的事件处理

基于监听的事件处理是一种更“面向对象”的事件处理方式,这种处理方式与Java的AWT、Swing的处理方式几乎完全相同。如果开发者有AWT、Swing事件处理的编程经验,基本上可以直接上手编程,甚至不需要学习。如果以前没有任何事件处理的编程经验,就需要花点时间先去理解事件监听的处理模型。

3.2.1 监听的处理模型

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

  • 事件源 (Event Source):事件发生的场所,通常就是各个组件,例如按钮、窗口、菜单等。
  • 事件 (Event):事件封装了界面组件上发生的特定事情(通常就是一次用户操作)。如果程序需要获得界面组件上所发生事件的相关信息,一般通过Event对象来取得。
  • 事件监听器 (Event Listener):负责监听事件源所发生的事件,并对各种事件做出相应的响应。事件响应的动作实际上就是一系列程序语句,通常以方法的形式组织起来。事件监听器的核心就是它所包含的方法——这些方法也被称为事件处理器(Event Handler)。

当用户按下一个按钮或者单击某个菜单项时,这些动作就会激发一个相应的事件,该事件就会触发事件源上注册的事件监听器(特殊的Java对象或Lambda表达式),事件监听器调用对应的事件处理器(事件监听器里的实例方法)来做出相应的响应。

基于监听的事件处理机制是一种委派式(Delegation)事件处理方式:普通组件(事件源)将整个事件处理委托给特定的对象(事件监听器);当该事件源发生指定的事件时,就通知所委托的事件监听器,由事件监听器来处理这个事件。

每个组件均可以针对特定的事件指定一个事件监听器,每个事件监听器也可监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上所有可能发生的事件分别授权给不同的事件监听器来处理;同时也可以让一类事件都使用同一个事件监听器来处理。

委派式事件处理方式明显“抄袭”了人类社会的分工协作,例如某个单位发生了火灾,该单位通常不会自己处理该事件,而是将该事件委派给消防局(事件监听器)处理;如果发生了打架斗殴事件,则委派给公安局(事件监听器)处理;而消防局、公安局也会同时监听多个单位的火灾、打架斗殴事件。这种委派式的处理方式将事件源和事件监听器分离,从而提供更好的程序模型,有利于提高程序的可维护性。

如图3.1所示为事件处理流程示意图。

图3.1 事件处理流程示意图

示例:基于监听的事件处理模型

下面以一个简单的入门程序来示范基于监听的事件处理模型。先看本程序的界面布局代码,该界面布局中只是定义了两个组件:一个文本框和一个按钮,界面布局代码如下。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">
    <TextView
        android:id="@+id/txt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="12dp"
        android:textSize="18sp" />
    <!--定义一个按钮,该按钮将作为事件源-->
    <Button
        android:id="@+id/bn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="单击我" />
</LinearLayout>

上面程序中定义的按钮将会作为事件源,接下来程序将会为该按钮绑定一个事件监听器——监听器类必须由开发者来实现。下面是该程序的Activity。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

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

    // 定义一个单击事件的监听器
    class MyClickListener implements View.OnClickListener {
        // 实现监听器类必须实现的方法,该方法将会作为事件处理器
        @Override
        public void onClick(View v) {
            TextView txt = findViewById(R.id.txt);
            txt.setText("bn按钮被单击了!");
        }
    }
}

上面程序中的粗体字代码定义了一个View.OnClickListener实现类,这个实现类将会作为事件监听器使用。程序中①号代码用于为bn按钮注册事件监听器。当程序中的bn按钮被单击时,该处理器被触发,将看到程序中文本框内变为“bn按钮被单击了”。

编程步骤

从上面的程序中可以看出,基于监听的事件处理模型的编程步骤如下:

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

当事件源上发生指定事件时,Android会触发事件监听器,由事件监听器调用相应的方法(事件处理器)来处理事件。

规则

把上面的程序与图3.1结合起来看,可以发现基于监听的事件处理有如下规则:

  • 事件源:就是程序中的bn按钮,其实开发者不需要太多的额外处理,应用程序中任何组件都可作为事件源。
  • 事件监听器:就是程序中的MyClickListener类。监听器类必须由程序员负责实现,实现监听器类的关键就是实现处理器方法。
  • 注册监听器:只要调用事件源的setXxxListener(XxxListener)方法即可。

对于上面三件事情,事件源可以是任何界面组件,不太需要开发者参与;注册监听器也只要一行代码即可,因此事件编程的关键就是实现事件监听器类。

3.2.2 事件和事件监听器

从图3.1中可以看出,当外部动作在Android组件上执行操作时,系统会自动生成事件对象,这个事件对象会作为参数传给事件源上注册的事件监听器。

基于监听的事件处理模型涉及三个成员:事件源、事件和事件监听器,其中事件源最容易创建,任意界面组件都可作为事件源;事件的产生无须程序员关心,它是由系统自动产生的;所以,实现事件监听器是整个事件处理的核心。

但在上面的程序中,我们并未发现事件的踪迹,这是什么原因呢?这是因为Android对事件监听模型做了进一步简化:如果事件源触发的事件足够简单,事件里封装的信息比较有限,那就无须封装事件对象,将事件对象传入事件监听器。

但对于键盘事件、屏幕触碰事件等,此时程序需要获取事件发生的详细信息。例如,键盘事件需要获取是哪个键触发的事件;触摸屏事件需要获取事件发生的位置等,对于这种包含更多信息的事件,Android同样会将事件信息封装成XxxEvent对象,并把该对象作为参数传入事件处理器。

示例:控制飞机移动

下面以一个简单的飞机游戏为例来介绍屏幕触碰事件的监听。游戏中的飞机会随用户的触碰动作而移动:触碰屏幕不同的位置,飞机向不同的方向移动。

为了实现该程序,先开发一个自定义View,该View负责绘制游戏的飞机。该View类的代码如下。

public class PlaneView extends View {
    float currentX;
    float currentY;
    // 创建画笔
    private Paint p = new Paint();
    private Bitmap plane0;
    private Bitmap plane1;
    private int index;

    public PlaneView(Context context) {
        super(context);
        // 定义飞机图片
        plane0 = BitmapFactory.decodeResource(context.getResources(), R.drawable.plane0);
        plane1 = BitmapFactory.decodeResource(context.getResources(), R.drawable.plane1);
        // 启动定时器来切换飞机图片,实现动画效果
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                index++;
                PlaneView.this.invalidate();
            }
        }, 0L, 100L);
        setFocusable(true);
    }

    @Override
    public void

 onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制飞机
        canvas.drawBitmap(index % 2 == 0 ? plane0 : plane1, currentX, currentY, p);
    }
}

上面的PlaneView足够简单,因为这个程序只需要绘制玩家自己控制的飞机,没有增加“敌机”。如果游戏中要增加“敌机”,那么还需要增加数据来控制敌机的坐标,并会在View上绘制敌机。

该游戏几乎不需要界面布局,直接使用PlaneView作为Activity显示的内容,并为该PlaneView增加触碰事件的监听器即可。下面是该程序的Activity代码。

public class MainActivity extends Activity {
    // 定义飞机的移动速度
    private int speed = 10;
    private PlaneView planeView;
    DisplayMetrics metrics;

    @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组件
        planeView = new PlaneView(this);
        setContentView(planeView);
        planeView.setBackgroundResource(R.drawable.background);
        // 获取窗口管理器
        WindowManager windowManager = getWindowManager();
        Display display = windowManager.getDefaultDisplay();
        metrics = new DisplayMetrics();
        // 获得屏幕宽和高
        display.getMetrics(metrics);
        // 设置飞机的初始位置
        planeView.currentX = (metrics.widthPixels / 2);
        planeView.currentY = (metrics.heightPixels - 200);
        planeView.setOnTouchListener(new MyTouchListener());
    }

    class MyTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getX() < metrics.widthPixels / 8) {
                planeView.currentX -= speed;
            }
            if (event.getX() > metrics.widthPixels * 7 / 8) {
                planeView.currentX += speed;
            }
            if (event.getY() < metrics.heightPixels / 8) {
                planeView.currentY -= speed;
            }
            if (event.getY() > metrics.heightPixels * 7 / 8) {
                planeView.currentY += speed;
            }
            return true;
        }
    }
}

上面程序中的粗体字代码就是控制飞机移动的关键代码。由于程序需要根据用户触碰屏幕的坐标来确定飞机的移动方向,所以上面的程序先调用了MotionEvent(事件对象)的xy两个属性来获取触碰事件的坐标,然后针对不同的坐标来改变游戏中飞机的坐标。

运行上面的程序,将看到如图3.2所示的界面。

图3.2 控制飞机的移动

对于图3.2所示的“游戏”,当用户触碰屏幕的四周时,将可以看到“游戏”中的飞机可以上、下、左、右自由移动。

上面这个“游戏”还只是一个“雏型”,为了增加这个游戏的可玩性,可以考虑为游戏随机增加“敌机”,并让“敌机”在屏幕上移动。为了提高用户操作的方便性,建议在界面上增加4个虚拟方向键和发弹键(其实就是图片按钮),并为这些虚拟按键提供事件监听器即可。

事件监听器接口

在基于监听的事件处理模型中,事件监听器必须实现事件监听器接口,Android为不同的界面组件提供了不同的监听器接口,这些接口通常以内部类的形式存在。以View类为例,它包含了如下几个内部接口:

  • View.OnClickListener:单击事件的事件监听器必须实现的接口。
  • View.OnCreateContextMenuListener:创建上下文菜单事件的事件监听器必须实现的接口。
  • View.OnFocusChangeListener:焦点改变事件的事件监听器必须实现的接口。
  • View.OnKeyListener:按键事件的事件监听器必须实现的接口。
  • View.OnLongClickListener:长按事件的事件监听器必须实现的接口。
  • View.OnTouchListener:触摸事件的事件监听器必须实现的接口。

实际上可以把事件处理模型简化成如下理解:当事件源组件上发生事件时,系统将会执行该事件源组件上监听器的对应处理方法。与普通Java方法调用不同的是,普通Java程序里的方法是由程序主动调用的,事件处理中的事件处理器方法是由系统负责调用的。

通过上面的介绍不难看出,所谓事件监听器,其实就是实现了特定接口的实例。在程序中实现事件监听器,通常有如下几种形式:

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

前面两个程序中所使用的事件监听器类都是内部类形式,使用内部类可以在当前类中复用该监听器类;因为监听器类是外部类的内部类,所以可以自由访问外部类的所有界面组件。这也是内部类的两个优势。

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

使用外部类定义事件监听器类的形式比较少见,主要因为如下两个原因:

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

但如果某个事件监听器确实需要被多个GUI界面所共享,而且主要是完成某种业务逻辑的实现,则可以考虑使用外部类形式来定义事件监听器类。下面的程序定义了一个外部类作为OnLongClickListener类,该事件监听器实现了发送短信的功能。

public class SendSmsListener implements View.OnLongClickListener {
    private Activity act;
    private String address;
    private String content;

    public SendSmsListener(Activity act, String address, String content) {
        this.act = act;
        this.address = address;
        this.content = content;
    }

    @Override
    public boolean onLongClick(View source) {
        // 获取短信管理器
        SmsManager smsManager = SmsManager.getDefault();
        // 创建发送短信的PendingIntent
        PendingIntent sentIntent = PendingIntent.getBroadcast(act, 0, new Intent(), 0);
        // 发送文本短信
        smsManager.sendTextMessage(address, null, content, sentIntent, null);
        Toast.makeText(act, "短信发送完成", Toast.LENGTH_LONG).show();
        return false;
    }
}

上面的事件监听器类没有与任何GUI界面耦合,创建该监听器对象时需要传入两个String对象和一个Activity对象,其中第一个String对象用于作为收信人号码,第二个String用于作为短信内容。

上面程序中的三行粗体字代码调用了SmsManagerPendingIntent来发送短信,关于SmsManagerIntent的用法可参看本书后面的内容。

该程序的界面布局比较简单,此处不再给出界面布局文件。该程序的Activity代码如下。

public class MainActivity extends Activity {
    private EditText address;
    private EditText content;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取页面中收件人地址、短信内容
        address = findViewById(R.id.address);
        content = findViewById(R.id.content);
        Button bn = findViewById(R.id.send);
        // 使用外部类的实例作为事件监听器
        bn.setOnLongClickListener(new SendSmsListener(this,
                address.getText().toString(), content.getText().toString()));
    }
}

上面程序中的粗体字代码用于为指定按钮的长单击事件绑定监听器,当用户长单击界面中的bn按钮时,程序将会触发SendSmsListener监听器,该监听器里包含的事件处理方法将会向指定手机发送短信。

由于本示例需要发送短信,使用模拟器不方便测试,因此建议读者使用真机测试该应用。

实际上不推荐将业务逻辑实现写在事件监听器中,包含业务逻辑的事件监听器将导致程序的显示逻辑和业务逻辑耦合,从而增加程序后期的维护难度。如果确实有多个事件监听器需要实现相同的业务逻辑功能,则可以考虑使用业务逻辑组件来定义业务逻辑功能,再让事件监听器来调用业务逻辑组件的业务逻辑方法。

3.2.5 Activity本身作为事件监听器类

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

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

下面的程序使用Activity对象作为事件监听器。

public class MainActivity extends Activity implements View.OnClickListener {
    private TextView show;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        show = findViewById(R.id.show);
        Button bn = findViewById(R.id.bn);
        // 直接使用Activity作为事件监听器
        bn.setOnClickListener(this);
    }

    // 实现事件处理方法
    @Override
    public void onClick(View v) {
        show.setText("bn按钮被单击了!");
    }
}

上面的程序让Activity类实现了View.OnClickListener事件监听器接口,从而可以在该Activity类中直接定义事件处理器方法:onClick(View v)(如上面的粗体字代码所示)。当为某个组件添加该事件监听器对象时,直接使用this作为事件监听器对象即可。

3.2.6 Lambda表达式作为事件监听器类

大部分时候,事件处理器都没有什么复用价值(可复用代码通常都被抽象成了业务逻辑方法),因此大部分事件监听器只是临时使用一次,所以使用Lambda表达式形式的事件监听器更合适。实际上,这种形式是目前使用最广泛的事件监听器形式。下面的程序使用Lambda表达式来创建事件监听器。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView show = findViewById(R.id.show);
        Button bn = findViewById(R.id.bn);
        // 使用Lambda表达式作为事件监听器
        bn.setOnClickListener(view -> show.setText("bn按钮被单击了!"));
    }
}

上面程序中的粗体字代码使用Lambda表达式创建了一个事件监听器对象,得益于Lambda表达式的简化写法,如果Lambda表达式的执行体只有一行代码,程序可以省略Lambda表达式的花括号。

3.2.7 直接绑定到标签

Android还有一种更简单的绑定事件监听器的方式,那就是直接在界面布局文件中为指定标签绑定事件处理方法。

对于很多Android界面组件标签而言,它们都支持onClick属性,该属性的属性值就是一个形如xx(View source)方法的方法名。例如如下界面布局文件。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">
    <TextView
        android:id="@+id/show"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:textSize="18sp" />
    <!--在标签中为按钮绑定事件处理方法-->
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="clickHandler"
        android:text="单击我" />
</LinearLayout>

上面程序中的粗体字代码用于在界面布局文件中为Button按钮绑定一个事件处理方法:clickHandler,这就意味着开发者需要在该界面布局对应的Activity中定义一个clickHandler(View source)方法,该方法将会负责处理该按钮上的单击事件。下面是该界面布局对应的Activity代码。

public class MainActivity extends Activity {
    private TextView show;

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

    // 定义一个事件处理方法
    // 其中source参数代表事件源
    public void clickHandler(View source) {
        show.setText("bn按钮被单击了");
    }
}

上面程序中的粗体字代码定义了一个clickHandler(View source)方法,当程序中的bn按钮被单击时,该方法将会被激发并处理bn按钮上的单击事件。

3.3 基于回调的事件处理

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

3.3.1 回调机制与监听机制

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

为了使用回调机制类处理GUI组件上所发生的事件,我们需要为该组件提供对应的事件处理方法——这种事件处理方法通常都是系统预先定义好的,因此通常需要继承GUI组件类,并通过重写该类的事件处理方法来实现。

为了实现回调机制的事件处理,Android为所有GUI组件都提供了一些事件处理的回调方法,以View为例,该类包含如下方法:

  • boolean onKeyDown(int keyCode, KeyEvent event): 当用户在该组件上按下某个按键时触发该方法。
  • boolean onKeyLongPress(int keyCode, KeyEvent event): 当用户在该组件上长按某个按键时触发该方法。
  • boolean onKeyShortcut(int keyCode, KeyEvent event): 当一个键盘快捷键事件发生时触发该方法。
  • boolean onKeyUp(int keyCode, KeyEvent event): 当用户在该组件上松开某个按键时触发该方法。
  • boolean onTouchEvent(MotionEvent event): 当用户在该组件上触发触摸屏事件时触发该方法。
  • boolean onTrackballEvent(MotionEvent event): 当用户在该组件上触发轨迹球事件时触发该方法。

下面的程序示范了基于回调的事件处理机制。正如前面所提到的,基于回调的事件处理机制可通过自定义View来实现,自定义View时重写该View的事件处理方法即可。下面是一个自定义按钮的实现类。

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        Log.v("crazyit.org", "the onTouchEvent in MyButton");
        // 返回true,表明该事件不会向外传播
        return true;
    }
}

在上面自定义的MyButton类中,我们重写了Button类的onTouchEvent(MotionEvent event)方法,该方法将会负责处理按钮上的用户触碰事件。

接下来在界面布局文件中使用这个自定义View,界面布局文件如下。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!--使用自定义View时应使用全限定类名-->
    <org.crazyit.event.MyButton
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="单击我" />
</LinearLayout>

上面程序中的粗体字代码在XML界面布局文件中使用MyButton组件,接下来Activity程序无须为该按钮绑定事件监听器,因为该按钮自己重写了onTouchEvent(MotionEvent event)方法,这意味着该按钮将会自己处理相应的事件。

运行上面的程序,触碰界面上的按钮,将可以看到Android Studio的Logcat中有如下的输出。

2021-10-22 18:18:56.0 2179-2179/org.crazyit.event V/crazyit.org: the onTouchEvent in MyButton
3.3.2 基于回调的事件传播

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

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

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

下面的程序示范了Android系统中的事件传播,该程序重写了Button类的onTouchEvent(MotionEvent event)方法,而且重写了该Button所在Activity的onTouchEvent(MotionEvent event)方法。程序没有阻止事件传播,因此可以看到事件从Button传播到Activity的情形。

下面是从Button派生的MyButton子类代码。

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        Log.v("crazyit.org", "the onTouchEvent in MyButton");
        // 返回false,表明该事件会向外传播
        return false;
    }
}

上面的MyButton子类重写了onTouchEvent(MotionEvent event)方法,当用户触碰该按钮时将会触发该方法。但由于该方法返回了false,这意味着该事件还会继续向外传播。

该程序也按前一个示例的方式使用自定义组件,并在Activity中重写了onTouchEvent(MotionEvent event)方法,该方法也会在它包含的组件被触碰时被回调。

看如下Activity类代码。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button bn = findViewById(R.id.bn);
        bn.setOnTouchListener((view, event) -> {
            // 只处理按下键的事件
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                Log.v("Listener", "the TouchDown in Listener");
                // 返回false,表明该事件会向外传播
                return false;
            }
            return true;
        });
    }

    // 重写onTouchEvent方法,该方法可监听它所包含的所有组件上的触碰事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        Log.v("Activity", "the onTouchEvent in Activity");
        // 返回false,表明该事件会向外传播
        return false;
    }
}

从上面的程序可以看出,粗体字代码重写了Activity的onTouchEvent(MotionEvent event)方法,当用户触碰该Activity包含的所有组件时,该方法都可能被触发——只要该组件没有完全处理该事件(即返回了false)。

运行上面的程序,触碰程序界面上的按钮,将可以在Android Studio的Logcat中看到如下的输出。

2021-10-22 18:18:56.0 2179-2179/org.crazyit.event V/Listener: the TouchDown in Listener
2021-10-22 18:18:56.0 2179-2179/org.crazyit.event V/Activity: the onTouchEvent in Activity

从图中不难看出,当该组件上发生触碰事件时,Android系统最先触发的应该是该组件绑定的事件监听器,然后才触发该组件提供的事件回调方法,最后还会传播到该组件所在的Activity——但如果让任何一个事件处理方法返回了true,那么该事件将不会继续向外传播。例如,改写上面的Activity代码,将程序中的return false改为return true,然后运行该程序并触碰界面上的按钮,在Android Studio的Logcat中将看到如下的输出。

2021-10-22 18:18:56.0 2179-2179/org.crazyit.event V/Listener: the TouchDown in Listener

这表明事件被监听器阻止了传播。

3.4 响应系统设置的事件

在开发Android应用时,有时候需要让应用程序随系统设置进行调整,比如判断系统的屏幕方向、判断系统方向的导航设备等。有时候可能还需要让应用程序监听系统设置的更改,并对此做出响应。

3.4.1 Configuration 类简介

Configuration类专门用于描述手机设备上的配置信息,这些配置信息既包括用户特定的配置项,也包括系统的动态设备配置。

程序可以调用Activity的如下方法来获取系统的Configuration对象:

Configuration cfg = getResources().getConfiguration();

一旦获得系统的Configuration对象,就可以使用该对象提供的如下常用属性来获取系统的配置信息。

  • float fontScale: 获取当前用户设置的字体的缩放因子。
  • int keyboard: 获取当前设备所关联的键盘类型。该属性可能返回KEYBOARD_NOKEYSKEYBOARD_QWERTY (普通电脑键盘)、KEYBOARD_12KEY (只有12个键的小键盘)等属性值。
  • int keyboardHidden: 该属性返回一个boolean值用于标识当前键盘是否可用。该属性不仅会判断系统的硬件键盘,也会判断系统的软键盘(位于屏幕上)。如果系统的硬件键盘不可用,但软键盘可用,该属性也会返回KEYBOARDHIDDEN_NO; 只有当两个键盘都不可用时才返回KEYBOARDHIDDEN_YES.
  • Locale locale: 获取用户当前的Locale。
  • int mcc: 获取移动信号的国家码。
  • int mnc: 获取移动信号的网络码。
  • int navigation: 判断系统上方向导航设备的类型。该属性可能返回NAVIGATION_NONAV (无导航)、NAVIGATION_DPAD (DPAD导航)、NAVIGATION_TRACKBALL (轨迹球导航)、NAVIGATION_WHEEL (滚轮导航)等属性值。
  • int orientation: 获取系统屏幕的方向,该属性可能返回ORIENTATION_LANDSCAPE (横向屏幕)、ORIENTATION_PORTRAIT (竖向屏幕)、ORIENTATION_SQUARE (方形屏幕)等属性值。
  • int touchscreen: 获取系统触摸屏的触摸方式。该属性可能返回TOUCHSCREEN_NOTOUCH (无触摸屏)、TOUCHSCREEN_STYLUS (触摸笔式的触摸屏)、TOUCHSCREEN_FINGER (接受手指的触摸屏)等属性值。

下面以一个实例来介绍Configuration的用法,该程序可以获取系统的屏幕方向、触摸屏方式等。

实例:获取系统设备状态

该程序的界面布局比较简单,程序只是提供了4个文本框来显示系统的屏幕方向、触摸屏方式等状态,故此处不再给出界面布局文件。该程序的Activity代码主要可分为两步:

  1. 获取系统的Configuration对象。
  2. 调用Configuration对象的属性来获取设备状态。

下面是该程序的Activity代码。

public class MainActivity extends Activity {
    private TextView ori;
    private TextView navigation;
    private TextView touch;
    private TextView mnc;

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

        // 获取应用界面中的界面组件
        ori = findViewById(R.id.ori);
        navigation = findViewById(R.id.navigation);
        touch = findViewById(R.id.touch);
        mnc = findViewById(R.id.mnc);
        Button bn = findViewById(R.id.bn);

        bn.setOnClickListener(view -> {
            // 获取系统的Configuration对象
            Configuration cfg = getResources().getConfiguration();
            String screen = cfg.orientation == Configuration.ORIENTATION_LANDSCAPE ? "横向屏幕" : "竖向屏幕";
            String mncCode = cfg.mnc + "";
            String naviName = cfg.navigation == Configuration.NAVIGATION_NONAV ? "没有方向控制" :
                    (cfg.navigation == Configuration.NAVIGATION_WHEEL ? "滚轮控制方向" :
                    (cfg.navigation == Configuration.NAVIGATION_DPAD ? "方向键控制方向" : "轨迹球控制方向"));
            navigation.setText(naviName);
            String touchName = cfg.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH ? "无触摸屏" : "支持触摸屏";
            ori.setText(screen);
            mnc.setText(mncCode);
            touch.setText(touchName);
        });
    }
}

上面程序中的代码用于获取系统的Configuration对象,一旦获得了系统的Configuration对象之后,程序就可以通过它来了解系统的设备状态。运行上面的程序,将显示如下所示的界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.4.2 重写 onConfigurationChanged 方法响应系统设置更改

如果程序需要监听系统设置的更改,则可以考虑重写Activity的onConfigurationChanged(Configuration newConfig)方法,该方法是一个基于回调的事件处理方法——当系统设置发生更改时,该方法会被自动触发。

为了在程序中动态地更改系统设置,可以调用Activity的setRequestedOrientation(int)方法来修改屏幕的方向。

实例:监听屏幕方向的改变

该实例的界面布局很简单,该界面中仅包含一个普通按钮,该按钮用于动态修改系统屏幕的方向,此处不再给出系统界面布局代码。该程序的Activity代码主要会调用Activity的setRequestedOrientation(int)方法来动态更改屏幕方向。除此之外,还重写了Activity的onConfigurationChanged(Configuration newConfig)方法,该方法可用于监听系统设置的更改。程序代码如下。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button bn = findViewById(R.id.bn);

        // 为按钮绑定事件监听器
        bn.setOnClickListener(view -> {
            Configuration config = getResources().getConfiguration();
            // 如果当前是横屏
            if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                // 设为竖屏
                MainActivity.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            } else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) {
                // 设为横屏
                MainActivity.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            }
        });
    }

    // 重写该方法,用于监听系统设置的更改,主要是监控屏幕方向的更改
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        String screen = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? "横向屏幕" : "竖向屏幕";
        Toast.makeText(this, "系统的屏幕方向发生改变\n修改后的屏幕方向为: " + screen, Toast.LENGTH_LONG).show();
    }
}

上面程序中的代码用于动态地修改手机屏幕的方向,接下来的代码重写了Activity的onConfigurationChanged(Configuration newConfig)方法,当系统设置发生更改时,该方法将会被自动回调。

除此之外,为了让该Activity能够监听屏幕方向更改的事件,需要在配置该Activity时指定android:configChanges属性,该属性可以支持mccmnclocaletouchscreenkeyboardkeyboardHiddennavigationorientationscreenLayoutuiModescreenSizesmallestScreenSizefontScale等属性值,其中orientationscreenSize属性值指定该Activity可以监听屏幕方向改变的事件。

因此,将应用的AndroidManifest.xml文件改为如下形式。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.crazyit.event">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!--设置Activity可以监听屏幕方向改变的事件-->
        <activity android:name=".MainActivity"
            android:configChanges="orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

上面的代码指定了该Activity可以监听屏幕方向改变的事件,这样当程序改变手机屏幕方向时,Activity的onConfigurationChanged()

方法就会被回调。

提供上面的程序和设置之后,运行该程序,单击应用程序中的“更改屏幕方向”按钮,将可以看到如下所示的界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.5 Handler 消息传递机制

出于性能优化考虑,Android 的UI操作并不是线程安全的,这意味着如果有多个线程并发操作UI组件,可能会导致线程安全问题。为了解决这个问题,Android制定了一条简单的规则:只允许UI线程修改Activity里的UI组件。

当一个程序第一次启动时,Android会同时启动一条主线程(Main Thread),主线程主要负责处理与UI相关的事件,如用户的按键事件、用户接触屏幕的事件及屏幕绘图事件,并把相关的事件分发到对应的组件进行处理。所以,主线程通常又被叫作UI线程。

Android的消息传递机制是另一种形式的"事件处理",这种机制主要是为了解决Android应用的多线程问题。Android平台只允许UI线程修改Activity里的UI组件,这样就会导致新启动的线程无法动态改变界面组件的属性值。但在实际Android应用开发中,尤其是涉及动画的游戏开发中,需要让新启动的线程周期性地改变界面组件的属性值,这就需要借助于Handler的消息传递机制来实现。

3.5.1 Handler 类简介

Handler类的主要作用有两个:

  1. 在新启动的线程中发送消息。
  2. 在主线程中获取、处理消息。

上面的说法看上去很简单,似乎只要分成两步即可:在新启动的线程中发送消息,然后在主线程中获取并处理消息。但这个过程涉及两个问题:新启动的线程何时发送消息?主线程何时去获取并处理消息?这个时机显然不好控制。

为了让主线程能“适时”地处理新启动的线程所发送的消息,只能通过回调的方式来实现。开发者只要重写Handler类中处理消息的方法,当新启动的线程发送消息时,消息会发送到与之关联的MessageQueue,而Handler会不断地从MessageQueue中获取并处理消息,这将导致Handler类中处理消息的方法被回调。

Handler类包含如下方法用于发送、处理消息:

  • handleMessage(Message msg): 处理消息的方法。该方法通常用于被重写。
  • hasMessages(int what): 检查消息队列中是否包含what属性为指定值的消息。
  • hasMessages(int what, Object object): 检查消息队列中是否包含what属性为指定值且object属性为指定对象的消息。
  • 多个重载的Message obtainMessage(): 获取消息。
  • sendEmptyMessage(int what): 发送空消息。
  • sendEmptyMessageDelayed(int what, long delayMillis): 指定多少毫秒之后发送空消息。
  • sendMessage(Message msg): 立即发送消息。
  • sendMessageDelayed(Message msg, long delayMillis): 指定多少毫秒之后发送消息。

借助于上面这些方法,程序可以方便地利用Handler来进行消息传递。

实例:自动播放动画

本实例通过一个新线程来周期性地修改ImageView所显示的图片,通过这种方式来开发一个动画效果。该程序的界面布局代码非常简单,程序只是在界面布局中定义了ImageView组件,此处不再给出界面布局代码。

接下来主程序使用java.util.Timer来周期性地执行指定任务,程序代码如下:

public class MainActivity extends Activity {
    private ImageView show;

    static class MyHandler extends Handler {
        private WeakReference<MainActivity> activity;

        public MyHandler(WeakReference<MainActivity> activity) {
            this.activity = activity;
        }

        // 定义周期性显示的图片ID
        private int[] imageIds = new int[] {
            R.drawable.java, R.drawable.javaee, R.drawable.ajax,
            R.drawable.android, R.drawable.swift
        };
        private int currentImageId = 0;

        @Override
        public void handleMessage(Message msg) {
            // 如果该消息是本程序所发送的
            if (msg.what == 0x1233) {
                // 动态修改所显示的图片
                activity.get().show.setImageResource(
                    imageIds[currentImageId++ % imageIds.length]
                );
            }
        }
    }

    MyHandler myHandler = new MyHandler(new WeakReference<>(this));

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

        // 定义一个计时器,让该计时器周期性执行指定任务
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                // 发送空消息
                myHandler.sendEmptyMessage(0x1233);
            }
        }, 0, 1200);
    }
}

上面程序中的第二段粗体字代码通过Timer周期性执行指定任务,Timer对象可调度TimerTask对象,TimerTask对象的本质就是启动一条新线程。由于Android不允许在新线程中访问Activity里的界面组件,因此程序只能在新线程里发送一条消息,通知系统更新ImageView组件。

上面程序中的第一段粗体字代码重写了HandlerhandleMessage(Message msg)方法,该方法用于处理消息。当新线程发送消息时,该方法会被自动回调,handleMessage(Message msg)方法依然位于主线程中,所以可以动态地修改ImageView组件的属性。这就实现了本程序所要达到的效果:由新线程来周期性地修改ImageView的属性,从而实现动画效果。运行上面的程序,可以看到应用程序中5张图片交替显示的动态效果。

3.5.2 Handler、Looper、MessageQueue 的工作原理

为了更好地理解Handler的工作原理,下面先介绍一下与Handler一起工作的几个组件。

  • Message: Handler接收和处理的消息对象。

  • Looper: 每个线程只能拥有一个Looper,它的loop方法负责读取MessageQueue中的消息,读到信息之后就把消息交给发送该消息的Handler进行处理。

  • MessageQueue: 消息队列,它采用先进先出的方式来管理Message。程序创建Looper对象时,会在它的构造器中创建MessageQueue对象。Looper的构造器源代码如下:

    private Looper() {
        mQueue = new MessageQueue();
        mRun = true;
        mThread = Thread.currentThread();
    }
    

    该构造器使用了private修饰,表明程序员无法通过构造器创建Looper对象。从上面的代码不难看出,程序在初始化Looper时会创建一个与之关联的MessageQueue,这个MessageQueue就负责管理消息。

  • Handler: 它的作用有两个,即发送消息和处理消息。程序使用Handler发送消息,由Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果希望Handler正常工作,必须在当前线程中有一个MessageQueue;否则消息就没有MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理:

    • 在主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可通过Handler来发送消息、处理消息了。
    • 程序员自己启动的子线程,必须自己创建一个Looper对象,并启动它。创建Looper对象调用它的prepare方法即可。prepare方法保证每个线程最多只有一个Looper对象。prepare方法的源代码如下:
      public static final void prepare() {
          if (sThreadLocal.get() != null) {
              throw new RuntimeException("Only one Looper may be created per thread");
          }
          sThreadLocal.set(new Looper());
      }
      
      接下来调用Looper的静态loop方法来启动它。loop方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。下面是Looper类的loop方法的源代码:
      for (;;) {
          Message msg = queue.next(); // 获取消息队列中的下一个消息,如果没有消息,将会阻塞
          if (msg == null) {
              // 如果消息为null,表明消息队列正在退出
              return;
          }
          Printer logging = me.mLogging;
          if (logging != null) {
              logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
          }
          msg.target.dispatchMessage(msg);
          if (logging != null) {
              logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
          }
          // 使用final修饰该标识符,保证在分发消息的过程中线程标识符不会被修改
          final long newIdent = Binder.clearCallingIdentity();
          if (ident != newIdent) {
              Log.wtf(TAG, "Thread identity changed from 0x"
                      + Long.toHexString(ident) + " to 0x"
                      + Long.toHexString(newIdent) + " while dispatching to "
                      + msg
      
      

.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycle();
}
```

归纳起来,Looper、MessageQueue、Handler各自的作用如下:

  • Looper: 每个线程只有一个Looper,它负责管理MessageQueue,会不断地从MessageQueue中取出消息,并将消息分给对应的Handler处理。
  • MessageQueue: 由Looper负责管理。它采用先进先出的方式来管理Message。
  • Handler: 它能把消息发送给Looper管理的MessageQueue,并负责处理Looper分给它的消息。

在线程中使用Handler的步骤如下:

  1. 调用Looper的prepare方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
  2. 有了Looper之后,创建Handler子类的实例,重写handleMessage方法,该方法负责处理来自其他线程的消息。
  3. 调用Looper的loop方法启动Looper。

下面通过一个实例来介绍Looper与Handler的用法。

实例:使用新线程计算质数

该实例允许用户输入一个数值上限,当用户单击“计算”按钮时,该应用会将该上限数值发送到新启动的线程中,让该线程来计算该范围内的所有质数。

之所以不直接在UI线程中计算该范围内的所有质数,是因为UI线程需要响应用户动作,如果在UI线程中执行一个“耗时”操作,将会导致UI线程被阻塞,从而让应用程序失去响应。比如在该实例中,如果用户输入的数值太大,系统可能需要较长时间才能计算出所有质数,这就可能导致UI线程失去响应。

尽量避免在UI线程中执行耗时操作,因为这样可能导致一个“著名”的异常:ANR异常。只要在UI线程中执行需要消耗大量时间的操作,都会引发ANR,因为这会导致Android应用程序无法响应输入事件和Broadcast。

为了将用户在UI界面输入的数值上限动态地传给新启动的线程,本实例将会在线程中创建一个Handler对象,然后UI线程的事件处理方法就可以通过该Handler向新线程发送消息了。

该实例的界面布局文件比较简单,只有一个文本框和一个按钮,故此处不再给出界面布局文件。

该实例的Activity代码如下:

public class MainActivity extends Activity {
    public static final String UPPER_NUM = "upper";
    private EditText etNum;
    private CalThread calThread;

    // 定义一个线程类
    class CalThread extends Thread {
        private Handler mHandler;

        @Override
        public void run() {
            Looper.prepare();
            mHandler = new Handler() {
                // 定义处理消息的方法
                @Override
                public void handleMessage(Message msg) {
                    if (msg.what == 0x123) {
                        int upper = msg.getData().getInt(UPPER_NUM);
                        List<Integer> nums = new ArrayList<>();
                        // 计算从2开始、到upper的所有质数
                        outer:
                        for (int i = 2; i <= upper; i++) {
                            // 用i除以从2开始、到i的平方根的所有数
                            for (int j = 2; j <= Math.sqrt(i); j++) {
                                // 如果可以整除,则表明这个数不是质数
                                if (i % j == 0) {
                                    continue outer;
                                }
                            }
                            nums.add(i);
                        }
                        // 使用Toast显示统计出来的所有质数
                        Toast.makeText(MainActivity.this, nums.toString(), Toast.LENGTH_LONG).show();
                    }
                }
            };
            Looper.loop();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        etNum = findViewById(R.id.et_num);
        calThread = new CalThread();
        // 启动新线程
        calThread.start();
    }

    // 为按钮的点击事件提供事件处理方法
    public void cal(View source) {
        // 创建消息
        Message msg = new Message();
        msg.what = 0x123;
        Bundle bundle = new Bundle();
        bundle.putInt(UPPER_NUM, Integer.parseInt(etNum.getText().toString()));
        msg.setData(bundle);
        // 向新线程中的Handler发送消息
        calThread.mHandler.sendMessage(msg);
    }
}

上面的粗体字代码是实例的关键代码,这些代码在新线程内创建了一个Handler。由于在新线程中创建Handler时必须先创建Looper,因此程序先调用Looper的prepare方法为当前线程创建了一个Looper实例,并创建了配套的MessageQueue。新线程有了Looper对象之后,接下来程序创建了一个Handler对象,该Handler可以处理其他线程发送过来的消息。程序最后还调用了Looper的loop方法。

运行该程序,无论用户输入多大的数值,计算该范围内的质数都将会交给新线程完成,而前台UI线程不会受到影响。该程序的运行效果如图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.6 异步任务 (AsyncTask)

前面已经介绍过,Android的UI线程主要负责处理用户的按键事件、用户触屏事件及屏幕绘图事件等,因此开发者的其他操作不应该、也不能阻塞UI线程,否则UI界面将会变得停止响应,用户体验非常糟糕。

Android默认约定当UI线程阻塞超过20秒时将会引发ANR (Application Not Responding)异常。但实际上,不要说20秒,即使是5秒甚至2秒,用户都会感觉非常不爽。因此,开发者需要牢记:不要在UI线程中执行一些耗时的操作。

为了避免UI线程失去响应的问题,Android建议将耗时操作放在新线程中完成,但新线程也可能需要动态更新UI组件,比如需要从网上获取一个网页,然后在TextView中将其源代码显示出来,此时就应该将连接网络、获取网络数据的操作放在新线程中完成。问题是:获取网络数据之后,新线程不允许直接更新UI组件。

为了解决新线程不能更新UI组件的问题,Android提供了如下几种解决方案:

  • 使用Handler实现线程之间的通信。
  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

上一节已经见到了使用Handler的实例,后面的三种方式可能导致编程略显烦琐,而异步任务 (AsyncTask) 则可进一步简化这种操作。相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现。

AsyncTask<Params, Progress, Result>是一个抽象类,通常用于被继承,继承AsyncTask时需要指定如下三个泛型参数:

  • Params: 启动任务执行的输入参数的类型。
  • Progress: 后台任务完成的进度值的类型。
  • Result: 后台执行任务完成后返回结果的类型。

使用AsyncTask只要如下三步即可:

  1. 创建AsyncTask的子类,并为三个泛型参数指定类型。如果某个泛型参数不需要指定类型,则可将它指定为Void
  2. 根据需要,实现AsyncTask的如下方法:
    • doInBackground(Params... params): 重写该方法,这个方法会在后台线程执行。该方法可以调用publishProgress(Progress... values)方法更新任务的执行进度。
    • onProgressUpdate(Progress... values): 在doInBackground方法中调用publishProgress方法更新任务的执行进度后,将会触发该方法。
    • onPreExecute(): 该方法将在执行后台耗时操作前被调用。通常该方法用于完成一些初始化的准备工作,比如在界面上显示进度条等。
    • onPostExecute(Result result): 当doInBackground完成后,系统会自动调用onPostExecute方法,并将doInBackground方法的返回值传给该方法。
  3. 调用AsyncTask子类的实例的execute(Params... params)开始执行耗时任务。

使用AsyncTask时必须遵守如下规则:

  • 必须在UI线程中创建AsyncTask的实例。
  • 必须在UI线程中调用AsyncTask的execute方法。
  • AsyncTask的onPreExecuteonPostExecute(Result result)doInBackground(Params... params)onProgressUpdate(Progress... values)方法,不应该由程序员代码调用,而是由Android系统负责调用。
  • 每个AsyncTask只能被执行一次,多次调用将会引发异常。
实例:使用异步任务执行下载

本实例示范如何使用异步任务下载网络资源。该实例的界面布局很简单,只包含两个组件:一个文本框用于显示从网络下载的页面代码;一个按钮用于激发下载任务。此处不再给出界面布局文件。该程序的Activity代码如下:

public class MainActivity extends Activity {
    private TextView show;

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

    // 重写该方法,为界面上的按钮提供事件响应方法
    public void download(View source) throws MalformedURLException {
        DownTask task = new DownTask(this, (ProgressBar) findViewById(R.id.progressBar));
        task.execute(new URL("http://www.crazyit.org/index.php"));
    }

    class DownTask extends AsyncTask<URL, Integer, String> {
        private ProgressBar progressBar;
        // 定义记录已经读取行的数量
        int hasRead = 0;
        Context mContext;

        public DownTask(Context ctx, ProgressBar progressBar) {
            mContext = ctx;
            this.progressBar = progressBar;
        }

        @Override
        protected String doInBackground(URL... params) {
            StringBuilder sb = new StringBuilder();
            try {
                URLConnection conn = params[0].openConnection();
                // 打开conn连接对应的输入流,并将它包装成BufferedReader
                BufferedReader br = new BufferedReader(
                    new InputStreamReader(conn.getInputStream(), "utf-8"));
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line).append("\n");
                    hasRead++;
                    publishProgress(hasRead);
                }
                return sb.toString();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        protected void onPostExecute(String result) {
            // 返回HTML页面的内容
            show.setText(result);
            // 设置进度条不可见
            progressBar.setVisibility(View.INVISIBLE);
        }

        @Override
        protected void onPreExecute() {
            // 设置进度条可见
            progressBar.setVisibility(View.VISIBLE);
            // 设置进度条的当前值
            progressBar.setProgress(0);
            // 设置该进度条的最大进度值
            progressBar.setMax(100);
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            // 更新进度
            show.setText("已经读取了[" + values[0] + "]行!");
            progressBar.setProgress(values[0]);
        }
    }
}

上面程序的download方法很简单,它只是创建了DownTask (AsyncTask的子类)实例,并调用它的execute方法开始执行异步任务。该程序的重点是实现AsyncTask的子类,实现该子类时实现了如下4个方法:

  • doInBackground: 该方法的代码完成实际的下载任务。
  • onPreExecute: 该方法的代码负责在下载开始的时候显示一个进度条。
  • onProgressUpdate: 该方法的代码负责随着下载进度的改变更新进度条的进度值。
  • onPostExecute: 该方法的代码负责当下载完成后,将下载的代码显示出来。

该程序使用了网络编程从网络下载数据。关于Android网络编程的知识,请参考本书第13章的内容。除此之外,本程序需要访问网络,因此还需要在AndroidManifest.xml文件中声明如下权限:

<uses-permission android:name="android.permission.INTERNET"/>

运行该程序并单击“下载”按钮,将可以看到如图所示的界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.7 本章小结

本章是对上一章内容的补充:图形界面编程肯定需要与事件处理结合。当我们开发了一个界面友好的应用之后,用户在程序界面上执行操作时,程序必须为这种用户操作提供响应动作,这种响应动作就是由事件处理来完成的。

学习本章的重点是掌握Android的两种事件处理机制:基于回调的事件处理和基于监听的事件处理。对于基于监听的事件处理来说,开发者需要掌握事件监听的处理模式,以及不同事件对应的监听器接口;对于基于回调的事件处理来说,开发者需要掌握不同事件对应的回调方法。除此之外,本章还介绍了重写onConfigurationChanged方法来响应系统设置更改。

需要指出的是,由于Android不允许在子线程中更新界面组件,如果想在子线程中更新界面组件,开发者需要借助于Handler对象来实现。本章详细介绍了Handler、Looper与MessageQueue之间的关系及工作原理。

此外,本章还介绍了使用AsyncTask类进行异步任务处理,进一步简化了在新线程中执行耗时操作并更新UI的流程。

通过学习本章内容,读者将能够:

  • 理解并使用基于监听和基于回调的两种事件处理机制。
  • 实现事件监听器和事件回调方法。
  • 使用Handler在子线程中安全地更新UI组件。
  • 利用AsyncTask执行简单的异步任务,并处理任务执行的结果。

掌握这些内容将使开发者能够编写出响应迅速、用户体验良好的Android应用。

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值