博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android View体系(五)从源码解析View的事件分发机制
阅读量:6691 次
发布时间:2019-06-25

本文共 10982 字,大约阅读时间需要 36 分钟。

1.处理点击事件的方法

View的层级

我们知道View的结构是树形的结构,View可以放在ViewGroup中,这个ViewGroup也可以放到另一个ViewGroup中,这样层层的嵌套就组成了View的层级。

什么是点击事件分发

当我们点击屏幕,就产生了触摸事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEvent在View的层级传递的过程就是点击事件分发。

点击事件分发的重要方法

点击事件有三个重要的方法它们分别是:

  • dispatchTouchEvent(MotionEvent ev):用来进行事件的分发
  • onInterceptTouchEvent(MotionEvent ev):用来进行事件的拦截,在dispatchTouchEvent()中调用,需要注意的是View没有提供该方法
  • onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent()方法中进行调用

为了了解这三个方法的关系,我们先来看看ViewGroup的dispatchTouchEvent()方法的部分源码:

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {      ...省略           if (actionMasked == MotionEvent.ACTION_DOWN                   || mFirstTouchTarget != null) {               final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;               if (!disallowIntercept) {                   intercepted = onInterceptTouchEvent(ev);                   ev.setAction(action); // restore action in case it was changed               } else {                   intercepted = false;               }           } else {               // There are no touch targets and this action is not an initial down               // so this view group continues to intercept touches.               intercepted = true;           }          ...省略       return handled;}
View Code

很明显在dispatchTouchEvent()方法中调用了onInterceptTouchEvent()方法来判断是否拦截事件,来看看onInterceptTouchEvent()方法:

public boolean onInterceptTouchEvent(MotionEvent ev) {       return false;   }
View Code

onInterceptTouchEvent()方法默认返回false,不进行拦截,接着来看看dispatchTouchEvent()方法剩余的部分源码:

public boolean dispatchTouchEvent(MotionEvent ev) { ...省略              final View[] children = mChildren;              for (int i = childrenCount - 1; i >= 0; i--) {                            final int childIndex = customOrder                                    ? getChildDrawingOrder(childrenCount, i) : i;                            final View child = (preorderedList == null)                                    ? children[childIndex] : preorderedList.get(childIndex);                            // If there is a view that has accessibility focus we want it                            // to get the event first and if not handled we will perform a                            // normal dispatch. We may do a double iteration but this is                            // safer given the timeframe.                            if (childWithAccessibilityFocus != null) {                                if (childWithAccessibilityFocus != child) {                                    continue;                                }                                childWithAccessibilityFocus = null;                                i = childrenCount - 1;                            }                            if (!canViewReceivePointerEvents(child)                                    || !isTransformedTouchPointInView(x, y, child, null)) {                                ev.setTargetAccessibilityFocus(false);                                continue;                            }                            newTouchTarget = getTouchTarget(child);                            if (newTouchTarget != null) {                                // Child is already receiving touch within its bounds.                                // Give it the new pointer in addition to the ones it is handling.                                newTouchTarget.pointerIdBits |= idBitsToAssign;                                break;                            }                            resetCancelNextUpFlag(child);                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }                            // The accessibility focus didn't handle the event, so clear                            // the flag and do a normal dispatch to all children.                            ev.setTargetAccessibilityFocus(false);                        } ...省略}
View Code

我们看到了for循环,首先遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到则交由子元素来处理。接下来看看37行的dispatchTransformedTouchEvent()方法中实现了什么:

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 = dispatchTouchEvent(event);           } else {               handled = child.dispatchTouchEvent(event);           }           event.setAction(oldAction);           return handled;       } ...省略      }
View Code

如果有子View则调用子View的dispatchTouchEvent(event)方法。如果ViewGroup没有子View则调用super.dispatchTouchEvent(event),ViewGroup是继承View的,我们再来看看View的dispatchTouchEvent(event):

public boolean dispatchTouchEvent(MotionEvent event) {      ...省略       boolean result = false;       if (onFilterTouchEventForSecurity(event)) {           //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;           }       }    ...省略       return result;   }
View Code

我们看到如果OnTouchListener不为null并且onTouch()方法返回true,则表示事件被消费,就不会执行onTouchEvent(event),否则就会执行onTouchEvent(event)。再来看看onTouchEvent()方法的部分源码

public boolean onTouchEvent(MotionEvent event) {     ...省略       final int action = event.getAction();       if (((viewFlags & CLICKABLE) == CLICKABLE ||               (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||               (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {           switch (action) {               case MotionEvent.ACTION_UP:                   boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                   if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                       // take focus if we don't have it already and we should in                       // touch mode.                       boolean focusTaken = false;                                             if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                           // This is a tap, so remove the longpress check                           removeLongPressCallback();                           // Only perform take click actions if we were in the pressed state                           if (!focusTaken) {                               // Use a Runnable and post this rather than calling                               // performClick directly. This lets other visual state                               // of the view update before click actions start.                               if (mPerformClick == null) {                                   mPerformClick = new PerformClick();                               }                               if (!post(mPerformClick)) {                                   performClick();                               }                           }                       }      ...省略           }       return true;      }                return false;   }
View Code

上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一个为true,那么onTouchEvent就会返回true消耗这个事件。CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按点击,可以通过View的setClickable和setLongClickable方法来设置,也可以通过View的setOnClickListenter和setOnLongClickListener来设置,他们会自动将View的设置为CLICKABLE和LONG_CLICKABLE。

接着在ACTION_UP事件会调用performClick()方法:

public boolean performClick() {    final boolean result;    final ListenerInfo li = mListenerInfo;    if (li != null && li.mOnClickListener != null) {        playSoundEffect(SoundEffectConstants.CLICK);        li.mOnClickListener.onClick(this);        result = true;    } else {        result = false;    }    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);    return result;}
View Code

如果View设置了点击事件OnClickListener,那么它的onClick()方法就会被执行。

2.点击事件分发的传递规则

看到这里我们就可以知道点击事件分发的这三个重要方法的关系,用伪代码来简单表示就是:

public boolean dispatchTouchEvent(MotionEvent ev) {boolean result=false;if(onInterceptTouchEvent(ev)){      result=onTouchEvent(ev); }else{      result=child.dispatchTouchEvent(ev);}return result;
View Code

点击事件由上而下的传递规则

当点击事件产生后会由Activity来处理在传递给Window再传递给顶层的ViewGroup,一般在事件传递中只考虑ViewGroup的onInterceptTouchEvent()方法,因为一般情况我们不会去重写dispatchTouchEvent()方法。

对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGroup的onInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理,如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则交给它的子元素的dispatchTouchEvent()来处理,如此的反复下去。如果传递给最底层的View,View是没有子View的,就会调用View的dispatchTouchEvent()方法,一般情况下最终会调用View的onTouchEvent()方法。

举个现实的例子,就是我们的应用产生了重大的bug,这个bug首先会汇报给技术总监那:

技术总监(顶层ViewGroup)→技术经理(中层ViewGroup)→工程师(底层View)

技术总监不拦截,把bug分给了技术经理,技术经理不拦截把bug分给了工程师,工程师没有下属只有自己处理了。
事件由上而下传递返回值规则为:true,拦截,不继续向下传递;false,不拦截,继续向下传递。

点击事件由下而上的传递规则

点击事件传给最底层的View,如果他的onTouchEvent()方法返回true,则事件由最底层的View消耗并处理了,如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()仍旧返回返回false,则继续传递给改父View的父View处理,如此的反复下去。

再返回我们现实的例子,工程师发现这个bug太难搞不定(onTouchEvent()返回false),他只能交给上级技术经理处理,如果技术经理也搞不定(onTouchEvent()返回false),那就把bug传给技术总监,技术总监一看bug很简单就解决了(onTouchEvent()返回true)。

事件由下而上传递返回值规则为:true,处理了,不继续向上传递;false,不处理,继续向上传递。

点击事件传递时的其他问题

  • 上面源码我们看到:如果我们设置了OnTouchListener并且onTouch()方法返回true,则onTouchEvent()方法不会被调用,否则则会调用onTouchEvent()方法,可见OnTouchListener的优先级要比onTouchEvent()要高。在OnTouchEvent()方法中,如果当前设置了OnClickListener则会执行它的onClick()方法。
  • View的OnTouchEvent()方法默认都会返回true,除非它是不可点击的也就是CLICKABLE和LONG_CLICKABLE都为false。

转载于:https://www.cnblogs.com/ganchuanpu/p/6034413.html

你可能感兴趣的文章
分享一个Cnblogs简易APP
查看>>
【原创】腾讯微博的XSS攻击漏洞
查看>>
ADO.NET Entity Framework Beta2(五)/快速入门(实体框架)
查看>>
linux sudo 命令
查看>>
HTML5本地存储——IndexedDB(二:索引)
查看>>
log4net 中错误 System.Web.HttpException (0x80004005): 文件不存在
查看>>
SqlServer将没有log文件的数据库文件附加到服务器中
查看>>
[Android Pro] Android的5个进程等级
查看>>
使用jenkins配置.net mvc网站进行持续集成一
查看>>
全网最全最详细的Windows下安装Anaconda2 / Anaconda3(图文详解)
查看>>
GNU make manual 翻译( 一百四十二)
查看>>
Excel里的多列求和(相邻或相隔皆适用)
查看>>
Python正则表达式应用示例
查看>>
Hadoop HBase概念学习系列之HBase里的时间戳(二十六)
查看>>
Android系统关机或几种方式重启
查看>>
Elasticsearch-2.4.3的3节点安装(多种方式图文详解)(含 head、kopf、marvel、shield和watcher插件安装和使用)...
查看>>
ASP.NET MVC3实战系列(一):简单示例
查看>>
Spark RDD/Core 编程 API入门系列 之rdd案例(map、filter、flatMap、groupByKey、reduceByKey、join、cogroupy等)(四)...
查看>>
jQuery:1.5.3,表单验证(父级(parent)层次追加(apped))
查看>>
METAL渲染是什么?
查看>>