杨子刚的博客


Android 响应用户屏幕手势操作

2013-11-13

现在的移动设备(手机、pad等)一般都支持多点触摸,有些设备支持十点触摸,有些支持五点触摸,有些可能只支持两点触摸。在Android平台上,用户对屏幕的操作(点按、缩放、拖动等)统称为Gesture(手势),在此我们就用“手势操作”来统称用户的这些操作。

函数实现

在程序中,如果某一控件需要响应用户手势操作、进行进一步处理时,有如下两种方法:

  1. 创建新类继承自android.view.View,覆盖父类方法public boolean onTouchEvent (MotionEvent event)
  2. 在处理这些触摸操作的类中实现接口android.view.View.OnTouchListener,该接口只有一个方法public abstract boolean onTouch (View v, MotionEvent event),并调用Viewpublic void setOnTouchListener (View.OnTouchListener l)方法对touchListener进行设置。

MotionEvent简介

用户操作信息全部在MotionEvent内,MotionEvent包含了以下信息:

  1. 当前操作的点数
  2. 每个点对应的坐标
  3. 每个点对应的手势动作

手势动作如下:

单点手势操作

我们由浅入深,先看看单点手势操作。请参考HelloEventSingglePoint项目

在该项目中,我们创建了一个自定义View:EventView,在EventView中实现了方法public boolean onTouchEvent (MotionEvent event)

//io.github.tianshanxuester.helloeventsinglepoing.EventView
    @Override
    public boolean onTouchEvent (MotionEvent event) {
        
        String actionString = null;
        switch(event.getAction()) {
        case MotionEvent.ACTION_CANCEL:
            actionString = "ACTION_CANCEL";
            break;
        case MotionEvent.ACTION_DOWN:
            actionString = "ACTION_DOWN";
            break;
        case MotionEvent.ACTION_MOVE:
            actionString = "ACTION_MOVE";
            break;
        case MotionEvent.ACTION_OUTSIDE:
            actionString = "ACTION_OUTSIDE";
            break;
        case MotionEvent.ACTION_UP:
            actionString = "ACTION_UP";
            break;
            default:
                actionString = "" + event.getAction();
        }
        Log.i("HelloEventSinglePoint", "Action is " + actionString);

        return true;
    }
<!-- res/layout/activity_hello_event_single_point.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <io.github.tianshanxuester.helloeventsinglepoint.EventView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    </io.github.tianshanxuester.helloeventsinglepoint.EventView>

</LinearLayout>

运行程序后,用单个手指在屏幕上进行触摸操作,会输出日志:

多点手势操作

我解释一下Android里面手势操作的基本理念,为了简单起见,以两个点A、B为例,那么下面是一个手势操作列表:

也就是说,在每一个MotionEvent事件中,最多只有一个点会产生DOWN或者UP,其他点(如果有的话)为ACTION_MOVE,读者不仅会问,为什么不存在两个点同时发出DWON或者UP的情况呢?这一点很好解释,一般来说人的动作就算是再训练有素,两个手指头按下的时间也不可能完全同步。退一步,就算两个手指头同时按下了,在系统处理层面,为了简化起见,系统也可以把同时按下的事件分解成DOWNDOWN+ACTION_MOVE两个事件传给应用程序。推而广之,更多点手势操作也是这个道理。

在多点手势操作过程中,最先发生的是ACTION_DOWN,之后其他点的按下、抬起产生的动作为ACTION_POINTER_DOWNACTION_POINTER_UP,最后一个点抬起会产生ACTION_UP。对于ACTION_DOWNACTION_UP之间的其他点,Android称之为maskedAction,可以使用函数public final int getActionMasked ()来查询这个动作是ACTION_POINTER_DOWN还是ACTION_POINTER_UP,如果getActionMasked()返回了ACTION_MOVE,则表明当前用户正在使用若干(一个或者多个)手指在屏幕上移动,没有手指按下或抬起。函数public final int getActionIndex ()用来获取当前按下/抬起的点的标识。如果当前没有任何点抬起/按下,该函数返回0

跟单点手势操作一样,多点手势操作的信息也是包含在方法onTouchEventMotionEvent参数内,MotionEvent中有如下函数可以获取多点触摸信息:

请参考项目HelloEventMultiPoint

22 //io.github.tianshanxuester.helloeventmultipoint
23  @Override
24  public boolean onTouchEvent (MotionEvent event) {
25      
26      Log.i("HelloEventMultiPoint", "Total pointer count is " + event.getPointerCount());
27      
28      String actionString = actionToString(event.getAction());
29      Log.i("HelloEventMultiPoint", "Main action is " + actionString);
30      
31      int maskedAction = event.getActionMasked();
32      int pointId = event.getActionIndex();
33      Log.i("HelloEventMultiPoint", "Masked action is " + actionToString(maskedAction) + "\tpointId is " + pointId);
34      Log.i("HelloEventMultiPoint", "====================");
35       return true;
36  }
37  
38  private String actionToString(int action) {
39      String actionString = null;
40      switch(action) {
41      case MotionEvent.ACTION_CANCEL:
42          actionString = "ACTION_CANCEL";
43          break;
44      case MotionEvent.ACTION_DOWN:
45          actionString = "ACTION_DOWN";
46          break;
47      case MotionEvent.ACTION_MOVE:
48          actionString = "ACTION_MOVE";
49          break;
50      case MotionEvent.ACTION_OUTSIDE:
51          actionString = "ACTION_OUTSIDE";
52          break;
53      case MotionEvent.ACTION_UP:
54          actionString = "ACTION_UP";
55          break;
56      case MotionEvent.ACTION_POINTER_DOWN:
57          actionString = "ACTION_POINTER_DOWN";
58          break;
59      case MotionEvent.ACTION_POINTER_UP:
60          actionString = "ACITON_POINTER_UP";
61          break;
62          default:
63              actionString = "" + action;
64      }
65      
66      return actionString;
67  }

触发ACTION_CANCEL事件

运行上面两个例子,如果没什么差错的话,你是不会看到ACTION_CANCEL事件的,为什么呢?

要触发ACTION_CANCEL,就先得了解一个类ViewGroupViewGroup是一个放置其他views(子view)的特殊view,它是布局类(*Layout)、视图容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基类。

也就是说ViewGroup一般是做为父视图来容纳、管理其他子视图的。既然管理,在用户手势操作过程中,就会存在父视图不希望子视图响应用户手势操作的情况。Android提供了一个函数public boolean onInterceptTouchEvent (MotionEvent ev),在用户手势操作时,系统先调用父视图(一个继承自ViewGroup的类)的这个函数,来决定当前手势操作是由父视图还是子视图来响应、处理。我们仔细看看这个函数名,函数名中有一个单词intercept,经过查词典,这个单词的中文意思是拦截。在用户的一个完整手势操作过程中(起自ACTION_DOWN,终于ACTION_UP),对于每一次的MotionEvent``Android都会调用该函数,向父视图查询是否拦截当前MotionEvent,如果父视图返回false:不拦截,则系统会调用子视图的onTouchEvent函数;如果父视图返回true:拦截,则系统调用父视图的onTouchEvent。等等,有人不禁要问了,如果在这个完整手势操作过程中,父视图初期返回false、后期返回true会是一个什么样的情况呢(捣乱的来了)?这个嘛,是这个样子的,一开始返回false,毫无疑问,子视图会被调用onTouchEvent,但凡父视图在函数onInterceptTouch中有一次返回了true,那这一完整手势操作内所有后续的MotionEvent都会调用父视图的onTouchEvent,即使父视图后期反悔而改成返回false也不行(没有后悔药)。在这种父视图先返回false,后返回true的情况下,子视图收不到后续的事件,而只是在父视图由返回false改成返回true(拦截)的时候收到ACTION_CANCEL事件。 见项目

ACTION_OUTSIDE事件

当使用WindowManager动态的显示一个Modal视图时,可以在显示视图时,指定布局参数的flags为FLAG_WATCH_OUTSIDE_TOUCH,这样当点击事件发生在这个Modal视图之外时,Modal视图就可以接收到ACTION_OUTSIDE事件,请参考项目HelloPopupWindow

//com.ingphone.hellopopupwindow.HelloPopupWindow
package com.ingphone.hellopopupwindow;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.view.WindowManager.LayoutParams;
import android.widget.Button;

public class HelloPopupWindow extends Activity {

    private View floatDialogView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hello_popup_window);
        
        Button openButton = (Button)findViewById(R.id.openWindow);
        openButton.setOnClickListener(openWindow);
        Button closeButton = (Button)findViewById(R.id.closeWindow);
        closeButton.setOnClickListener(closeWindow);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.hello_popup_window, menu);
        return true;
    }

    private OnClickListener openWindow = new OnClickListener() {

        @Override
        public void onClick(View arg0) {
            if(floatDialogView != null) {
                return;
            }
            WindowManager windowManager = (WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            LayoutParams params = new WindowManager.LayoutParams();
            params.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            LayoutInflater inflater = LayoutInflater.from(HelloPopupWindow.this);
            floatDialogView = inflater.inflate(R.layout.floatwindow, null);
            Button closeButton = (Button)floatDialogView.findViewById(R.id.close);
            closeButton.setOnClickListener(new OnClickListener(){

                @Override
                public void onClick(View view) {
                    WindowManager windowManager = (WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
                    windowManager.removeView(floatDialogView);
                    floatDialogView = null;
                }
                
            });
            windowManager.addView(floatDialogView, params);
        }
        
    };
    
    private OnClickListener closeWindow = new OnClickListener() {

        @Override
        public synchronized void onClick(View v) {
            if (floatDialogView == null) {
                return;
            }
            WindowManager windowManager = (WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            windowManager.removeView(floatDialogView);
            floatDialogView = null;
        }};
}