【Android API】Android事件分发机制和滑动冲突

难度★★☆☆☆ 了解Android的事件分发机制是写出好的自定义控件的基石,同样,熟悉事件分发机制也有利于解决各种滑动冲突

Android事件分发机制

  前面我们学习了setContentView流程,知道了我们自己的布局是如何显示在屏幕上:Activity持有Window,Window持有DecorView,DecorView中的FrameLayout就是我们自己视图的起点,同时,在我们将DecorView添加到WindowManager中的时候,会创建ViewRootImpl,它也持有DecorView。
  那么我们如何知道事件是怎么传递到我们的View的呢?这里可以有个技巧,就是我们自定义个View,然后在onTouchEvent()中抛个异常,堆栈信息中就能看到事件是从哪里传递过来的。通过这个方法,我们知道ViewRootImpl的内部类WindowInputEventReceiver的方法onInputEvent()会接收到所有事件,然后通过ViewRootImpl,最终事件传递到DecorView的dispatchTouchEvent()方法中,到此,这是事件分发的根部。
  紧接着DecorView调用Activity的dispatchTouchEvent(),然后里面调用DecorView的super.dispatchTouchEvent(),DecorView的父类就是FrameLayout,FrameLayout又继承自ViewGroup,所以super.dispatchTouchEvent()调用的是ViewGroup的dispatchTouchEvent()。
  整个视图树的事件分发就从这开始了。我们再看ViewGroup中的dispatchTouchEvent()做了什么

  // 检查是否拦截事件
 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
     if (!disallowIntercept) {
         intercepted = onInterceptTouchEvent(ev);
     } else {
         intercepted = false;
     }
 } else {
    //如果这个事件不是Down事件,而且没有要消费事件的View(mFirstTouchTarget==null)
    //那么我们就拦截事件
     intercepted = true;
//没有拦截
if (!intercepted) {
    //如果是按下事件
     if (actionMasked == MotionEvent.ACTION_DOWN) {
         if (childrenCount != 0) {
               //获取点击的坐标(相对于屏幕的)
               final float x = ev.getX(actionIndex);
               final float y = ev.getY(actionIndex);
               final View[] children = mChildren;
               //倒序遍历子View
               for (int i = childrenCount - 1; i >= 0; i--) {
                   //第一个代表能否接收事件(如果是Visible或者有Animation动画,代表能接收的状态),这里说明Invisible的View是不能响应点击事件的
                   //第二个判断是点击的坐标(x,y)是否在View的范围内
                   if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {       
                        continue;
                   }
                   //在这里我们将事件分发到子View,如果子View消费了事件,就返回True,那么事件就不会传递给剩下的View了
                   if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                       //这里记录下消费的View,下次直接传给他了
                       mFirstTouchTarget = TouchTarget.obtain(child, pointerIdBits);     
                       break;
                   }
               }
          }
     }
}
//
if (mFirstTouchTarget == null) {
     // 如果没有发现子View消费了事件,那么这个事件传到自己的onTouchEvent()中
     handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
 }else {
      //如果不是Down事件,而且 mFirstTouchTarget !=null,说明它有子View要继续消费接下来的事件,那么就将事件分发下去  
     TouchTarget target = mFirstTouchTarget;
     while (target != null) {
         final TouchTarget next = target.next;
         if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
             handled = true;
         } else {
             //这里将事件传递给子View
             if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                 handled = true;
             }
         }
         target = next;
     }
 }
//返回它或者它的子View是否有消费了事件
return handled;

上面就是ViewGroup中的事件分发,我们再简单看一下上面dispatchTransformedTouchEvent()里面是如何分发事件的

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

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        final MotionEvent transformedEvent;
        //如果新的事件的触点等于旧事件的触点(点击事件触点就为1)
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                //调用View的dispatchTouchEvent()
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);
                    //调用子View的dispatchTouchEvent
                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                //返回是否消费
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

下面我们用图形来看看事件分发是怎么样的,我们所有的View可以组成一颗树,事件分发就是事件沿树走的路径,图中的标号就是事件分发的顺序,可以看到,事件是按照右侧红色箭头的轨迹来遍历树的。
事件分发

  • 我们看到,如果事件没有被拦截,那么Down事件一定会走到View-3,然后调用View-3的onTouchEvent(),如果没有被消费,那么会接着遍历剩下的View
  • 所以,我们onTouchEvent的调用顺序就是,3-4-2-6-7-5-1,记住,没有拦截,那么事件一定会先传递到视图树的右下角
  • 3-4-2-6-7-5-1 依次调用onTouchEvent,只要有一个View消费了事件,那么后面的onTouchEvent就不会被调用,比如View-4的onTouchEvent返回true,那么2-6-7-5-1的onTouchEvent都不会被调用
  • 如果有拦截的情况,那么事件会分发给拦截事件的View,调用它的onTouchEvent,比如,ViewGroup-2拦截了事件,那么就直接掉用ViewGroup-2的onTouchEvent,View-3、View-4的dispatchTouchEvent都不会调用了,如果ViewGroup-2没有消费掉事件,那么还是按照2-6-7-5-1的顺序调用接下来的onTouchEvent()
  • 事件分发其实就是树的遍历,事件先要传递到叶子节点,然后根据图中的方向来遍历树,原则就是有拦截事件就不向下传递,只遍历剩下的;有事件被消费,那么剩余节点的onTouchEvent都不会被调用

解决滑动冲突

  滑动冲突其实就是一个可以滑动的控件,嵌套在另一个可以滑动的控件之中,那么我们就要根据业务需求来决定事件到底要传递给谁,比如我们的ListView中有侧滑删除的控件,那么我们竖直滑动的时候需要ListView来响应事件,水平滑动的时候需要侧滑控件来响应。
  我们有两种通用的解决办法

  1. 重写外部控件的onInterceptTouchEvent,如果是水平滑动我们返回false,不拦截事件;如果是竖直滑动,我们就返回true,让父控件本身消费事件
  2. 第二种方法就是重写子控件的dispatchTouchEvent,在Down时候我们getParent().requestDisallowInterceptTouchEvent(true)这句话的意思就是让父控件不要调用onInterceptTouchEvent,但是要想子控件调用这句话就先要让Down事件传递过来,所以要重写父控件的拦截事件,让他不要拦截Down事件。然后接下来Move事件让子控件来判断,如果是水平滑动就继续让父控件不要拦截,如果是竖直滑动就调用getParent().requestDisallowInterceptTouchEvent(false),这样父控件就能调用onInterceptTouchEvent来拦截事件了

  最后,滑动冲突解决就是父控件根据业务来判断是否要拦截事件,这是解决滑动冲突的本质。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值