Android 事件分发

什么是事件分发

  • 事件:用户通过屏幕与手机交互的时候,每一次点击、长按、移动等都是一个事件。
  • 事件分发机制:某一个事件从屏幕传递到各个View,由View来消费这个事件或者不消费者这一事件的整个过程。
  • 事件分发的对象:系统把事件封装为MotionEvent对象。
  • 事件类型: 按下(ACTION_DOWN) 移动(ACTION_MOVE) 抬起(ACTION_UP)取消(ACTION_CANCEL)
    //取消这个动作例如:在一个可以滚动的listview上有一个button,当点击这个button的时候,不去进行抬起而是去滚动这个listview,button父容器listview就会拦截这个事件,同时button就会接收一个取消事件。也就是对于当前这个button的点击事件已经取消了。
  • 传递层级:Activity -> Window -> DecorView -> ViewGroup -> View

源码分析

  • Activity事件分发流程图
    涉及的主要的两个方法
    dispatchTouchEvent();
    onTouchEvent();

Android 事件分发_第1张图片
我们先从Activity的dispatchTouchEvent方法进入Activity源码查看。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 第一次按下,调该方法
            onUserInteraction();
        }
        /**
        * 事件传递给window,window是抽象类,具体实现是PhoneWindow
        */
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 在当前视图没有view消费该事件则调用Activity的onTouchEvent方法
        return onTouchEvent(ev);
    }

我们再看看PhoneWindow实现的源码。

 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    /**
    * mDecor是DecorView,它是Activity顶层的View,DecorView继承FrameLayout
    */
        return mDecor.superDispatchTouchEvent(event);
    }

我们继续看看 DecorView的superDispatchTouchEvent方法源码。

    public boolean superDispatchTouchEvent(MotionEvent event) {
    /**
     * 继续调用父类的方法,FrameLayout本身没有dispatchTouchEvent这个方法。
     * 在FrameLayout父类,ViewGroup中调用。
     */
        return super.dispatchTouchEvent(event);
    }

截止到这里,从Activity的事件,被传递到了ViewGroup。
而Activity的 getWindow().superDispatchTouchEvent的返回值就是ViewGroup.dispatchTouchEvent的方法返回值。如果getWindow().superDispatchTouchEvent,返回为true,则该事件被消费了,否者调用Activity 的onTouchEvent的方法。
我们看看 Activity的onTouchEvent的方法。

    public boolean onTouchEvent(MotionEvent event) {
    //判断事件是否在可响应范围之类
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

Activity的dispatchTouchEvent方法的返回值就等于 Activity的onTouchEvent方法的返回值。无论返回值是什么到这里都结束了。

  • GroupView 事件分发流程
    涉及主要的方法
    dispatchTouchEvent();
    onInterceptTouchEvent();
    //onTouchEvent方法在ViewGroup中没有,在其父类,View中实现。
    onTouchEvent();
    Android 事件分发_第2张图片
    由于ViewGroup的dispatchTouchEvent方法里面代码比较多,可以自行去查看源码,但总结出来主要是干了三件事。
    1.去判断是否需要拦截事件。
    2.在当前的ViewGroup中找到用户真正点击的View.
    3.分发事件到View上。
  • View事件分发流程
    涉及主要的方法
    dispatchTouchEvent();
    onTouchEvent();
    Android 事件分发_第3张图片
    可自行查看源码。到此事件分发结束,下面来验证一下事件分发。

事件验证

  • 验证事件传递流程
    MainActivity类
public class MainActivity extends AppCompatActivity {
    public  static final String TAG = "事件分发流程";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(MainActivity.TAG, "MainActivity:onTouchEvent: ");
        return super.onTouchEvent(event);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(MainActivity.TAG, "MainActivity:dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }
}

创建MyViewGroup

public class MyViewGroup  extends FrameLayout {
    public MyViewGroup(@NonNull Context context) {
        super(context);
    }
    public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(MainActivity.TAG, "MyViewGroup:onTouchEvent: ");
        return super.onTouchEvent(event);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(MainActivity.TAG, "MyViewGroup:onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(MainActivity.TAG, "MyViewGroup:dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }
}

创建MyView

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(MainActivity.TAG, "MyView:onTouchEvent: ");
        return super.onTouchEvent(event);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(MainActivity.TAG, "MyView:dispatchTouchEvent: ");
        return super.dispatchTouchEvent(event);
    }
}

MainActivity 的activity_main布局

<com.yc.reviewandroid.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/colorPrimaryDark">

<com.yc.reviewandroid.MyView
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@color/colorAccent"/>
com.yc.reviewandroid.MyViewGroup>

运行代码
Android 事件分发_第4张图片
点击红色view查看日志
Android 事件分发_第5张图片
总结:
事件默认传递流程
MainActivity:dispatchTouchEvent -> MyViewGroup:dispatchTouchEvent
-> MyViewGroup:onInterceptTouchEvent -> MyView:dispatchTouchEvent
-> MyView:onTouchEvent -> MyGroupView:onTouchEvent
-> MainActivity:onTouchEvent

Android 事件分发_第6张图片
总结:同一个事件,如果子View(ViewGroup)没有消费该事件,
那么后续事件 就不会再传递到子View中。
MainActivity:dispatchTouchEvent -> MainActivity:onTouchEvent

  • 验证cancel事件
    创建MyScrollView
public class MyScrollView extends ScrollView {
    public MyScrollView(Context context) {
        super(context);
    }
    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result=super.onInterceptTouchEvent(ev);
        Log.d(MainActivity.TAG, "MyScrollView:onInterceptTouchEvent: "+result);
        return result;
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }
}

创建MyButton

public class MyButton extends AppCompatButton {
    public MyButton(Context context) {
        super(context);
    }
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_CANCEL){
            Log.d(MainActivity.TAG, "MyButton:onTouchEvent: 取消");
        }else{
            Log.d(MainActivity.TAG, "MyButton:onTouchEvent: "+event.getAction());
        }
        return super.onTouchEvent(event);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_CANCEL){
            Log.d(MainActivity.TAG, "MyButton:dispatchTouchEvent: 取消");
        }else{
            Log.d(MainActivity.TAG, "MyButton:dispatchTouchEvent: "+event.getAction());
        }
        return super.dispatchTouchEvent(event);
    }
}

修改MainActivity 的activity_main布局

<com.yc.reviewandroid.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark"
    tools:context=".MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.yc.reviewandroid.MyButton
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@color/colorAccent"
            android:text="button" />
        <View
            android:layout_width="match_parent"
            android:layout_height="2000dp" />
    LinearLayout>

com.yc.reviewandroid.MyScrollView>

运行代码
Android 事件分发_第7张图片
点击button,不抬起继续向上滚动列表,然后查看打印日志
Android 事件分发_第8张图片
MyScrollView的onInterceptTouchEvent返回true,事件被拦截,MyButton的dispatchTouchEvent就产生了
取消事件

  • 验证ViewGroup事件拦截
    还原MainActivity 的activity_main布局
<com.yc.reviewandroid.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/colorPrimaryDark">

<com.yc.reviewandroid.MyView
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@color/colorAccent"/>
com.yc.reviewandroid.MyViewGroup>

修改MyViewGroup里面的代码

public class MyViewGroup extends FrameLayout {
    public MyViewGroup(@NonNull Context context) {
        super(context);
    }
    public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(MainActivity.TAG, "MyViewGroup:onTouchEvent: 手指按下");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(MainActivity.TAG, "MyViewGroup:onTouchEvent: 手指移动");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(MainActivity.TAG, "MyViewGroup:onTouchEvent: 手指抬起");
                break;
        }
        super.onTouchEvent(event);
        return true;
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(MainActivity.TAG, "MyViewGroup:onInterceptTouchEvent: ");
        super.onInterceptTouchEvent(ev);
        return true;
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(MainActivity.TAG, "MyViewGroup:dispatchTouchEvent:");
        return super.dispatchTouchEvent(ev);
    }
}

运行点击MyView,查看日志
Android 事件分发_第9张图片
日志显示到MyViewGroup 手指抬起就终止了,MyView没被打印,事件被MyViewGroup的onTouchEvent消费了。

结言

我也是学习别人的自己总结记录下来,方便以后查看,俗话说,好记性不如烂笔头,文章写得不是很好,不喜勿喷,谢谢,有错误的地方,欢迎留言。

你可能感兴趣的:(Android学习专栏,Android,事件分发)