ViewGroup事件分发机制源码解析(二)ViewGroup篇(含滑动冲突源码解析和解决)

上一篇讲解了View的事件分发机制,查看点击链接View事件分发机制查看。本文基于Android9.0的源码进行分析ViewGroup的事件分发机制和事件冲突解决方案,源码点击https://github.com/Oaman/Forward查看。

概述

本文分如下几个步骤分析

  • ViewGroup的Down事件的分发源码分析
  • ViewGroup的Move事件的分发源码分析
  • ViewGroup的滑动事件冲突处理实战 + 源码分析
ViewGroup的事件分发机制源码概览

ViewGroup的dispatchTouchEvent是由Activity的dispatchTouchEvent,到PhoneWindow的superDispatchTouchEvent,再到DecorView的superDispatchTouchEvent,最后才到ViewGroup的dispatchTouchEvent的, 流程图如下:

在这里插入图片描述
ViewGroup的dispatchTouchEvent是用来进行事件分发的,我们将其分为三个部分进行分析,后面分析中主要是对这三个大的步骤进行分析(后面分析中提到的步骤1|2|3指的就是这里)。

	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            // 此处会清除requestDisallowInterceptTouchEvent设置的FLAG
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
			
			// 1 判断是否拦截事件  intercepted代表是否拦截,
			// intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的
            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 {
                intercepted = true;
            }

			// 2  如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                             ev.setTargetAccessibilityFocus(false);
                              continue;
                          }
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                             newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             alreadyDispatchedToNewTouchTarget = true;
                             break;
                        }
                    }
                }
            }

			// 3 根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                }
            }
        return handled;
    }

三个步骤分别是:

  • 注释1处判断是否拦截事件 intercepted代表是否拦截。
  • 注释2处如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发
  • 注释3根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled。

ViewGroup的Down事件的分发源码分析

根据父容器是否拦截,这里分为两种情况来分析源码

(1)父容器拦截即onInterceptTouchEvent返回true

这一种情况比较简单,因为onInterceptTouchEvent返回true, 所以在上面的三步分析中,第一步intercepted为true, 那么第二步不会进入,直接走到第三步,源码如下:

	if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }

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

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

        if (child == null) {
        	// 1 
            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);
        }
        return handled;
    }

上面进入到dispatchTransformedTouchEvent方法中,因为参数child为null, 所以直接走到了注释1处的super.dispatchTouchEvent中,就是进入到了View的事件分发处理中,Down事件结束。点击链接查看View事件分发机制


(2)父容器不拦截即onInterceptTouchEvent返回false(默认也为false)

如果父容器不拦截的话,那么intercepted就为false, 就会走第二步的事件Down的分发逻辑,源码如下:

		// 1
		if (!canceled && !intercepted) {
				// 2
                if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
                    final View[] children = mChildren;
                    //3 
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        // 4
                        if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                             ev.setTargetAccessibilityFocus(false);
                              continue;
                          }
                        // 5
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        	 // 6
                             newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             alreadyDispatchedToNewTouchTarget = true;
                             break;
                        }
                    }
                }
            }

上面因为注释1处的intercepted为false,所以进入if条件。在注释2处,当前是Down事件,所以进入注释2处的if判断,在注释3处for循环获取子View,在注释4处判断获取的View是否满足事件分发的条件,比如点击的坐标是否位于View内部等;在注释5处真正的用来判断是否view能处理这个Down事件,如果所有View都不能处理的话,还是走到类似于父类拦截的那种情况,最终是由父容器处理此次Down事件。

如果有子View处理Down事件的话,注释5这里返回true,就会走注释6处的addTouchTarget方法,并且将alreadyDispatchedToNewTouchTarget赋值true, 然后break跳出for循环, addTouchTarget源码如下:

	private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

上面主要实现的目的是将mFirstTouchTarget赋值不为null, 它是一个TouchTarget类型,里面包含了处理此次Down事件的View,并且内部维护了一个next指针指向下一个TouchTarget。这里mFirstTouchTarget和alreadyDispatchedToNewTouchTarget的值在后面会用到。接下来我们分析三大步骤中的第三步,源码如下:

	   // 1
	   if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            // 2
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 3
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
            }
        }

因为mFirstTouchTarget这时候已经不为null了,所以这里不会进入到注释1处,在注释2处将mFirstTouchTarget赋值给target,在注释3处很明显两个值都满足条件,所以进入注释3的if判断,然后handled为true, 返回true,Down事件结束。

(3)Down事件分发小结
  • 事件的分发是在Down事件进行的分发
  • 父容器拦截的话就由父容器处理,如果父容器不拦截的话,但是子View不处理的话(比如ImageView),还是得父容器处理
  • 如果父容器不拦截,子View处理的话,那么在mFirstTouchTarget中就会保存Down事件的处理View,方便在后续的Move事件中找到这个View

ViewGroup的Move事件的分发源码分析

我们接着上面Down事件的情况(父容器不拦截)分析,Move事件在第一个步骤中,intercepted为false,在步骤2中是对Down事件进行分发的,Move事件是进入不到步骤2中的,接着进入到步骤3中的源码,源码如下:

	   // 1
	   boolean alreadyDispatchedToNewTouchTarget = false;
	   if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            // 2
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 3
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                     // 3
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
            }
        }

Move事件在上面注释1处会将alreadyDispatchedToNewTouchTarget赋值为false,在注释2处将mFirstTouchTarget赋值给target, 在注释3处执行事件的分发,如何找到处理事件的View的呢?我们看到就是通过target.child找到的,也就是我们在Down事件中保存的view,到此正常的Move事件分发分析完毕。

Move事件分发小结

Move事件因为不做事件分发处理,所以直接找到在Down事件中保存的view来处理Move事件即可。


ViewGroup的滑动事件冲突处理实战 + 源码分析

(demo源码见https://github.com/Oaman/Forward)

事件冲突解决源码分析

首先要明确一点,就是滑动事件冲突的解决都是在Move事件中解决的。我们首先通过源码来分析事件冲突应该怎么做?

	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            // 此处会清除requestDisallowInterceptTouchEvent设置的FLAG
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                // 0 
                resetTouchState();
            }
			
			// 1 判断是否拦截事件  intercepted代表是否拦截,
			// intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 2 
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                	// 3
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

			//   如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                             ev.setTargetAccessibilityFocus(false);
                              continue;
                          }
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                             newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             alreadyDispatchedToNewTouchTarget = true;
                             break;
                        }
                    }
                }
            }

			//  根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                }
            }
        return handled;
    }

我们前面说过,第一步中的intercepted的最终取值也受disallowIntercept的影响,在Down事件分发的时候,从代码上看如果disallowIntercept为true的话,那么直接就会走到注释3的地方,将intercepted赋值为false。而disallowIntercept取值是由ViewGroup.requestDisallowInterceptTouchEvent决定的,源码如下:

	/**
     * @param disallowIntercept if true means son will get this event.
     */
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            //0x10000 = 524288
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            //~   1000 0000 0000 0000 0000
            //~ 0 0111 1111 1111 1111 1111
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

从上面方法注释处知道,如果这里将disallowIntercept赋值为true的话,理论上是可以禁止父类拦截的,则会由子类得到事件。但是实际上真的是这样吗?经过测试就会发现,Down事件如果父容器不主动分发给子View的话,子View是拿不到的,源码逻辑见resetTouchState(),这个方法在Down事件的时候会触发,会重置requestDisallowInterceptTouchEvent设置的FLAG值。所以如果想要在Down事件的时候将Down事件分发给子View的话,需要父容器协助,下面分析滑动冲突的解决思路。

一般滑动事件冲突解决方案有两种,内部拦截法和外部拦截法,

内部拦截法

我们这里首先看内部拦截法,假如ViewPage嵌套了ListView(子ListView上下滑动,父ViewPager左右滑动),那么这种冲突的处理通过内部拦截法的代码如下:

public class MyListView extends ListView {
   	...
    private int mLastX, mLastY;

    /**
     * 处理之间冲突 - 内部拦截法
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            	// 1 
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                	// 2
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}

我们自定义了ListView, 重写了它的dispatchTouchEvent,在Down事件的时候,注释1处调用了getParent().requestDisallowInterceptTouchEvent(true),在注释2的Move事件的时候调用了getParent().requestDisallowInterceptTouchEvent(false), 上面分析过了,需要父容器协助来帮助子View得到Down事件,所以父容器的代码如下:

public class MyViewPager extends ViewPager {
	...
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        super.onInterceptTouchEvent(ev);
        boolean intercept = ev.getAction() != MotionEvent.ACTION_DOWN;
        return intercept;
    }
}

就是说在Down事件的时候,父容器不拦截事件,并且子ListView设置了getParent().requestDisallowInterceptTouchEvent(true),那么Down事件就由子View处理了,当左右滑动的时候,子ListView设置getParent().requestDisallowInterceptTouchEvent(false)将事件重新交给父ViewPager处理,就实现了滑动事件冲突的解决。

外部拦截法
public class MyViewPager extends ViewPager {
	...
    /**
     * 处理之间冲突 - 外部拦截法
     */
    private int mLastX, mLastY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                return true;
            }
        }

        mLastX = x;
        mLastY = y;
        return super.onInterceptTouchEvent(ev);
    }
}

外部拦截法的实现思想和内部拦截法一样的,下面我们从源码层分析子ListView是如何将Move事件还给父ViewPager的。

滑动冲突中子View(ListView)如何将Move事件换给父容器(ViewPager)的?

因为现在分析的是Move事件,假如子ListView左右滑动就会将Move事件交给父ViewPager,下面从源码层分析如何实现的。

因为左右滑动的时候执行了requestDisallowInterceptTouchEvent(false),所以此时intercepted=true,接着就走到了下面的代码中:

		  if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                    	// 1 
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 2
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                            	// 3
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                }
            }

因为mFirstTouchTarget不等与null, 所以直接走到了注释1处,因为intercepted为true,所以cancelChild就为true,就会执行注释2处的dispatchTransformedTouchEvent方法,在注释3处将mFirstTouchTarget置为null方法源码如下:

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

        final int oldAction = event.getAction();
        // 1
        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;
        }

        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());
            }
            // 2
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

上面注释1处,因为cancel为true,所以会执行child, 也就是ListView的CANCEL事件,所以CANCEL事件什么时候执行?CANCEL事件在事件被上层拦截的时候触发。

接着执行到注释2处的代码,此Move事件结束,此次的Move事件其实对于ViewPager没有任何影响,不过因为Move事件有多个,我们继续看下一个Move事件。

			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 {
                intercepted = true;
            }

这里因为是Move事件并且mFirstTouchTarget=null, 所以intercepted = true,所以直接走到了第三步骤的if,源码如下:

  if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
  } 

到这里就将Move又交给了父容器,实现了滑动冲突的处理,另外在此实战中,左右滑动时候上下滑动是不可能的,因为父类处理了事件,是不会再给子类处理的;但是上下滑动时候可以左右滑动,因为事件是可以再次给父类的。就是父容器可以抢子view的事件。

事件分发和滑动冲突总结
  • 事件分发是在Down事件时候进行的分发。
  • 滑动事件冲突是在Move事件进行处理。
  • 是否拦截的intercepted是由onInterceptTouchEvent和disallowIntercept共同决定的,如果父容器不主动把Down事件交给子View的话,子View通过调用requestDisallowInterceptTouchEvent(true)是永远拿不到Down事件的,如果子View拿到了事件,如果不主动交给父容器的话,父容器后期也是永远拿不到事件的。
  • 如果Down事件没有处理,Move事件也不会处理这个结论是只针对叶节点View的。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值