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