Android中的事件处理研究


处理用户界面事件Handling UI Events

在Android上,不止一个途径来侦听用户和应用程序之间交互的事件。对于用户界面里的事件,侦听方法就是从与用户交互的特定视图对象截获这些事件。视图类提供了相应的手段。

在各种用来组建布局的视图类里面,你可能会注意到一些公共的回调方法看起来对用户界面事件有用。这些方法在该对象的相关动作发生时被Android框架调用。比如,当一个视图(如一个按钮)被触摸时,该对象上的onTouchEvent()方法会被调用。不过,为了侦听这个事件,你必须扩展这个类并重写该方法。很明显,扩展每个你想使用的视图对象(只是处理一个事件)是荒唐的。这就是为什么视图类也包含了一个嵌套接口的集合,这些接口含有实现起来简单得多的回调函数。这些接口叫做事件侦听器event listeners,是用来截获用户和你的界面交互动作的“门票”。

当你更为普遍的使用事件侦听器来侦听用户动作时,总有那么一次你可能得为了创建一个自定义组件而扩展一个视图类。也许你想扩展按钮Button类来使某些事更花哨。在这种情况下,你将能够使事件处理器event handlers类来为你的类定义缺省事件行为。

 

事件侦听器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 = new OnClickListener() {

    public void onClick(View v) {

      // do something when the button is clicked

    }

};

 

protected void onCreate(Bundle savedValues) {

    ...

    // Capture our button from layout

    Button button = (Button)findViewById(R.id.corky);

    // Register the onClick listener with the implementation above

    button.setOnClickListener(mCorkyListener);

    ...

}

你可能会发现把OnClickListener作为活动的一部分来实现会便利的多。这将避免额外的类加载和对象分配。比如:

public class ExampleActivity extends Activity implements OnClickListener {

    protected void onCreate(Bundle savedValues) {

        ...

        Button button = (Button)findViewById(R.id.corky);

        button.setOnClickListener(this);

    }

 

    // Implement the OnClickListener callback

    public void onClick(View v) {

      // do something when the button is clicked

    }

    ...

}

注意上面例子中的onClick()回调没有返回值,但是一些其它事件侦听器必须返回一个布尔值。原因和事件相关。对于其中一些,原因如下:

·         onLongClick() – 返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处理它。也就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它和/或这个事件应该继续交给其他on-click侦听器。

·         onKey() –返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处理它。也就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它和/或这个事件应该继续交给其他on-key侦听器。

·         onTouch() - 返回一个布尔值来指示你的侦听器是否已经消费了这个事件。重要的是这个事件可以有多个彼此跟随的动作。因此,如果当接收到向下动作事件时你返回false,那表明你还没有消费这个事件而且对后续动作也不感兴趣。那么,你将不会被该事件中的其他动作调用,比如手势或最后出现向上动作事件。

记住按键事件总是递交给当前焦点所在的视图。它们从视图层次的顶层开始被分发,然后依次向下,直到到达恰当的目标。如果你的视图(或者一个子视图)当前拥有焦点,那么你可以看到事件经由dispatchKeyEvent()方法分发。除了从你的视图截获按键事件,还有一个可选方案,你还可以在你的活动中使用onKeyDown() and onKeyUp()来接收所有的事件。

注意: Android 将首先调用事件处理器,其次是类定义中合适的缺省处理器这样,从这些事情侦听器中返回true 将停止事件向其它事件侦听器传播并且也会阻塞视图中的缺事件处理器的回调函数。因此当你返回true时确认你希望终止这个事件。

事件处理器Event Handlers

如果你从视图创建一个自定义组件,那么你将能够定义一些回调方法被用作缺省的事件处理器。在创建自定义组件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属性提供显式的重写:nextFocusDownnextFocusLeftnextFocusRight, 和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一章所描述的那样。

onInterceptTouchEvent和onTouchEvent调用时序

onInterceptTouchEvent()是ViewGroup的一个方法,目的是在系统向该ViewGroup及其各个childView触发onTouchEvent()之前对相关事件进行一次拦截,Android这么设计的想法也很好理解,由于ViewGroup会包含若干childView,因此需要能够统一监控各种touch事件的机会,因此纯粹的不能包含子view的控件是没有这个方法的,如LinearLayout就有,TextView就没有。 

onInterceptTouchEvent()使用也很简单,如果在ViewGroup里覆写了该方法,那么就可以对各种touch事件加以拦截。但是如何拦截,是否所有的touch事件都需要拦截则是比较复杂的,touch事件在onInterceptTouchEvent()和onTouchEvent以及各个childView间的传递机制完全取决于onInterceptTouchEvent()和onTouchEvent()的返回值。并且,针对down事件处理的返回值直接影响到后续move和up事件的接收和传递。 

关于返回值的问题,基本规则很清楚,如果return true,那么表示该方法消费了此次事件,如果return false,那么表示该方法并未处理完全,该事件仍然需要以某种方式传递下去继续等待处理。

SDK给出的说明如下:

·  You will receive the down event here.

·  The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.

·  For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target's onTouchEvent().

·  If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here.

 

由于onInterceptTouchEvent()的机制比较复杂,上面的说明写的也比较复杂,总结一下,基本的规则是:

1.       down事件首先会传递到onInterceptTouchEvent()方法

2.       如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。

3.       如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。

4.       如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。

5.       如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。

 

下面用一个简单的实验说明上述复杂的规则。视图自底向上共3层,其中LayoutView1和LayoutView2就是LinearLayout, MyTextView就是TextView:

对应的xml布局文件如下:

<?xml version="1.0" encoding="utf-8"?>

<com.touchstudy.LayoutView1 xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent" >

    <com.touchstudy.LayoutView2

        android:orientation="vertical"

        android:layout_width="fill_parent"

        android:layout_height="fill_parent"

        android:gravity="center">

       <com.touchstudy.MyTextView 

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:id="@+id/tv"

            android:text="AB"

            android:textSize="40sp"

            android:textStyle="bold"

            android:background="#FFFFFF"

            android:textColor="#0000FF"/>

   </com.touchstudy.LayoutView2>

</com.touchstudy.LayoutView1>

 

下面看具体情况:

1.       onInterceptTouchEvent()处理down事件均返回falseonTouchEvent()处理事件均返回true

------------------------------------------------------------------------------------------------------------------------------

04-11 03:58:42.620: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:58:42.620: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:58:42.620: DEBUG/MyTextView(614): onTouchEvent action:ACTION_DOWN

04-11 03:58:42.800: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_MOVE

04-11 03:58:42.800: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_MOVE

04-11 03:58:42.800: DEBUG/MyTextView(614): onTouchEvent action:ACTION_MOVE

…… //省略过多的ACTION_MOVE

04-11 03:58:43.130: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_UP

04-11 03:58:43.130: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_UP

04-11 03:58:43.150: DEBUG/MyTextView(614): onTouchEvent action:ACTION_UP

------------------------------------------------------------------------------------------------------------------------------

这是最常见的情况,onInterceptTouchEvent并没有做任何改变事件传递时序的操作,效果上和没有覆写该方法是一样的。可以看到,各种事件的传递本身是自底向上的,次序是:LayoutView1->LayoutView2->MyTextView。意,在onInterceptTouchEvent均返回false时,LayoutView1和LayoutView2的onTouchEvent并不会收到事件,而是最终传递给了MyTextView。

 

2.     LayoutView1onInterceptTouchEvent()处理down事件返回true

MyTextViewonTouchEvent()处理事件返回true

------------------------------------------------------------------------------------------------------------------------------

04-11 03:09:27.589: DEBUG/LayoutView1(446): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:09:27.589: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_DOWN

04-11 03:09:27.629: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_MOVE

04-11 03:09:27.689: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_MOVE

…… //省略过多的ACTION_MOVE

04-11 03:09:27.959: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_UP

------------------------------------------------------------------------------------------------------------------------------

从Log可以看到,由于LayoutView1在拦截第一次down事件时return true,所以后续的事件(包括第一次的down)将由LayoutView1本身处理,事件不再传递下去。

 

3.       LayoutView1LayoutView2onInterceptTouchEvent()处理down事件返回false

MyTextViewonTouchEvent()处理事件返回false

LayoutView2onTouchEvent()处理事件返回true

----------------------------------------------------------------------------------------------------------------------------

04-11 09:50:21.147: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/LayoutView2(301): onInterceptTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/MyTextView(301): onTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_DOWN

下面view视图的move和up都没有了

onTouchEvent()事件向上传递

04-11 09:50:21.176: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_MOVE

04-11 09:50:21.176: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_MOVE

04-11 09:50:21.206: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_MOVE

04-11 09:50:21.217: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_MOVE

…… //省略过多的ACTION_MOVE

04-11 09:50:21.486: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_UP

04-11 09:50:21.486: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_UP

----------------------------------------------------------------------------------------------------------------------------

可以看到,由于MyTextView在onTouchEvent()中return false,down事件被传递给其父view,即LayoutView2的onTouchEvent()方法处理,由于在LayoutView2的onTouchEvent()中return true,所以down事件传递并没有上传到LayoutView1。注意,后续的move和up事件均被传递给LayoutView2的onTouchEvent()处理,而没有传递给MyTextView。

 

----------------------------------------------------------------------------------------------------------------

应大家的要求,我把源代码贴上,其实很简单,就是基础文件,主要是用来观察事件的传递。

 

主Activity: InterceptTouchStudyActivity.java:

 

public class InterceptTouchStudyActivity extends Activity {

    static final String TAG = "ITSActivity";

    TextView tv;

   

    /** Called when the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.layers_touch_pass_test);

     }

 }

 


      LayoutView1.java:


      public class LayoutView1 extends LinearLayout {

      private final String TAG = "LayoutView1";

        public LayoutView1(Context context, AttributeSet attrs) {

         super(context, attrs);

         Log.d(TAG,TAG);

     }

 

     @Override

     public boolean onInterceptTouchEvent(MotionEvent ev) {

         int action = ev.getAction();

         switch(action){

         case MotionEvent.ACTION_DOWN:

              Log.d(TAG,"onInterceptTouchEvent action:ACTION_DOWN");

//            return true;

              break;

         case MotionEvent.ACTION_MOVE:

              Log.d(TAG,"onInterceptTouchEvent action:ACTION_MOVE");

              break;

         case MotionEvent.ACTION_UP:

              Log.d(TAG,"onInterceptTouchEvent action:ACTION_UP");

              break;

         case MotionEvent.ACTION_CANCEL:

              Log.d(TAG,"onInterceptTouchEvent action:ACTION_CANCEL");

              break;

         }

        

         return false;

     }

 

     @Override

     public boolean onTouchEvent(MotionEvent ev) {

         int action = ev.getAction();

         switch(action){

         case MotionEvent.ACTION_DOWN:

              Log.d(TAG,"onTouchEvent action:ACTION_DOWN");

              break;

         case MotionEvent.ACTION_MOVE:

              Log.d(TAG,"onTouchEvent action:ACTION_MOVE");

              break;

         case MotionEvent.ACTION_UP:

              Log.d(TAG,"onTouchEvent action:ACTION_UP");

              break;

         case MotionEvent.ACTION_CANCEL:

              Log.d(TAG,"onTouchEvent action:ACTION_CANCEL");

              break;

         }

        

         return true;

     }

 

     @Override

     protected void onLayout(boolean changed, int l, int t, int r, int b) {

         // TODO Auto-generated method stub

         super.onLayout(changed, l, t, r, b);

     }

 

     @Override

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

         // TODO Auto-generated method stub

         super.onMeasure(widthMeasureSpec, heightMeasureSpec);

     }

}


LayoutView2.java:

 

public class LayoutView2 extends LinearLayout {

    private final String TAG = "LayoutView2";

   

    public LayoutView2(Context context, AttributeSet attrs) {

       super(context, attrs);

       Log.d(TAG,TAG);

    }

 

    @Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onInterceptTouchEvent action:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onInterceptTouchEvent action:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onInterceptTouchEvent action:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onInterceptTouchEvent action:ACTION_CANCEL");

           break;

       }

      

       return false;

    }

 

    @Override

    public boolean onTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onTouchEvent action:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onTouchEvent action:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onTouchEvent action:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onTouchEvent action:ACTION_CANCEL");

           break;

       }

      

       return true;

    } 

}


MyTextView.java:

public class MyTextView extends TextView {

    private final String TAG = "MyTextView";

   

    public MyTextView(Context context, AttributeSet attrs) {

       super(context, attrs);

       Log.d(TAG,TAG);

    }

 

    @Override

    public boolean onTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onTouchEvent action:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onTouchEvent action:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onTouchEvent action:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onTouchEvent action:ACTION_CANCEL");

           break;

       }

      

       return false;

    }

   

    public void onClick(View v) {

       Log.d(TAG, "onClick");

    }

   

    public boolean onLongClick(View v) {

       Log.d(TAG, "onLongClick");

       return false;

    }

}


源代码下载:http://download.csdn.net/detail/lxq_xsyu/5896207

Android  onTouchEvent, onClick及onLongClick的调用机制

针对屏幕上的一个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()方法。

 onInterceptTouchEvent用于改变事件的传递方向。决定传递方向的是返回值,返回为false时事件会传递给子控件,返回值为true时事件会传递给当前控件的onTouchEvent(),这就是所谓的Intercept(拦截)。

[tisa ps:正确的使用方法是,在此方法内仅判断事件是否需要拦截,然后返回。即便需要拦截也应该直接返回true,然后由onTouchEvent方法进行处理。]

    onTouchEvent用于处理事件,返回值决定当前控件是否消费(consume)了这个事件。尤其对于ACTION_DOWN事件,返回true,表示我想要处理后续事件;返回false,表示不关心此事件,并返回由父类进行处理。

    可能你要问是否消费了又区别吗,反正我已经针对事件编写了处理代码?答案是有区别!比如ACTION_MOVE或者ACTION_UP发生的前提是一定曾经发生了ACTION_DOWN,如果你没有消费ACTION_DOWN,那么系统会认为ACTION_DOWN没有发生过,所以ACTION_MOVE或者ACTION_UP就不能被捕获。

在没有重写onInterceptTouchEvent()和onTouchEvent()的情况下(他们的返回值都是false), 对上面这个布局,MotionEvent事件的传递顺序如下:

Android中的事件处理研究_第1张图片

当某个控件的onInterceptTouchEvent()返回值为true时,就会发生截断,事件被传到当前控件的onTouchEvent()。如我们将LayoutView2的onInterceptTouchEvent()返回值为true,则传递流程变成:

Android中的事件处理研究_第2张图片

 如果我们同时将LayoutView2的onInterceptTouchEvent()和onTouchEvent()设置成true,那么LayoutView2将消费被传递的事件,同时后续事件(如跟着ACTION_DOWN的ACTION_MOVE或者ACTION_UP)会直接传给LayoutView2的onTouchEvent(),不传给其他任何控件的任何函数。同时传递给子空间一个ACTION_CANCEL事件。传递流程变成(图中没有画出ACTION_CANCEL事件):

Android中的事件处理研究_第3张图片         Android中的事件处理研究_第4张图片



你可能感兴趣的:(android,事件处理)