Android触摸事件分发机制

本文部分内容参考自这篇博客:Android事件分发机制 详解攻略,您值得拥有,非常感谢作者Carson_Ho

1. 事件基础

事件分发过程中的对象:Touch事件(当用户触摸屏幕时(View或ViewGroup派生的视图控件),将产生点击事件)。

Touch事件的相关细节被封装到MotionEvent对象中(发生触摸的位置、时间等)。

事件类型:

(1)MotionEvent.ACTION_DOWN:按下手指(事件开始);

(2)MotionEvent.ACTION_MOVE:手指移动;

(3)MotionEvent.ACTION_UP:抬起手指(事件结束);

(4)MotionEvent.CANCEL:事件结束(非用户操作原因正常结束);

事件列:从手指触摸屏幕到手指离开屏幕,这个过程中产生的一系列事件。

事件分发的本质:将用户触摸屏幕产生的MotionEvent事件对象传递到某个具体的View上,并对该事件进行响应及处理的过程。

事件在这些对象中传递:Activity ——> ViewGroup ——> View(从父级元素往子级元素进行传递)。

事件分发过程中的相关方法:

(1)dispatchTouchEvent:分发传递点击事件(当事件对象能够传递到当前ViewGroup或View时就会执行);

(2)onInterceptTouchEvent:在ViewGroup控件的dispatchTouchEvent方法中调用,表示是否需要拦截事件对象的向下传递;

(3)onTouchEvent:同样在dispatchTouchEvent方法中调用,用于处理(消费)接收到的事件;

如果同时对父View和子View设置触摸事件回调,优先响应的一定是子View的触摸事件回调。

假设当前有嵌套的Activity ——> ViewGroup ——> View,用户手指点击处于最深层的子View,此时dispatchTouchEvent将会把事件对象从上往下传递,在传递到最深的子View节点后,默认情况下将按照View ——> ViewGroup ——> Activity的顺序执行onTouchEvent方法(所以尽管传递事件是从父控件到子控件,但是优先响应的依旧是最深的子控件节点)。

dispatchTouchEvent和onTouchEvent方法均返回布尔值,表示当前事件是否被消费(处理)。

当某个控件想要自行消费某个事件,需要重写该控件的onTouchEvent方法并返回true。此时事件列中的所有关联事件都将在该子控件的onTouchEvent方法中消费(处理),相关事件不会再冒泡传递给父级的ViewGroup和Activity的onTouchEvent方法执行,最终在子控件的dispatchTouchEvent方法中返回true,表示相关事件已经被消费。

假如某个中间控件ViewGroup想要拦截MotionEvent.ACTION_DOWN事件的向下传递,需要在该ViewGroup控件中重写onInterceptTouchEvent方法,并返回true。这样事件在从上往下分发时,在执行到该ViewGroup的dispatchTouchEvent方法中的onInterceptTouchEvent方法后,所有当前ViewGroup的子View或子ViewGroup的dispatchTouchEvent方法将不会执行,事件将不会继续往下分发。

下面通过自定义ViewGroup和View的方式,来验证以上结论。

 

2. 例子

在这个例子中,我们会自定义ViewGroupA,ViewGroupB以及ViewA,嵌套关系如下:ViewGroupA包含ViewGroupB,ViewGroupB包含ViewA,同时,所有的ViewGroup和View都是相对于父级控件居中显示,activity_dispatch_event.xml布局文件代码如下:

<?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=".CustomViews.DisptachEventActivity">

    <com.zjhtest.learnviewwidget.CustomViews.ViewGroupA
        android:layout_width="320dp"
        android:layout_height="320dp"
        android:layout_marginTop="20dp"
        android:background="@color/colorAccent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.zjhtest.learnviewwidget.CustomViews.ViewGroupB
            android:layout_width="240dp"
            android:layout_height="240dp"
            android:background="@color/colorPrimary">

            <com.zjhtest.learnviewwidget.CustomViews.ViewA
                android:layout_width="160dp"
                android:layout_height="160dp"
                android:background="@color/colorPrimaryDark" />

        </com.zjhtest.learnviewwidget.CustomViews.ViewGroupB>

    </com.zjhtest.learnviewwidget.CustomViews.ViewGroupA>
</androidx.constraintlayout.widget.ConstraintLayout>

然后是ViewGroupA的自定义过程,自定义ViewGroup主要是需要定义构造函数,以及重写onMeasure方法和onLayout方法。

首先是标准的三个构造函数:

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

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

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

接着在onMeasure方法中,需要遍历子View,然后调用measureChild方法通知子View对自身进行测量。代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);  // 通知子View对自身进行测量
        }
    }

在onLayout方法中,同样需要遍历子View,然后根据子View的measure值(可以简单理解为宽高),确定子View如何在父View中进行布局。代码如下:

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int count = getChildCount();
        for (int j = 0; j < count; j++) {
            View childView = getChildAt(j);
            if (childView.getVisibility() != View.GONE) {
                int viewGroupAWidth = getMeasuredWidth();
                int viewGroupAHeight = getMeasuredHeight();
                int childViewWidth = childView.getMeasuredWidth();
                int childViewHeight = childView.getMeasuredHeight();

                int childViewHorizontal = (viewGroupAWidth - childViewWidth) / 2;
                int childViewVertical = (viewGroupAHeight - childViewHeight) / 2;
                childView.layout(childViewHorizontal, childViewVertical, childViewHorizontal + childViewWidth, childViewVertical + childViewHeight);
            }
        }
    }

上面代码中,通过getMeasuredWidth和getMeasuredHeight方法获取父级ViewGroup和子View的宽高,计算出子View的在视图坐标系(相对于父视图)中的位置(左上右下),调用子View的layout方法进行布局。一定要注意,左上右下的值都是相对于坐标系的值,如下图:

所以上面的代码中,为了子View能够居中显示,左(上)的值是父控件宽(高)与子控件宽(高)差值的一半。而右(下)则需要在左(上)的基础上加上子控件的宽(高)。

ViewGroupB的定义方式与ViewGroupA完全相同,因此ViewGroupB完全可以继承ViewGroupA。但是为了避免在接下来的日志打印输出事件传递调用方法的过程中出现ViewGroupB的dispatchTouchEvent方法调用了父级ViewGroupA的dispatchTouchEvent方法,导致输出日志不“清晰”的问题。所以这里直接复制ViewGroupA中的onMeasure方法和onLayout方法到ViewGroupB中。

ViewA的定义方式也很简单,继承View即可,由于这里不需要对子View的布局和渲染进行额外的处理,因此使用默认继承的onMeasure、onLayout以及onDraw方法即可。

到这里,嵌套的自定义ViewGroup和View就完成,显示效果如下:

接下来就开始测试事件的分发过程,在ViewGroupA和ViewGroupB中,重写如下方法:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(CommonTag, "ViewGroupA dispatchTouchEvent " + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(CommonTag, "ViewGroupA onInterceptTouchEvent " + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(CommonTag, "ViewGroupA onTouchEvent " + event.getAction());
        return super.onTouchEvent(event);
    }

在ViewA中则只重写dispatchTouchEvent和onToucheEvent方法。

然后点击ViewA,LogCat输出如下:

com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewA dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewA onTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA onTouchEvent 0

从输出中,我们可以看出,事件的派发是由父控件到子控件的过程,而事件的处理,则是子控件有优先的响应权。

接下来,我们重写ViewGroupB的onInterceptTouchEvent,返回true,禁止事件的向下传递,看看会输出什么结果:

com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA onTouchEvent 0

可以看到,由于事件被ViewGroupB拦截,所以ViewA的dispatchTouchEvent方法并没有执行,事件传递到ViewGroupB就终止了,接着执行ViewGroupB的onTouch方法处理事件。当然,如果我们不仅希望ViewGroupB拦截事件的向下传递,还想独自响应并处理该事件,可以重写其onTouch方法,并返回true。输出结果如下:

com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupA onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB dispatchTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onInterceptTouchEvent 0
com.zjhtest.learnviewwidget D/Dispatch Event: ViewGroupB onTouchEvent 0

可以看到,ViewGroupB的onTouch方法执行后,ViewGroupA的onTouch方法并没有执行。ACTION_DOWN事件被ViewGroupB独自“消化”了。

总之,整个事件的传递和处理流程,我们只需要记住以下两点:

(1)事件传递是从父控件到子控件的过程,只要其中的某个ViewGroup的onInterceptTouchEvent方法返回true,则会拦截该事件继续向子控件传递(默认是false);

(2)事件的处理是从子控件到父控件的过程,子控件有优先处理的权力。如果子控件(View或ViewGroup)不希望父控件继续处理当前的事件,则需要在onTouch方法中返回true(默认是false)。

感谢阅读~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值