l Android的事件
事件:就是对象通知本身的状态发生了改变,并且可以通过该事件获取事件的相关信息。UI编程通常都会伴随事件处理,Android也不例外,它提供了两种方式的事件处理:基于回调的事件处理和基于监听器的事件处理。
对于基于监听器的事件处理而言,主要就是为Android界面组件绑定特定的事件监听器;对于基于回调的事件处理而言,主要做法是重写Android组件特定的回调函数,Android大部分界面组件都提供了事件响应的回调函数,我们只要重写它们就行。
一 基于监听器的事件处理
相比于基于回调的事件处理,这是更具“面向对象”性质的事件处理方式。在监听器模型中,主要涉及三类对象:
1)事件源Event Source:产生事件的来源,通常是各种组件,如按钮,窗口等。
2)事件Event:事件封装了界面组件上发生的特定事件的具体信息,如果监听器需要获取界面组件上所发生事件的相关信息,一般通过事件Event对象来传递。
3)事件监听器Event Listener:负责监听事件源发生的事件,并对不同的事件做相应的处理。
基于监听器的事件处理机制是一种委派式Delegation的事件处理方式,事件源将整个事件委托给事件监听器,由监听器对事件进行响应处理。这种处理方式将事件源和事件监听器分离,有利于提供程序的可维护性。
举例:
View类中的OnLongClickListener监听器定义如下:(不需要传递事件)
- public interface OnLongClickListener {
- boolean onLongClick(View v);
- }
View类中的OnTouchListener监听器定义如下:(需要传递事件MotionEvent)
- public interface OnTouchListener {
- boolean onTouch(View v, MotionEvent event);
- }
二 基于回调的事件处理
相比基于监听器的事件处理模型,基于回调的事件处理模型要简单些,该模型中,事件源和事件监听器是合一的,也就是说没有独立的事件监听器存在。当用户在GUI组件上触发某事件时,由该组件自身特定的函数负责处理该事件。通常通过重写Override组件类的事件处理函数实现事件的处理。
举例:
View类实现了KeyEvent.Callback接口中的一系列回调函数,因此,基于回调的事件处理机制通过自定义View来实现,自定义View时重写这些事件处理方法即可。
- public interface Callback {
- // 几乎所有基于回调的事件处理函数都会返回一个boolean类型值,该返回值用于
- // 标识该处理函数是否能完全处理该事件
- // 返回true,表明该函数已完全处理该事件,该事件不会传播出去
- // 返回false,表明该函数未完全处理该事件,该事件会传播出去
- boolean onKeyDown(int keyCode, KeyEvent event);
- boolean onKeyLongPress(int keyCode, KeyEvent event);
- boolean onKeyUp(int keyCode, KeyEvent event);
- boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
- }
三 监听器和回调的比对
基于监听器的事件模型符合单一职责原则,事件源和事件监听器分开实现;
Android的事件处理机制保证基于监听器的事件处理会优先于基于回调的事件处理被触发;
某些特定情况下,基于回调的事件处理机制会更好的提高程序的内聚性。
四 基于自定义监听器的事件处理流程
在实际项目开发中,我们经常需要自定义监听器来实现自定义业务流程的处理,而且一般都不是基于GUI界面作为事件源的。这里以常见的app自动更新为例进行说明,在自动更新过程中,会存在两个状态:下载中和下载完成,而我们的程序需要在这两个状态做不同的事情,“下载中”需要在UI界面上实时显示软件包下载的进度,“下载完成”后,取消进度条的显示。这里进行一个模拟,重点在说明自定义监听器的事件处理流程。
4.1)定义事件监听器如下:
- public interface DownloadListener {
- public void onDownloading(int progress); //下载过程中的处理函数
- public void onDownloaded(); //下载完成的处理函数
- }
4.2)实现下载操作的工具类代码如下:
- public class DownloadUtils {
- private static DownloadUtils instance = null;
- private DownloadUtils() {
- }
- public static synchronized DownloadUtils instance() {
- if (instance == null) {
- instance = new DownloadUtils();
- }
- return instance;
- }
- private boolean isDownloading = true;
- private int progress = 0;
- // 实际开发中这个函数需要传入url作为参数,以获取服务器端的安装包位置
- public void download(DownloadListener listener) throws InterruptedException {
- while (isDownloading) {
- listener.onDownloading(progress);
- // 下载过程的简单模拟
- Thread.sleep(1000);
- progress += 10;
- if (progress >= 100) {
- isDownloading = false;
- }
- }
- // 下载完成
- listener.onDownloaded();
- }
- }
4.3)最后在main函数中模拟事件源:
- public class DownloadUI {
- public static void main(String[] args) {
- try {
- DownloadUtils.instance().download(new MyDownloadListener());
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- private static class MyDownloadListener implements DownloadListener {
- @Override
- public void onDownloading(int progress) {
- System.out.println("下载进度是:" + progress);
- }
- @Override
- public void onDownloaded() {
- System.out.println("下载完成");
- }
- }
- }
运行上面的模拟程序,输出如下所示:
Android中的事件处理机制
android系统中的每个ViewGroup的子类都具有下面三个和TouchEvent处理密切相关的方法:
1)public booleandispatchTouchEvent(MotionEventev) 这个方法用来分发TouchEvent
2)public booleanonInterceptTouchEvent(MotionEventev) 这个方法用来拦截TouchEvent
3)public boolean onTouchEvent(MotionEventev) 这个方法用来处理TouchEvent
注意:不是所有的View的子类,很多教程都说的是所有的View的子类,只有可以向里面添加View的控件才需要分发,比如TextView它本身就是最小的view了,所以不用再向它的子视图分发了,它也没有子视图了,所以它没有dispatch和Intercept,只有touchEvent。
说明: 白色为最外层,它占满整个屏幕;
红色为中间区域,属于白色中的一层;
黑色为中心区域,必于红色中的一层。
注意:他们本质上是:LinearLayout,而不是RelativeLayout或者其它布局。
1.由中心区域处理touch事件
布局文件如下:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_out"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:background="#fff"
- android:gravity="center">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_mid"
- android:layout_width="300px"
- android:layout_height="400px"
- android:background="#f00"
- android:gravity="center">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_center"
- android:layout_width="150px"
- android:layout_height="150px"
- android:background="#000"
- android:gravity="center"
- android:clickable="true">
- </com.kris.touch.widget.TouchView>
- </com.kris.touch.widget.TouchView>
- </com.kris.touch.widget.TouchView>
- </LinearLayout>
复制代码
注意: android:clickable="true"
接下来我们看一下打印的日志:
结合是上面的日志,我们可以看一下ACTION_DOWN事件处理流程:
说明:
首先触摸事件发生时(ACTION_DOWN),由系统调用Activity的dispatchTouchEvent方法,分发该事件。根据触摸事件的坐标,将此事件传递给out的dispatchTouchEvent处理,out则调用onInterceptTouchEvent 判断事件是由自己处理,还是继续分发给子View。此处由于out不处理Touch事件,故根据事件发生坐标,将事件传递给out的直接子View(即middle)。
Middle及Center中事件处理过程同上。但是由于Center组件是clickable 表示其能处理Touch事件,故center中的onInterceptTouchEvent方法将事件传递给center自己的onTouchEvent方法处理。至此,此Touch事件已被处理,不继续进行传递。
2.没有指定谁会处理touch事件
布局文件如下:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_out"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:background="#fff"
- android:gravity="center">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_mid"
- android:layout_width="300px"
- android:layout_height="400px"
- android:background="#f00"
- android:gravity="center">
- <com.kris.touch.widget.TouchView
- android:id="@+id/view_center"
- android:layout_width="150px"
- android:layout_height="150px"
- android:background="#000"
- android:gravity="center">
- </com.kris.touch.widget.TouchView>
- </com.kris.touch.widget.TouchView>
- </com.kris.touch.widget.TouchView>
- </LinearLayout>
复制代码
注意:只是比上一次的布局少了android:clickable="true"
接下来我们看一下打印的日志
结合是上面的日志,我们可以看一下ACTION_DOWN事件处理流程:
说明:
事件处理流程大致同上,区别是此状态下,所有组件都不会处理事件,事件并不会被center的onTouchEvent方法“消费”,则事件会层层逆向传递回到Activity,若Activity也不对此事件进行处理,此事件相当于消失了(无效果)。
对于后续的move、up事件,由于第一个down事件已经确定由Activity处理事件,故up事有由Activity的dispatchTouchEvent直接分发给自己的onTouchEvent方法处理。
通常我们都处理对于视图的onTouchEvent 通过返回值来控制事件是否传递给外层的事件处理
总结:
1) Touchevent 中,返回值是 true ,则说明消耗掉了这个事件,返回值是 false ,则没有消耗掉,会继续传递下去,这个是最基本的。
2) 事件传递的两种方式:
隧道方式:从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递。
冒泡方式:从最内层子元素依次往外传递直到根元素或在中间某一元素中由于某一条件停止传递。
android对Touch Event的分发逻辑是View从上层分发到下层(dispatchTouchEvent函数)类似于隧道方式,然后下层优先开始处理Event(先mOnTouchListener,再onTouchEvent)并向上返回处理情况(boolean值),若返回true,则上层不再处理。类似于冒泡方式
于是难题出现了,你若把Touch Event都想办法给传到上层了(只能通过返回false来传到上层),那么下层的各种子View就不能处理后续事件了。而有的时候我们需要在下层和上层都处理Touch事件
Android中的事件处理机制二
Android中有很多视图,这些视图时有层次结构的,视图之间有父子关系。每个视图都有自己对应的事件,视图的事件会在父子视图之间传递(比如父视图的事件可能会传给子视图,反之亦然)。在这里只讲触摸事件。
在每个 View 中跟 Touch 事件相关的函数以下3个(返回值都是boolean):
a、dispatchTouchEvent : 负责分发事件的,事件从 activity 传递出来之后,最先到达的就是最顶层 view 的 dispatchTouchEvent ,然后它进行分发,如果返回 false ,则交给这个 view 的 interceptTouchEvent 方法,如果返回true该函数则等待下一事件的到来。
b、interceptTouchEvnet :该方法来决定是否要拦截这个事件,如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给该View的 onTouchEvent 来处理 ,如果返回false,也就是该View并不拦截该事件,那么这个时间会传递给它的子View,由子View处理这个事件。如果是对底层的View(没有子View)那么返回false时该事件消失,并且接不到下一个事件。
C、onTouchEvent :对事件作相应的处理,并返回一个boolean值。如果返回true则表示该事件已经被处理了,不会继续传递,并且该函数继续等待下一个事件;如果返回false则之歌事件会传给它的父View,并且由父View的 onTouchEvent 来接收,并做相同的处理。如果顶层的onTouchEvent 也返回 false 的话,这个事件就会“消失”,而且接收不到下一次事件。(参看帮助文档该函数的返回值解释是:Return true if you have consumed the event, false if you haven't. The default implementation always returns false. )
原理可以参看下图:
上面介绍了事件的大致传递流程,下面介绍一下如何为事件添加监听器,即将定义好的监听器注册到视图。添加监听器之后,该视图就能获得事件的通知,并执行相应的函数。
可以定义好Listener再添加到视图(这里是ViewFlipper控件flipper),如下:
也可以在添加Listener的时候直接定义Listener,如下:
还有一种方法就是让Activity类实现接口OnTouchListener,然后再类中实现其方法onTouch,然后再onCreate中调用flipper.setOnTouchListener(this);即可。
这里有个问题,this所指的是一个Activity类的子类,并实现了OnTouchListener的test类。由于test实现了OnTouchListener,所以它可以转为setOnTouchListener所需的参数类型OnTouchListener。
l Android中的事件侦听器Event Listeners
事件侦听器是视图View类的接口,包含一个单独的回调方法。这些方法将在视图中注册的侦听器被用户界面操作触发时由Android框架调用。下面这些回调方法被包含在事件侦听器接口中:
onClick()
包含于View.OnClickListener。当用户触摸这个item(在触摸模式下),或者通过浏览键或跟踪球聚焦在这个item上,然后按下“确认”键或者按下跟踪球时被调用。
onLongClick()
包含于View.OnLongClickListener。当用户触摸并控制住这个item(在触摸模式下),或者通过浏览键或跟踪球聚焦在这个item上,然后保持按下“确认”键或者按下跟踪球(一秒钟)时被调用。
onFocusChange()
包含于View.OnFocusChangeListener。当用户使用浏览键或跟踪球浏览进入或离开这个item时被调用。
onKey()
包含于View.OnKeyListener。当用户聚焦在这个item上并按下或释放设备上的一个按键时被调用。
onTouch()
包含于View.OnTouchListener。当用户执行的动作被当做一个触摸事件时被调用,包括按下,释放,或者屏幕上任何的移动手势(在这个item的边界内)。
onCreateContextMenu()
包含于View.OnCreateContextMenuListener。当正在创建一个上下文菜单的时候被调用(作为持续的“长点击”动作的结果)。参阅创建菜单Creating Menus章节以获取更多信息。
这些方法是它们相应接口的唯一“住户”。要定义这些方法并处理你的事件,在你的活动中实现这个嵌套接口或定义它为一个匿名类。然后,传递你的实现的一个实例给各自的View.set...Listener() 方法。(比如,调用setOnClickListener()并传递给它你的OnClickListener实现。)
下面的例子说明了如何为一个按钮注册一个点击侦听器:
// Create an anonymous implementation of OnClickListener
private OnClickListener mCorkyListener = newOnClickListener() {
public void onClick(View v) {
// do something whenthe button is clicked
}
};
protected void onCreate(Bundle savedValues) {
...
// Capture our button from layout
Button button =(Button)findViewById(R.id.corky);
// Register the onClick listenerwith the implementation above
button.setOnClickListener(mCorkyListener);
...
}
你可能会发现把OnClickListener作为活动的一部分来实现会便利的多。这将避免额外的类加载和对象分配。比如:
public class ExampleActivity extends Activity implementsOnClickListener {
protected void onCreate(BundlesavedValues) {
...
Buttonbutton = (Button)findViewById(R.id.corky);
button.setOnClickListener(this);
}
// Implement the OnClickListenercallback
public void onClick(View v) {
// do something whenthe button is clicked
}
...
}
注意上面例子中的onClick()回调没有返回值,但是一些其它事件侦听器必须返回一个布尔值。原因和事件相关。对于其中一些,原因如下:
· onLongClick() – 返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处理它。也就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它和/或这个事件应该继续交给其他on-click侦听器。
· onKey() –返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处理它。也就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它和/或这个事件应该继续交给其他on-key侦听器。
· onTouch() - 返回一个布尔值来指示你的侦听器是否已经消费了这个事件。重要的是这个事件可以有多个彼此跟随的动作。因此,如果当接收到向下动作事件时你返回false,那表明你还没有消费这个事件而且对后续动作也不感兴趣。那么,你将不会被该事件中的其他动作调用,比如手势或最后出现向上动作事件。
记住按键事件总是递交给当前焦点所在的视图。它们从视图层次的顶层开始被分发,然后依次向下,直到到达恰当的目标。如果你的视图(或者一个子视图)当前拥有焦点,那么你可以看到事件经由dispatchKeyEvent()方法分发。除了从你的视图截获按键事件,还有一个可选方案,你还可以在你的活动中使用onKeyDown() and onKeyUp()来接收所有的事件。
注意: Android 将首先调用事件处理器,其次是类定义中合适的缺省处理器。这样,从这些事情侦听器中返回true 将停止事件向其它事件侦听器传播并且也会阻塞视图中的缺事件处理器的回调函数。因此当你返回true时确认你希望终止这个事件。
如果你从视图创建一个自定义组件,那么你将能够定义一些回调方法被用作缺省的事件处理器。在创建自定义组件Building Custom Components的文档中,你将学习到一些用作事件处理的通用回调函数,包括:
· onKeyDown(int, KeyEvent) - 当一个新的按键事件发生时被调用。
· onKeyUp(int, KeyEvent) -当一个向上键事件发生时被调用。
· onTrackballEvent(MotionEvent) -当一个跟踪球运动事件发生时被调用。
· onTouchEvent(MotionEvent) -当一个触摸屏移动事件发生时调用。
· onFocusChanged(boolean, int,Rect) - 当视图获得或者丢失焦点时被调用。
你应该知道还有一些其它方法,并不属于视图类的一部分,但可以直接影响你处理事件的方式。所以,当在一个布局里管理更复杂的事件时,考虑一下这些方法:
· Activity.dispatchTouchEvent(MotionEvent) -这允许你的活动可以在分发给窗口之前捕获所有的触摸事件。
· ViewGroup.onInterceptTouchEvent(MotionEvent) -这允许一个视图组ViewGroup 在分发给子视图时观察这些事件。ViewParent.requestDisallowInterceptTouchEvent(boolean) -在一个父视图之上调用这个方法来表示它不应该通过onInterceptTouchEvent(MotionEvent)来捕获触摸事件。
触摸模式Touch Mode
当用户使用方向键或跟踪球浏览用户界面时,有必要给用户可操作的item(比如按钮)设置焦点,这样用户可以知道哪个item将接受输入。不过,如果这个设备有触摸功能,而且用户通过触摸来和界面交互,那么就没必要高亮items,或者设定焦点到一个特定的视图。这样,就有一个交互模式 叫“触摸模式”。
对于一个具备触摸功能的设备,一旦用户触摸屏幕,设备将进入触摸模式。自此以后,只有isFocusableInTouchMode()为真的视图才可以被聚焦,比如文本编辑部件。其他可触摸视图,如按钮,在被触摸时将不会接受焦点;它们将只是在被按下时简单的触发on-click侦听器。任何时候用户按下方向键或滚动跟踪球,这个设备将退出触摸模式,然后找一个视图来接受焦点,用户也许不会通过触摸屏幕的方式来恢复界面交互。
触摸模式状态的维护贯穿整个系统(所有窗口和活动)。为了查询当前状态,你可以调用isInTouchMode() 来查看这个设备当前是否处于触摸模式中。
处理焦点Handling Focus
框架将根据用户输入处理常规的焦点移动。这包含当视图删除或隐藏,或者新视图出现时改变焦点。视图通过isFocusable()方法表明它们想获取焦点的意愿。要改变视图是否可以接受焦点,可以调用setFocusable()。在触摸模式中,你可以通过isFocusableInTouchMode()查询一个视图是否允许接受焦点。你可以通过setFocusableInTouchMode()方法来改变它。焦点移动基于一个在给定方向查找最近邻居的算法。少有的情况是,缺省算法可能和开发者的意愿行为不匹配。在这些情况下,你可以通过下面布局文件中的XML属性提供显式的重写:nextFocusDown, nextFocusLeft, nextFocusRight, 和nextFocusUp。为失去焦点的视图增加这些属性之一。定义属性值为拥有焦点的视图的ID。比如:
<LinearLayout
android:orientation="vertical"
... >
<Button android:id="@+id/top"
android:nextFocusUp="@+id/bottom"
.../>
<Button android:id="@+id/bottom"
android:nextFocusDown="@+id/top"
.../>
</LinearLayout>
通常,在这个竖向布局中,从第一个按钮向上浏览或者从第二个按钮向下都不会移动到其它地方。现在这个顶部按钮已经定义了底部按钮为nextFocusUp (反之亦然),浏览焦点将从上到下和从下到上循环移动。
如果你希望在用户界面中声明一个可聚焦的视图(通常不是这样),可以在你的布局定义中,为这个视图增加android:focusable XML 属性。把它的值设置成true。你还可以通过android:focusableInTouchMode在触摸模式下声明一个视图为可聚焦。
想请求一个接受焦点的特定视图,调用requestFocus()。
要侦听焦点事件(当一个视图获得或者失去焦点时被通知到),使用onFocusChange(),如上面事件侦听器Event Listeners一章所描述的那样。
android 触摸事件、点击事件的区别
针对屏幕上的一个View控件,Android如何区分应当触发onTouchEvent,还是onClick,亦或是onLongClick事件?
在Android中,一次用户操作可以被不同的View按次序分别处理,并将完全响应了用户一次UI操作称之为消费了该事件(consume),那么Android是按什么次序将事件传递的呢?又在什么情况下判定为消费了该事件?
搞清楚这些问题对于编写出能正确响应UI操作的代码是很重要的,尤其当屏幕上的不同View需要针对此次UI操作做出各种不同响应的时候更是如此,一个典型例子就是用户在桌面上放置了一个Widget,那么当用户针对widget做各种操作时,桌面本身有的时候要对用户的操作做出响应,有时忽略。只有搞清楚事件触发和传递的机制才有可能保证在界面布局非常复杂的情况下,UI控件仍然能正确响应用户操作。
1. onTouchEvent
onTouchEvent中要处理的最常用的3个事件就是:ACTION_DOWN、ACTION_MOVE、ACTION_UP。
这三个事件标识出了最基本的用户触摸屏幕的操作,含义也很清楚。虽然大家天天都在用它们,但是有一点请留意,ACTION_DOWN事件作为起始事件,它的重要性是要超过ACTION_MOVE和ACTION_UP的,如果发生了ACTION_MOVE或者ACTION_UP,那么一定曾经发生了ACTION_DOWN。
从Android的源代码中能看到基于这种不同重要性的理解而实现的一些交互机制,SDK中也有明确的提及,例如在ViewGroup的onInterceptTouchEvent方法中,如果在ACTION_DOWN事件中返回了true,那么后续的事件将直接发给onTouchEvent,而不是继续发给onInterceptTouchEvent。
2. onClick、onLongClick与onTouchEvent
曾经看过一篇帖子提到,如果在View中处理了onTouchEvent,那么就不用再处理onClick了,因为Android只会触发其中一个方法。这个理解是不太正确的,针对某个view,用户完成了一次触碰操作,显然从传感器上得到的信号是手指按下和抬起两个操作,我们可以理解为一次Click,也可以理解为发生了一次ACTION_DOWN和ACTION_UP,那么Android是如何理解和处理的呢?
在Android中,onClick、onLongClick的触发是和ACTION_DOWN及ACTION_UP相关的,在时序上,如果我们在一个View中同时覆写了onClick、onLongClick及onTouchEvent的话,onTouchEvent是最先捕捉到ACTION_DOWN和ACTION_UP事件的,其次才可能触发onClick或者onLongClick。主要的逻辑在View.java中的onTouchEvent方法中实现的:
case MotionEvent.ACTION_DOWN:
mPrivateFlags |=PRESSED;
refreshDrawableState();
if ((mViewFlags &LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick();
}
break;
case MotionEvent.ACTION_UP:
if ((mPrivateFlags &PRESSED) != 0) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()){
focusTaken = requestFocus();
}
if(!mHasPerformedLongPress) {
if(mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
if(!focusTaken) {
performClick();
}
}
…
break;
可以看到,Click的触发是在系统捕捉到ACTION_UP后发生并由performClick()执行的,performClick里会调用先前注册的监听器的onClick()方法:
public boolean performClick() {
…
if (mOnClickListener !=null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
LongClick的触发则是从ACTION_DOWN开始,由postCheckForLongClick()方法完成:
private void postCheckForLongClick() {
mHasPerformedLongPress= false;
if(mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout());
}
可以看到,在ACTION_DOWN事件被捕捉后,系统会开始触发一个postDelayed操作,delay的时间在Eclair2.1上为500ms,500ms后会触发CheckForLongPress线程的执行:
class CheckForLongPress implements Runnable{
…
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
…
}
如果各种条件都满足,那么在CheckForLongPress中执行performLongClick(),在这个方法中将调用onLongClick():
public boolean performLongClick() {
…
if(mOnLongClickListener != null) {
handled= mOnLongClickListener.onLongClick(View.this);
}
…
}
从实现中可以看到onClick()和onLongClick()方法是由ACTION_DOWN和ACTION_UP事件捕捉后根据各种情况最终确定是否触发的,也就是说如果我们在一个Activity或者View中同时监听或者覆写了onClick(),onLongClick()和onTouchEvent()方法,并不意味着只会发生其中一种。
下面是一个onClick被触发的基本时序的Log:
04-05 05:57:47.123: DEBUG/TSActivity(209):onTouch ACTION_DOWN
04-05 05:57:47.263: DEBUG/TSActivity(209):onTouch ACTION_UP
04-05 05:57:47.323: DEBUG/TSActivity(209):onClick
可以看出是按ACTION_DOWN -> ACTION_UP -> onClick的次序发生的。
下面是一个onLongClick被触发的基本时序的Log:
04-05 06:00:04.133: DEBUG/TSActivity(248):onTouch ACTION_DOWN
04-05 06:00:04.642: DEBUG/TSActivity(248):onLongClick
04-05 06:00:05.083: DEBUG/TSActivity(248):onTouch ACTION_UP
可以看到,在保持按下的状态一定时间后会触发onLongClick,之后抬起手才会发生ACTION_UP。
3. onClick和onLongClick能同时发生吗?
要弄清楚这个问题只要理解Android对事件处理的所谓消费(consume)概念即可,一个用户的操作会被传递到不同的View控件和同一个控件的不同监听方法处理,任何一个接收并处理了该次事件的方法如果在处理完后返回了true,那么该次event就算被完全处理了,其他的View或者监听方法就不会再有机会处理该event了。
onLongClick的发生是由单独的线程完成的,并且在ACTION_UP之前,而onClick的发生是在ACTION_UP后,因此同一次用户touch操作就有可能既发生onLongClick又发生onClick。这样是不是不可思议?所以及时向系统表示“我已经完全处理(消费)了用户的此次操作”,是很重要的事情。例如,我们如果在onLongClick()方法的最后return true,那么onClick事件就没有机会被触发了。
下面的Log是在onLongClick()方法return false的情况下,一次触碰操作的基本时序:
04-05 06:00:53.023: DEBUG/TSActivity(277):onTouch ACTION_DOWN
04-05 06:00:53.533: DEBUG/TSActivity(277):onLongClick
04-05 06:00:55.603: DEBUG/TSActivity(277):onTouch ACTION_UP
04-05 06:00:55.663: DEBUG/TSActivity(277):onClick
可以看到,在ACTION_UP后仍然触发了onClick()方法。
很多时候,利用触摸屏的Fling、Scroll等Gesture(手势)操作来操作会使得应用程序的用户体验大大提升,比如用Scroll手势在 浏览器中滚屏,用Fling在阅读器中翻页等。在Android系统中,手势的识别是通过GestureDetector.OnGestureListener接口来实现的,不过William翻遍了Android的官方文档也没有找到一个相 关的例子,API Demo中的TouchPaint也仅仅是提到了onTouch事件的处理,没有涉及到手势。Android Developer讨论组里也有不少人有和我类似的问题,结合他们提到的方法和我所做的实验,我将给大家简单讲述一下Android中手势识别的实现。
我们先来明确一些概念,首先,Android的事件处理机制是基于Listener(监听器)来实现的,比我们今天所说的触摸屏相关的事件,就是通过onTouchListener。其次,所有View的子类都可以通过setOnTouchListener()、 setOnKeyListener()等方法来添加对某一类事件的监听器。第三,Listener一般会以Interface(接口)的方式来提供,其中包含一个或多个abstract(抽象)方法,我们需要实现这些方法来完成onTouch()、onKey()等等的操作。这样,当我们给某个view设 置了事件Listener,并实现了其中的抽象方法以后,程序便可以在特定的事件被dispatch到该view的时候,通过callbakc函数给予适 当的响应。
看一个简单的例子,就用最简单的TextView来说明(事实上和ADT中生成的skeleton没有什么区别)。
[java] view plaincopy
1. public class GestureTest extends Activity implements OnTouchListener{
2.
3. @Override
4. protected void onCreate(Bundle savedInstanceState) {
5. super.onCreate(savedInstanceState);
6. setContentView(R.layout.main);
7.
8. // init TextView
9. TextView tv = (TextView) findViewById(R.id.page);
10.
11. // set OnTouchListener on TextView
12. tv.setOnTouchListener(this);
13.
14. // show some text
15. tv.setText(R.string.text);
16. }
17.
18. @Override
19. public boolean onTouch(View v, MotionEvent event) {
20. Toast.makeText(this, "onTouch", Toast.LENGTH_SHORT).show();
21. return false;
22. }
我们给TextView的实例tv设定了一个onTouchListener,因为GestureTest类实现了OnTouchListener 接口,所以简单的给一个this作为参数即可。onTouch方法则是实现了OnTouchListener中的抽象方法,我们只要在这里添加逻辑代码即 可在用户触摸屏幕时做出响应,就像我们这里所做的——打出一个提示信息。
这里,我们可以通过MotionEvent的getAction()方法来获取Touch事件的类型,包括 ACTION_DOWN, ACTION_MOVE, ACTION_UP, 和ACTION_CANCEL。ACTION_DOWN是指按下触摸屏,ACTION_MOVE是指按下触摸屏后移动受力点,ACTION_UP则是指松 开触摸屏,ACTION_CANCEL不会由用户直接触发(所以不在今天的讨论范围,请参考ViewGroup.onInterceptTouchEvent(MotionEvent))。借助对于用户不同操作的判断,结合getRawX()、getRawY()、getX()和getY()等方法来获取坐标后,我们可以实现诸如拖动某一个按钮,拖动滚动条等功能。待机可以看看MotionEvent类的文档,另外也可以看考TouchPaint例子。
回到今天所要说的重点,当我们捕捉到Touch操作的时候,如何识别出用户的Gesture?这里我们需要GestureDetector.OnGestureListener接口的帮助,于是我们的GestureTest类就变成了这个样子。
Java代码
[java] view plaincopy
1. public class GestureTest extends Activity implements OnTouchListener,
2. OnGestureListener {
3. ....
4. }
随后,在onTouch()方法中,我们调用GestureDetector的onTouchEvent()方法,将捕捉到的MotionEvent交给 GestureDetector 来分析是否有合适的callback函数来处理用户的手势。
[java] view plaincopy
1. @Override
2. public boolean onTouch(View v, MotionEvent event) {
3.
4. // OnGestureListener will analyzes the given motion event
5. return mGestureDetector.onTouchEvent(event);
6. }
接下来,我们实现了以下6个抽象方法,其中最有用的当然是onFling()、onScroll()和onLongPress()了。我已经把每一个方法代表的手势的意思写在了注释里,大家看一下就明白了。
// 用户轻触触摸屏,由1个MotionEvent ACTION_DOWN触发Java代码
[java] view plaincopy
1. @Override
2. public boolean onDown(MotionEvent e) {
3.
4. // TODO Auto-generated method stub
5. Toast.makeText(this, "onDown", Toast.LENGTH_SHORT).show();
6.
7. return false;
8. }
9.
10. // 用户轻触触摸屏,尚未松开或拖动,由一个1个MotionEvent ACTION_DOWN触发
11. // 注意和onDown()的区别,强调的是没有松开或者拖动的状态
12.
13. @Override
14. public void onShowPress(MotionEvent e) {
15.
16. // TODO Auto-generated method stub
17. }
用户(轻触触摸屏后)松开,由一个1个MotionEvent ACTION_UP触发
[java] view plaincopy
1. @Override
2.
3. public boolean onSingleTapUp(MotionEvent e) {
4. // TODO Auto-generated method stub
5. return false;
6. }
用户按下触摸屏、快速移动后松开,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE, 1个ACTION_UP触发
[java] view plaincopy
1. @Override
2. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
3.
4. float velocityY) {
5. // TODO Auto-generated method stub
6. return false;
7. }
8.
9. // 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发
10. @Override
11. public void onLongPress(MotionEvent e) {
12. // TODO Auto-generated method stub
13. }
14.
15. // 用户按下触摸屏,并拖动,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE触发
16. @Override
17. public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
18. // TODO Auto-generated method stub
19. return false;
20. }
我们来试着做一个onFling()事件的处理吧,onFling()方法中每一个参数的意义我写在注释中了,需要注意的是Fling事件的处理代 码中,除了第一个触发Fling的ACTION_DOWN和最后一个ACTION_MOVE中包含的坐标等信息外,我们还可以根据用户在X轴或者Y轴上的 移动速度作为条件。比如下面的代码中我们就在用户移动超过100个像素,且X轴上每秒的移动速度大于200像素时才进行处理。
[java] view plaincopy
1. @Override
2. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
3.
4. // 参数解释:
5. // e1:第1个ACTION_DOWN MotionEvent
6. // e2:最后一个ACTION_MOVE MotionEvent
7. // velocityX:X轴上的移动速度,像素/秒
8. // velocityY:Y轴上的移动速度,像素/秒
9. // 触发条件 :
10. // X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒
11. if (e1.getX() - e2.getX() > FLING_MIN_DISTANCE
12. && Math.abs(velocityX) > FLING_MIN_VELOCITY) {
13.
14. // Fling left
15. Toast.makeText(this, "Fling Left", Toast.LENGTH_SHORT).show();
16. } else
17. if (e2.getX() - e1.getX() > FLING_MIN_DISTANCE
18. && Math.abs(velocityX) > FLING_MIN_VELOCITY) {
19.
20. // Fling right
21. Toast.makeText(this, "Fling Right", Toast.LENGTH_SHORT).show();
22. }
23.
24. return false;
25. }
问题是,这个时候如果我们尝试去运行程序,你会发现我们根本得不到想要的结果,跟踪代码的执行的会发现onFling()事件一直就没有被捕捉到。这正是一开始困扰我的问题,这到底是为什么呢?
我在讨论组的Gesture detection这个帖子里找到了答案,即我们需要在onCreate中tv.setOnTouchListener(this);之后添加如下一句代码。
tv.setLongClickable(true);
只有这样,view才能够处理不同于Tap(轻触)的hold(即ACTION_MOVE,或者多个ACTION_DOWN),我们同样可以通过layout定义中的android:longClickable来做到这一点。
这次遇到的这个问题和上次MapView中setOnKeyListener遇到的问题挺类似,其实都是对SDK的了解不够全面,遇到了一次记住了就好。不过话说回来,Google在文档方面确实需要加强了,起码可以在OnGestureListener中说明需要满足那些条件才可以保证手势被正确识别。