Android View的事件分发机制

Posted 從前以後

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View的事件分发机制相关的知识,希望对你有一定的参考价值。

新版的view的事件分发机制相较于老版本的还是改动了一些地方,这个准备记录下来,以免忘记和不时之需。
android中的触摸事件的分发执行顺序是从ViewGroup中的dispatchTouchEvent–onInterceptTouchEvent,如果onInterceptTouchEvent返回true,表示拦截,这时执行ViewGroup中的onTouch方法。如果onInterceptTouchEvent返回false,这时触摸事件就分发到下一级别,也就是ViewGroup 里面的view中了。
代码测试,首先定义一个MyLayout继承LinearLayout

package com.example.administrator.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;


public class MyLayout extends LinearLayout 

    public MyLayout(Context context, AttributeSet attrs) 
        super(context, attrs);
    

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) 
        switch (ev.getAction())
        
            case MotionEvent.ACTION_DOWN:
                Log.e("ethan","layout dispatch Action Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("ethan","layout dispatch Action Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("ethan","layout dispatch Action Up");
                break;
        
        return super.dispatchTouchEvent(ev);

    

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        switch (ev.getAction())
        
            case MotionEvent.ACTION_DOWN:
                Log.e("ethan","InterceptionTouchEvent Action Down");
                return false;
            case MotionEvent.ACTION_MOVE:
                Log.e("ethan","InterceptionTouchEvent Action Move");

                return false;
            case MotionEvent.ACTION_UP:
                Log.e("ethan","InterceptionTouchEvent Action Up");
                return false;
        
        return false;
    


该类重写了LinearLayout中的dispatchTouchEventon和InterceptTouchEvent方法,同时对于每一个事件传递都打印出相应的状态。然后定义一个xml布局文件。

<?xml version="1.0" encoding="utf-8"?>
<com.example.administrator.view.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/myLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/button"

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button" />

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

</com.example.administrator.view.MyLayout>

该布局文件包含一个Button,和一个ImageView,至于为什么包含这两个,因为Button和ImageView的处理效果有一些不一样。下面就是主布局文件

package com.example.administrator.view;


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener 
    private Button button;
    private ImageView imageView;
    private MyLayout mylayot;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.ethan);
        init();
    

    private void init() 
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
        button.setOnTouchListener(new View.OnTouchListener() 
            @Override
            public boolean onTouch(View v, MotionEvent event) 
                switch (event.getAction()) 
                    case MotionEvent.ACTION_DOWN:
                        Log.e("ethan", "button onTouch ACTION_DOWN");
                        return false;
                    case MotionEvent.ACTION_MOVE:
                        Log.e("ethan", "button onTouch ACTION_MOVE");
                        return false;
                    case MotionEvent.ACTION_UP:
                        Log.e("ethan", "button onTouch ACTION_UP");
                        return false;
                
                return false;
            
        );
        imageView = (ImageView) findViewById(R.id.imageview);
        imageView.setOnClickListener(this);
        imageView.setOnTouchListener(new View.OnTouchListener() 
            @Override
            public boolean onTouch(View v, MotionEvent event) 
                switch (event.getAction()) 
                    case MotionEvent.ACTION_DOWN:
                        Log.e("ethan","imageView onTouch ACTION_DOWN");
                        return false;
                    case MotionEvent.ACTION_MOVE:
                        Log.e("ethan","imageView onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e("ethan","imageView onTouch ACTION_UP");
                        return false;
                
                return false;
            
        );
        mylayot = (MyLayout) findViewById(R.id.myLayout);
        mylayot.setOnTouchListener(new View.OnTouchListener() 
            @Override
            public boolean onTouch(View v, MotionEvent event) 
                switch (event.getAction()) 
                    case MotionEvent.ACTION_DOWN:
                        Log.e("ethan","mylayot onTouch ACTION_DOWN");
                        return false;
                    case MotionEvent.ACTION_MOVE:
                        Log.e("ethan","mylayot onTouch ACTION_MOVE");
                        return false;
                    case MotionEvent.ACTION_UP:
                        Log.e("ethan","mylayot onTouch ACTION_UP");
                        return false;
                
                return false;
            

        );
        mylayot.setOnClickListener(this);
        

        @Override
        public void onClick (View v)
            switch (v.getId())
            
                case R.id.button:
                    Log.e("ethan","button clicked");
                    break;
                case R.id.imageview:
                    Log.e("ethan","imageview clicked");
                    break;
                case R.id.myLayout:
                    Log.e("ethan","layout clicked");
                    break;
            

        


    

主类里面分别注册了 layout,button,imageview的onTouch事件和OnClick事件。大体的事件分发机制架构实现了,测试下。
点击 layout(空白处)和button,imageview 分别提示为
layout log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked

button log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
E/ethan: button onTouch ACTION_UP
E/ethan: button clicked

imageview log:

 E/ethan: layout dispatch Action Down
 E/ethan: InterceptionTouchEvent Action Down
 E/ethan: imageView onTouch ACTION_DOWN
 E/ethan: layout dispatch Action Up
 E/ethan: InterceptionTouchEvent Action Up
 E/ethan: imageView onTouch ACTION_UP
 E/ethan: imageview clicked

三次都是点击事件为了简单没有触发Action_Move,咋一看三次点击log都是一样的。同时事件分发是Action_Down分发完毕后,再次从viewgroup分发Action_up,Action_Move同理。这时候在MainActivity中把 button.setOnClickListener(this);和imageView.setOnClickListener(this);都注释掉,再次看一下点击效果。
button log:

 E/ethan: layout dispatch Action Down
 E/ethan: InterceptionTouchEvent Action Down
 E/ethan: button onTouch ACTION_DOWN
 E/ethan: layout dispatch Action Up
 E/ethan: InterceptionTouchEvent Action Up
 E/ethan: button onTouch ACTION_UP

imageview log:

 E/ethan: layout dispatch Action Down
 E/ethan: InterceptionTouchEvent Action Down
 E/ethan: imageView onTouch ACTION_DOWN
 E/ethan: mylayot onTouch ACTION_DOWN
 E/ethan: layout dispatch Action Up
 E/ethan: mylayot onTouch ACTION_UP
 E/ethan: layout clicked

这时候一个奇怪的现象出现了 两个View都没有注册点击事件,button执行到button onTouch ACTION_UP结束,而imageview执行到layout clicked结束。这是因为在事件分发中,一次点击事件首先由ViewGroup分发,如果不拦截,进入到子View中,如果子View中没有消耗该点击事件的方法,那么这次点击事件就会会传到ViewGroup中去执行。 可以想象为机场行李的传送带,行李出来后,相当与点击事件下发到子View中,如果不拿行李,相当于不消耗该点击事件,那么行李还会回传回去,也就是点击事件又传回ViewGroup,由ViewGroup中的onTouch再次进行处理。
但是button的点击事件为什么没有回传到ViewGroup中呢?明明button的注册点击事件也注释掉了。这是因为button默认是可以点击的,而imageview默认是不可点击的。可点击的button,虽然没有注册点击事件,但是系统还是默认它消耗掉了该事件,imageview默认不可点击,默认不消耗该事件。当执行imageView.setOnClickListener(this)的时候相当于imageview也可以点击了,自然和button一样,也就是上面的button和imageview的打印log一样的情况。
这时候在xml中把button的android:clickable=”false”再次运行点击
button log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked

这时候发现,button和imageview都没有注册点击事件,但是两个的log一样了,最终都回传到ViewGroup中进行处理了。
总结:事件分发先从Viewgroup下发,到子View中进行处理,如果子View消耗点击事件,那么事件不回传,如果子View不消耗点击事件,子View回传到ViewGroup中处理。同时就是子View没有注册点击事件,但是只要是子View是可以点击的,那么系统就默认该点击事件被该子View消耗掉了,也就不回传到Viewgroup中处理。
事件分发流程处理完了,继续拦截事件的分析,拿button为例,其余同理:
首先把mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_DOWN:return true。
button log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked

当mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_DOWN:return true的时候也就是down点击事件就被拦截了,那么直接执行layout中的touch方法和click方法,并且Viewgroup再分发Action Up的时候就不经过onInterceptTouchEvent 了,直接执行onTouch ACTION_UP;
这是换一种,在mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_Up:return true其他的都不拦截
button log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up

这时候由于ViewGroup没有拦截action down 所以能够执行button的 ontouch,分发到 Action Up时拦截,就不继续分发了。但是log中并没有显示任何onclick,那么这次点击事件是被谁消耗掉了呢?由于执行了button 的ontouch方法,button可点击并注册了点击事件,那么默认点击事件是被button消耗了,但是由于action up被ViewGroup拦截掉了,因此button的onclick方法并不会执行。这时候把button的clicable改为false 并且取消注册点击事件
button log:

E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked

这时候 看到layout clicked,由于button不可点击并且没有注册点击事件(其实注册点击事件默认的也就把View设置为可点击),因此该点击事件回传到ViewGroup中被ViewGroup消耗掉。

同理,如果在button的ontouch中拦截了点击事件,哪怕只拦截一个,最终button的onclick方法都不会执行,但是点击事件还是被button给消耗掉了。
修改layout 三个都不拦截,在button的ontouch的action up中拦截
button log:

 E/ethan: layout dispatch Action Down
 E/ethan: InterceptionTouchEvent Action Down
 E/ethan: button onTouch ACTION_DOWN
 E/ethan: layout dispatch Action Up
 E/ethan: InterceptionTouchEvent Action Up
 E/ethan: button onTouch ACTION_UP

最终执行到button ontouch action up就不执行了,点击事件被消耗,但是由于action up被拦截因此并不会执行。
总结:事件分发到View的时候,如果view是可以点击的,不管是否注册了setonclicklistener,默认该点击事件被该View给消耗掉,继续下发action move 和action down。如果action down,action move,action up中有一个被拦截,那么虽然消耗点击事件但是不会继续执行onclick方法。如果view是不可点击的,默认该View不能消耗掉点击事件,那么会回传到Viewgroup中,由ViewGroup的Ontouch方法继续执行。

以上是关于Android View的事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android中View的事件分发机制

Android 源码解析View的touch事件分发机制

Android View体系从源码解析View的事件分发机制

Android事件分发机制总结

Android查缺补漏(View篇)--事件分发机制源码分析

android基础 -View的事件分发机制