当前位置: 移动技术网 > IT编程>移动开发>Android > Android事件分发机制(上) ViewGroup的事件分发

Android事件分发机制(上) ViewGroup的事件分发

2019年07月24日  | 移动技术网IT编程  | 我要评论

神木与瞳是情侣吗,万凤之皇,csol自雷脚本

综述

  android中的事件分发机制也就是view与viewgroup的对事件的分发与处理。在viewgroup的内部包含了许多view,而viewgroup继承自view,所以viewgroup本身也是一个view。对于事件可以通过viewgroup下发到它的子view并交由子view进行处理,而viewgroup本身也能够对事件做出处理。下面就来详细分析一下viewgroup对时间的分发处理。

motionevent

  当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成。
  1. action_down: 手指按下屏幕
  2. action_move: 手指在屏幕上移动
  3. action_up: 手指从屏幕上抬起
  例如一个简单的屏幕触摸动作触发了一系列touch事件:action_down->action_move->…->action_move->action_up
  对于android中的这个事件分发机制,其中的这个事件指的就是motionevent。而view的对事件的分发也是对motionevent的分发操作。可以通过getrawx和getrawy来获取事件相对于屏幕左上角的横纵坐标。通过getx()和gety()来获取事件相对于当前view左上角的横纵坐标。

三个重要方法

public boolean dispatchtouchevent(motionevent ev)

  这是一个对事件分发的方法。如果一个事件传递给了当前的view,那么当前view一定会调用该方法。对于dispatchtouchevent的返回类型是boolean类型的,返回结果表示是否消耗了这个事件,如果返回的是true,就表明了这个view已经被消耗,不会再继续向下传递。  

public boolean onintercepttouchevent(motionevent ev)

  该方法存在于viewgroup类中,对于view类并无此方法。表示是否拦截某个事件,viewgroup如果成功拦截某个事件,那么这个事件就不在向下进行传递。对于同一个事件序列当中,当前view若是成功拦截该事件,那么对于后面的一系列事件不会再次调用该方法。返回的结果表示是否拦截当前事件,默认返回false。由于一个view它已经处于最底层,它不会存在子控件,所以无该方法。   

public boolean ontouchevent(motionevent event)

  这个方法被dispatchtouchevent调用,用来处理事件,对于返回的结果用来表示是否消耗掉当前事件。如果不消耗当前事件的话,那么对于在同一个事件序列当中,当前view就不会再次接收到事件。   

view事件分发流程图

  对于事件的分发,在这里先通过一个流程图来看一下整个分发过程。

 

viewgroup事件分发源码分析

  根据上面的流程图现在就详细的来分析一下viewgroup事件分发的整个过程。
  手指在触摸屏上滑动所产生的一系列事件,当activity接收到这些事件通过调用activity的dispatchtouchevent方法来进行对事件的分发操作。下面就来看一下activity的dispatchtouchevent方法。

public boolean dispatchtouchevent(motionevent ev) {
 if (ev.getaction() == motionevent.action_down) {
 onuserinteraction();
 }
 if (getwindow().superdispatchtouchevent(ev)) {
 return true;
 }
 return ontouchevent(ev);
}

  通过getwindow().superdispatchtouchevent(ev)这个方法可以看出来,这个时候activity又会将事件交由window处理。window它是一个抽象类,它的具体实现只有一个phonewindow,也就是说这个时候,activity将事件交由phonewindow中的superdispatchtouchevent方法。现在跟踪进去看一下这个superdispatchtouchevent代码。

public boolean superdispatchtouchevent(motionevent event) {
 return mdecor.superdispatchtouchevent(event);
}

  这里面的mdecor它是一个decorview,decorview它是一个activity的顶级view。它是phonewindow的一个内部类,继承自framelayout。于是在这个时候事件又交由decorview的superdispatchtouchevent方法来处理。下面就来看一下这个superdispatchtouchevent方法。

public boolean superdispatchtouchevent(motionevent event) {
 return super.dispatchtouchevent(event);
}

  在这个时候就能够很清晰的看到decorview它调用了父类的dispatchtouchevent方法。在上面说到decorview它继承了framelayout,而这个framelayout又继承自viewgroup。所以在这个时候事件就开始交给了viewgroup进行处理了。下面就开始详细看下这个viewgroup的dispatchtouchevent方法。由于dispatchtouchevent代码比较长,在这里就摘取部分代码进行说明。

// handle an initial down.
if (actionmasked == motionevent.action_down) {
 // throw away all previous state when starting a new touch gesture.
 // the framework may have dropped the up or cancel event for the previous gesture
 // due to an app switch, anr, or some other state change.
 cancelandcleartouchtargets(ev);
 resettouchstate();
}

  从上面代码可以看出,在dispatchtouchevent中,会对接收的事件进行判断,当接收到的是action_down事件时,便会清空事件分发的目标和状态。然后执行resettouchstate方法重置了触摸状态。下面就来看一下这两个方法。

1. cancelandcleartouchtargets(ev)

private touchtarget mfirsttouchtarget;

......

private void cancelandcleartouchtargets(motionevent event) {
 if (mfirsttouchtarget != null) {
 boolean syntheticevent = false;
 if (event == null) {
  final long now = systemclock.uptimemillis();
  event = motionevent.obtain(now, now,
   motionevent.action_cancel, 0.0f, 0.0f, 0);
  event.setsource(inputdevice.source_touchscreen);
  syntheticevent = true;
 }

 for (touchtarget target = mfirsttouchtarget; target != null; target = target.next) {
  resetcancelnextupflag(target.child);
  dispatchtransformedtouchevent(event, true, target.child, target.pointeridbits);
 }
 cleartouchtargets();

 if (syntheticevent) {
  event.recycle();
 }
 }
}

  在这里先介绍一下mfirsttouchtarget,它是touchtarget对象,touchtarget是viewgroup的一个内部类,touchtarget采用链表数据结构进行存储view。而在这个方法中主要的作用就是清空mfirsttouchtarget链表并将mfirsttouchtarget设为空。

2. resettouchstate()

private void resettouchstate() {
 cleartouchtargets();
 resetcancelnextupflag(this);
 mgroupflags &= ~flag_disallow_intercept;
 mnestedscrollaxes = scroll_axis_none;
}

  在这里介绍一下flag_disallow_intercept标记,这是禁止viewgroup拦截事件的标记,可以通过requestdisallowintercepttouchevent方法来设置这个标记,当设置了这个标记以后,viewgroup便无法拦截除了action_down以外的其它事件。因为在上面代码中可以看出,当事件为action_down时,会重置flag_disallow_intercept标记。
  那么下面就再次回到dispatchtouchevent方法中继续看它的源代码。

// check for interception.
final boolean intercepted;
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;
}

  这段代码主要就是viewgroup对事件是否需要拦截进行的判断。下面先对mfirsttouchtarget是否为null这两种情况进行说明。当事件没有被拦截时,viewgroup的子元素成功处理事件后,mfirsttouchtarget会被赋值并且指向其子元素。也就是说这个时候mfirsttouchtarget!=null。可是一旦事件被拦截,mfirsttouchtarget不会被赋值,mfirsttouchtarget也就为null。
  在上面代码中可以看到根据actionmasked==motionevent.action_down||mfirsttouchtarget!=null这两个情况进行判断事件是否需要拦截。对于actionmasked==motionevent.action_down这个条件很好理解,对于mfirsttouchtarget!=null的两种情况上面已经说明。那么对于一个事件序列,当事件为motionevent.action_down时,会重置flag_disallow_intercept,也就是说!disallowintercept一定为true,必然会执行onintercepttouchevent方法,对于onintercepttouchevent方法默认返回为false,所以需要viewgroup拦截事件时,必须重写onintercepttouchevent方法,并返回true。这里有一点需要注意,对于一个事件序列,一旦序列中的某一个事件被成功拦截,执行了onintercepttouchevent方法,也就是说onintercepttouchevent返回值为true,那么该事件之后一系列事件对于条件actionmasked==motionevent.action_down||mfirsttouchtarget!=null必然为false,那么这个时候该事件序列剩下的一系列事件将会被拦截,并且不会执行onintercepttouchevent方法。于是在这里得出一个结论:对于一个事件序列,当其中某一个事件成功拦截时,那么对于剩下的一系列事件也会被拦截,并且不会再次执行onintercepttouchevent方法
  下面再来看一下对于viewgroup并没有拦截事件是如何进行处理的。

final int childrencount = mchildrencount;
if (newtouchtarget == null && childrencount != 0) {
 final float x = ev.getx(actionindex);
 final float y = ev.gety(actionindex);
 // find a child that can receive the event.
 // scan children from front to back.
 final arraylist<view> preorderedlist = buildorderedchildlist();
 final boolean customorder = preorderedlist == null
  && ischildrendrawingorderenabled();
 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);
 }
 if (preorderedlist != null) preorderedlist.clear();
}

  对于这段代码虽然说比较长,但是在这里面的逻辑去不是很复杂。首先获取当前viewgroup中的子view和viewgroup的数量。然后对该viewgroup中的元素进行逐步遍历。在获取到viewgroup中的子元素后,判断该元素是否能够接收触摸事件。子元素若是能够接收触摸事件,并且该触摸坐标在子元素的可视范围内的话,便继续向下执行。否则就continue。对于衡量子元素能否接收到触摸事件的标准有两个:子元素是否在播放动画和点击事件的坐标是否在子元素的区域内。
  一旦子view接收到了触摸事件,然后便开始调用dispatchtransformedtouchevent方法对事件进行分发处理。对于dispatchtransformedtouchevent方法代码比较多,现在只关注下面这五行代码。从下面5行代码中可以看出,这时候会调用子view的dispatchtouchevent,也就是在这个时候viewgroup已经完成了事件分发的整个过程。

if (child == null) {
 handled = super.dispatchtouchevent(event);
} else {
 handled = child.dispatchtouchevent(event);
}

  当子元素的dispatchtouchevent返回为true的时候,也就是子view对事件处理成功。这时候便会通过addtouchtarget方法对mfirsttouchtarget进行赋值。
  如果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。也就是执行了super.dispatchtouchevent(event)方法。由于viewgroup继承自view,所以这个时候又将事件交由父类的dispatchtouchevent进行处理。对于父类view是如何通过dispatchtouchevent对事件进行处理的,在下篇文章中会进行详细说明。
  到这里对于viewgroup的事件分发已经讲完了,在这一路下来,不难发现对于dispatchtouchevent有一个boolean类型返回值。对于这个返回值,当返回true的时候表示当前事件处理成功,若是返回false,一般来说是因为在事件处理ontouchevent返回了false,这时候变会交由它的父控件进行处理,以此类推,若是一直处理失败,则最终会交由activity的ontouchevent方法进行处理。

总结

  在这里从宏观上再看一下这个viewgroup对事件的分发,当viewgroup接收一个事件序列以后,首先会判断是否拦截该事件,若是拦截该事件,则通过调用父类view的dispatchtouchevent来处理这个事件。若是不去拦截这一事件,便将该事件下发到子view当中。若果说viewgroup没有子view,或者说子view对事件处理失败,则将该事件有交由该viewgroup处理,若是该viewgroup对事件依然处理失败,最终则会将事件交由activity进行处理。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网