Android View的事件分发机制

前言

本文由于介绍本人关于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()方法,里面由这么一段代码:

            //代码1if (actionMasked == MotionEvent.ACTION_DOWN) {//判断事件是不是ACTION_DOWNcancelAndClearTouchTargets(ev);resetTouchState();}//代码2final 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中有一个不满足就continueif (!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 SimplifiableIfStatementListenerInfo 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()方法返回trueif (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.mzph.cn/web/56761.shtml

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

相关文章

K8S配置storage-class

简介 Kubernetes支持NFS存储&#xff0c;需要安装nfs-subdir-external-provisioner&#xff0c;它是一个存储资源自动调配器&#xff0c;它可将现有的NFS服务器通过持久卷声明来支持Kubernetes持久卷的动态分配。该组件是对Kubernetes NFS-Client Provisioner的扩展&#xff0…

腾讯云跨AZ部署FortigateHA备忘录

随时保存配置 config system globalset admintimeout 480set alias "FortiGate-VM64-KVM"set gui-auto-upgrade-setup-warning disableset hostname "FG-Slave"set revision-backup-on-logout enableset revision-image-auto-backup enableset timezone &…

面向对象与设计模式第一节:深入理解OOP

第三章&#xff1a;面向对象与设计模式 第一节&#xff1a;深入理解OOP 面向对象编程&#xff08;OOP&#xff09;是一种编程范式&#xff0c;它将程序结构视为由对象组成&#xff0c;促进了代码的重用性和可维护性。在这一课中&#xff0c;我们将深入分析OOP的四个基本特性&…

[JAVAEE] 多线程的案例(三) - 线程池

目录 一. 什么是线程池 二. 线程池的作用 三. java提供的线程池类 四. ThreadPoolExecutor的构造方法及参数理解 1. int corePoolSize: 核心线程数. 2. int maximumPoolSize: 最大线程数 核心线程数 非核心线程数 3. int keepAliveTime:非核心线程允许空闲的最大时间. …

DataX简介及使用

目录 一、DataX离线同步工具DataX3.0介绍 1.1、 DataX 3.0概览 1.2、特征 1.3、DataX3.0框架设计 1.4、支持的数据元 1.5、DataX3.0核心架构 1.6、DataX 3.0六大核心优势 1.6.1、可靠的数据质量监控 1.6.2、丰富的数据转换功能 1.6.3、精准的速度控制 1.6.4、强劲的…

正则表达式和通配符

文章目录 正则表达式和通配符的区别正则表达式&#xff08;Regex&#xff09;通配符&#xff08;Wildcards&#xff09;总结 正则表达式的概念正则表达式的由来为什么要使用正则表达式 正则表达式的语法组成修饰符元字符\f\b\B 在Linux中的基础正则和扩展正则基础正则(BRE)^$.*…

面试时被问到“Scaling Law”,该怎么答?

在大模型的研发中&#xff0c;通常会有下面一些需求&#xff1a; 计划训练一个 10B 的模型&#xff0c;想知道至少需要多大的数据&#xff1f; 收集到了 1T 的数据&#xff0c;想知道能训练一个多大的模型&#xff1f; 老板准备 1 个月后开发布会&#xff0c;给的资源是 100 …

Linux安装Nginx教程(rpm安装方式)

本章教程,主要介绍如何在Linux Centos7系统上,使用rpm的方式进行安装Nginx。 一、安装wget插件 如果不存在wget下载插件,需要安装一下。 yum install -y wget二 、下载rpm安装包 官方提供的rpm下载地址:https://nginx.org/packages/centos/7/x86_64/RPMS/ <

【Nginx系列】499错误

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Postman常见问题及解决方(全)

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、网络连接问题 如果Postman无法发送请求或接收响应&#xff0c;可以尝试以下操作&#xff1a; 检查网络连接是否正常&#xff0c;包括检查网络设置、代理设置…

软考中级嵌入式系统设计师笔记分享(二)

1.TTL 电路是电流控制器件&#xff0c;而CMOS 电路是电压控制器件。 2.TTL 电路的速度快&#xff0c;传输延迟时间短(5-10ns)&#xff0c;但是功耗大。 常见的串行总线有 SPI、II2C、USB、RS232/RS422/RS485、CAN等;高速串行总线主要有 SATA、PCIE、IEEE 1394、Rapidl0、USB 3…

1.DBeaver连接hive数据库

1.hive开启远程服务&#xff0c;linux中直接输入&#xff1a;hiveserver2 2.解压dbeaver和hive-jdbc-2.1.1.zip 3.双击打开 4.数据库&#xff0c;新建连接 5.搜索hive 6.配置参数 7.编辑驱动设置 8.添加jar包 9.测试连接 10.右击&#xff0c;新建sql编辑器 11.执行sql 12.调整字…

【每日一题】LeetCode - 整数转罗马数字

在罗马数字系统中&#xff0c;七个不同的符号代表不同的值&#xff1a; 符号值I1V5X10L50C100D500M1000 罗马数字的表示方式是从最大值开始逐次减去每个符号的值&#xff0c;通过组合这些符号构建最终的表示形式。本文将介绍一个基于贪心策略的解决方案&#xff0c;将整数转换…

unity开发之Line Renderer

Line Renderer 是一个有用的工具&#xff0c;可让您在游戏中绘制线条。 它可以用作游戏的函数或调试标记。 在这里&#xff0c;让我们创建一个程序&#xff0c;根据基本用法在 Line Renderer 上移动。 目录 如何使用 Line Renderer 和基础知识 在场景中放置 Line Renderer关键组…

Catalan数 C++解决

输入描述 输入一个正整数n。 输出描述 输出Catalan数的前n项。 用例输入 1 0 用例输出 1 1 用例输入 2 5 用例输出 2 1 1 2 5 14 42 #include<bits/stdc.h> using namespace std; int main() {int n;cin>>n;int dp[n1]{0};dp[0]dp[1]1;for(int m2;…

守护头顶安全——AI高空抛物监测,让悲剧不再重演

在城市的喧嚣中&#xff0c;我们享受着高楼林立带来的便捷与繁华&#xff0c;却往往忽视了那些隐藏在高空中的危险。近日&#xff0c;震惊全国的高空抛物死刑案件被最高院核准并执行。案件中被告人多次高空抛物的举动&#xff0c;夺去了无辜者的生命&#xff0c;也让自己付出了…

Go 语言中的 for range 循环教程

在 Go 语言中&#xff0c;for range 循环是一个方便的语法结构&#xff0c;用于遍历数组、切片、映射和字符串。本教程将通过示例代码来帮助理解如何在 Go 中使用 for range 循环。 package mainimport "fmt"func main() {// 遍历切片并计算和nums : []int{2, 3, 4}…

Kafka-代码示例

一、构建开发环境 File > New > Project 选择一个最简单的模板 项目和坐标命名 配置maven路径 添加maven依赖 <dependencies><!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients --><dependency><groupId>org.apache.kaf…

深度学习 基本函数01

np.dot 是 NumPy 库中的一个函数&#xff0c;用于计算两个数组的点积&#xff08;也称为内积或数量积&#xff09;。点积是两个向量的对应元素乘积之和。 np.random.normal 是 NumPy 库中的一个函数&#xff0c;用于生成符合正态分布&#xff08;也称为高斯分布&#xff09;的…

项目管理软件中这6个小技巧帮助项目经理同时管理多个项目

在网上看到一个数据&#xff0c;只有15%的项目经理一次只需要负责一个项目&#xff0c;其他的项目经理都需要同时负责多个项目&#xff0c;甚至有15%的项目经理一次需要负责10个以上的项目。 我在工作中&#xff0c;也只有很少很少的时间里&#xff0c;是一次性只负责一个项目…