Android开发权威指南(第二版)
上QQ阅读APP看书,第一时间看更新

5.3 窗口的常用事件

源代码目录:src/ch05/ActivityEvent

在上一节的Activity生命周期中已经介绍过7个生命周期方法,这些方法也是窗口的事件方法。除此之外,还有很多其他的窗口事件会经常使用到,例如,键盘事件、触摸事件等。本节将详细介绍这些窗口事件的使用方法。

本节的例子代码都在ActivityEvent工程中,读者可查看该工程获取完整的源代码。在测试本例时要注意,由于将本节所涉及的所有事件的测试代码都加到了该程序中,所以在运行程序时会在触发某个事件时也可能会将触发其他事件,输出的日志信息会混在一起,读者在运行程序时要注意这一点。

5.3.1 设置窗口标题事件(onTitleChanged)

窗口的标题可以在声明窗口时通过<activity>标签的android:label属性指定,也可以在代码中通过setTitle方法动态设置。当窗口标题被设置后,会调用onTitleChanged方法。该方法的原型如下:

protected void onTitleChanged(CharSequence title, int color);

其中title参数表示改变后的窗口标题,可通过setTitle方法设置,color参数表示改变后的窗口标题颜色,可以通过setTitleColor方法设置。无论是调用setTitle方法还是调用setTitleColor方法都会调用onTitleChanged来捕获标题的变化。下面的代码同时在onCreate和按钮单击事件方法中调用了setTitle方法设置窗口标题,并在按钮单击事件方法中调用setTitleColor方法设置窗口标题颜色。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_activity_event_main);

    setTitle("窗口标题");

  }

  // “设置窗口标题”按钮的单击事件方法

  public void onClick_SetTitle(View view)

  {

    setTitle("新窗口标题");

    setTitleColor(12345);

  }

  // 在窗口完全开始后调用该方法

  @Override

  protected void onPostCreate(Bundle savedInstanceState)

  {

    Log.d("ActivityEventMain", "onPostCreate");

    super.onPostCreate(savedInstanceState);

  }

  // 调用setTitle和setTitleColor方法会触发标题改变事件,也就是会调用onTitleChanged方法

  @Override

  protected void onTitleChanged(CharSequence title, int color)

  {

    super.onTitleChanged(title, color);

    Log.d("ActivityEventMain", "onTitleChanged_title:" + title);

    Log.d("ActivityEventMain", "onTitleChanged_color:" + color);

  }

}

运行程序,并单击如图5-19所示的“设置窗口标题”按钮,会在LogCat视图中看到如图5-20所示的输出信息。

图5-20所示的输出信息可分为两部分(以gralloc_goldfish作为分界线),前一部分是在onCreate方法中设置标题时输出的,后一部分是单击“设置窗口标题”按钮时输出的。对于后面一部分并不感到奇怪,因为在onClick_SetTitle方法中同时调用了setTitle和setTitleColor方法,而调用这两个方法中的任何一个都会触发一次标题变化事件,所以onTitleChanged方法实际上被调用了两次,所以会输出两组日志信息。第一组和第二组的最新标题都是“新窗口标题”,但调用setTitle方法时并未设置标题文本颜色,所以第一组的标题文本颜色值为0,而后面调用了setTitleColor方法将颜色值设为12345,所以输出的颜色值也是12345。

 

▲图5-19 ActivityEvent程序主界面

 

▲图5-20 onTitleChanged事件输出信息

不过看到第一组的日志信息可能有的读者会感到奇怪,为什么会输出两组值呢?明明只调用了setTitle方法。按着前面的结论应该只调用一次onTitleChanged方法才对。

实际上,前一部分的第一组确实是由于调用了setTitle方法而输出的日志信息,但第二组却是由于系统调用了onPostCreate方法而输出的信息。这里涉及了一个onPostCreate方法,该方法会在窗口完全显示后调用,也就是说该方法会在onCreate和onRestoreInstanceStateonRestoreInstanceState方法用于恢复在onSaveInstanceState方法中保存的状态,这两个方法会在7.2.2小节详细介绍。方法之后调用。下面看一下Activity.onPostCreate方法的源代码如果读者已经下载了Android源代码,可以在Eclipse中按住Ctrl键(Mac OS X是Command键),然后用鼠标单击super.onPostCreate就可以跟踪到onPostCreate方法的源代码。

protected void onPostCreate(Bundle savedInstanceState)

{

  if (!isChild())

  {

    mTitleReady = true;

    onTitleChanged(getTitle(), getTitleColor());

  }

  mCalled = true;

}

在onPostCreate方法中除了设置一些状态变量外,还做了一件重要的工作:调用onTitleChanged方法,也就是说在默认情况下onPostCreate方法也调用了onTitleChanged方法。所以在窗口启动的过程中会调用两次onTitleChanged方法,这也是为什么会在前一部分输出两组日志信息的原因。

注意

经笔者测试,setTitle可以成功修改窗口标题,但setTitleColor虽然可以成功调用,但无法成功修改窗口标题文本颜色,这可能是Android的一个bug。不过还有更多、更灵活的方法定制窗口标题,当然,不仅仅是修改标题文本和标题文本颜色这么简单,甚至可以重新设计整个标题布局。在本章后面的部分会介绍使用多种方法定制窗口标题。

5.3.2 键盘按下和抬起事件(onKeyDown和onKeyUp)

目前尽管Android手机的屏幕越来越大,物理按键越来越少,但大多数手机还是有一些物理按键的,例如,“Menu”键、“Back”键等。通常按键的动作分为单击、按下和抬起,但实际上单击并不能算单独的按键事件,因为系统底层(Linux内核)在按键按下时会发出一个中断,在按键抬起时也会发送一个中断,并没有在按键单击时发中断。其实按键单击就是按键按下和抬起的组合动作,所以窗口事件中并不能捕获按键单击事件,而只能捕获按键按下和抬起事件,分别用onKeyDown和onKeyUp方法捕获。这两个方法的原型如下:

public boolean onKeyDown(int keyCode, KeyEvent event)

public boolean onKeyUp(int keyCode, KeyEvent event)

这两个方法的参数和类型完全一样,其中keyCode表示按下或抬起的按键代码,通过event参数则可以获得更多的按键信息。其中有一个KeyEvent.getKeyCode方法与keyCode参数的含义完全一样。如果这两个方法返回true,表示按键事件已经处理完毕处理键盘事件的方法不止一个,这些方法是以一个链表形式组织在一起的,当前方法执行完,会执行下一个方法。如果其中某一个方法返回true,系统就不会再调用后面的方法处理按键事件了。,不会再调用其他回调方法处理按键事件了。如果还想将处理动作继续传递下去,该方法应返回false。

下面的代码捕获的按键按下和抬起事件,并在按下“Menu”键时输出一条日志信息,而且在按下“Menu”按键一定时间(大约8秒)之后会显示一个Toast信息提示框。在按键抬起事件中也会输出一条日志信息。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  // 捕捉按键按下事件

  @Override

  public boolean onKeyDown(int keyCode, KeyEvent event)

  {

    // 输出按键代码

    Log.d("onKeyDown:KeyCode", String.valueOf(keyCode));

    // 输出按键的重复次数

    Log.d("onKeyDown:RepeatCount", String.valueOf(event.getRepeatCount()));

    // 如果重复次数到了200(8秒左右),显示提示信息

    if (event.getRepeatCount() == 200)

    {

      Toast.makeText(this, "已经按一会了,累了吧,该松开了!", Toast.LENGTH_LONG).show();

    }

    // 如果按下的是“菜单”按钮,输出日志信息

    if (keyCode == KeyEvent.KEYCODE_MENU)

    {

      Log.d("onKeyDown", "MenuKey Down");

    }

    return super.onKeyDown(keyCode, event);

  }

  // 捕获按键抬起事件

  @Override

  public boolean onKeyUp(int keyCode, KeyEvent event)

  {

    if (keyCode == KeyEvent.KEYCODE_MENU)

    {

      Log.d("onKeyUp", "MenuKey Up");

    }

    return super.onKeyUp(keyCode, event);

  }

}

现在运行程序,然后按下手机或Android模拟器上的“Menu”按钮,会看到在LogCat视图中输出相应的日志信息(键码、按键重复数等信息)。当长按“Menu”按钮(大约8秒)后,在屏幕下方会显示如图5-21所示的提示信息。

 

▲图5-21 长按“Menu”按钮后显示的提示信息

这时松开“Menu”按钮,系统会调用onKeyUp方法处理按键抬起事件,会看到LogCat视图中输出如图5-22所示的信息。

使用onKeyDown和onKeyUp方法捕捉按键事件应了解如下几点。

onKeyDown和onKeyUp方法不能捕捉“Home”按键。

按键重复次数是指按下按键还没有抬起的期间系统会以一定时间间隔(很短,通常以毫秒为单位,例如,30毫秒)不断发送键盘按下中断,也就是说会不断调用onKeyDown方法,使用KeyEvent.getRepeatCount方法可以获取调用onKeyDown方法的次数,也就是按键重复次数,RepeatCount从0开始。如果按键抬起,并再次按下,RepeatCount会清零。利用这个特性可以处理当按键按下一段时间才处理的动作。

 

▲图5-22 输出的按键相关信息

所有的物理按键的按下和抬起动作都由onKeyDown和onKeyUp方法捕获,如果想判断具体按下了哪个按键,可以使用keyCode参数,也可以使用KeyEvent.getKeyCode方法。可以直接通过按键编码进行判断,例如,“Menu”按键的编码是82。但最好直接使用在KeyEvent类中定义的表示按键编码的常量,例如,表示“Menu”按钮编码的常量是KeyEvent.KEYCODE_MENU,所有表示按键编码的常量都以“KEYCODE”开头。

扩展学习:如何捕捉Home键的动作

onKeyDown和onKeyUp方法无法来捕获Home键的动作,那么如何来捕获Home键呢?

大家都知道,按Home键程序并没有退出(窗口的生命周期只执行到onStop方法),只是切换到后台运行了。而按Back键或调用finish方法则关闭当前程序,当前窗口的生命周期也就结束了。系统会在调用onDestroy方法后释放窗口对象(但一些资源并未释放,如静态变量),按Home和Back键都执行的最后一个方法是onStop,所以很容易想到在onStop方法中处理按Home键的动作。

处理按Home键的动作的位置虽然找到了,但还有一个问题,我们如何判断用户是通过按Back键还是按Home键关闭窗口呢?当然,很多读者可能会想到在onKeyDown方法中设置一个标志不就可以解决问题了吗!因为onKeyDown方法可以捕获Back按键动作。但这样做还会有一个问题没解决,就是不仅按Back键会关闭窗口,执行finish方法同样也会关闭窗口。除非程序限制只能通过Back键关闭窗口,否则还得添加更多的代码来处理这个问题。

解决问题的方法没有最好,只有更好,幸好这里有一个更好的解决方案。由于窗口不管以何种方式关闭(按Back键、执行finish方法等),都会调用Activity.finish方法,而且该方法是在调用窗口生命周期方法之前就调用了,也就是说调用onPause方法之前会首先调用finish方法。而按Home键系统并不会调用finish方法,所以覆盖finish方法,并在finish方法中设置标志即可解决这个问题。

下面的代码完美地模拟了捕获Home键动作的情景。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  // true:窗口退出  false:窗口切换到后台执行,并没有退出

  private boolean isFinished;

  // 当窗口重新开始时执行该方法,该方法可以处理重新切换回当前程序后要完成的工作

  @Override

  protected void onStart()

  {

    Toast.makeText(this, "窗口已经恢复!", Toast.LENGTH_LONG).show();

    super.onStart();

  }

  // 当窗口移到后台执行或退出时执行该方法

  @Override

  protected void onStop()

  {

    // 如果isFinished=false,表示当前窗口只是切换到后台运行,并没有退出

    if (!isFinished)

    {

      Toast.makeText(this, "窗口已经在后台运行(按了Home键)!", Toast.LENGTH_LONG)

          .show();

    }

    super.onStop();

  }

  // 只要窗口退出,就一定会调用finish方法

  @Override

  public void finish()

  {

    // 设置标志

    isFinished = true;

    super.finish();

  }

}

读者可以运行程序,当按Home键时,会显示Toast信息框,按Back键时什么也不会发生。

总结:按Home键就相当于将当前程序切换到后台运行,而按Back键相当于退出程序,因此该问题也就转换成判断程序是否切换到后台运行的问题。如果读者理解了第7章介绍的任务(Task)和回退栈(Back Stack)就会对这个解决方案有更深入的理解。

5.3.3 任意情况下捕捉键盘事件(dispatchKeyEvent)

有些时候使用onKeyDown和onKeyUp仍然无法捕捉按键事件,例如,使用TabActivity时就无法使用这两个方法捕捉到窗口的按键事件,但可以通过dispatchKeyEvent方法在任何情况下捕捉到按键事件(仍然无法捕捉到Home按键的动作)。dispatchKeyEvent方法的原型如下:

public boolean dispatchKeyEvent(KeyEvent event)

其中event参数的含义与onKeyDown及onKeyUp方法的event参数的含义相同。该方法返回true,表示已经处理了按键事件,其他的处理方法不再调用,如果还想让其他按键事件方法进行处理,该方法要返回false。由于按键分为按下和抬起两个动作,而只有一个dispatchKeyEvent方法,所以该方法会被调用两次(按下调用一次,抬起调用一次),因此要在该方法中通过如下代码判断是按下的动作,还是抬起的动作。

下面的代码演示了displatchKeyEvent的使用方法。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  @Override

  public boolean dispatchKeyEvent(KeyEvent event)

  {

    // 按下动作

    if (event.getAction() == KeyEvent.ACTION_DOWN)

    {

      Log.d("displatchKeyEvent", "down");

    }

    // 抬起动作

    else if(event.getAction() == KeyEvent.ACTION_UP)

    {

      Log.d("displatchKeyEvent", "up");

    }

    return super.dispatchKeyEvent(event);

  }

}

5.3.4 回退事件(onBackPressed)

除了可以使用onKeyDown、onKeyUp和dispatchKeyEvent方法捕捉Back键的动作外,还可以使用onBackPressed方法单独捕捉Back键的动作。该方法没有参数,也没有返回值,因为该方法是调用链上最后一个调用的,而且只捕获Back按键的动作,所以并不需要参数和返回值。

尽管按键动作分为按下和抬起,但onBackPressed方法始终只调用一次,这是因为onBackPressed方法在Android 2.0.1(API Level = 5)及以后的版本在onKeyUp方法中调用onBackPressed方法,而在Android 2.0.1以前的版本在onKeyDown方法中调用onBackPressed方法。这一点从onKeyDown和onKeyUp方法的实现代码就可以看出,了解了onBackPressed方法的调用原理可以打消读者对onBackPressed方法是否调用两次的顾虑(只可能调用一次)。

下面是onKeyDown和onKeyUp方法中与onBackPressed方法相关的代码。从这些方法可以看出,无论是onKeyDown,还是onKeyUp,开始都判断了targetSdkVersion,也就是当前Android程序使用的Android SDK版本。如果API Level >= 5。在onKeyDown中就执行event.startTracking方法,然后在onKeyUp方法中可以通过event.isTracking判断是否调用了event.startTracking方法,最后在onKeyUp方法中调用onBackPressed方法,否则直接在onKeyDown方法中调用onBackPressed方法。从这些代码可以看出,无论使用哪个版本的Android SDK,都只会在一处调用onBackPressed方法。

public boolean onKeyDown(int keyCode, KeyEvent event) 

{

  if (keyCode == KeyEvent.KEYCODE_BACK)

  {

    // API Level >= 5

    if (getApplicationInfo().targetSdkVersion>= Build.VERSION_CODES.ECLAIR)

    {

       event.startTracking();

    }

    else

    {

       onBackPressed();

    }

    return true;

  }

  ……

}

public boolean onKeyUp(int keyCode, KeyEvent event)

{

  if (getApplicationInfo().targetSdkVersion>= Build.VERSION_CODES.ECLAIR)

  {

    if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()

       && !event.isCanceled())

  {

      onBackPressed();

      return true;

    }

  }

  return false;

}

下面的代码演示了如何使用onBackPressed方法捕捉Back按键事件。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  @Override

  public void onBackPressed()

  {

    Log.d("onBackPressed", "OK");

    super.onBackPressed();

  }

}

5.3.5 按键长按事件(onKeyLongPress)

从Android 2.x开始支持使用onKeyLongPress方法捕捉按键长按事件在onKeyDown方法中通过event.getRepeatCount方法获取按键重复次数的方式也可以实现捕获按键长按动作的目的,但这需要不断调用onKeyDown方法并判断按键是否达到了某一个重复次数,比较麻烦。因此如果要捕获按键长按动作,最好使用onKeyLongPress方法。。但只是简单地实现onKeyLongPress方法并不会被调用,还必须在onKeyDown方法中调用KeyEvent.startTracking方法,并且onKeyDown方法返回true才可能捕捉到按键长按事件。其中startTracking方法用于跟踪按键动作,例如按下的时间等。在onKeyUp方法中还可以使用KeyEvent.isTracking方法判断在onKeyDown方法中是否调用了startTracking方法进行跟踪。关于startTracking和isTracking方法的用法见上一节给出的onKeyDown和onKeyUp源代码。

使用startTracking方法还应注意的是该方法同时只能跟踪一个按键,如果在跟踪的过程中按下了别的按键,当前跟踪过程将停止,转而跟踪新按下的按键。

下面的代码演示了如何使用onKeyLongPress捕获按键长按事件。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{ 

  @Override

  public boolean onKeyDown(int keyCode, KeyEvent event)

  {

    ……

    // 必须调用startTracking方法

    event.startTracking();

    // 必须返回true

    return true;

  }

  // 捕捉按键长按事件

  @Override

  public boolean onKeyLongPress(int keyCode, KeyEvent event)

  {

    Log.d("onKeyLongPress", "onKeyLongPress_KeyCode:" + keyCode);

    return super.onKeyLongPress(keyCode, event);

  }

}

运行程序后,长按某个按键,会在LogCat视图中输出相应的日志信息。如果读者认为各种日志信息太多,可以在上方的过滤文本输入框中输入“onkeylong”(不区分大小写),就可以只显示本例中由onKeyLongPress方法输出的日志信息,如图5-23所示。

5.3.6 屏幕触摸事件(onTouchEvent)

可以使用onTouchEvent方法捕捉屏幕的触摸事件,该方法的原型如下:

public boolean onTouchEvent(MotionEvent event)

其中event参数可以获取与触摸相关的信息,例如,触摸点的屏幕坐标,当前的触摸状态是按下,还是抬起,如果Android支持鼠标,还可以获取按下的是鼠标的哪几个键。如果onTouchEvent方法返回true,表示该方法已经处理完了屏幕触摸工作,不会再将屏幕触摸动作传递给下一个可以接收到该事件的方法。如果该方法返回false,则会允许调用下一个可以处理屏幕触摸动作的方法。

 

▲图5-23 长按不同按键的日志信息

下面的代码在onTouchEvent方法中输出了当前触摸屏幕的状态以及触摸点的X、Y坐标值。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{ 

  @Override

  public boolean onTouchEvent(MotionEvent event)

  {

    // 按下状态

    if(event.getAction() == MotionEvent.ACTION_DOWN)

    {

      Log.d("onTouchEvent", "onTouchEvent_Action:Down");

    }

    // 抬起状态

    else if(event.getAction() == MotionEvent.ACTION_UP)

    {

      Log.d("onTouchEvent", "onTouchEvent_Action:Up");

    }

    // 输出触摸点的X坐标值

    Log.d("onTouchEvent", "onTouchEvent_X:" + event.getX());

    // 输出触摸点的Y坐标值

    Log.d("onTouchEvent", "onTouchEvent_Y:" + event.getY());

     return super.onTouchEvent(event);

  } 

}

 

注意

Android系统使用的坐标系的0点坐标在屏幕的左上角,坐标值是(0,0)。屏幕从左向右为x轴的正方向,屏幕从上到下为y轴的正方向。MotionEvent.getY方法返回的Y坐标值包括标题栏的高度。

5.3.7 窗口获得焦点事件(onWindowFocusChanged)

除了可以使用窗口生命周期的onResume和onPause方法处理窗口获得和失去焦点的动作,还可以使用onWindowFocusChanged方法来处理窗口获得和失去焦点的动作。onWindowFocusChanged方法的原型如下:

public void onWindowFocusChanged(boolean hasFocus)

其中hasFocus参数表示窗口是否获得了焦点,如果该参数值为true,表示窗口已经获得了焦点;如果为false,表示窗口失去了焦点。onWindowFocusChanged方法与窗口生命周期方法没有任何关系,如果窗口的焦点发生变化,该方法会在onResume或onPause方法后面调用。

下面的代码同时使用onWindowFocusChanged、onResume和onPause方法处理窗口的焦点变化动作,读者可以观察这3个方法的调用顺序。

源代码文件:src/ch05/ActivityEvent/src/mobile/android/activity/event/ActivityEventMain.java

public class ActivityEventMain extends Activity

{  

  // 窗口获得或失去焦点时调用

  @Override

  public void onWindowFocusChanged(boolean hasFocus)

  {

    Log.d("onWindowFocusChanged",

        hasFocus ? "onWindowFocusChanged:has Focus"

            : "onWindowFocusChanged:hasn't focus");

    super.onWindowFocusChanged(hasFocus);

  }

  // 窗口获得焦点时调用该方法

  @Override

  protected void onResume()

  {

    Log.d("onResume", "onResume:has Focus");

    super.onResume();

  }

  // 窗口失去焦点时调用该方法

  @Override

  protected void onPause()

  {

    Log.d("onPause", "onResume:hasn't Focus");

    super.onPause();

  }

}

现在运行程序,然后按Home键使当前窗口失去焦点,最后再恢复窗口的焦点,就会看到图5-24黑框中所示的日志信息。

 

▲图5-24 窗口焦点变化后输出的日志信息