android开发分享Android 深入探究自定义view之事件的分发机制与处理详解

目录题引activity对事件的分发过程父布局拦截的分发处理过程action_down 事件action_move 事件父布局不拦截时的分发处理过程action_downaction_move解决冲突

目录
  • 题引
  • activity对事件的分发过程
  • 父布局拦截的分发处理过程
    • action_down 事件
    • action_move 事件
  • 父布局不拦截时的分发处理过程
    • action_down
    • action_move
  • 解决冲突方案
    • 外部拦截
      • 内部拦截

        android开发分享Android 深入探究自定义view之事件的分发机制与处理详解主要探讨下面几个问题:

        上述就是android开发分享Android 深入探究自定义view之事件的分发机制与处理详解的全部内容,如果对大家有所用处且需要了解更多关于Android学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

        • 学习事件分发机制是为了解决什么问题
        • activity对事件的分发过程
        • 父布局拦截的分发处理过程
        • 父布局不拦截时的分发处理过程
        • 冲突解决方案

        题引

        事件只有一个,多个人想要处理,处理的对象不是我们想给的对象就是事件冲突。

        Android 深入探究自定义view之事件的分发机制与处理详解

        如上图,recyclerview 的父布局是viewpager,左右滑动时没问题,上下滑动时recyclerview好像没收到滑动事件一样,无法达到我们预期的效果。我们的触摸被封装成motionevent事件传递,在多个层级中它是如何传递的呢?又是根据什么来确定哪个view处理这个事件的呢,咱们抽丝剥茧一步步揭开她的面纱!

        activity对事件的分发过程

        追溯本源,寻找事件分发的开始。

        当一个点击操作发生时,事件最先传递给当前的activity,由activity的dispatchtouchevent进行分发

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

        这里的getwindow返回的window类只有一个实现,phonewindow

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

        我们继续看 decorview的superdispatchtouchevent方法实现

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

        decorview 继承于 viewgroup。此时应该理解了,activity 的 事件分发交给了 decorview 处理,而 decorview 又是什么

        Android 深入探究自定义view之事件的分发机制与处理详解

        decorview是activity窗口的根视图,是一个framelayout,decorview内部又分为两部分,一部分是actionbar,另一部分是contentparent,即activity在setcontentview对应的布局。如此一来,事件分发从系统层面开始向我们写的布局分发事件!

        事件分发是一个递归的过程,主要涉及三个函数

        • dispatchtouchevent
        • onintercepttouchevent
        • ontouchevent

        三者关系

          public boolean dispatchtouchevent(motionevent ev){  	boolean result = false;  	if(onintercepttouchevent(ev)){	//	如果拦截则交给自己的 ontouchevent 处理事件  		result = ontouchevent(ev);  	}else{  		//	如果不拦截,交给子布局分发,这是个层层递归过程  		result = chlid.dispatchtouchevent(ev);  	}  	return result;  }  

        直接撸源码是一件很痛苦的事情,多种可能的发生让源码可读性很差。下面我们会从某一种特定逻辑下分析,这样会清晰很多。每次只分析一种情境!

        父布局拦截的分发处理过程

        父布局拦截我们分两步,action_down、action_move

        action_down 事件

        进入 viewgroup 的 dispatchtouchevent 方法内

             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();     }  

        因为是 action_down 事件,先清空状态,一个是touchtarget的状态,一个是 mgroupflags。这个用不到继续走

          // check for interception.  final boolean intercepted;  //	因为是第一次过来	 mfirsttouchtarget = null ,且是 action_down 事件,走入 if 内  if (actionmasked == motionevent.action_down           || mfirsttouchtarget != null) {       //	咱们走的是父布局拦截事件,子布局用尚方宝剑,disallowintercept =false       final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0;       if (!disallowintercept) {       	//	咱们在这拦截, intercepted = true           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;   }  

        代码注释的比较全,这边主要是判断本view是否拦截,如果拦截 intercepted = true 。所以后面的遍历子view分发都进不去

          //	intercepted = true , 进不去  if (!canceled && !intercepted) {  	//	这是一个遍历子 view 接盘的故事  	for (int i = childrencount - 1; i >= 0; i--) {  	}  }  

        一直往下走

          // dispatch to touch targets.  if (mfirsttouchtarget == null) {      // no touch targets so treat this as an ordinary view.      handled = dispatchtransformedtouchevent(ev, canceled, null, touchtarget.all_pointer_ids);  }  

        符合这个条件,深入 dispatchtransformedtouchevent 函数,第三个参数是 null

          private boolean dispatchtransformedtouchevent(motionevent event, boolean cancel,           view child, int desiredpointeridbits) {              final boolean handled;       if (child == null) {        	//	===== 执行位置 ====            handled = super.dispatchtouchevent(transformedevent);        } else {            final float offsetx = mscrollx - child.mleft;            final float offsety = mscrolly - child.mtop;            transformedevent.offsetlocation(offsetx, offsety);            if (! child.hasidentitymatrix()) {                transformedevent.transform(child.getinversematrix());            }              handled = child.dispatchtouchevent(transformedevent);        }  }          

        第三个参数传的是 null ,即 child = null 。调用 super 的 dispatchtouchevent 。viewgroup 的 super 即是 view。

          handled = view.dispatchtouchevent(event);  

        深入 view 的 dispatchtouchevent 方法,主要处理逻辑是下面两段代码

          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;   }  

        我们可以得出结论:ontouch 比 ontouchevent 优先级高,如果ontouch 拦截事件则 ontouchevent 无法接到事件。这也是为什么我们在ontouch方法返回true后onclick事件失效的原因。ontouchevent 的逻辑比较简单,此处不做分析

        这里要说明一点,事件分发机制的分发其实有两种含义。一是事件在不同view之间的分发,父布局到子布局的分发;二是事件在view中对不同监听的分发,ontouch、onclick、onlongclick 在分发时也是有顺序的。

        到这里父布局拦截的down事件算结束了,下面是move事件,继down后的滑动,这是个连续的过程

        action_move 事件

        手指点击后开始滑动,继续分发move事件

          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;  }  

        actionmasked = action_move mfirsttouchtarget = null ,直接走 else 模块,即 intercepted = true

          //	intercepted = true , 进不去  if (!canceled && !intercepted) {  	for (int i = childrencount - 1; i >= 0; i--) {  	}  }  

        同样的分发子view的模块我们依旧进不去

          // dispatch to touch targets.  if (mfirsttouchtarget == null) {      // no touch targets so treat this as an ordinary view.      handled = dispatchtransformedtouchevent(ev, canceled, null, touchtarget.all_pointer_ids);  }  

        到这里就跟之前的逻辑完全一样了,至此父布局的拦截过程结束
        总结:

        • viewgroup 的 ontouchevent 方法直接调用父类(view)的实现
        • 父布局一旦拦截down事件,后续的move事件都直接由父布局执行

        这么分析的好处是咱们的状态是确定的,分析代码不会有太多可能性搞乱逻辑,下面是父布局不拦截的情况下事件分发

        父布局不拦截时的分发处理过程

        父布局不拦截,咱们按照正常流程走一遍,还是按上面那个思路,先 down 后 move

        action_down

        进入 groupview 的 dispatchtouchevent 方法后依旧西先是清空状态,然后判断当前布局是否拦截

          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;  }  

        咱们的设定是不拦截,所以 intercepted = false。下面是遍历子view的代码

          final view[] children = mchildren;  for (int i = childrencount - 1; i >= 0; i--) {      final int childindex = getandverifypreorderedindex(childrencount, i, customorder);      //	逆序拿到一个 child ,即从最上层的子view开始往内层遍历      final view child = getandverifypreorderedview(preorderedlist, children, childindex);            //	判断触点的位置是否在view的范围之内或者view是否在播放动画,如果都不满足则直接遍历下一个      if (!child.canreceivepointerevents()|| !istransformedtouchpointinview(x, y, child, null)) {          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);      //	dispatchtransformedtouchevent 函数是处理分发的函数,父布局处理用的也是这个      //	如果子view消费了事件则给标志位赋值,并 break 结束循环,如果没有消费则继续循环寻找分发      if (dispatchtransformedtouchevent(ev, false, child, idbitstoassign)) {					注释1          // 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();          //	如果子view消费了事件则给 alreadydispatchedtonewtouchtarget  和 mfirsttouchtarget 赋值          //	保存 child          newtouchtarget = addtouchtarget(child, idbitstoassign);							注释2          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);  }  

        分析上面干了啥

        1. 从最上层的子view开始往内层遍历
        2. 判断当前的view在位置上是否满足触点位置
        3. 调用 dispatchtransformedtouchevent 判断是否子view消费了事件

        如果消费了事件则记录 mfirsttouchtarget 和标志位,并跳出循环

        如果没有没有消费事件则继续循环

        注释1的逻辑 dispatchtransformedtouchevent(ev, false, child, idbitstoassign)

          private boolean dispatchtransformedtouchevent(motionevent event, boolean cancel,           view child, int desiredpointeridbits) {              final boolean handled;       if (child == null) {            handled = super.dispatchtouchevent(transformedevent);        } else {            final float offsetx = mscrollx - child.mleft;            final float offsety = mscrolly - child.mtop;            transformedevent.offsetlocation(offsetx, offsety);            if (! child.hasidentitymatrix()) {                transformedevent.transform(child.getinversematrix());            }            //	===== 执行位置 ====            handled = child.dispatchtouchevent(transformedevent);        }  }    

        这次过来 child != null ,调用的是 child.dispatchtouchevent(event) 。child 可能是view,也可能是 viewgroup。如果是 viewgroup 又是一个递归的过程 。层层的递归返回 handled 告诉父布局是否消费了事件!

        再看注释2的逻辑

           private touchtarget addtouchtarget(@nonnull view child, int pointeridbits) {       final touchtarget target = touchtarget.obtain(child, pointeridbits);       //	此时 mfirsttouchtarget = null       target.next = mfirsttouchtarget;       mfirsttouchtarget = target;       return target;   }  

        给 mfirsttouchtarget 赋值,下次 move 事件过来时 mfirsttouchtarget 就是有值的了!!即

        • target.next = null
        • mfirsttouchtarget = newtouchtarget
        • 保存 child 在 target 中

        至此 action_down 事件结束

        action_move

        继上面点击后开始滑动

          if (actionmasked == motionevent.action_down) {        cancelandcleartouchtargets(ev);        resettouchstate();    }  

        move事件不会重置,继续走

           if (actionmasked == motionevent.action_down|| mfirsttouchtarget != null)  

        记得down事件中给mfirsttouchtarget 赋过值嘛,虽然不是down事件依旧可以进入此方法。也就是说这里依旧会判断父布局是否要拦截子view,这里也是以后咱们处理事件冲突的重点。当前的逻辑是不拦截,所以 intercepted = false

          if (actionmasked == motionevent.action_down         || (split && actionmasked == motionevent.action_pointer_down)          || actionmasked == motionevent.action_hover_move)  

        只有action_down事件才会进行分发,所以不会进入遍历子view的逻辑代码!move事件不会分发事件!

          //	mfirsttouchtarget  有值,走else模块  if (mfirsttouchtarget == null) {      // no touch targets so treat this as an ordinary view.      handled = dispatchtransformedtouchevent(ev, canceled, null,touchtarget.all_pointer_ids);  } else {      // dispatch to touch targets, excluding the new touch target if we already      // dispatched to it.  cancel touch targets if necessary.      touchtarget predecessor = null;      touchtarget target = mfirsttouchtarget;      while (target != null) {          final touchtarget next = target.next;          //	alreadydispatchedtonewtouchtarget  是 false          if (alreadydispatchedtonewtouchtarget && target == newtouchtarget) {              handled = true;          } else {          	//	此处的结果是 false              final boolean cancelchild = resetcancelnextupflag(target.child)|| intercepted;              //	在这里被分发处理 child就是我们要分发的对象              if (dispatchtransformedtouchevent(ev, cancelchild,target.child, target.pointeridbits)) {                  handled = true;              }              if (cancelchild) {                  if (predecessor == null) {                      mfirsttouchtarget = next;                  } else {                      predecessor.next = next;                  }                  target.recycle();                  target = next;                  continue;              }          }          predecessor = target;          target = next;      }  }  

        alreadydispatchedtonewtouchtarget 在每次进来时都会重置为 false ,最后又会调用 dispatchtransformedtouchevent 处理分发

          if (child == null) {      handled = super.dispatchtouchevent(transformedevent);  } else {      final float offsetx = mscrollx - child.mleft;      final float offsety = mscrolly - child.mtop;      transformedevent.offsetlocation(offsetx, offsety);      if (! child.hasidentitymatrix()) {          transformedevent.transform(child.getinversematrix());      }  	//	递归调用它来分发      handled = child.dispatchtouchevent(transformedevent);  }  

        至此move事件也结束,做个总结

        • down 事件是事件分发,寻找接盘的 child 并保存在 mfirsttouchtarget 中
        • move 事件虽然不需要遍历寻找接盘的view,但还可以被viewgroup拦截的(比如viewpager包裹着recyclerview,down事件时被recyclerview拦截,横向滑动时被抛弃,这时候viewpager是可以拦截横向滑动接盘的)

        解决冲突方案

        滑动冲突解决方案有两种:内部拦截、外部拦截。顾名思义,内部拦截是在子view中写逻辑拦截,外部拦截则是从父布局下手解决问题

        都以viewpager包裹recyclerview滑动冲突为例

        外部拦截

          public class badviewpager extends viewpager {        private int mlastx, mlasty;        public badviewpager(@nonnull context context) {          super(context);      }        public badviewpager(@nonnull context context, @nullable attributeset attrs) {          super(context, attrs);      }        // 外部拦截法:父容器处理冲突      // 我想要把事件分发给谁就分发给谁      @override      public boolean onintercepttouchevent(motionevent event) {            int x = (int) event.getx();          int y = (int) event.gety();            switch (event.getaction()) {              case motionevent.action_down: {                  mlastx = (int) event.getx();                  mlasty = (int) event.gety();                  break;              }              case motionevent.action_move: {                  int deltax = x - mlastx;                  int deltay = y - mlasty;                  if (math.abs(deltax) > math.abs(deltay)) {	//	横向滑动时拦截                      return true;                  }                  break;              }              case motionevent.action_up: {                  break;              }              default:                  break;          }            return super.onintercepttouchevent(event);        }  }  

        内部拦截

        viewpager 代码

          public class badviewpager extends viewpager {        private int mlastx, mlasty;        public badviewpager(@nonnull context context) {          super(context);      }        public badviewpager(@nonnull context context, @nullable attributeset attrs) {          super(context, attrs);      }      @override      public boolean onintercepttouchevent(motionevent event) {          if (event.getaction() == motionevent.action_down){              super.onintercepttouchevent(event);              //	此处是重点              return false;          }          return true;      }  }  

        recyclerview 代码

          public class mylistview extends listview {        public mylistview(context context) {          super(context);      }        public mylistview(context context, attributeset attrs) {          super(context, attrs);      }        // 内部拦截法:子view处理事件冲突      private int mlastx, mlasty;        @override      public boolean dispatchtouchevent(motionevent event) {          int x = (int) event.getx();          int y = (int) event.gety();            switch (event.getaction()) {              case motionevent.action_down: {                  getparent().requestdisallowintercepttouchevent(true);                  break;              }              case motionevent.action_move: {                  int deltax = x - mlastx;                  int deltay = y - mlasty;                  if (math.abs(deltax) > math.abs(deltay)) {                      getparent().requestdisallowintercepttouchevent(false);                  }                  break;              }              case motionevent.action_up: {                  break;                }              default:                  break;          }            mlastx = x;          mlasty = y;          return super.dispatchtouchevent(event);      }  }  

        此处一定要注意,父布局在 action_down 时一定要返回false。原因如下:

        当分发down事件时,执行了 resettouchstate(); 函数

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

        mgroupflags &= ~flag_disallow_intercept

        在判断父布局拦截时

          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;  }  

        即 mgroupflags &= ~flag_disallow_intercept & flag_disallow_intercept != 0 ==》false
        使用 if 语句永远是true,在这里viewpager会拦截事件,所以recyclerview无法上下滑动。所以内部拦截时要修改父布局的 onintercepttouchevent 函数!

        到此这篇关于android 深入探究自定义view之事件的分发机制与处理详解的文章就介绍到这了,更多相关android 自定义view内容请搜索<计算机技术网(www.ctvol.com)!!>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<计算机技术网(www.ctvol.com)!!>!

        本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

        ctvol管理联系方式QQ:251552304

        本文章地址:https://www.ctvol.com/addevelopment/937638.html

        (0)
        上一篇 2021年11月12日
        下一篇 2021年11月12日

        精彩推荐