Android View的事件分发机制

news2024/12/25 1:03:44

前言

本文由于介绍本人关于View的事件分发机制的学习,如有不恰当的描述欢迎指出。

View基础

什么是View

​ View是Android中所有控件的基类,不管是Button、TextView、LinearLayout,它们的共同基类都是View。也就是说,View是界面层控件的一种抽象

​ 知道了View,还有一个ViewGroup,从名字来看可以将它翻译为控件组,也就是一组控件(一组View)。ViewGroup是继承于View的,也就是说View本身既可以是单个控件,也可以是多个控件组成的整体。根据这些概念,我们知道Button显然是一个View,而LinearLayout既是一个View也是一个ViewGroup,而且我们可以得出一个View中可以有多个ViewGroup,一个ViewGroup中也可以有多个View。

View的位置参数

​ View的位置由四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,top是左上角纵坐标、left是左上角横坐标、right是右下角横坐标、bottom是右下角纵坐标。

​ 需要注意的是,这些坐标都是相对坐标,它们都是相对于父容器来说的。在Android中,x轴和y轴的正方向分别为右和下,所以View的位置坐标与父容器的关系如下图所示:

在这里插入图片描述

所以,View的宽高与坐标的关系如下:

width = right - left
height = bottom - top

上面几个参数我们都可以通过方法获得,如下:

int width=getWidth();
int height=getHeight();
int left=getLeft();
int right=getRight();
int top=getTop();
int boottom=getBottom();

MotionEvent

知道了什么是View以及View的位置参数,接下来我们介绍一个十分重要的类——MotionEvent,MotionEvent是描述触摸事件的数据类,可以根据它获取触摸的位置(X和Y坐标)、触摸类型、触摸时间戳

触摸类型比较典型的有以下三种:

  • ACTION_DOWN:手指刚接触屏幕
  • ACTION_MOVE:手指在屏幕上移动
  • ACTION_UP:手指从屏幕上松开的一瞬间

存在以下两种常见的触摸情况:

  • 点击屏幕后松开:事件序列为ACTION_DOWN->ACTION_UP
  • 点击屏幕滑动一会松开:事件序列为ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP

另外我们还知道通过MotionEvent对象可以获得点击事件发生的X和Y坐标,是通过getX/getY和getRawX/getRawY这两组方法获得的,getX/getY是相对于当前View左上角的X和Y坐标,getRawX/getRawY是相对于手机屏幕左上角的X和Y坐标。

View的事件分发机制

接下来开始正式介绍View的事件分发机制。可能大家或多或少都听说过啥滑动冲突的,那是View的一大难题,它的解决方法的理论知识就是事件分发机制,所以掌握View的事件分发机制是十分重要的。

点击事件的分发机制

首先我们要搞清楚事件分发机制是啥意思啊,其实事件就是我们上面介绍到的点击事件,也就是MotionEvent对象,分发机制就是一种传递规则。所以说所谓的点击事件的分发,就是对MotionEvent的分发,当一个MotionEvent对象产生时,系统需要将这个MotionEvent对象传递给具体的View。

MotionEvent的分发需要有三个重要的方法完成:

  • public boolean dispatchTouchEvent(MotionEvent ev)

dispatchTouchEvent()方法用于进行事件的分发。如果点击事件能够传递给当前View,那么该方法就一定会被调用,返回结果受当前View的onTouchEvent()方法和下级View的dispatchTouchEvent()方法的影响,表示是否消耗该事件。当onTouchEvent()方法或下级View的dispatchTouchEvent()方法返回了true时,dispatchTouchEvent()方法就返回true,表示消耗掉该事件。

  • public boolean onInterceptTouchEvent(MotionEvent ev)

onInterceptTouchEvent()方法在dispatchTouchEvent()方法内调用,用于判断是否拦截某个事件。如果当前View拦截了某个事件,那么在同一个事件序列当中,该方法不会被再次调用,返回结果表示是否拦截当前事件,返回true表示拦截。onInterceptTouchEvent()方法返回true时,表示会拦截点击事件,接下来就是对点击事件进行处理,返回false就表示不拦截点击事件,接着点击事件就往子元素中传递了。

  • public boolean onTouchEvent(MotionEvent ev)

onTouchEvent()方法同样在dispatchTouchEvent()方法中被调用,用来处理点击事件。返回结果表示是否消耗当前事件(执行处理事件),如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

接下来使用一部分伪代码来加深理解一下上述三个方法的作用与关系:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume=false;
    if(onInterceptTouchEvent(ev)){//返回true表示拦截
        consume=onTouchEvent(ev);//返回true表示处理点击事件
    }else{
        consume=child.dispatchTouchEvent(ev);
    }
    return consume;//返回true表示点击事件被处理了,返回false表示去调用父View的onTouchEvent()方法处理
}

接着对上面代码进行一下解读:当点击事件发生后,会将这个点击事件传递给根ViewGroup,此时这个ViewGroup的dispatchMotionEvent()方法就会被调用,如果该方法返回的是true,就表示这个点击事件交给这个ViewGroup来处理,那什么时候调用dispatchMotionEvent()方法会返回true呢,根据上面的代码,我们可以知道,当onInterceptTouchEvent()方法和onTouchEvet()方法都被调用且都返回true时最终dispatchMotionEvent()方法就会返回true,也就是将这个点击事件拦截并进行处理,或者子View的dispatchTouchEvent()方法返回了true,那么这个dispatchMotionEvent()方法也会返回true。那如果onInterceptTouchEvent()方法没有被调用不拦截该点击事件时,就调用子元素的dispatchTouchEvent()方法来处理该点击事件,直到事件最终被处理了。

当一个点击事件产生时,它的传递遵循此顺序:Activity->Window->GroupView->View,即点击事件总是先传递给Activity,然后传递给Window,接着传递给顶级View(一个ViewGroup),接着就是按事件分发机制去分发事件了。接着我们可以思考一下,如果所有的View的onTouchEvent()方法都返回false,也就是都不处理(Window也不处理)这个点击事件那会发生什么呢?答案是这个点击事件会交给Activity来处理,即Activity的onTouchEvent()方法被调用。

关于事件传递机制,这里列出一些定义与结论,有助于更好理解这个事件传递机制:

  • 同一事件序列是指从手指接触屏幕开始,到手指离开屏幕的那一刻结束,这个过程中产生的一系列事件,这一系列事件中,也就是事件序列是以down事件开始,中间经过一系列Move事件,最后以up事件结束
  • 正常情况,一个事件序列只能被一个View拦截并处理
  • 某个View一旦决定拦截,那么这个事件序列都只能由它来处理,并且它的onInterceptTouchEvent()方法不会再被调用
  • 某个View一旦开始处理某个事件,如果它的onTouchEvent()方法返回了false(不消耗ACTION_DOWN事件),那么同一事件序列中的其他事件都不会再交给这个View处理
  • 某个View开始处理事件,如果他不消耗ACTION_DOWN事件(onTouchEvent()方法返回了false),那么同一事件序列中的其他事件都不会交给它来处理
  • View没有onInterceptTouchEvent方法(View就是一个单独的控件,它无法向子View传递了,所以一旦事件传递给View,它的onTouchEvent()方法直接执行)
  • View的onTouchEvent()方法默认都会消耗事件,除非它是不可点击的
  • onClick会发生的前提是当前View是可点击的,并且它受到了down和up事件
  • dispatchTouchEvent()方法和onTouchEvent()方法都返回false,就会将点击事件交给父View处理
  • 事件的传递过程是由外向内的,即事件总是先传递给父元素,再由父元素传递给子元素(父View分发给子View)

源码分析

Activity对点击事件的分发

Activity对点击事件的分发可以概述为:当一个点击事件MotionEvent发生时,这个MotionEvent是先传递给当前Activity的,由当前Activity的dispatchTouchEvent()方法进行分发,具体的工作则是由Activity内部的Window来完成的(Activity传递给Window)。Window会将点击事件传递给DecorView(即我们通过setContentView所设置的布局,这就是顶层父View),Activity源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;//如果getWindow().superDispatchTouchEvent(ev)返回了true,也就是Activity将MotionEvent对象传递给了Window,那么Activity的dispatchTouchEvent()就返回true。表示点击事件向下传递的同时dispatchTouchEvent()方法结束了
    }
    return onTouchEvent(ev);
}
//当一个点击事件未被Activity下任何一个View接收/处理时
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) { 
        finish();
        return true;
    }
    return false;
}

根据源码,我们可以知道,Activity的dispatchTouchEvent()方法执行时,会调用getWindow()方法获得一个PhoneWindow对象(Window是个抽象类,PhoneWindow是它的实现类),接着调用它的superDispatchTouchEvent()方法,表示Activity将这个MotionEvent对象交给这个PhoneWindow对象,方法返回true时表示点击事件向下传递了。否则的话就调用Activity的onTouchEvent()方法来处理这个事件。

OK现在我们知道了点击事件已经由Activity传递给了PhoneWindow,这个过程是通过getWindow().superDispatchTouchEvent(ev)完成的,接下来我们打开PhoneWindow来看看它的superDispatchTouchEvent()方法:

private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

可以看到PhoneWindow的superDispatchTouchEvent()方法十分简单,就调用了DecorView的superDispatchTouchEvent()方法。上面调用getWindow().superDispatchTouchEvent(ev)我们知道是将点击事件由Activity传递给Window,那么很显然mDecor.superDispatchTouchEvent(event)就是将点击事件由Window传递给DecorView(下面有介绍),最终就实现了点击事件由Activity传递给Window,再由Window传递给顶级View的过程,就完成了Activity对点击事件分发。

这里我稍微介绍一下DecorView是个啥:DecorView是Android应用窗口中的顶层视图,是所有窗口视图层次结构的最外层容器,它通常包括标题栏与内容栏(如下图所示结构),通常我们在Activity中通过setContentView(R.layout.layout1)设置布局就是将这个视图设置进DocerView的ContentView中。

在这里插入图片描述

通过上面的描述,我们可以知道如下层次结构:

在这里插入图片描述

最后我们总结一下Activity对点击事件的分发:首先点击事件产生时先传递给Activity,接着在Activity中通过getWindow().superDispatchTouchEvent(ev)将点击事件传递给PhoneWindow处理,接着在PhoneWindow中通过mDecor.superDispatchTouchEvent(event)将点击事件交给DocerView处理,也就是实现了将点击事件由Activity传递给ViewGroup的过程。

顶层View对点击事件的分发

顶层View(一般是个ViewGroup)对点击事件的分发逻辑:如果顶层View拦截事件,即这个ViewGroup的onInterceptTouchEvent()方法返回了true,则事件由ViewGroup处理;如果顶层View不拦截点击事件,那么点击事件会接着往子View中传递,子View的dispatchTouchEvent()方法被调用。这么看好像顶层View对点击事件的分发很简单的样子,其实不然,内部还有非常多的细节需要我们去仔细钻研,那么接下来我们打开源码来看看。

顶层View对点击事件的分发主要实现在ViewGroup的dispatchTouchEvent()方法中,打开ViewGroup的dispatchTouchEvent()方法,里面由这么一段代码:

            //代码1
			if (actionMasked == MotionEvent.ACTION_DOWN) {//判断事件是不是ACTION_DOWN
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
			//代码2
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                //disallowIntercept相当于一个开关,可以关闭ViewGroup对除DOWN以外的事件的拦截
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//ViewGroup真正决定是否拦截事件
                    ev.setAction(action);
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

我们先来代码1,代码1是用来判断触摸类型是不是ACTION_DOWN,如果是ACTION_DOWN,就会就会进行一个重置操作——重置FLAG_DISALLOW_INTERCEPT(下面讲解)。为什么要重置呢?因为一个完整的事件序列都是从ACTION_DOWN开始,以ACTION_UP结束的。如果触摸类型是ACTION_DOWN的话,那么就代表该事件序列是一个新的事件序列,需要重置为最初状态(接着来判断要不要拦截)。

接下来看代码2,ViewGroup会在两种情况下要判断是不是需要拦截点击事件(除这两种情况直接就是默认拦截),即actionMasked == MotionEvent.ACTION_DOWNmFirstTouchTarget != null,actionMasked == MotionEvent.ACTION_DOWN很好理解,ACTION_DOWN是一个事件序列开始的标志,我们必须在顶层View中来判断是不是要拦截。那mFirstTouchTarget != null是啥意思呢,这里直接给出结论:当事件由ViewGroup的子View成功处理时,mFirstTouchTarget != null成立,也就是说,当前的ViewGroup不拦截点击事件并交给子View处理这个点击事件时mFirstTouchTarget != null成立。那么反过来说,如果当前的ViewGroup拦截这个点击事件,那mFirstTouchTarget = null就成立。当ViewGroup决定拦截点击事件时,ACTION_MOVE和ACTION_UP事件到来,actionMasked == MotionEvent.ACTION_DOWN和mFirstTouchTarget != null这两个条件它们都不满足,那么onInterceptTouchEvent(ev)就不会被调用,同时同一事件序列中的其他所有事件默认交给它处理。

上面源码中还有一个FLAG_DISALLOW_INTERCEPT标记位,这个标记位一旦设置以后,ViewGroup将无法拦截除ACTION_DOWN以外的事件了。上面我们说到每有一个ACTION_DOWN事件到来时,都会重置FLAG_DISALLOW_INTERCEPT这个标记位,这也就使得子View中的这个标记位失效,这样一来,每当有一个ACTION_DOWN事件到来时,都是调用的ViewGroup的onInterceptTouchEvent()方法来判断是否拦截点击事件。

根据上面的分析可以得出两条结论:

  • 当ViewGroup决定拦截事件后,那么后续的同一事件序列中的所有点击事件都默认交给它处理,不再调用onInterceptTouchEvent()方法判断是否拦截

  • FLAG_DISALLOW_INTERCEPT这个标志的作用是ViewGroup不再拦截除ACTION_DOWN外的点击事件

上面探讨的是ViewGroup拦截点击事件,当ViewGroup不拦截事件时,事件会向下分发交给它的子View进行处理

						final View[] children = mChildren;
						/*
						对子View进行遍历,判断子View是否能接收点击事件(子View满足在播放动画和点击事件的坐标落在子元素区域							内)
						*/
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount;
                            }
							//判断满足接收条件,播放动画和坐标在子View中有一个不满足就continue
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //当某个子View满足上面两个条件了,调用dispatchTransformedTouchEvent方法来将点击事件交给子								View处理
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = x;
                                mLastTouchDownY = y;
                                //下面三行代码完成了mFirstTouchTarget的赋值并重量了对子View的遍历
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            ev.setTargetAccessibilityFocus(false);
                        }

上面代码我们需要注意的是遍历子View时是倒序遍历,即从最上层的子View开始向内层遍历。

如果某个子View满足了在播放动画同时点击事件的坐标落在子元素的区域内,那么点击事件就会交给这个子View处理。上面调用的dispatchTransformedTouchEvent()方法其内部其实是将这个点击事件交给子View处理,这个child就是我们的子View,点开这个方法:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    ...
	}

因为我们传过去了一个child,child不为空,所以会调用child.dispatchTouchEvent这个方法,表示子View调用dispatchMotionEvent()方法。这样我们就将点击事件交给子View处理了。

我们先暂时不考虑事件在子View内部是如何分发的,如果子View的dispatchTouchEvent()方法返回了true,那么mFirstTouchTarget就会被赋值同时跳出for循环,这段逻辑由以下代码完成:

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

上面三行代码完成了mFirstTouchTarget的赋值并终止对子View的遍历。如果子View的dispatchTouchEvent()方法返回了false,那么ViewGroup就会将点击事件分发给下一个子View。

上面说到mFirstTouchTarget赋值啥的,但我们并没有看到mFirstTouchTarget被赋值的过程啊,其实mFirstTouchTarget的赋值过程是addTouchTarget()方法里完成的,源码如下:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到mFirstTouchTarget是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对点击事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一事件序列中的所有事件,这点我们在上面ViewGroup对点击事件拦截时相关源码讲到了。

如果遍历完所有的子View后点击事件都没有被合适处理,包括两种情况:一种是ViewGroup种没有子元素,另一种是子元素处理了点击事件但dispatchTouchEvent()方法返回了false。在这两种情况下,ViewGroup会自己处理点击事件,源码如下:

if (mFirstTouchTarget == null) {
	// No touch targets so treat this as an ordinary view.
	handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}

我们可以看到dispatchTransformedTouchEvent()这个方法第三个参数child为null,上面我们有介绍到dispatchTransformedTouchEvent这个方法的源码,当child为null时,就会调用handled = super.dispatchTouchEvent(event);这行代码,这段代码就转到了View的dispatchTouchEvent()方法,即点击事件开始交给View处理,接下来我们分析View对点击事件的处理过程。

View对点击事件的处理过程

这里的View是不包含ViewGroup的。它的dispatchTouchEvent()方法如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
}

因为View没有子元素了,它无法向下传递,只能自己处理事件。我们会先判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回了true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。

接着我们看看onTouchEvent中对点击事件的具体处理:

public boolean onTouchEvent(MotionEvent event) {
    ...
        //只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就消耗事件,即onTouchEvent()方法返回true
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();//这个方法很关键
                                }
                            }
                        }
						...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
        }
    }
}

分析一下上述代码,当View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就消耗事件,即onTouchEvent()方法返回true。接着当ACTION_UP事件发生时,会触发performClickInternal()方法,我们打开performClickInternal()方法看看:

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }

performClickInternal()方法会返回一个performClick()方法,接着打开performClick()方法看看:

public boolean performClick() {
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);//调用OnClick()方法
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

performClick()方法就表示如果View设置了OnClickListener,那么performClick()方法内部就会调用它的onClick()方法。

总结

最后总结一下View的事件分发机制。所谓View的事件分发机制指的是点击事件的分发机制,也就是MotionEvent这个对象的传递规则。首先要完成View的分发机制,有三个非常重要的方法,分别是dispatchTouchEvent()用于进行带点击事件的分发、onInterceptTouchEvent()用于判断是不是要拦截点击事件、onTouchEvent()用于对点击事件进行处理。知道了这三个方法,接下来具体来说事件的分发流程,首先当一个点击事件发生时,它是先传递给当前Activity的,接着通过Activity的dispatchTouchEvent()方法由Activity传递到Window(PhoneWindow)中,接着PhoneWindow又会将这个点击事件传递给DecorView,所谓DecorView可以简单理解为我们通过setConetntView()设置的那个ViewGroup,它是一个顶级View,接着点击事件要在ViewGroup中分发,同样是调用ViewGroup的dispatchTouchEvent()方法,在dispatchTouchEvent()方法中,我们要判断是不是要拦截点击事件,也就是要不要调用onInterceptTouchEvent()方法,判断要不要拦截点击事件在两种情况下才需要去判断,一种是事件为ACTION_DOWN,一种是mFirstTouchTarget!=null成立(当子View处理了点击事件时成立),拦截了点击事件的话就对点击事件去进行处理,如果ViewGroup不拦截点击事件时,这个点击事件就传递给子View了,我们会通过for循环遍历子View,当子View满足在播放动画并且触摸坐标在子View的坐标范围内时,那么子View就要对点击事件进行处理了。子View要对点击事件进行处理,同样是先调用View的dispatchTouchEvent()方法,我们会先去判断有没有设置OnTouchListener,如果OnTouchListener的OnTouch()方法返回了true,那么onTouchEvent()方法就不会执行了,因为OnTouchListener的优先级高于OnTouchEvent(),这样做的好处是方便我们在外部处理点击事件。如果没有设置OnTouchListener,那么就会去执行onTouchEvent()方法,在调用onTouchEvent()方法时,只要CLICKABLE和LONG_CLICKABLE有一个为true,那么就消耗事件,即onTouchEvent()方法返回true。还有一点就是在onTouchEvent()方法中当ACTION_UP事件发生时,如果View设置了OnClickListener,那么就会通过一系列调用去调用它的onClick()方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2224343.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++进阶篇】——STL的简介

【C进阶篇】——STL的简介 1.什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。 2.STL的版本 原始版本 Alexander Stepanov、Meng Lee 在…

Github优质项目推荐(第八期)

文章目录 Github优质项目推荐 - 第八期一、【manim】&#xff0c;66.5k stars - 创建数学动画的 Python 框架二、【siyuan】&#xff0c;19.5k stars - 个人知识管理软件三、 【GetQzonehistory】&#xff0c;1.3k stars - 获取QQ空间发布的历史说说四、【SecLists】&#xff0…

<Project-11 Calculator> 计算器 0.3 年龄计算器 age Calculator HTML JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

leetCode算法题爬楼梯递归写法

题目&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2输出&#xff1a;2解释&#xff1a;有两种方法可以爬到楼顶。1. 1 阶 1 阶2. 2 阶 …

互联网的无形眼睛:浏览器指纹与隐私保护攻略

你是否曾有过这样的经历&#xff1a;在某个电商网站上搜索了某件商品&#xff0c;随后无论你打开哪个网页&#xff0c;都能看到与之相关的广告&#xff1f;或者当你再次访问某个网站时&#xff0c;它居然记得你之前的浏览记录&#xff1f;这一切&#xff0c;背后都有一只“看不…

GEE引擎架设好之后进游戏时白屏的解决方法——gee引擎白屏修复

这两天测试GeeM2引擎的服务端&#xff0c;最常见的问题就是点击开始游戏出现白屏&#xff0c;最早还以为是服务端问题&#xff0c;结果是因为升级了引擎&#xff0c;而没有升级NewUI这份文件导致的。解决方法如下&#xff1a; 下载GEE引擎包最新版&#xff0c;&#xff08;可以…

vue+spreadjs开发

创建vue3项目 pnpm create vite --registryhttp://registry.npm.taobao.org安装spreadjs包 pnpm install "grapecity-software/spread-sheets17.1.7" "grapecity-software/spread-sheets-resources-zh17.1.7" "grapecity-software/spread-sheets-vu…

Linux操作进程

前言 这次的主要内容就是进程的实操&#xff0c;主要是进程创建&#xff0c;进程终止&#xff0c;进程等待和进程程序替换&#xff0c;最后我们在手写一个简单的shell 1.进程创建 进程创建就是fork&#xff0c;所以我们就讲一些知识性的就可以了 首先在创建子进程的时候&…

【ArcGIS Pro实操第5期】全局及局部空间插值:GPI、LPI、IDW等

ArcGIS Pro实操第5期&#xff1a;全局及局部空间插值 ArcGIS Pro-用于空间插值的丰富工具箱实操&#xff1a;空间插值方法1&#xff1a;Trend Surface Model for Interpolation-以降水数据为例方法2&#xff1a;Kernel Density Estimation Method-以单位面积鹿的目击数为例方法…

爆破(使用Burp Suite)

以此靶场为例 1.启动此靶场&#xff0c;双击靶机进入 2.进入后页面如下 3.打开Burp Suite中的代理中的拦截 4.再随便往输入框里面输入什么 5.提交后为这个页面&#xff0c;或其他 6.将系统代理改为proxy&#xff0c;按图片顺序点 本来选中的是系统代理&#xff0c;改为proxy …

ruoyi域名跳转缓存冲突问题(解决办法修改:session名修改session的JSESSIONID名称)

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 目录 前…

2024“源鲁杯“高校网络安全技能大赛-Misc-WP

Round 1 hide_png 题目给了一张图片&#xff0c;flag就在图片上&#xff0c;不过不太明显&#xff0c;写个python脚本处理一下 from PIL import Image ​ # 打开图像并转换为RGB模式 img Image.open("./attachments.png").convert("RGB") ​ # 获取图像…

新手直播方案

简介 新手直播方案 &#xff0c;低成本方案 手机/电脑 直接直播手机软件电脑直播手机采集卡麦电脑直播多摄像机 机位多路采集卡 多路麦加电脑&#xff08;高成本方案&#xff09; 直播推流方案 需要摄像头 方案一 &#xff1a;手机 电脑同步下载 网络摄像头 软件&#xff08…

MySQL-DQL练习题

文章目录 简介初始化表练习题 简介 本节简介: 主要是一些给出一些习题, 关于DQL查询相关的, DQL查询语句是最重要的SQL语句, 功能性最复杂, 功能也最强, 所以本节建议适合以及有了DQL查询基础的食用, 另外注意我们使用的是Navicat, SQL编辑的格式规范也是Navicat指定的默认格式…

基于信号分解和多种深度学习结合的上证指数预测模型

大家好&#xff0c;我是带我去滑雪&#xff01; 为了给投资者提供更准确的投资建议、帮助政府和监管部门更好地制定相关政策&#xff0c;维护市场稳定&#xff0c;本文对股民情绪和上证指数之间的关系进行更深入的研究&#xff0c;并结合信号分解、优化算法和深度学习对上证指数…

codimd更改登录超时时限

codimd更改登录超时时限不生效&#xff0c;总是大概15分钟退出 现象&#xff1a;更改CMD_SESSION_LIFE&#xff0c;无论怎么改大都不生效&#xff0c;总是大概15分钟。 解决&#xff1a; 发现需要同步修改CMD_SESSION_SECRET&#xff0c;修改完毕之后终于更新了。 CMD_SESSIO…

Spring Cloud --- Sentinel 熔断规则

熔断规则 慢调用比例 发送10个请求&#xff0c;每个请求理想响应时长为200毫秒。统计1秒钟&#xff0c;如果10个请求响应时间超过200毫秒的比例大于等于10%&#xff0c;则触发熔断&#xff0c;熔断5秒。 异常比例 1秒内&#xff0c;发送请求出现异常率为20%&#xff0c;则触…

2024年10月27日 十二生肖 今日运势

小运播报&#xff1a;2024年10月27日&#xff0c;星期日&#xff0c;农历九月廿五 &#xff08;甲辰年甲戌月甲子日&#xff09;&#xff0c;法定节假日。 红榜生肖&#xff1a;牛、猴、龙 需要注意&#xff1a;羊、兔、马 喜神方位&#xff1a;东北方 财神方位&#xff1a…

报名了,奖金6万!2024年四川省大学生数据科学与统计建模竞赛(算法赛)-基于新网银行数据集

为进一步培养学生创新精神和实践能力&#xff0c;鼓励学生运用统计学模型、机器学习模型等数据科学专业知识&#xff0c;协助解决经济社会领域中的实际问题&#xff0c;由四川省教育厅主办&#xff0c;西南财经大学与四川新网银行承办&#xff0c;四川省普通本科高等学校统计学…

LLM生命周期

LLM生命周期 1.Using LLMs 第一种方式&#xff1a; Public API or Private API. 第二种方式&#xff1a; 感谢开源模型&#xff0c;Deploy or Using them via private API. 2.Stage1&#xff1a;BUILDING 1.准备数据&#xff08;Data preparation & Sampling&#xff…