Android经典应用程序开发
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 设备事件的响应

在应用程序的控制方面,在大多数场合下使用的是屏幕上的控件,控件本身也是包含了事件处理的载体。

在某些情况下也需要直接响应由输入设备发送过来的事件。在Android系统中,输入设备主要为键盘、触摸屏、轨迹球和鼠标,在Android中,键盘响应的是KeyEvent,后三者通常响应的是MotionEvent。

2.3.1 键盘事件的响应

键盘是Android中的主要输入设备,对按键事件的处理是在程序中使用键盘的核心内容。

键盘的事件通常用KeyEvent来表示,KeyEvent是android.view包中的一个类,主要包含以下一些方法:

        final int  getKeyCode()         // 获得按键码
        final int  getAction()          // 获得按键的动作
        final int  getFlags()           // 获得标志
        final int  getRepeatCount()     // 获得重复的信息
        final int  getScanCode()        // 获得扫描码

通过KeyEvent接口,可以获得按键相关的详细信息,这些信息都是用整数值来表示的。按键相关的信息中,其中最主要的是KeyCode,用于标识哪一个按键发生了事件,Action表示按键的动作(抬起、按下),RepeatCount表示同一个按键被按下的次数,ScanCode(扫描码)表示底层按键的原始标识。

android.view.KeyEvent.Callback是一个接口,用于表示发生按键事件后的回调,其中包含了如下几个方法:

        public abstract boolean onKeyDown(int keyCode, KeyEvent event)
        public abstract boolean onKeyUp(int keyCode, KeyEvent event)
        public abstract boolean onKeyMultiple(int keyCode, int count, KeyEvent event)
        public abstract boolean onKeyLongPress(int keyCode, KeyEvent event)

KeyEvent.Callback接口中的几个方法,均具有整型和KeyEvent类型两个参数,KeyEvent类型中实际上包含整型的信息。

以上几个方法的返回类型均为boolean类型,如果返回true,表示自己完成这个事件的处理,如果返回false,表示由下一个接收器处理这个事件。

View和Activity都已经实现KeyEvent.Callback接口。在这两个类的继承者中,可以通过重新实现以上的几个方法来响应按键的事件。

通常情况下,按键事件属于整个屏幕,因此在Activity中响应即可。按照一般的逻辑,当键盘上一个按键被按下的时候,不会去区分这个按键属于哪一个控件。

Activity中的dispatchKeyEvent()方法用于截获一个按键的事件:

        public boolean dispatchKeyEvent (KeyEvent event)

这个方法将在按键到达窗口之前被调用,因此通过这个方法可以阻止系统的其他部分不获得按键事件。

以下的示例需要实现的内容是通过键盘来控制一个图片的Alpha值,使用上键和右键增加图片的Alpha值,使用下键和左键减少图片的Alpha值。

这个按键事件响应程序的运行效果如图2-4所示。

图2-4 按键事件响应程序运行效果(左:初始化;中:中间Alpha值;右:Alpha值为0)

本例的布局文件testkeyevent.xml如下所示:

        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/screen" android:orientation="vertical"
            android:layout_width="fill_parent" android:layout_height="fill_parent" >
            <TextView android:id="@+id/alphavalue" android:layout_gravity="center"
              android:layout_width="wrap_content" android:layout_height="wrap_content" />
            <ImageView android:id="@+id/image"
              android:src="@drawable/robot" android:layout_gravity="center"
              android:layout_width="wrap_content" android:layout_height="wrap_content" />
        </LinearLayout>

从以上的布局文件可以看到,本例包含了一个文本框和一个显示图片的控件,这样文本框可用来显示当前Alpha的比例值,显示图片的控件ImageView用于显示一个图片。

本例的源代码的核心部分实现如下所示:

        public class TestKeyEvent extends Activity {
            private static final String TAG = "TestKeyEvent";
            private ImageView mImage;                           // 界面中的图片
            private TextView  mAlphavalueText;
            private int mAlphavalue;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.testkeyevent);
              mImage = (ImageView) findViewById(R.id.image);
              mAlphavalueText = (TextView) findViewById(R.id.alphavalue);
              mAlphavalue = 100;
              mImage.setAlpha(mAlphavalue);                      // 设置初始化的透明数值
              mAlphavalueText.setText("Alpha = " + mAlphavalue*100/0xff + "%");
            }
            @Override
            public boolean onKeyDown(int keyCode, KeyEvent msg){ // 按下事件的处理
              Log.v(TAG, "onKeyDown: keyCode =  "+ keyCode);    // 打印事件信息
              Log.v(TAG, "onKeyDown: String =  " + msg.toString());
              switch (keyCode) {
                  case KeyEvent.KEYCODE_DPAD_UP:              // 上键和右键的处理
                  case KeyEvent.KEYCODE_DPAD_RIGHT:
                      mAlphavalue += 20;
                      break;
                  case KeyEvent.KEYCODE_DPAD_DOWN:            // 下键和左键的处理
                  case KeyEvent.KEYCODE_DPAD_LEFT:
                      mAlphavalue -= 20;
                      break;
                  default:
                      break;
              }
              if(mAlphavalue>=0xFF)mAlphavalue = 0xFF;       // 控制Alpha数值的范围
              if(mAlphavalue<=0x0)mAlphavalue = 0x0;
              mImage.setAlpha(mAlphavalue);
              mAlphavalueText.setText("Alpha = " + mAlphavalue*100/0xff + "%");
              return super.onKeyDown(keyCode, msg);
            }
            @Override
            public boolean onKeyUp(int keyCode, KeyEvent msg){  // 抬起事件的处理
              Log.v(TAG, "onKeyUp: keyCode =  "+ keyCode);     // 打印事件信息
              Log.v(TAG, "onKeyUp: String =  " + msg.toString());
              return super.onKeyUp(keyCode, msg);
            }
        }

本例使用onKeyDown()方法来获得按键的事件,同类的方法还包括onKeyUp()方法,其参数int keyCode为按键码,KeyEvent msg表示按键事件的消息(其中包含了更详细的内容)。

通过keyCode(int)可以获得是哪一个按键响应,而通过msg(KeyEvent)除了按键码之外,可以获得更多的信息,例如按键的动作、重复信息、扫描码等内容。

本程序打出的一段Log信息如下所示:

        VERBOSE/TestKeyEvent(771): onKeyDown: keyCode =  20
        VERBOSE/TestKeyEvent(771): onKeyDown: String =  KeyEvent{action=0 code=20 repeat=7
    meta=0 scancode=108 mFlags=8}
        VERBOSE/TestKeyEvent(771): onKeyDown: keyCode =  20
        VERBOSE/TestKeyEvent(771): onKeyDown: String =  KeyEvent{action=0 code=20 repeat=8
    meta=0 scancode=108 mFlags=8}
        VERBOSE/TestKeyEvent(771): onKeyDown: keyCode =  20
        VERBOSE/TestKeyEvent(771): onKeyDown: String =  KeyEvent{action=1 code=20 repeat=0
    meta=0 scancode=108 mFlags=8}

由此可见,按键事件KeyEvent可以获得重复按键的次数和这个历史过程相关的数据信息,在一个按键的响应过程中,通常是先发送一个或若干个Down事件,最后是一个Up事件。

按键事件的响应有以下几个需要注意的细节:

(1)对于普通按键,通常响应onKeyUp()事件,表示按下的时候不再处理,按键抬起时才有效果;

(2)onKeyDown()可以连续响应一个按键按下的情况,并且通过KeyEvent的getRepeatCount()可以为重复按键增加特殊的处理。

(3)在按键处理方法的最后,调用super.onKeyDown()和super.onKeyUp()表示由父类进行默认的按键处理。

2.3.2 运动事件的处理

触摸屏(TouchScreen)和轨迹球(TrackBall)是Android中除键盘之外的主要输入设备。如果需要使用触摸屏和轨迹球,可以通过使用运动事件(MotionEvent)来接收它们的信息。

对于触摸屏,一般通过绝对坐标描述它的事件;对于轨迹球,一般通过相对坐标描述它的事件。在Android系统中,鼠标设备的处理方式类似于轨迹球。

MotionEvent是android.view包中的类,用于表示运动事件的坐标、动作等信息。MotionEvent中主要的几个方法如下所示:

        public final float  getX()                   // 获得X坐标
        public final float  getY()                   // 获得Y坐标
        public final int  getAction()                // 获得动作
        public final float getRawX()                 // 获得原始的X坐标
        public final float getRawY()                 // 获得原始的Y坐标

对于这种运动事件,最重要的属性为运动事件发生的坐标(X,Y),运动的动作表示按下、按住移动、抬起等信息。getRawX()和getRawY()表示没有根据窗口或者控件做出调整的原始坐标。

运动事件有其历史属性,MotionEvent中相关的方法如下所示:

        public final int  getHistorySize()           // 获得历史的点数
        public final float  getHistoricalX(int pos)  // 获得历史的X坐标
        public final float  getHistoricalY(int pos)  // 获得历史的Y坐标

在Android API级别5版本之后,MotionEvent类中还包含了多点触摸的相关内容,当有多个触点同时起作用的时候,可以获得触点的数目和每一个触点的坐标。

        public final int  getPointerCount()          // 获得触点的数目
        public final float  getX(int pointerIndex)   // 获得某个触点的X坐标
        public final float  getY(int pointerIndex)   // 获得某个触点的Y坐标

MotionEvent可以获得多点的基础信息,而没有包含手势解释的功能。

在Activity和View中,都具有以下两个方法,用于接收运动事件:

        public boolean onTouchEvent(MotionEvent event)
        public boolean onTrackballEvent(MotionEvent event)

在以上两个方法中,MotionEvent类作为参数传入,在这个参数中可以获得运动事件的各种信息。对于触摸屏设备的事件,通常使用onTouchEvent()获取信息,对于轨迹球、鼠标的事件,使用onTrackballEvent()获取信息。

Activity中的以下两个方法用于阶段触摸屏和轨迹球的事件:

        public boolean dispatchTouchEvent (MotionEvent ev)
        public boolean dispatchTrackballEvent (MotionEvent ev)

通过dispatchTouchEvent()和dispatchTrackballEvent()两个方法,可以使得运动事件不再向窗口中传递。

1.在活动中响应运动事件

运动事件可以属于整个窗口,在活动中响应运动事件表示的就是对全窗口的运动事件进行处理。其方法为在Activity中重新实现的onTouchEvent()和onTrackballEvent()方法,接收触摸屏和轨迹球等事件。

下面是处理简单的运动事件的处理程序,这个程序在UI的界面中,显示当前的MotionEvent的动作和位置。触摸事件程序的运行效果如图2-5所示。

图2-5 触摸事件程序运行效果

Action=0

为ACTION_DOWN,按下动作

Action=1

为ACTION_UP,抬起动作

Action=2

为ACTION_MOVE,移动动作

布局文件testmotionevent.xml中包含了两个简单的TextView,分别用于显示运动事件的坐标和动作。本例程序的Java代码如下所示:

        import android.app.Activity;
        import android.content.Context;
        import android.os.Bundle;
        import android.view.MotionEvent;
        import android.widget.TextView;
        public class TestMotionEvent extends Activity {
            private static final String TAG = "TestMotionEvent";
            TextView mAction;
            TextView mPostion;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.testmotionevent);
              mAction  = (TextView)findViewById(R.id.action);
              mPostion = (TextView)findViewById(R.id.postion);
            }
            @Override
            public boolean onTouchEvent(MotionEvent event) {      // 触摸屏事件的处理
              int Action = event.getAction();
              float X = event.getX();
              float Y = event.getY();
              mAction.setText("Action = "+ Action);              // 显示触摸的动作
              mPostion.setText("Postion = ("+X+","+Y+")");       // 显示触摸的位置
              return true;
            }
            @Override
            public boolean onTrackballEvent(MotionEvent event) {  // 轨迹球事件的处理
              int Action = event.getAction();
              float X = event.getX();
              float Y = event.getY();
              mAction.setText("Trackball Action = "+ Action);
              mPostion.setText("Trackball Postion = ("+X+","+Y+")");
              return true;
            }}

对于运行事件中的信息,轨迹球提供的是相对坐标,信息比较单一。对于触摸屏,有以下几个注意点:

(1)Activity的onTouchEvent()中获得的MotionEvent,是相对硬件屏幕的坐标,因此getX()和getRawX()及getY()和getRawY()的含义一般是相同的。

(2)Activity是包含标题栏的,因此对于这种响应方式,触摸标题栏也可以触发Activity的触摸事件。

(3)状态栏不属于一个Activity,由于状态栏的存在,在一个Activity中获得的最小y值,可能不是0。

2.控件响应运动事件

除了响应整个活动的运动事件,也有专门属于某一个区域的运动事件的情况。在这种情况下运动事件属于一个控件。为了实现这种情况,就需要在Vi e w类中进行处理。

其中一种方式是实现View.OnTouchListener()接口,然后将其设置给某个控件,表示由其中的onTouch()方法处理这个控件内的触摸事件。

以下程序接收一个控件内部的触摸事件,并将结果显示到文本框和标题栏当中。程序运行效果如图2-6所示。

图2-6 控件内的触摸屏运行效果

Action=0

为ACTION_DOWN,按下动作

Action=1

为ACTION_UP,抬起动作

Action=2

为ACTION_MOVE,移动动作

本例在布局文件中留出边缘,白色的区域为一个控件,它并未充满整个屏幕,当其发生触摸事件的时候,程序界面中的文本框显示的是当前触摸事件的坐标;而标题栏中显示的是触摸时间的原始坐标。从中可以看出坐标的原点是控件的左上角,原始坐标的原点是屏幕的左上角。

上述程序实现的核心内容如下所示:

        public class TestMotionEvent1 extends Activity implements View.OnTouchListener{
            private static final String TAG = "TestMotionEvent1";
            private TextView   mAction;
            private TextView   mPosition;
            private View      mView;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.testmotionevent1);
              mAction   = (TextView) findViewById(R.id.action);
              mPosition = (TextView) findViewById(R.id.position);
              mView = findViewById(R.id.touchview);
              mView.setOnTouchListener(this);      // 设置监听器为当前的Activity
            }
            public boolean onTouch(View v, MotionEvent event) { // OnTouchListener的方法
              int action = event.getAction();
              float x = event.getX();               // 获得坐标
              float y = event.getY();
              float rawx = event.getRawX();         // 获得原始坐标
              float rawy = event.getRawY();
              Log.v(TAG, "Action = "+ action );    // 获得原始坐标
              Log.v(TAG, "("+x+","+y+")");
              Log.v(TAG, "("+rawx+","+rawy+")");
              setTitle("A = " + action +  " Raw ["+ rawx +","+ rawy +"]"); // 显示原始坐标
              mAction.setText("Action = "+ action);
              mPosition.setText( "Position = ("+x+","+y+")");              // 显示坐标
              return true;
            }
        }

控件仅仅对其区域内的触摸事件做出响应,因此在上述程序中,只有白色区域的部分能做出响应。一般情况下,对于一个控件,使用getX()和getY()得到相对其左上角的坐标的情况比较多,getRawX()和getRawY()没有必要使用。

控件响应事件的另外一种方式,就是让控件自己去实现onTouchEvent()和onTrackballEvent()两个方法。

以下示例程序用标题栏不同颜色的点表示运动事件的类型,程序的结果如图2-7所示。

图2-7 控件自己实现并响应触摸事件的运行结果(按下、移动、抬起)

当触摸屏按下、移动、抬起的时候,在坐标处绘制不同颜色的点,在标题栏中显示当时的动作和坐标。

这里使用的程序的核心内容如下所示:

        public class TestMotionEvent2 extends Activity {
            private static final String TAG = "TestMotionEvent2";
            @Override
            protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(new TestMotionView(this)); // 设置View为Activity的内容
            }
            public class TestMotionView extends View {    // 继承实现一个新的View
              private Paint   mPaint = new Paint();
              private int    mAction;
              private float  mX;
              private float  mY;
              public TestMotionView(Context c) {
                  super(c);
                  mAction = MotionEvent.ACTION_UP;       // 初始化一些数值
                  mX = 0;
                  mY = 0;
              }
              @Override
              protected void onDraw(Canvas canvas) {
                  Paint paint = mPaint;
                  canvas.drawColor(Color.WHITE);
                  if(MotionEvent.ACTION_MOVE == mAction) {        // 移动动作
                      paint.setColor(Color.RED);
                  }else if(MotionEvent.ACTION_UP == mAction) {    // 抬起动作
                      paint.setColor(Color.GREEN);
                  }else if(MotionEvent.ACTION_DOWN == mAction) {  // 按下动作
                      paint.setColor(Color.BLUE);
                  }
                  canvas.drawCircle(mX, mY,10, paint);           // 绘制一个点
                  setTitle("A = " + mAction +  " ["+ mX +","+ mY +"]");  // 标题栏显示
              }
              @Override
              public boolean onTouchEvent(MotionEvent event) {
                  mAction = event.getAction();           // 获得动作
                  mX = event.getX();                      // 获得坐标
                  mY = event.getY();
                  Log.v(TAG, "Action = "+ mAction );
                  Log.v(TAG, "("+mX+","+mY+")");
                  invalidate();                           // 重新绘制
                  return true;
              }
            }
        }

本程序使用了“自定义控件”的方法,重新实现控件内部onTouchEvent()方法来接收触摸事件,接收到它,并且记录发生事件的坐标(x,y)和动作(action)。调用invalidate()重新进行绘制。绘制在onDraw()中完成,根据不同的事件,绘制不同颜色的点,并设置标题栏。

在这个例子中,虽然自定义的Vi e w充满了窗口,但是依然不包括标题栏的部分。标题栏不在控件范围内,不会产生这个控件的触摸事件。