SouthWind0 2013-07-08
原文自:http://android.eoe.cn/topic/ui
在Android中有多种方法可以用来拦截用户与程序的交互事件。如果想处理用户界面中触发的事件,可以通过从用户交互的View捕获事件来实现。View这个类提供了这些方法。
在用来构成布局的各种View类中,我们可以看到有几个用于UI事件的公共回调方法。当这些对象中有用户行为产生时,Android框架就会调用相应的回调方法。例如,当一个view(比如一个按钮Button)被触摸了,那么它的onTouchEvent()方法就会被调用。但是,为了拦截到这个事件,你必须扩展这个类并重写这些方法。然而,为了处理这样一个事件就扩展每一个View对象是不实际的。这就是为什么View类还提供了包含一些很方便定义的回调方法的嵌套接口的原因。这些叫做事件监听器(event listener)接口就是你拦截UI交互事件的入口。
虽然多数情况下使用事件监听器来监听用户交互,但是有时候为了创建一个自定义的组件,也需要扩展一个View类。比如你可能想要扩展一个Button类来使它更好用。这种情况下,你就可以使用事件处理器(event handler)来定义默认的事件行为。
一个事件监听器是View类中一个包含单一回调方法的接口。当注册了监听器的View发生了跟监听器对应的UI交互事件时,Android框架就会调用这些回调方法。
事件监听器接口中包含了以下回调方法:
onClick()
:来自View.OnClickListener接口。当用户触摸了一个对象(在触摸模式下),或者通过导航键或轨迹球使它获得焦点后再按下兼容"enter"的按键或是按下轨迹球,这个方法被调用。
onLongClick()
:来自View.OnLongClickListener接口。当用户在一个对象上触摸并按住不放(触摸模式下),或者通过导航键或轨迹球使它获得焦点后再按下兼容"enter"的按键或是轨迹球并按住不放(一秒钟时间),这个方法被调用。
onFocusChange()
:来自View.OnFocusChangeListener。当用户使用导航键或者轨迹球导航到一个对象或离开一个对象时,这个方法被调用。
onKey()
:来自View.OnKeyListener接口。当用户让焦点落在一个对象上后按下或释放设备上的一个键时,这个方法被调用。
onTouch()
:来自View.OnTouchListener接口,当用户执行一个触屏事件的动作时,包括按下动作、释放动作,或者任何在屏幕上的手势(当然必须在对象所在的区域内),这个方法被调用。
onCreateContextMenu()
:来自View.OnCreateContextMenuListener接口。当一个上下文菜单被创建(长按后的结果)时,这个方法被调用。关于上下文菜单的更多讨论,请参考关于Menus | 菜单 - Menus的开发指南。
这些方法是各自接口的唯一成员。如果要定义这些方法来处理事件,就要在Activity中实现这些嵌套接口,或者定义一个匿名内部类。然后,把你实现的实例传递给对应的View.set…Listener()方法。(例如,以实现的OnClickListener作为参数调用setOnClickListener()方法)。
下面的例子演示了如何给一个Button注册一个on-click监听器:
// 创建一个实现了OnClickListener的匿名内部类的对象
private OnClickListener mCorkyListener = new OnClickListener() {
public void onClick(View v) {
// do something when the button is clicked
}
};
protected void onCreate(Bundle savedValues) {
...
// 从布局中找到需要的按钮
Button button = (Button)findViewById(R.id.corky);
// 用上面实现的对象注册点击监听器
button.setOnClickListener(mCorkyListener);
...
}
你可能也发现把OnClickListener作为Activity的一部分来实现会更方便。这样可以避免额外的类加载和对象内存分配。如:
public class ExampleActivity extends Activity implements OnClickListener {
protected void onCreate(Bundle savedValues) {
...
Button button = (Button)findViewById(R.id.corky);
button.setOnClickListener(this);
}
1 2 3 4 5 | // 实现OnClickListener的回调
public void onClick(View v) {
// 这里做按钮点击后需要做的事情
}
... |
}
注意上例中的onClick()回调方法没有返回值,但是有些其他的事件监听器方法必须要返回一个布尔值。具体情况取决于事件。以下是针对部分情况进行说明:
* onLongClick() - 这个方法返回一个布尔值,用来指明这个事件是否已经被你消耗(使用)了而不应进一步传递。也就是说,如果返回true,表明你已经处理了这个事件,并且事件应该就此停止;如果返回false,表明你没有处理这个事件,并且/或者这个事件应该继续传递给其他的on-click监听器来处理。
* onKey() - 这个方法返回一个布尔值,用来指明这个事件是否已经被你消耗了而不应进一步传递。也就是说,如果返回true,表明你已经处理了这个事件,并且事件应该就此停止;如果返回false,表明你没有处理这个事件,并且/或者这个事件应该继续传递给其他的on-key监听器来处理。
* onTouch() - 这个方法返回一个布尔值,用来指明你的监听器是否消耗这个事件。重要的是这个事件可能包含多个彼此跟随的动作。因此,如果在接收到按下动作事件时返回了false,表明你不使用这个事件,并且对这个事件中按下动作的后续动作也不感兴趣了。这样的话,你将不会再为这个事件中的任何其他动作调用这个方法,比如手势或者结束动作事件。
记住,硬件按键事件始终是发送给当前焦点所在的View。它们从View层次结构的最顶层开始从上而下派发,直到到达合适的目标。如果你当前的View(或View的子视图)有焦点,那么通过dispatchKeyEvent()方法你能够看到事件的派发路线。通过View捕获按键事件的另一个方法是,你可以在Activity内部通过onKeyDown()和onKeyUp()两个方法来接受所有的按键事件。
而且,当考虑到程序的文本输入的时候,请注意许多设备只有软键盘输入法。这些输入法不要求必须有实体按键;一些设备可能使用语音输入、手写输入等等。即使一个输入法展现了像实体键盘一样的界面,通常它也不会触发onKeyDown()一类的事件。你永远不应该让你的UI依赖特定的按键来实现操作,除非你想将你的程序限制在拥有实体键盘的设备上。尤其,不要依赖在这些按键上按下return键来确认输入;而应该用IME_ACTION_DONE一类的动作来表示输入法完成了程序中你想要的动作,以便程序用应有的方式来改变UI。不用去猜测软键盘工作的方式,只相信它会为程序提供格式化的文本就够了。
注意:Android会首先调用事件处理器,然后再调用类定义的合适的默认处理程序。这样,如果这些事件监听器返回了true,那么事件向其他事件监听器的传递会被中止,View中默认的事件处理程序的回调也会被阻止。因此,当你却确信要中止一个事件的时候才返回true。
如果你要从View构建一个自定义的组件,你可以定义几个回调方法作为默认的事件处理器。在Custom Components | 自定义组件 - Custom Components文档中,你可以学到一些用来处理事件的常用回调,包括:
* onKeyDown(int, KeyEvent) - 当新的按键事件产生的时候调用
* onKeyUp(int, KeyEvent) - 当一个按键松开事件产生的时候调用
* onTrackballEvent(MotionEvent) - 当轨迹球滚动的时候调用
* onTouchEvent(MotionEvent) - 当触屏动作产生时调用
* onFocusChanged(boolean, int, Rect) - 当View获取或者失去焦点的时候调用
另外还有一些需要注意的方法,它们不属于View类,但是能够直接影响到你处理事件的方式。因此,当你管理布局中更复杂的事件的时候,考虑下面这些方法:
* Activity.dispatchTouchEvent(MotionEvent) - 这个方法允许Activity在触屏事件被传递到window之前拦截它们。
* ViewGroup.onInterceptTouchEvent(MotionEvent) - 这个方法允许ViewGroup查看传递到它的子view的触屏事件。
* ViewParent.requestDisallowInterceptTouchEvent(boolean) - 在父View中调用这个方法来表明它不应该使用onInterceptTouchEvent(MotionEvent)来拦截触屏事件。
当用户使用方向键或者轨迹球浏览到一个用户界面的时候,就需要把焦点交给可交互的对象(比如按钮)以便用户能够看到这些对象能够接受输入。但是,如果一个设备支持触摸,并且用户通过触摸来实现交互,那么就不必高亮显示这些对象或者把焦点交给某个View了。这样,就有了一个名为触摸模式的交互模式了。
对一个支持触摸的设备,一旦用户触摸了屏幕,设备就进入了触摸模式。从此,只要一个View的isFocusableInTouchMode()方法返回值为true,那它就是可以获得焦点的,比如一个文本编辑框。其他可以触摸的View比如按钮,在被触摸的时候是不会获得焦点的;它们仅仅是在按下的时候触发一下它们on-click监听器。
每当用户点击了一个方向键或者滚动轨迹球的时候,设备就将退出触摸模式,并寻找一个View让其获得焦点。现在,用户就恢复到了不需要触摸屏的交互模式了。
触摸模式的状态是整个系统维护的(所有的窗口和活动视图)。要查看当点设备所处的触摸模式状态,可以调用isInTouchMode()方法得到当前设备是否处于触摸模式。
Android框架会处理焦点的移动来响应用户的输入,包括当View被删除或隐藏,或者新的View变得可见时的焦点变化。View通过isFocusable()方法来表明它们是否希望获得焦点。通过调用setFocusable()方法来设置一个View能否获得焦点。在触摸模式下,你通过isFocusableInTouchMode()方法查询一个View是否允许获得焦点。当然你可以通过调用setFocusableInTouchMode()方法来改变设置。
焦点的移动是基于一种在特定方向查找最近的可接受焦点的View的算法。在某些不常见的情况下,这种默认的算法找到的焦点可能与开发者的期望表现不一致。这时,你就要在下面的布局文件中的xml属性来精确指定焦点的移动:nextFocusDown, nextFocusLeft, nextFocusRight, 以及 nextFocusUp。给将要失去焦点的View添加一个上面的属性,在属性值中定义下一个将要获得焦点的View的id。比如:
通常,在这样一个垂直布局中,从第一个按钮向上浏览不会到任何地方,同样从第二个按钮向下浏览也不会到任何地方。现在,top按钮定义下一个向上导航的焦点获得者为bottom按钮(反之亦然),这样焦点就可以在这两个按钮之间上下循环了。
如果你习惯在UI中声明一个默认拥有焦点的View(通常不这样做),就需要在布局声明文件中给这个View添加一个android:focusable属性并把这个属性值设置为true。你也可以在触摸模式中使用android:focusableInTouchMode属性来定义默认拥有焦点的View。
要强制让某个View获得焦点,调用它的requestFocus()方法。
就像上面“事件监听器”章节讨论的一样,使用onFocusChange()方法来监听焦点变化事件(当一个View获取到或者失去焦点的时候会发出通知)。