Android事件传递机制

最近想学习一下自定义View,发现其中有个麻烦的滑动冲突处理问题,为了应对滑动冲突,决定先研究一下Android的事件传递机制。

首先开局一张图,借鉴一篇神文中的图Android事件分发机制详解:史上最全面、最易懂
在这里插入图片描述如果能将上面这张图脱稿画出,基本上事件传递机制也掌握地差不多了。

上图中,将事件传递分为三个模块,一般情况下也就主要是这几个模块的组合,无非是

  • Activity-ViewGroup-View
  • Activity-ViewGroup-ViewGroup-View
  • Activity-ViewGroup-ViewGroup-ViewGroup…-View

所以只需要将第一种最经典的事件传递机制搞清楚了,其他的组合也都类似的处理即可。

首先分析一下各个模块中的关键方法:

Activity
  1. dispatchTouchEvent
    事件分发的原始出口,所有的事件分发都从这里开发,dispatchTouchEvent方法决定是否分发事件,如果这里将事件消费掉(返回true or false),那所有的布局,控件都无法收到事件。
  2. onTouchEvent
    事件的最终处理会在这里,如果ViewGroup不继续分发事件,或者后面所有的布局都不消费事件,那事件的最终归宿就是这里,无论在这返回什么,事件到这将彻底结束,不再分发。
ViewGroup
  1. dispatchTouchEvent
    当ViewGroup的父布局(可能是Activity,可能是ViewGroup)决定分发事件时,即父布局决定分发并且不拦截该事件时,事件将传递给ViewGroup进行分发,这里的分发机制跟Activity有所差异。
    (1)Activity中的dispatchTouchEvent如果返回false,事件将被消费掉,而这里返回false,不会消费事件,而是将事件抛给父布局的onTouchEvent处理
    (2)返回true时与Activity处理一致,消费事件,事件传递到此结束
    (3)调用默认实现super的处理与Activity也不同,Activity会直接扔给子布局的dispatch函数处理,这里会扔给事件拦截函数onInterceptTouchEvent处理。
  2. onInterceptTouchEvent
    onInterceptTouchEvent方法并不消费事件,只有在dispatchTouchEvent默认实现的时候调用,如果返回true表示拦截该事件,交给自己的onTouchEvent处理,否则就扔给子布局分发。
  3. onTouchEvent
    这里的onTouchEvent有三种情况会被调用,一种是onInterceptTouchEvent拦截了该事件,一种是子布局拒绝分发该事件,最后一种是子布局分发了该事件,但并不消费该事件。
    (1)返回true,消费该事件,事件终止
    (2)返回false,不消费事件,事件上抛给父布局的onTouchEvent处理
    (3)默认(super),调用performClick,消费该事件
View
  1. dispatchTouchEvent
    到这里一般是分发的终点了,因为View并没有子布局,往下已经没办法分发了,所以要么在这里消费掉,要么往上抛,下面是没有接盘侠的,所以这里的处理会比ViewGroup处理简单一些:
    (1)返回true,消费掉该事件,事件终结。
    (2)返回false,不接受该事件,抛给ViewGroup自己去处理
    (3)默认实现(super),扔给自己的onTouchEvent去处理
  2. onTouchEvent
    如果调用到这,也是我们日常开发中最常遇到的了,调用到这里只有一种情况,那就是所有的传递没有人为干预,事件按照正常的流转进行的。
    (1)返回true,消费该事件,事件终止
    (2)返回false,不消费事件,事件上抛给父布局的onTouchEvent处理
    (3)默认(super),调用performClick,消费该事件

针对上面分析的方法,再回过头去看图,应该会有更清晰的认识。但程序中所有的认识都是基于实操而加深印象的,任何没有代码的分析都是扯淡,所以要想更深入地了解事件传递机制,只有一个办法,代码验证,虽然通过打log验证的方案很low,但对于理解事件分发流程是没有任何阻碍的,所以这里也就不多废话了,直接上用来测试的代码,大家可以根据测试代码自行验证。

自定义Button:

很简单的自定义Button,只是重写了dispatchTouchEvent和onTouchEvent方法而已。

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

import androidx.appcompat.widget.AppCompatButton;

public class CustomButton extends AppCompatButton {
    public CustomButton(Context context) {
        super(context);
    }

    public CustomButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    /**
     * 当ViewGroup返回super时调用
     * */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e("CustomButton", "CustonButton-----------dispatchTouchEvent");

        /**
         * 默认处理
         * 事件扔给onTouchEvent处理
         * */
        return super.dispatchTouchEvent(event);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 表示事件不再分发,扔给viewGroup的onTouchEvent处理
         * */
        // return false;
    }

    /**
     * 当dispatchTouchEvent返回super时调用
     * */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("CustomButton", "CustonButton-----------onTouchEvent");

        /**
         * 默认处理
         * 调用performClick处理
         * */
        return super.onTouchEvent(event);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 表示事件在这里不做处理,不会调用click事件
         * 而是将事件向上抛,扔给viewGroup的onTouchEvent处理
         * */
        // return false;
    }
}
自定义Layout

很简单的自定义布局,继承自线性布局,重写了dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent方法。

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

public class CustomLayout extends LinearLayout {
    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    /**
     * 当Activity的dispatchTouchEvent返回super时调用
     * */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("CustomLayout", "CustomLayout------------------dispatchTouchEvent");

        /**
         * 默认处理
         * 因为是viewGroup,拥有众多子View
         * 所以调用onInterceptTouchEvent方法判断是否拦截该事件
         * */
        return super.dispatchTouchEvent(ev);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 表示事件不再分发,扔给Activity的onTouchEvent处理
         * */
        // return false;
    }


    /**
     * 只做事件拦截,不消费事件
     * 但dispatchTouchEvent返回super时调用
     * */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("CustomLayout", "CustomLayout------------------onInterceptTouchEvent");

        /**
         * 默认处理
         * 默认不拦截,事件下发到View的dispatchTouchEvent中处理
         * */
        return super.onInterceptTouchEvent(ev);

        /**
         * return true
         * 拦截事件,事件在onTouchEvent中处理
         * */
        // return true;

        /**
         * return false
         * 不拦截,事件下发到View的dispatchTouchEvent中处理
         * */
        // return false;
    }

    /**
     * 当onInterceptTouchEvent返回true时调用
     * */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("CustomLayout", "CustomLayout------------------onTouchEvent");

        /**
         * 默认处理
         * 调用performClick处理
         * */
        return super.onTouchEvent(event);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 表示事件在这里不做处理,不会调用click事件
         * 而是将事件向上抛,扔给Activity的onTouchEvent处理
         * */
        // return false;
    }
}
Activity

最基本的Activity,重写了dispatchTouchEvent和onTouchEvent方法,并对控件设置了点击监听。

import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;

import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    CustomButton customButton;
    CustomLayout customLayout;
    ConstraintLayout activityView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        customButton = findViewById(R.id.custom_button);
        customButton.setOnClickListener(this);
        customLayout = findViewById(R.id.custom_layout);
        customLayout.setOnClickListener(this);
        activityView = findViewById(R.id.activity_view);
        activityView.setOnClickListener(this);
    }

    /**
     * 事件分发的入口,Touch事件的分发就是从这开始的
     * 一旦这里返回true,事件就被消费了,后续将再也收不到任何事件
     * */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("MainActivity", "MainActivity----------dispatchTouchEvent");
        /**
         * 默认处理
         * 将事件下发给ViewGroup的dispatchTouchEvent处理
         * */
        return super.dispatchTouchEvent(ev);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 理论上应该交给DecorView的onTouchEvent处理
         * 一般也就代表被消费了,后续不会出现任何与该事件相关的处理
         * */
         // return false;
    }

    /**
     * 当view和viewGroup的onTouchEvent都返回false,即他们都不消费事件时
     * 事件被抛到最上层Activity处理
     * */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("MainActivity", "MainActivity----------onTouchEvent");

        /**
         * 默认处理
         * 调用performClick处理
         * */
        return super.onTouchEvent(event);

        /**
         * return true
         * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理
         * */
        // return true;

        /**
         * return false
         * 表示事件在这里不做处理
         * 而是将事件向上抛,但因为上面都不处理
         * 所以无论这里返回什么,事件分发到此都结束了
         * 实践证明,即使这里返回false,还是会调用Activity的click方法(这里还需研究一下)
         * */
        // return false;
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.custom_button:
                Log.e("MainActivity", "Button------onClick: button clicked!");
                break;
            case R.id.custom_layout:
                Log.e("MainActivity", "Layout------onClick: layout clicked!");
                break;
            case R.id.activity_view:
                Log.e("MainActivity", "Activity------onClick: activity clicked!");
                break;
        }
    }
}

布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.skydianshi.toucheventtest.CustomLayout
        android:id="@+id/custom_layout"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:background="@color/colorAccent"
        app:layout_constraintTop_toTopOf="parent">

        <com.skydianshi.toucheventtest.CustomButton
            android:id="@+id/custom_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Button"/>
    </com.skydianshi.toucheventtest.CustomLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

针对不同的返回值在代码中都有对应的解释,读者可以放开对应的返回值调试,体验不同返回值下的事件传递机制,体验完了再去看一下最上面的图,就更明白其中所述了。

最终效果就是能默写出事件传递图就基本差不多啦,想要深入到源码探究的,可以点进开头的那篇神文中,其中做了最详尽的解释。

Demo

下面通过一个滑动冲突的解决demo来实践一下事件传递,相信通过这个案例,应该可以更好地理解上文所述。
在这里插入图片描述

上图中A为横向ScrollView,B为纵向ScrollView,A在B中,需要实现的效果很简单:

  • 在A中横向滑动可以正常横向滑动A界面
  • 在B中纵向滑动可以正常纵向滑动B界面
  • 在A中纵向滑动可以纵向滑动B界面

前两个效果很简单,只要正常添加好滚动布局就行了,系统自动就是这种效果。但要实现第三个效果,就要解决A和B的横向纵向滑动冲突问题。

先分析一下怎么实现,到底需要修改哪个布局的哪个方法?肉眼就能看出是A布局,通俗点说,就是在A布局中纵向滑动时不处理事件,把事件扔给B处理不就行了嘛。

对,就是这个意思,老规矩,直接看代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TouchTestActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#999999"
            android:orientation="vertical">

            <com.skydianshi.toucheventtest.CustomHorizontalScrollView
                android:id="@+id/scroll_horizon"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toTopOf="parent">

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="#ffffff"
                    android:orientation="horizontal">

                    <TextView
                        android:layout_width="3000dp"
                        android:layout_height="400dp"
                        android:layout_gravity="center"
                        android:gravity="center"
                        android:text="hello world"
                        android:textSize="50sp"
                        android:textColor="#000000"/>
                </LinearLayout>
            </com.skydianshi.toucheventtest.CustomHorizontalScrollView>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="800dp"
                android:layout_gravity="center"
                android:gravity="center"
                android:textSize="50sp"
                android:textColor="#000000"
                android:text="HELLO WORLD"/>
        </LinearLayout>
    </ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

布局文件很简单,就是两个滚动布局的叠加,并在其中添加了一些控件。

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;

public class CustomHorizontalScrollView extends HorizontalScrollView {

    private float x1 = 0.0f;
    private float x2 = 0.0f;
    private float y1 = 0.0f;
    private float y2 = 0.0f;

    public CustomHorizontalScrollView(Context context) {
        super(context);
    }

    public CustomHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean isHandleEvent = true;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x1 = ev.getX();
                y1 = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x2 = ev.getX();
                y2 = ev.getY();
                isHandleEvent = judgeHorizonMove(x1, x2, y1, y2);
                x1 = ev.getX();
                y1 = ev.getY();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        if (isHandleEvent) {
            // 如果横向移动,就自己处理
            return super.onTouchEvent(ev);
        } else {
            // 如果纵向移动,就不处理,扔给父布局处理
            return false;
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 也可以把上面的判断处理放到这里,如果横向移动,则分发事件,否则拒绝分发,抛给父布局处理。
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /**
         * 也可以把上面的判断处理放到这里,如果横向移动,则拦截事件自己处理,否则就分发下去
         * 但由于分发下去也没人处理,因此还是会将事件向上抛
         * */
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * 判断是否横向移动
     * */
    private boolean judgeHorizonMove(float x3, float x4, float y3, float y4) {
        if (Math.abs(x4 - x3) > Math.abs(y4 - y3)) {
            return true;
        }
        return false;
    }
}

关键的处理就是在这个自定义横向滚动布局中,在这里重写了onTouchEvent方法,当然也可以重写dispatchTouchEvent或者onInterceptTouchEvent方法,都可以实现效果,代码比较简单,关键地方也做了解释,就不啰嗦了。感兴趣的同学可以自己写一下,感受一下事件传递的乐趣,还挺有意思的。

本来想传一下效果图的,拍了一下发现太大,传不了,就不传了,各位可以自行尝试!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值