Android事件分发机制

在我们 Android 开发中经常会遇到多个View、ViewGroup嵌套的问题,例如:当我们在一个ViewPager里面嵌套Fragment,而又在Fragment再次嵌套一个ViewPager的时候,那么两个ViewPager就可能发生冲突,这时候就要我们对分发事件进行处理了。

在一次的完整的事件传递中,主要包括了三个阶段:事件分发、拦截、消费。

触摸事件的类型

首先要有事件的传递,那么先有事件的产生才行。那么事件的产生无疑就是通过手指对屏幕触摸,在触摸后,就会产生一系列的触摸事件,触摸事件对应的MotionEvent类,其类型主要有以下三种:

MotionEvent.ACTION_DOWN  按下View,是所有事件的开始

MotionEvent.ACTION_MOVE  滑动事件

MotionEvent.ACTION_UP    与down对应,表示抬起

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,通常有如下情况:

点击屏幕后立即松开,事件序列为Down -> Up ,

点击屏幕滑动滑动一会在松开,事件序列为Down -> Move => .... => Move -> up .

事件分发的三个阶段

在了解了触摸事件的三种主要类型之后,在讲解Activity、View、ViewGroup事件分发的具体实现之前,先要讲述事件分发分发的三个阶段。

分发(Dispatch):

在Android中,所有的触摸事件都是通过以下方法来分发事件的。

public boolean dispatchTouchEvent (MotionEvent ev)

在这个方法中,根据当前视图的具体实现逻辑,来决定这个事件是直接消耗还是继续分发给子视图。

方法返回true表示事件被当前视图消耗掉,不再继续分发事件;方法返回值为 super.dispatchTouchEvent 表示继续分发这个事件。

如果当前视图是ViewGroup或者是ViewGroup的子类,则会调用 onInterceptTouchEvent 方法判断是否拦截该事件。

拦截(Intercept):

public boolean onInterceptTouchEvent (MotionEvent ev)

这个方法只在ViewGroup或者是ViewGroup的子类中存在,View和Activity中不存在。

同理:返回true表示事件被当前视图消耗掉,不再继续分发事件给子视图,同时交由自身的onTouchEvent方法进行消费;返回 false 或者返回 super.onInterceptTouchEvent 表示继续分发这个事件。

消费(Consume):

public boolean onTouchEvent (MotionEvent ev)

事件的消费对应着 onTouchEvent 方法。

返回 true 表示当前视图可以处理对应的事件,事件不会向上传递给父视图;
返回 false 表示当前视图不处理这个事件,事件会传递给父视图的onTouchEvent方法进行处理。

用例子来深入说明一下吧:

View事件分发

新建一个View , 继承 TextView

public class MyView extends TextView {
    private String TAG = "MyView" ;

    public MyView(Context context) {
        this(context,null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatchTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatchTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatchTouchEvent: ACTION_UP");
                break;
        }

        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onTouchEvent: ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
}

编写 MyActivity ,也实现 onTouchEvent 与 dispatchTouchEvent 方法。同时也为 mTvMyView 设置点击事件与触碰事件。代码如下:

public class MyActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {

    private TextView mTvMyView;
    private  String TAG = "MyActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        mTvMyView = (TextView) findViewById(R.id.tv_my_view);
        mTvMyView.setOnClickListener(this);
        mTvMyView.setOnTouchListener(this);
    }


    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.tv_my_view:
                Log.i(TAG, "MyView onClick ");
                break ;
        }
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (view.getId()){
            case R.id.tv_my_view:
                switch (motionEvent.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        Log.i(TAG, "MyView onTouch: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.i(TAG, "MyView onTouch: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.i(TAG, "MyView onTouch: ACTION_UP");
                        break;
                }
        }
        return false;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onTouchEvent: ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatchTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatchTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatchTouchEvent: ACTION_UP");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

布局如图所示,只是在一个布局中添加了自己的自定义控件而已。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_my"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
        <com.zwr.androideventdispatch.view.MyView
            android:id="@+id/tv_my_view"
            android:layout_width="180dp"
            android:layout_height="180dp"
            android:layout_centerInParent="true"
            android:background="#868062"
            android:gravity="center"
            android:text="MyView"
            android:textSize="35sp" />  
</RelativeLayout>

运行 并 点击 MyView 。

运行结果如下:

MyActivity: dispatchTouchEvent: ACTION_DOWN
MyView: dispatchTouchEvent: ACTION_DOWN
MyActivity: MyView onTouch: ACTION_DOWN
MyView: onTouchEvent: ACTION_DOWN
MyActivity: dispatchTouchEvent: ACTION_UP
MyView: dispatchTouchEvent: ACTION_UP
MyActivity: MyView onTouch: ACTION_UP
MyView: onTouchEvent: ACTION_UP
MyActivity: MyView onClick

在onTouchEvent 与dispatchTouchEvent返回的值有以下三种情况:

  1. 返回true
  2. 返回false
  3. 返回父类的同名方法

不同的返回值,最后事件的分发也会受一定的影响,所以可以画出以下的流程图:

  1. 由上图可知,只要事件被拦截了,就不会再继续分发了。
  2. 先执行onTouch方法,然后执行onClick方法。如果onTouch方法返回了true,那么onClick方法将不会被调用。

ViewGroup的事件分发机制

  1. 现在只要自定义一个ViewGroup并实现onTouchEvent、dispatchTouchEvent、onInterceptTouchEvent方法并

  2. 包裹在上述的自定义View

  3. 进行相似的操作。

     public class MyViewGroup extends RelativeLayout {
         private String TAG = "MyViewGroup";
     
         public MyViewGroup(Context context) {
             this(context,null);
         }
     
         public MyViewGroup(Context context, AttributeSet attrs) {
             this(context, attrs,0);
         }
     
         public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
             super(context, attrs, defStyleAttr);
         }
     
         @Override
         public boolean dispatchTouchEvent(MotionEvent event) {
             switch (event.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_UP");
                     break;
             }
     
             return super.dispatchTouchEvent(event);
         }
     
         @Override
         public boolean onTouchEvent(MotionEvent event) {
             switch (event.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "onTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "onTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "onTouchEvent: ACTION_UP");
                     break;
             }
             return super.onTouchEvent(event);
         }
     
         @Override
         public boolean onInterceptTouchEvent(MotionEvent ev) {
             switch (ev.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_UP");
                     break;
             }
             return super.onInterceptTouchEvent(ev);
         }
     }
    

运行结果如下:

MyActivity: dispatchTouchEvent: ACTION_DOWN
MyViewGroup: dispatchTouchEvent: ACTION_DOWN
MyViewGroup: onInterceptTouchEvent: ACTION_DOWN
MyView: dispatchTouchEvent: ACTION_DOWN
MyActivity: MyView onTouch: ACTION_DOWN
MyView: onTouchEvent: ACTION_DOWN
MyActivity: dispatchTouchEvent: ACTION_UP
MyViewGroup: dispatchTouchEvent: ACTION_UP
MyViewGroup: onInterceptTouchEvent: ACTION_UP
MyView: dispatchTouchEvent: ACTION_UP
MyActivity: MyView onTouch: ACTION_UP
MyView: onTouchEvent: ACTION_UP
MyActivity: MyView onClick

相似的,返回不同的结果,事件的分发就有所不同。总结成流程图如下:

事件的分发由Activity到ViewGroup,再由ViewGroup到子View。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容