第12章 线程与消息处理

tb教学录像:50分钟)

在程序开发时,对于一些比较耗时的操作,通常会为其开辟一个单独的线程来执行,以尽可能减少用户的等待时间。在Android中,默认情况下,所有的操作都在主线程中进行,主线程负责管理与UI相关的事件,而在用户自己创建的子线程中,不能对UI组件进行操作。因此,Android提供了消息处理传递机制来解决这一问题。本章将对Android中如何实现多线程以及如何通过线程和消息处理机制操作UI界面进行详细介绍。

通过阅读本章,您可以:

★ 掌握如何创建及开启线程

★ 掌握如何让线程休眠

★ 掌握如何中断线程

★ 了解循环者Looper

★ 掌握消息处理类Handler的应用

★ 掌握消息类Message的应用

12.1 实现多线程

tb教学录像:光盘\TM\lx\12\实现多线程.exe

在现实生活中,很多事情都是同时进行的,例如,我们可以一边看书,一边喝咖啡;而计算机则可以一边播放音乐,一边打印文档。对于这种可以同时进行的任务,可以用线程来表示,每个线程完成一个任务,并与其他线程同时执行,这种机制被称为多线程。下面就来介绍如何创建线程、开启线程、让线程休眠和中断线程。

12.1.1 创建线程

在Android中,提供了两种创建线程的方法:一种是通过Thread类的构造方法创建线程对象,并重写run()方法实现;另一种是通过实现Runnable接口实现,下面分别进行介绍。

  1. 通过Thread类的构造方法创建线程

在Android中,可以使用Thread类提供的以下构造方法来创建线程。

  1. Thread(Runnable runnable)

该构造方法的参数runnable可以通过创建一个Runnable类的对象并重写其run()方法来实现,例如,要创建一个名称为thread的线程,可以使用下面的代码:

  1. Thread thread=new Thread(new Runnable(){
  2. //重写run()方法
  3. @Override
  4. public void run() {
  5. //要执行的操作
  6. }
  7. });

说明:在run()方法中,可以编写要执行的操作的代码,当线程被开启时,run()方法将被执行。

  1. 通过实现Runnable接口创建线程

在Android中,还可以通过实现Runnable接口来创建线程。实现Runnable接口的语法格式如下:

  1. public class ClassName extends Object implements Runnable

当一个类实现Runnable接口后,还需要实现其run()方法,在run()方法中,可以编写要执行的操作的代码。

例如,要创建一个实现了Runnable接口的Activity,可以使用下面的代码:

  1. public class MainActivity extends Activity implements Runnable {
  2. @Override
  3. public void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.main);
  6. }
  7. @Override
  8. public void run() {
  9. //要执行的操作
  10. }
  11. }

12.1.2 开启线程

创建线程对象后,还需要开启线程,线程才能执行。Thread类提供了start()方法用于开启线程,其语法格式如下:

  1. start()

例如,存在一个名称为thread的线程,如果想开启该线程,可以使用下面的代码:

  1. thread.start(); //开启线程

12.1.3 线程的休眠

线程的休眠就是让线程暂停一段时间后再次执行。同Java一样,在Android中,也可以使用Thread类的sleep()方法让线程休眠指定的时间。sleep()方法的语法格式如下:

  1. sleep(long time)

其中参数time用于指定休眠的时间,单位为毫秒。

例如,想要线程休眠1秒钟,可以使用下面的代码:

  1. Thread.sleep(1000);

12.1.4 中断线程

当需要中断指定的线程时,可以使用Thread类提供的interrupt()方法来实现。使用interrupt()方法可以向指定的线程发送一个中断请求,并将该线程标记为中断状态。interrupt()方法的语法格式如下:

  1. interrupt()

例如,存在一个名称为thread的线程,如果想中断该线程,可以使用下面的代码:

  1. //省略部分代码
  2. thread.interrupt();
  3. //省略部分代码
  4. public void run() {
  5. while(!Thread.currentThread().isInterrupted()){
  6. //省略部分代码
  7. }
  8. }

另外,由于当线程执行wait()、join()或sleep()方法时,线程的中断状态将被清除并抛出InterruptedException,所以,如果想在线程中执行了wait()、join()或sleep()方法时中断线程,就需要使用一个boolean型的标记变量来记录线程的中断状态,并通过该标记变量来控制循环的执行与停止。例如,通过名称为isInterrupt的boolean型变量来标记线程的中断,关键代码如下:

  1. private boolean isInterrupt=false; //定义标记变量
  2. //省略部分代码
  3. //在需要中断线程时,将isInterrupt的值设置为true
  4. public void run() {

  5. while(!isInterrupt){
  6. //省略部分代码
  7. }
  8. }

12.1.5 范例1:通过实现Runnable接口来创建线程

例12.1 在Eclipse中创建Android项目,名称为12.1,通过实现Runnable接口来创建线程、开启线程和中断线程。(实例位置:光盘\TM\sl\12\12.1)

(1)修改新建项目的res\layout目录下的布局文件main.xml,将默认添加的TextView组件删除,然后在默认添加的线性布局管理器中添加两个按钮,一个用于开启线程,另一个用于中断线程,具体代码请参见光盘。

(2)打开默认添加的MainActivity,让该类实现Runnable接口,修改后的创建类的代码如下:

  1. public class MainActivity extends Activity implements Runnable{}

(3)实现Runnable接口中的run()方法,在该方法中,判断当前线程是否被中断,如果没有被中断,则将循环变量值加1,并在日志中输出循环变量的值,具体代码如下:

  1. @Override
  2. public void run() {
  3. while (!Thread.currentThread().isInterrupted()) {
  4. i++;
  5. Log.i("循环变量:", String.valueOf(i));
  6. }
  7. }

(4)在该MainActivity中,创建两个成员变量,具体代码如下:

  1. private Thread thread; //声明线程对象
  2. int i; //循环变量

(5)在onCreate()方法中,首先获取布局管理器中添加的“开始”按钮,然后为该按钮添加单击事件监听器,在重写的onCreate()方法中,根据当前Activity创建一个线程,并开启该线程,具体代码如下:

  1. Button startButton = (Button) findViewById(R.id.button1); //获取“开始”按钮
  2. startButton.setOnClickListener(new OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. i = 0;
  6. thread = new Thread(MainActivity.this); //创建一个线程
  7. thread.start(); //开启线程
  8. }
  9. });

(6)获取布局管理器中添加的“停止”按钮,并为其添加单击事件监听器,在重写的onCreate()方法中,如果thread对象不为空,则中断线程,并向日志中输出提示信息,具体代码如下:

  1. Button stopButton = (Button) findViewById(R.id.button2); //获取“停止”按钮
  2. stopButton.setOnClickListener(new OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. if (thread != null) {
  6. thread.interrupt(); //中断线程
  7. thread = null;
  8. }
  9. Log.i("提示:", "中断线程");
  10. }
  11. });

(7)重写MainActivity的onDestroy()方法,在该方法中中断线程,具体代码如下:

  1. @Override
  2. protected void onDestroy() {
  3. if (thread != null) {
  4. thread.interrupt(); //中断线程
  5. thread = null;
  6. }
  7. super.onDestroy();
  8. }

运行本实例,在屏幕上将显示一个“开始”按钮和一个“停止”按钮,单击“开始”按钮,将在日志面板中输出循环变量的值;单击“停止”按钮,将中断线程。日志面板的显示结果如图12.1所示。

389-1 图12.1 在日志面板中输出的内容

12.1.6 范例2:开启一个新线程播放背景音乐

例12.2 在Eclipse中创建Android项目,名称为12.2,开启一个新线程播放背景音乐,在音乐文件播放完毕后,暂停5秒钟后重新开始播放。(实例位置:光盘\TM\sl\12\12.2)

(1)修改新建项目的res\layout目录下的布局文件main.xml,将默认添加的TextView组件删除,然后在默认添加的线性布局管理器中添加一个“开始”按钮,用于开启线程并播放背景音乐,具体代码请参见光盘。

(2)在该MainActivity中,创建两个成员变量,具体代码如下:

  1. private Thread thread; //声明一个线程对象
  2. private static MediaPlayer mp = null; //声明一个MediaPlayer对象

(3)在onCreate()方法中,获取布局管理器中添加的“开始”按钮,并为该按钮添加单击事件监听器,在重写的onCreate()方法中,首先设置该按钮不可用,然后创建一个用于播放背景音乐的线程,并开启该线程,在重写的run()方法中,调用playBGSound()方法播放背景音乐,具体代码如下:

  1. Button button = (Button) findViewById(R.id.button1); //获取布局管理器中添加的“开始”按钮
  2. button.setOnClickListener(new OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. ((Button) v).setEnabled(false); //设置按钮不可用
  6. //创建一个用于播放背景音乐的线程
  7. thread = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. playBGSound(); //播放背景音乐
  11. }
  12. });
  13. thread.start(); //开启线程
  14. }
  15. });

(4)编写playBGSound()方法,首先判断MediaPlayer对象是否为空,如果不为空,则释放该对象,然后创建一个用于播放背景音乐的MediaPlayer对象,并开始播放,再为该MediaPlayer对象添加播放完成事件监听器,在重写的onCompletion()方法中,让线程休眠5秒钟,并调用playBGSound()方法重新播放音乐,具体代码如下:

  1. private void playBGSound() {
  2. if (mp != null) {
  3. mp.release(); //释放资源
  4. }
  5. mp = MediaPlayer.create(MainActivity.this, R.raw.jasmine);
  6. mp.start(); //开始播放
  7. //为MediaPlayer添加播放完成事件监听器
  8. mp.setOnCompletionListener(new OnCompletionListener() {
  9. @Override
  10. public void onCompletion(MediaPlayer mp) {
  11. try {
  12. Thread.sleep(5000); //线程休眠5秒钟
  13. playBGSound(); //重新播放音乐
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. });
  19. }

(5)重写MainActivity的onDestroy()方法,停止播放背景音乐并释放资源,具体代码如下:

  1. @Override
  2. protected void onDestroy() {
  3. if (mp != null) {
  4. mp.stop(); //停止播放
  5. mp.release(); //释放资源
  6. mp = null;
  7. }
  8. if (thread != null) {
  9. thread = null;
  10. }
  11. super.onDestroy();
  12. }

运行本实例,在屏幕上将显示一个“开始”按钮,单击该按钮,该按钮将变为不可用状态,并且开始播放背景音乐,如图12.2所示。

390-1 图12.2 程序运行效果

12.2 Handler消息传递机制

tb教学录像:光盘\TM\lx\12\Handler消息传递机制.exe

在12.1节中,已经介绍了在Android中如何创建、开启、休眠和中断线程。不过,此时并没有在新创建的子线程中对UI界面上的内容进行操作,如果应用前面介绍的方法对UI界面进行操作,将抛出异常。例如,在子线程的run()方法中循环修改文本框的显示文本,将抛出如图12.3所示的异常信息。

391-1 图12.3 抛出的异常信息

为此,Android中引入了Handler消息传递机制,来实现在新创建的线程中操作UI界面。下面将对Handler消息传递机制进行介绍。

12.2.1 循环者(Looper)简介

在介绍Looper之前,需要先来了解一下MessageQueue的概念。在Android中,一个线程对应一个Looper对象,而一个Looper对象又对应一个MessageQueue(消息队列)。MessageQueue用于存放Message(消息),在MessageQueue中,存放的消息按照FIFO(先进先出)原则执行,由于MessageQueue被封装到Looper里面,所以这里不对MessageQueue进行过多介绍。

Looper对象用来为一个线程开启一个消息循环,从而操作MessageQueue。默认情况下,Android中新创建的线程是没有开启消息循环的,但是主线程除外。系统自动为主线程创建Looper对象,开启消息循环。所以,当在主线程中应用下面的代码创建Handler对象时不会出错,而如果在新创建的非主线程中应用下面的代码创建Handler对象,将产生如图12.4所示的异常信息。

  1. Handler handler2 = new Handler();

如果想要在非主线程中创建Handler对象,首先需要使用Looper类的prepare()方法来初始化一个Looper对象,然后创建该Handler对象,再使用Looper类的loop()方法启动Looper,从消息队列中获取和处理消息。

392-1 图12.4 在非主线程中创建Handler对象产生的异常信息

例12.3 在Eclipse中创建Android项目,名称为12.3,创建一个继承Thread类的LooperThread,并在重写的run()方法中创建一个Handler对象,发送并处理消息。(实例位置:光盘\TM\sl\12\12.3)

(1)创建一个继承了Thread类的LooperThread,并在重写的run()方法中创建一个Handler对象,发送并处理消息,关键代码如下:

  1. public class LooperThread extends Thread {
  2. public Handler handler1; //声明一个Handler对象
  3. @Override
  4. public void run() {
  5. super.run();
  6. Looper.prepare(); //初始化Looper对象
  7. //实例化一个Handler对象
  8. handler1 = new Handler() {
  9. public void handleMessage(Message msg) {
  10. Log.i("Looper",String.valueOf(msg.what));
  11. }
  12. };
  13.  
  14. Message m=handler1.obtainMessage(); //获取一个消息
  15. m.what=0x11; //设置Message的what属性的值
  16. handler1.sendMessage(m); //发送消息
  17. Looper.loop(); //启动Looper
  18. }
  19. }

(2)在MainActivity的onCreate()方法中,创建一个LooperThread线程,并开启该线程,关键代码如下:

  1. LooperThread thread=new LooperThread(); //创建一个线程
  2. thread.start(); //开启线程

运行本实例,在日志面板(LogCat)中输出如图12.5所示的内容。

392-2 图12.5 在日志面板(LogCat)中输出的内容

Looper类提供的常用方法如表12.1所示。

表12.1 Looper类提供的常用方法

方 法 描 述
prepare() 用于初始化Looper
loop() 启动Looper线程,线程会从消息队列里获取和处理消息
myLooper() 可以获取当前线程的Looper对象
getThread() 用于获取Looper对象所属的线程
quit() 用于结束Looper循环

注意:写在Looper.loop()之后的代码不会被执行,该函数内部是一个循环,当调用Handler. getLooper().quit()方法后,loop()方法才会中止,其后面的代码才能运行。

12.2.2 消息处理类(Handler)简介

消息处理类(Handler)允许发送和处理Message或Runnable对象到其所在线程的MessageQueue中。Handler主要有以下两个作用。

(1)将Message或Runnable应用post()或sendMessage()方法发送到MessageQueue中,在发送时可以指定延迟时间、发送时间及要携带的Bundle数据。当MessageQueue循环到该Message时,调用相应的Handler对象的handlerMessage()方法对其进行处理。

(2)在子线程中与主线程进行通信,也就是在工作线程中与UI线程进行通信。

说明:在一个线程中,只能有一个Looper和MessageQueue,但是可以有多个Handler,而且这些Handler可以共享同一个Looper和MessageQueue。

Handler类提供的发送和处理消息的常用方法如表12.2所示。

表12.2 Handler类提供的常用方法

方 法 描 述
handleMessage(Message msg) 处理消息的方法。通常重写该方法来处理消息,在发送消息时,该方法会自动回调
post(Runnable r) 立即发送Runnable对象,该Runnable对象最后将被封装成Message对象
postAtTime(Runnable r, long uptimeMillis) 定时发送Runnable对象,该Runnable对象最后将被封装成Message对象
postDelayed(Runnable r, long delayMillis) 延迟发送Runnable对象,该Runnable对象最后将被封装成Message对象
sendEmptyMessage(int what) 发送空消息
sendMessage(Message msg) 立即发送消息
sendMessageAtTime(Message msg, long uptimeMillis) 定时发送消息
sendMessageDelayed(Message msg, long delayMillis) 延迟发送消息

12.2.3 消息类(Message)简介

消息类(Message)被存放在MessageQueue中,一个MessageQueue中可以包含多个Message对象。每个Message对象可以通过Message.obtain()或Handler.obtainMessage()方法获得。一个Message对象具有如表12.3所示的5个属性。

表12.3 Message对象的属性

属 性 类 型 描 述
arg1 int 用来存放整型数据
arg2 int 用来存放整型数据
obj Object 用来存放发送给接收器的Object类型的任意对象
replyTo Messenger 用来指定此Message发送到何处的可选Messager对象
what int 用于指定用户自定义的消息代码,这样接收者可以了解这个消息的信息

说明:使用Message类的属性可以携带int型数据,如果要携带其他类型的数据,可以先将要携带的数据保存到Bundle对象中,然后通过Message类的setDate()方法将其添加到Message中。

总之,Message类的使用方法比较简单,在使用时,需注意以下3点:

[√]尽管Message有public的默认构造方法,但是通常情况下,需要使用Message.obtain()或Handler.obtainMessage()方法来从消息池中获得空消息对象,以节省资源。

[√]如果一个Message只需要携带简单的int型信息,应优先使用Message.arg1和Message.arg2属性来传递信息,这比用Bundle更节省内存。

[√]尽可能使用Message.what来标识信息,以便用不同方式处理Message。

12.2.4 范例1:开启新线程获取网络图片并显示到ImageView中

例12.4 在Eclipse中创建Android项目,名称为12.4,开启新线程获取网络图片并显示到ImageView中。(实例位置:光盘\TM\sl\12\12.4)

(1)修改新建项目的res\layout目录下的布局文件main.xml,将默认添加的TextView组件删除,然后在默认添加的线性布局管理器中添加一个ImageView组件,并且设置该组件默认显示的图片,关键代码如下:

  1. <ImageView
  2. android:id="@+id/imageView1"
  3. android:layout_width="wrap_content"
  4. android:layout_height="wrap_content"
  5. android:padding="10dp"
  6. android:src="@drawable/hint"/>

(2)在该MainActivity中,声明一个代表ImageView组件的对象,具体代码如下:

  1. private ImageView iv; //声明ImageView组件的对象

(3)编写getPicture()方法,用于根据给定的网址从网络上获取图片,并根据获取到的图片创建一个Bitmap对象。getPicture()方法的具体代码如下:

  1. /**
  2. * 功能:根据网址获取图片对应的Bitmap对象
  3. * @param path
  4. * @return
  5. */
  6. public Bitmap getPicture(String path){
  7. Bitmap bm=null;
  8. try {
  9. URL url=new URL(path); //创建URL对象
  10. URLConnection conn=url.openConnection(); //获取URL对象对应的连接
  11. conn.connect(); //打开连接
  12. InputStream is=conn.getInputStream(); //获取输入流对象
  13. bm=BitmapFactory.decodeStream(is); //根据输入流对象创建Bitmap对象
  14. } catch (MalformedURLException e1) {
  15. e1.printStackTrace(); //输出异常信息
  16. } catch (IOException e) {
  17. e.printStackTrace(); //输出异常信息
  18. }
  19. return bm;
  20. }

(4)在onCreate()方法中,获取布局管理器中添加的ImageView组件,并创建和开启一个新线程,在创建线程时,需要重写它的run()方法,在重写的run()方法中调用getPicture()方法从网络上获取图片,然后让线程休眠2秒钟,再通过View组件的post()方法发送一个Runnable对象,修改ImageView中显示的图片,具体代码如下:

  1. iv = (ImageView) findViewById(R.id.imageView1); //获取布局管理器中添加的ImageView
  2. //创建一个新线程,用于从网络上获取图片
  3. new Thread(new Runnable() {
  4. public void run() {
  5. //从网络上获取图片
  6. final Bitmap bitmap=getPicture("http://192.168.1.66:8081/test/images/android.png");
  7. try {
  8. Thread.sleep(2000); //线程休眠2秒钟
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. //发送一个Runnable对象
  13. iv.post(new Runnable() {
  14. public void run() {
  15. iv.setImageBitmap(bitmap); //在ImageView中显示从网络上获取到的图片
  16. }
  17. });
  18. }
  19. }).start(); //开启线程

(5)由于在本实例中需要访问网络资源,所以还需要在AndroidManifest.xml文件中指定允许访问网格资源的权限,具体代码如下:

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

运行本实例,首先显示如图12.6所示的默认图片,几秒钟后,将显示如图12.7所示的从网络中获取的图片。

396-1 396-2
图12.6 显示默认的图片 图12.7 显示网络图片

12.2.5 范例2:开启新线程实现电子广告牌

例12.5 在Eclipse中创建Android项目,名称为12.5,开启新线程实现电子广告牌。(实例位置:光盘\TM\sl\12\12.5)

(1)修改新建项目的res\layout目录下的布局文件main.xml,在默认添加的TextView组件上方添加一个ImageView组件,用于显示广告图片,并设置垂直线性布局管理器内的组件水平居中显示,具体代码请参见光盘。

(2)打开默认添加的MainActivity,让该类实现Runnable接口,修改后的创建类的代码如下:

  1. public class MainActivity extends Activity implements Runnable{}

(3)实现Runnable接口中的run()方法,在该方法中,判断当前线程是否被中断,如果没有被中断,则首先产生一个随机数,然后获取一个Message,并将要显示的广告图片的索引值和对应标题保存到该Message中,再发送消息,最后让线程休眠2秒钟,具体代码如下:

  1. @Override
  2. public void run() {
  3. int index = 0;
  4. while (!Thread.currentThread().isInterrupted()) {
  5. index = new Random().nextInt(path.length); //产生一个随机数
  6. Message m = handler.obtainMessage(); //获取一个Message
  7. m.arg1 = index; //保存要显示广告图片的索引值
  8. Bundle bundle = new Bundle(); //获取Bundle对象
  9. m.what = 0x101; //设置消息标识
  10. bundle.putString("title", title[index]); //保存标题
  11. m.setData(bundle); //将Bundle对象保存到Message中
  12. handler.sendMessage(m); //发送消息
  13. try {
  14. Thread.sleep(2000); //线程休眠2秒钟
  15. } catch (InterruptedException e) {
  16. e.printStackTrace(); //输出异常信息
  17. }
  18. }
  19. }

(4)在该MainActivity中,创建程序中所需的成员变量,具体代码如下:

  1. private ImageView iv; //声明一个显示广告图片的ImageView对象
  2. private Handler handler; //声明一个Handler对象
  3. private int[] path = new int[] { R.drawable.img01, R.drawable.img02,
  4. R.drawable.img03, R.drawable.img04, R.drawable.img05,
  5. R.drawable.img06 }; //保存广告图片的数组
  6. private String[] title = new String[] { "编程词典系列产品", "高效开发", "快乐分享", "用户人群",
  7. "快速学习", "全方位查询" }; //保存显示标题的数组

(5)在onCreate()方法中,首先获取布局管理器中添加的ImageView组件,然后创建一个新线程,并开启该线程,再实例化一个Handler对象,在重写的handleMessage()方法中,更新UI界面中的ImageView和TextView组件,具体代码如下:

  1. iv = (ImageView) findViewById(R.id.imageView1); //获取显示广告图片的ImageView
  2. Thread t = new Thread(this); //创建新线程
  3. t.start(); //开启线程
  4. //实例化一个Handler对象
  5. handler = new Handler() {
  6. @Override
  7. public void handleMessage(Message msg) {
  8. //更新UI
  9. TextView tv = (TextView) findViewById(R.id.textView1); //获取TextView组件
  10. if (msg.what == 0x101) {
  11. tv.setText(msg.getData().getString("title")); //设置标题
  12. iv.setImageResource(path[msg.arg1]); //设置要显示的图片
  13. }
  14. super.handleMessage(msg);
  15. }
  16. };

运行本实例,在屏幕上将每隔两秒钟随机显示一张广告图片,如图12.8所示。

397-1 图12.8 电子广告牌

12.3 经典范例

12.3.1 多彩的霓虹灯

例12.6 在Eclipse中创建Android项目,名称为12.6,实现多彩霓虹灯。(实例位置:光盘\TM\sl\ 12\12.6)

(1)修改新建项目的res\layout目录下的布局文件main.xml,将默认添加的TextView组件删除,并为默认添加的线性布局管理器设置ID属性,具体代码请参见光盘。

(2)在res/values目录下,创建一个保存颜色资源的colors.xml文件,在该文件中,定义7个颜色资源,名称依次为color1、color2、…、color7,颜色值分别对应赤、橙、黄、绿、青、蓝、紫。colors.xml文件的关键代码如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <color name="color1">#ffff0000</color>
  4. <color name="color2">#ffff6600</color>
  5. <color name="color3">#ffffff00</color>
  6. <color name="color4">#ff00ff00</color>
  7. <color name="color5">#ff00ffff</color>
  8. <color name="color6">#ff0000ff</color>
  9. <color name="color7">#ff6600ff</color>
  10. </resources>

(3)在该MainActivity中,声明程序中所需的成员变量,具体代码如下:

  1. private Handler handler; //创建Handler对象
  2. private static LinearLayout linearLayout; //整体布局
  3. public static TextView[] tv = new TextView[14]; //TextView数组
  4. int[] bgColor=new int[]{R.color.color1,R.color.color2,R.color.color3,
  5. R.color.color4,R.color.color5,R.color.color6,R.color.color7}; //使用颜色资源
  6. private int index=0; //当前颜色值

(4)在MainActivity的onCreate()方法中,首先获取线性布局管理器,然后获取屏幕的高度,接下来再通过一个for循环创建14个文本框组件,并添加到线性布局管理器中,具体代码如下:

  1. linearLayout=(LinearLayout)findViewById(R.id.ll); //获取线性布局管理器
  2. int height=this.getResources().getDisplayMetrics().heightPixels; //获取屏幕的高度
  3. for(int i=0;i<tv.length;i++){
  4. tv[i]=new TextView(this); //创建一个文本框对象
  5. tv[i].setWidth(this.getResources().getDisplayMetrics().widthPixels); //设置文本框的宽度
  6. tv[i].setHeight(height/tv.length); //设置文本框的高度
  7. linearLayout.addView(tv[i]); //将TextView组件添加到线性布局管理器中
  8. }

(5)创建并开启一个新线程,在重写的run()方法中实现一个循环,在该循环中,首先获取一个Message对象,并为其设置一个消息标识,然后发送消息,最后让线程休眠1秒钟,具体代码如下:

  1. Thread t = new Thread(new Runnable(){
  2. @Override
  3. public void run() {
  4. while (!Thread.currentThread().isInterrupted()) {
  5. Message m = handler.obtainMessage(); //获取一个Message
  6. m.what=0x101; //设置消息标识
  7. handler.sendMessage(m); //发送消息
  8. try {
  9. Thread.sleep(new Random().nextInt(1000)); //休眠1秒钟
  10. } catch (InterruptedException e) {
  11. e.printStackTrace(); //输出异常信息
  12. }
  13. }
  14. }
  15. });
  16. t.start(); //开启线程

(6)创建一个Handler对象,在重写的handleMessage()方法中,为每个文本框设置背景颜色,该背景颜色从颜色数组中随机获取,具体代码如下:

  1. handler = new Handler() {
  2. @Override
  3. public void handleMessage(Message msg) {
  4. int temp=0; //临时变量
  5. if (msg.what == 0x101) {
  6. for(int i=0;i<tv.length;i++){
  7. temp=new Random().nextInt(bgColor.length); //产生一个随机数
  8. //去掉重复的并且相邻的颜色
  9. if(index==temp){
  10. temp++;
  11. if(temp==bgColor.length){
  12. temp=0;
  13. }
  14. }
  15. index=temp;
  16. //为文本框设置背景
  17. tv[i].setBackgroundColor(getResources().getColor(bgColor[index]));
  18. }
  19. }
  20. super.handleMessage(msg);
  21. }
  22. };

(7)在AndroidManifest.xml文件的<activity>标记中,设置android:theme属性,实现全屏显示,关键代码如下:

  1. android:theme="@android:style/Theme.Black.NoTitleBar"

运行本实例,将全屏显示一个多彩的霓虹灯,它可以不断地变换颜色,如图12.9所示。

400-1 图12.9 多彩的霓虹灯

12.3.2 简易打地鼠游戏

例12.7 在Eclipse中创建Android项目,名称为12.7,实现简易打地鼠游戏。(实例位置:光盘\TM\sl\12\12.7)

(1)修改新建项目的res\layout目录下的布局文件main.xml,首先将默认添加的布局管理器和TextView组件删除,然后添加一个帧布局管理器,最后在该布局管理器中添加一个用于显示地鼠的ImageView组件,并设置其显示一张地鼠图片,关键代码如下:

  1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:id="@+id/fl"
  3. android:background="@drawable/background"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent">
  6. <ImageView
  7. android:id="@+id/imageView1"
  8. android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:src="@drawable/mouse"/>
  11. </FrameLayout>

(2)在该MainActivity中,声明程序中所需的成员变量,具体代码如下:

  1. private int i = 0; //记录其打到了几只地鼠
  2. private ImageView mouse; //声明一个ImageView对象
  3. private Handler handler; //声明一个Handler对象
  4. public int[][] position = new int[][] { { 231, 325 }, { 424, 349 },
  5. { 521, 256 }, { 543, 296 }, { 719, 245 }, { 832, 292 },
  6. { 772, 358 } }; //创建一个表示地鼠位置的数组

(3)创建并开启一个新线程,在重写的run()方法中,创建一个记录地鼠位置的索引值的变量,并实现一个循环,在该循环中,首先生成一个随机数,并获取一个Message对象,然后将生成的随机数作为地鼠位置的索引值保存到Message对象中,再为该Message设置一个消息标识并发送消息,最后让线程休眠一段时间(该时间随机产生),具体代码如下:

  1. Thread t = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. int index = 0; //创建一个记录地鼠位置的索引值
  5. while (!Thread.currentThread().isInterrupted()) {
  6. index = new Random().nextInt(position.length); //产生一个随机数
  7. Message m = handler.obtainMessage(); //获取一个Message
  8. m.arg1 = index; //保存地鼠标位置的索引值
  9. m.what = 0x101; //设置消息标识
  10. handler.sendMessage(m); //发送消息
  11. try {
  12. Thread.sleep(new Random().nextInt(500) + 500); //休眠一段时间
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. });
  19. t.start(); //开启线程

(4)创建一个Handler对象,在重写的handleMessage()方法中,首先定义一个记录地鼠位置索引值的变量,然后使用if语句根据消息标识判断是否为指定的消息,如果是,则获取消息中保存的地鼠位置的索引值,并设置地鼠在指定位置显示,具体代码如下:

  1. handler = new Handler() {
  2. @Override
  3. public void handleMessage(Message msg) {
  4. int index = 0;
  5. if (msg.what == 0x101) {
  6. index = msg.arg1; //获取位置索引值
  7. mouse.setX(position[index][0]); //设置X轴位置
  8. mouse.setY(position[index][1]); //设置Y轴位置
  9. mouse.setVisibility(View.VISIBLE); //设置地鼠显示
  10. }
  11. super.handleMessage(msg);
  12. }
  13. };

(5)获取布局管理器中添加的ImageView组件,并为该组件添加触摸监听器,在重写的onTouch()方法中,首先设置地鼠不显示,然后将i的值加1,再通过消息提示框显示打到了几只地鼠,具体代码如下:

  1. mouse = (ImageView) findViewById(R.id.imageView1); //获取ImageView对象
  2. mouse.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. v.setVisibility(View.INVISIBLE); //设置地鼠不显示
  6. i++;
  7. Toast.makeText(MainActivity.this, "打到[ " + i + " ]只地鼠!",
  8. Toast.LENGTH_SHORT).show(); //显示消息提示框
  9. return false;
  10. }
  11. });

运行本实例,在屏幕上将随机显示地鼠,触摸地鼠后,该地鼠将不显示,同时在屏幕上通过消息提示框显示打到了几只地鼠,如图12.10所示。

402-1 图12.10 简易打地鼠游戏

12.4 小 结

本章主要介绍了在Android中如何实现多线程。由于在Android中,不能在子线程(也称为工作线程)中更新主线程(也称为UI线程)中的UI组件,因此Android引入了消息传递机制,通过使用Looper、Handler和Message就可以轻松实现多线程中更新UI界面的功能,这与Java中的多线程不同,希望读者能很好的理解,并能灵活应用。另外,多线程是游戏开发中非常重要的一项技术。

12.5 实践与练习

  1. 编写Android项目,实现在屏幕上来回移动的气球。(答案位置:光盘\TM\sl\12\12.8)

  2. 编写Android项目,实现颜色不断变化的文字。(答案位置:光盘\TM\sl\12\12.9)