[Android进阶笔记]Android触摸事件的拦截机制

本文详细介绍了Android中触摸事件的分发与响应机制,从自顶向下的分发到自底向上的响应。重点探讨了View和ViewGroup在事件处理中的角色,包括dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent的方法作用。通过实例分析,解释了如何控制事件的拦截和传递,帮助读者深入理解Android事件处理的内部工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一句话总是最重要的:

Android的拦截机制是一个自顶向下的事件分发与自底向上的事件响应机制

自顶向下的分发,就是我从View树的顶部开始向下分发事件
自底向上的响应,就是当事件传递到View树的底层都不被拦截,那么他就开始往上层层响应

View


View有2个方法 dispatchTouchEvent和onTouchEvent,源码如下:

3363        public boolean dispatchTouchEvent(MotionEvent event) {
3364            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
3365                    mOnTouchListener.onTouch(this, event)) {
3366                return true;
3367            }
3368            return onTouchEvent(event);
3369        }

根据我上边的话,可以这么理解
dispatchTouchEvent 是用来 事件分发的
onTouchEvent 是用来事件响应的
在没有设置OnTouchListener的情况下,当事件分发到该View上,dispatchTouchEvent和onTouchEvent就会被调用。如果onTouchEvent返回false,则事件继续向下传递。

View的onTouchEvent源码


3792        public boolean onTouchEvent(MotionEvent event) {
3793            final int viewFlags = mViewFlags;
3794    
3795            if ((viewFlags & ENABLED_MASK) == DISABLED) {
3796                // A disabled view that is clickable still consumes the touch
3797                // events, it just doesn't respond to them.
3798                return (((viewFlags & CLICKABLE) == CLICKABLE ||
3799                        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
3800            }
3801    
3802            if (mTouchDelegate != null) {
3803                if (mTouchDelegate.onTouchEvent(event)) {
3804                    return true;
3805                }
3806            }
3807    
3808            if (((viewFlags & CLICKABLE) == CLICKABLE ||
3809                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
3810                switch (event.getAction()) {
3811                    case MotionEvent.ACTION_UP:
3812                        if ((mPrivateFlags & PRESSED) != 0) {
3813                            // take focus if we don't have it already and we should in
3814                            // touch mode.
3815                            boolean focusTaken = false;
3816                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
3817                                focusTaken = requestFocus();
3818                            }
3819    
3820                            if (!mHasPerformedLongPress) {
3821                                // This is a tap, so remove the longpress check
3822                                if (mPendingCheckForLongPress != null) {
3823                                    removeCallbacks(mPendingCheckForLongPress);
3824                                }
3825    
3826                                // Only perform take click actions if we were in the pressed state
3827                                if (!focusTaken) {
3828                                    performClick();
3829                                }
3830                            }
3831    
3832                            if (mUnsetPressedState == null) {
3833                                mUnsetPressedState = new UnsetPressedState();
3834                            }
3835    
3836                            if (!post(mUnsetPressedState)) {
3837                                // If the post failed, unpress right now
3838                                mUnsetPressedState.run();
3839                            }
3840                        }
3841                        break;
3842    
3843                    case MotionEvent.ACTION_DOWN:
3844                        mPrivateFlags |= PRESSED;
3845                        refreshDrawableState();
3846                        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
3847                            postCheckForLongClick();
3848                        }
3849                        break;
3850    
3851                    case MotionEvent.ACTION_CANCEL:
3852                        mPrivateFlags &= ~PRESSED;
3853                        refreshDrawableState();
3854                        break;
3855    
3856                    case MotionEvent.ACTION_MOVE:
3857                        final int x = (int) event.getX();
3858                        final int y = (int) event.getY();
3859    
3860                        // Be lenient about moving outside of buttons
3861                        int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
3862                        if ((x < 0 - slop) || (x >= getWidth() + slop) ||
3863                                (y < 0 - slop) || (y >= getHeight() + slop)) {
3864                            // Outside button
3865                            if ((mPrivateFlags & PRESSED) != 0) {
3866                                // Remove any future long press checks
3867                                if (mPendingCheckForLongPress != null) {
3868                                    removeCallbacks(mPendingCheckForLongPress);
3869                                }
3870    
3871                                // Need to switch from pressed to not pressed
3872                                mPrivateFlags &= ~PRESSED;
3873                                refreshDrawableState();
3874                            }
3875                        } else {
3876                            // Inside button
3877                            if ((mPrivateFlags & PRESSED) == 0) {
3878                                // Need to switch from not pressed to pressed
3879                                mPrivateFlags |= PRESSED;
3880                                refreshDrawableState();
3881                            }
3882                        }
3883                        break;
3884                }
3885                return true;
3886            }
3887    
3888            return false;

如果消化了点击事件的Action_Down,则返回true。若不消耗则返回false,返回false则对应的dispatchTouchEvent也返回fasle。 则表示不拦截,继续向下分发事件。至于怎么继续向下分发呢,这个放在ViewGroup的事件分发机制来说。返回True 则表示这个View拦截这个事件,这样这个完整的点击事件(Action_Down到Up)都交给这个View来处理。

ViewGroup

ViewGroup有三个方法 dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent

调用流程的伪代码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev)
    }else{
        consume = child.dispatchTouchEvent(ev)
    }
    return consume;
}

再看看dispatchTouchEvent源码里的一小段

if (child.dispatchTouchEvent(ev))  {
// Event handled, we have a target now.
    mMotionTarget = child;
    return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.

依次调用ViewGroup里所包含的childView的dispatchTouchEvent,若该ViewGroup里的childView,有一个选择了拦截,那么这个ViewGroup的dispatchTouchEvent就返回True,而事件的传递也就会在拦截的childView的onTouchEvent 里停止。如果ViewGroup里所有的childView的dispatchTouchEvent都返回false,即子View都不拦截,那么就相当于这个点击事件已经传递到底了,只能逐层向上响应,即调用该ViewGroup的onTouchEvent,直到某个ViewGroup的onTouchEvent返回True,否则一直向上响应,直至rootView。

废话一堆不如代码来的实在。请看例子!
先写2个自定义的ViewGroup,ViewGroupB代码同ViewGroupA,只是在三个事件拦截的方法里加了打印信息

package com.dongua.toucheventtest;


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

public class ViewGroupA extends RelativeLayout
{

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("click", "dispatchTouchEvent: ViewGroupA");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("click", "onInterceptTouchEvent: ViewGroupA");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("click", "onTouchEvent: ViewGroupA");
        return super.onTouchEvent(event);
    }


}

和一个自定义的View,这里先继承自TextView,因为TextView对点击事件是默认不响应,当然如果熟悉的话你继承一个Button,然后手动的在dispatchTouchEvent里返回fasle,使button不拦截也是ok 的~

package com.dongua.toucheventtest;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * Created by dongua on 2016/11/21.
 */
public class ViewC extends TextView {

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

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

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


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("click", "dispatchTouchEvent: ViewC");
        return super.dispatchTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("click", "onTouchEvent: ViewC");
        return super.onTouchEvent(event);
    }
}

然后写一下布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="com.dongua.toucheventtest.MainActivity">

    <com.dongua.toucheventtest.ViewGroupA
        android:id="@+id/A"
        android:background="#0000ff"
        android:layout_width="200dp"
        android:layout_height="200dp">
        <com.dongua.toucheventtest.ViewGroupB
            android:id="@+id/B"
            android:background="#00ff00"
            android:layout_width="100dp"
            android:layout_height="100dp">
            <com.dongua.toucheventtest.ViewC
                android:id="@+id/C"
                android:background="#ff0000"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:text="View C 文本"/>

        </com.dongua.toucheventtest.ViewGroupB>
    </com.dongua.toucheventtest.ViewGroupA>
</LinearLayout>

布局如图:
这里写图片描述

然后试着点击一下TextView,看看打印消息

11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupA
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupA
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupB
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupB
11-22 18:11:36.770 7412-7412/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewC
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewC
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewGroupB
11-22 18:11:36.771 7412-7412/com.dongua.toucheventtest I/click: onTouchEvent: ViewGroupA

这就很好理解了吧 这次我们让TextView强行拦截,即ViewC的dispatchTouchEvent返回一个true,我们在看看打印的消息

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("click", "dispatchTouchEvent: ViewC");
        return true;
    }
11-22 18:17:55.295 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupA
11-22 18:17:55.295 15435-15435/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupA
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewGroupB
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: onInterceptTouchEvent: ViewGroupB
11-22 18:17:55.296 15435-15435/com.dongua.toucheventtest I/click: dispatchTouchEvent: ViewC

可以看到它只到ViewC的dispatchTouchEvent就结束了整个事件的拦截。因为ViewC我们手动设置了拦截,如果我们对onTouchEvent也强行返回true,那么就会执行到ViewC的onTouchEvent方法。
同理的大家可以试试在ViewGroupB的dispatchTouchEvent直接返回true,这就使得事件向下的传递到此为止。

例子就这些,大家想要熟悉的话肯定要自己去手动实现一遍,我觉得一定要在反复的看几遍文字的描述之后,再去写代码才能理解其中深意,盲目的堆代码只会让你的了解停留在表面。之前看完了老是不自己写一遍,结果一阵子之后又忘了,如果多做笔记,在做笔记的过程中无疑又是一次学习的过程。
以上为个人学习笔记,动如有错误,请不吝赐教~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值