触摸屏给用户带来了全新的体检。几年前,它还是高端手机的专利,现在,触摸屏、多点触摸已成为手机必须支持的内容。手机对触摸屏的支持极大地方便了用户操作,多点触摸、手势的引入成就了很多经典应用,如滑动页面、浏览器、图片放大等。Android、iOS、Windows Phone对触摸屏的支持非常好,本章将介绍如何设计触摸屏程序、如何支持手势等。 单点是指每次一个指头触控屏幕;而两个以上的指头同时触屏称为多点触摸。一系列的触摸点形成的路径称为手势。 4.1 Android触摸屏 输入是用户交互的一个重要组成部分,触摸屏和键盘是Android系统的标配输入设备,多数游戏、应用都离不开这两个输入设备。本节给出一个小程序,演示基本的触摸屏和按键处理,虽然不能覆盖全部的情况,但至少可以起到抛砖引玉的作用。 4.1.1 Android输入处理 在Android系统中,输入事件一般是由View类来处理的,通过实现一些事件监听接口,就能够处理这些事件,并且决定是否拦截下来。 1)事件监听接口及设置监听 Android系统提供了几个事件监听接口,见表4-1。 表4-1 Android系统事件监听接口 事件监听接口 方 法 功 能 View.OnClickListener onClick(View) 监听在View上面的屏幕单击事件 View.OnLongClickListener onLongClick(View) 监听在View上面的屏幕长按事件 View.OnKeyListener onKey(View) 监听在View上面的按键事件 View.OnTouchListener onTouch(View,MotionEvent) 监听在View上面的屏幕长按事件 View.OnKeyListener只有在View被选中的情况下才会监听到事件,所以这个按键处理的方法不是很常用。 如果要监听某个View窗口中的输入事件,只要实现对应的接口,并且在View中设置监听即可。比如: public class TouchDemo extends Activity implements OnClickListener { //..省略部分代码 mTouchView.setOnClickListener(this); //..省略部分代码 @Override public void onClick(View arg0) { if(arg0.getId() == R.id.tv1) { mLogView.append("onClick eventn"); mLogView.scrollBy(0, 10); } } } } 上面的代码实现了一个简单的监听接口(interface),并将其添加到View窗口的监听队列中去。 除了监听接口以外,View类里面也有默认的处理方法,在实现View类的时候进行重载就可以了: onKeyDown(int, KeyEvent)——处理按键按下事件; onKeyUp(int, KeyEvent)——处理按键弹起事件; onTrackballEvent(MotionEvent)——处理轨迹球运动事件; onTouchEvent(MotionEvent)——处理触摸事件。 2)按键事件处理 上一节已介绍过可以在View中处理按键事件,只要实现了OnKeyEventListener接口即可,同时也可以使用View类的onKeyDown()或者onKeyUp()。但是我们往往希望按键事件是在整个程序屏幕上都可以响应的,所以这个时候在Activity中进行响应会更加方便。 Activity提供了“boolean onKeyDown(int keyCode,KeyEvent event)”方法,可以监听到按键,而KeyEvent类提供了按键编码的常量定义,例如KeyEvent.KEYCODE_MENU表示菜单按键。 3)输入事件拦截器 在了解了触屏和按键操作的处理以后,我们来完成一个输入事件拦截器的小应用,如图4-1所示。 图4-1 程序实现了对click事件longclick、touch轨迹的拦截,从图4-1中可以看出,实现了所有4种拦截全部打开,而触摸的轨迹在屏幕上面也形成了“test”字样。也就是说,这个小程序可以实现单击、长按、触摸事件的拦截,而要实现按键、多点触摸、方向的拦截只需在这个小程序的基础上进行一定的扩充即可。 下面来看具体的程序实现: (1)布局文件main.xml的片段 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android" Android:id="@+id/mainview" Android:orientation="vertical" Android:layout_width="fill_parent" Android:layout_height="fill_parent" > <LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android" Android:orientation="horizontal" Android:layout_width="fill_parent" Android:layout_height="200px" > <TextView Android:id="@+id/tv0" Android:layout_width="200px" Android:layout_height="200px" Android:text="event logsn" /> <LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android" Android:orientation="vertical" Android:layout_width="fill_parent" Android:layout_height="fill_parent" > <CheckBox Android:id="@+id/box_click" Android:text="拦截click" Android:layout_width="fill_parent" Android:layout_height="wrap_content" /> <CheckBox Android:id="@+id/box_longclick" Android:text="拦截longclick" Android:layout_width="fill_parent" Android:layout_height="wrap_content" /> <CheckBox Android:id="@+id/box_touch" Android:text="拦截touch" Android:layout_width="fill_parent" Android:layout_height="wrap_content" /> <CheckBox Android:id="@+id/box_touch_track" Android:text="touch轨迹" Android:layout_width="fill_parent" Android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout> <devdiv.example.TouchDemo.SimpleView Android:id="@+id/tv1" Android:layout_width="320px" Android:layout_height="200px" Android:text="@string/hello" /> </LinearLayout> (2)主类(实现了所有的事件监听器) public class TouchDemo extends Activity implements OnClickListener, OnLongClickListener, OnTouchListener ,OnCheckedChangeListener{ private TextView mLogView; private SimpleView mTouchView; private CheckBox mBoxClick; private CheckBox mBoxLongClick; private CheckBox mBoxTouch; private CheckBox mBoxTouchTrack; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mLogView = (TextView)findViewById(R.id.tv0); mTouchView = (SimpleView)findViewById(R.id.tv1); mTouchView.setBackgroundColor(Color.CYAN); mTouchView.setOnClickListener(this); mTouchView.setOnLongClickListener(this); mTouchView.setOnTouchListener(this); mLogView.setTextSize((float) 10.0); mBoxClick = (CheckBox)findViewById(R.id.box_click); mBoxLongClick = (CheckBox)findViewById(R.id.box_longclick); mBoxTouch = (CheckBox)findViewById(R.id.box_touch); mBoxTouchTrack = (CheckBox)findViewById(R.id.box_touch_track); mBoxTouchTrack.setOnCheckedChangeListener(this); } @Override public void onClick(View arg0) { // 单击 if(mBoxClick.isChecked()) { // 确定用户是否选择了监听单击事件 if(arg0.getId() == R.id.tv1) { mLogView.append("onClick eventn"); // 记录到日志窗口 mLogView.scrollBy(0, 10); // 滚动日志 } } } @Override public boolean onLongClick(View arg0) { // 长按 if(mBoxLongClick.isChecked()) { if(arg0.getId() == R.id.tv1) { mLogView.append("onLongClick eventn"); } // 下面进行随机返回处理,以便于观察事件处理之间的关系,请参考后面的小结 if(System.currentTimeMillis() % 2 == 0) { mLogView.append("onLongClick return truen"); return true; } else { mLogView.append("onLongClick return falsen"); return false; } } return false; // 默认不处理,交由后面的环节去处理 } @Override public boolean onTouch(View arg0, MotionEvent ev) { if(mBoxTouchTrack.isChecked()) { // FIXME: 多点触摸版本 // 参考后面对多点触摸API的描述,很容易就能够修改成支持多点触摸的版本 final int historySize = ev.getHistorySize(); for (int h = 0; h < historySize; h++) { mTouchView.DrawPoint(ev.getHistoricalX(h), ev.getHistoricalY(h)); } mTouchView.DrawPoint(ev.getX(), ev.getY()); } if(mBoxTouch.isChecked()) { if(arg0.getId() == R.id.tv1) { mLogView.append("onTouch event point(" + ev.getX() +"," + ev.getY() + ")" ); mLogView.scrollBy(0, 10); } // 同样,随机返回处理,只是用于实验,真实的场景不必如此 if(System.currentTimeMillis() % 2 == 0) { mLogView.append("onTouch return truen"); mLogView.scrollBy(0, 10); return true; } else { mLogView.append("onTouch return falsen"); mLogView.scrollBy(0, 10); return false; } } return false; } @Override public void onCheckedChanged(CompoundButton arg0, boolean arg1) { if(arg0 == mBoxTouchTrack) { if(!arg1) { mTouchView.InitCache(); // 清空轨迹面板 } } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { mLogView.append("keyCode " + keyCode + " action " + event.getAction()); if (keyCode == KeyEvent.KEYCODE_MENU) { // 按下菜单按键的处理 mLogView.append("按下菜单"); return true; } return super.onKeyDown(keyCode, event); } } (3)触摸类(负责接收触摸事件并画出轨迹) public class SimpleView extends View { private final static int WIDTH = 320; private final static int HEIGHT = 200; private Bitmap mBitmapCache; private Canvas mCanvasCache; private Paint mPaint; public SimpleView(Context context) { super(context); Construct(context); } public SimpleView(Context context,AttributeSet attr) { super(context,attr); Construct(context); } private void Construct(Context context) { // FIXME: onMesured的时候要重新创建cache mBitmapCache = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); mCanvasCache = new Canvas(mBitmapCache); mPaint = new Paint(); mPaint.setAlpha(0x40); mPaint.setColor(Color.BLUE); } // 在缓冲中划一个点,并且通知刷新画面 public void DrawPoint(float x, float y) { mCanvasCache.drawPoint(x, y, mPaint); invalidate(); } public void InitCache() { mCanvasCache.drawColor(Color.WHITE); invalidate(); } protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(mBitmapCache, 0, 0, mPaint); } } 4.1.2 Android多点触摸与手势 单点触摸不能完成缩放、滑动等动作,这就需要多点触摸与手势的支持。 1)多点触摸 Android从2.0版本开始支持多点触摸,在API中体现出来就是一个MotionEvent事件里面包含若干个点的信息。因为不同硬件支持的点数不一样,所以需要通过API获取得到的点的个数。下面是一个SDK文档中的例子,并做了简单的注释: void printSamples(MotionEvent ev) { final int historySize = ev.getHistorySize(); // 获取历史采样的集合大小 final int pointerCount = ev.getPointerCount(); // 获取事件点数 for (int h = 0; h < historySize; h++) { // 对历史采样进行记录 // 历史采样的时间点 System.out.printf("At time %d:", ev.getHistoricalEventTime(h)); for (int p = 0; p < pointerCount; p++) { // System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h)); // 打印每个点的坐标 } } // 处理当前的点,输出时间以及每个点的坐标 System.out.printf("At time %d:", ev.getEventTime()); for (int p = 0; p < pointerCount; p++) { System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getX(p), ev.getY(p)); } } 2)手势 手势是用户输入的某个或者某系列参数动作,比如按下、上拉、下拉、划圈等。通过对onTouch(MotionEvent)方法里面的MotionEvent进行一段时间的采样并且进行模式检测,即可得到手势。具体的手势检测算法,对于某些应用来说可能是独特的,但是对于常用的软件,使用到的手势一般只有某个方向的滑动、快速滑动、按下、长按等,若是这类常规需求,可以考虑使用Android提供的GestureDetector类进行手势检测。 “GestureDetector”检测手势的输入源是MotionEvent,为了能够把MotionEvent输入到GestureDetector里面进行检测,需实现“GestureDetector.OnGestureListener”接口,一般在Activity里面实现。如果没有特殊要求,按照SDK的推荐,实现一个子接口“GestureDetector.SimpleOn GestureListener”,它有如下一些手势事件: boolean onDoubleTap(MotionEvent e)——双击。 boolean onDoubleTapEvent(MotionEvent e)——双击按下和弹起都触发,通过e.getAction()判断按下或者弹起。 boolean onDown(MotionEvent e)——按下。 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)——滑动,其中后两个参数是X轴和Y轴的速度,在弹起的时候触发。 void onLongPress(MotionEvent e)——长按,按住的时候触发。 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)——滑动,滑动过程中触发。 void onShowPress(MotionEvent e)——若按下较长时间未滑动,则触发该事件。 boolean onSingleTapConfirmed(MotionEvent e)——如果只有按下事件而后面不带滑动事件,就触发该事件。 boolean onSingleTapUp(MotionEvent e)——按下并且弹起的时候触发。 创建GestureDetector,把onTouch得到的MotionEvent交由GestureDetector.onTouchEvent(event)处理。示例代码如下: public boolean onTouch(View view, MotionEvent event) { return mGestureDetector.onTouchEvent(event); } 屏幕单击、长按、滑动、键盘单击等只不过是一些离散的输入信号,它们被组织成各种事件、手势的过程,有潜在的相互影响,比如在一个事件被当做长按处理以后,便不会产生单击事件,这一点从上面的小程序中的返回值可以看出,如图4-2所示。 图4-2 第一次长按的处理函数中,返回了“true”,结果监听器就监听不到单击事件,而第二次长按处理函数中返回了“false”,接着就发生了一次单击事件。所以当程序需要进行很复杂的输入事件、手势处理的时候,需要仔细研究一下它们产生的顺序以及相互之间的影响。