事件传递机制分析

687 阅读14分钟

事件传递是从Activity->Window->DecorView。而Window的唯一实现是PhoneWindow。PhoneWindow又包含DecorView,而我们在setContentView设置的布局文件其实是添加到DecorView中一个id为@android:id/content的View中。AppCompatActivity和Activity setContentView加载机制有些不同,但最终结果一样,我们的布局都是添加到一个id为content的View中。

本文是基于compileSdkVersion28,AppCompatActivity分析。

1.DecorView的创建

前面我们提到setContentView()设置的布局其实是添加到DecorView中一个id为@android:id/content的View中,也就是如下结构:

DecorView
    //省略中间布局
    ...
    FrameLayout @android:id/content
        setContentView设置的View

那我们看下setContentView()后如何创建DecorView并将布局添加到DecorView的一个id为@android:id/content的View中。

接下来以AppCompatActivity作为分析

setContentView()调用后会调用AppCompatDelegateImpl#setContentView(),实现如下。

    public void setContentView(int resId) {
        //1.创建mSubDecor
        ensureSubDecor();
        //2.查找mSubDecor中一个id为content的ViewGroup
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        //3.将布局添加到contentParent中
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

上面这段代码主要是第一步的调用。看看ensureSubDecor()调用逻辑。

    private void ensureSubDecor() {
       if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
       }
       ...

createSubDecor()调用实现

//AppCompatDelegateImpl#createSubDecor()
private ViewGroup createSubDecor() {
    <!--以下是第一步-->
    //调用PhoneWindow#getDecorView()创建DecorView
    mWindow.getDecorView();
    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
    //我们使用WindowNoTitle主题,所以省略
    ...
    inflater.inflate(R.layout.abc_screen_simple, null);
    <!--以下是第二步-->
    //查找subDecor中的ContentFrameLayout
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);
    //查找PhoneWindow#mContentParent,第一步时已经分析过了   
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    //如果PhoneWindow#mContentParent有子View,那么移除并添加到subDecor中的ContentFrameLa//yout中。
    if (windowContentView != null) {
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }
        //清除PhoneWindow#mContentParent的id,并将android.R.id.content设置给subDecor#ContentFrameLayout
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
    }
    //把subDecor设置给PhoneWindow#mContentParent当做子View
    mWindow.setContentView(subDecor);
    return subDecor;
}

第一步创建DecorView

//PhoneWindow#getDecorView(),最终调用PhoneWindow#installDecor()
private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        //创建DecorView
        mDecor = generateDecor(-1);
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        //创建DecorView中一个id为@android:id/content的ViewGroup。
        mContentParent = generateLayout(mDecor);
    }
}

第二步是将subDecor添加到 ==PhoneWindow#DecorView#mContentParent== 中,而我们的布局是添加到==subDecor#ContentFrameLayout==中。

通过代码我们知道Android对Activity和AppCompatActivity加载布局文件做了调整, AppCompatActivity#Window视图:

AppCompatActivity#Window视图

Activity#Window视图:

Activity#Window视图

其中ParentView是我们自己的View

由此可见中间多了一层==FitWindowsLinearLayout==,而AppCompatActivity设置的布局是加载到FitWindowsLinearLayout#ContentFrameLayout中。

2.事件分发分析

前面我们了解了DecorView创建过程,并且知道DecorView是PhoneWindow中一个成员变量,是布局层级的最顶端。

在分析事件分发之前有必要先了解以下说明:

  • 事件传递是一层一层传递,按照View布局从外向内;按照层级从上往下。

  • ViewGroup 在整个事件分发机制有三个方法参与分别是:

    dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()

  • View 在整个事件分发机制有两个方法参与分别是:

    dispatchTouchEvent(),onTouchEvent()

因为View没有子View,所以它不存在拦截事件一说,只有消不消费一说。

  • dispatchTouchEvent()代表事件分发,是事件到这一层后被调用,只要事件能到该层View,那么dispatchTouchEvent()一定会被调用。
  • onInterceptTouchEvent()代表事件是否拦截,只有ViewGroup才拥有此方法,返回true代表拦截此事件,接着该层View#onTouchEvent()被调用;false代表不拦截事件,事件继续向下层View传递。
  • onTouchEvent()代表事件消费,返回true代表消费事件,false代表不消费此事件。

2.1 Activity事件分发

//Activity#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //一个空方法,不用管它
        onUserInteraction();
    }
    //这里调用PhoneWindow#superDispatchTouchEvent()
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

//PhoneWindow#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
}
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

由于DecorView继承自FrameLayout,FrameLayout继承ViewGroup。所以此时事件到达ViewGoup。

由此可见当顶层ViewGroup#dispatchTouchEvent()返回false时,也就是整个View树都不消费事件,Activity#onTouchEvent()将被调用。而顶层ViewGroup#dispatchTouchEvent()的返回值由它的下层View#dispatchTouchEvent()决定,如果每一层View都不消耗事件。最终这个事件就会交给Activity#onTouchEvent()处理。

2.2 ViewGroup事件分发

ViewGroup事件分发逻辑由于繁琐,这里对ViewGroup#dispatchTouchEvent()进行拆解,。这里从主到次分析,只是为了能够更好记忆并理解事件分发过程。

ViewGroup是否拦截事件判断

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
	//将mFirstTouchTarget置空,清除mGroupFlags中FLAG_DISALLOW_INTERCEPT标记位
	cancelAndClearTouchTargets(ev);
	resetTouchState();
}
//是否拦截此事件
final boolean intercepted;
//一个事件序列开始于down事件,判断条件左边成立
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
	//disallowIntercept 代表子View请求父View不要拦截事件。
	//FLAG_DISALLOW_INTERCEPT标记位通过View#requestDisallowInterceptTouchEvent()设置
	final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
	if (!disallowIntercept) {
		//询问ViewGroup是否拦截事件
		intercepted = onInterceptTouchEvent(ev);
		ev.setAction(action); 
	} else {
		intercepted = false;
	}
} else {
	intercepted = true;
}

mFirstTouchTarget的赋值意味着找到消费事件的View了。

mFirstTouchTarget类型为TouchTarget,TouchTarget 类描述如下

private static final class TouchTarget {
        //消耗事件的View
        public View child;
        //触控点集合
        public int pointerIdBits;
        //记录下一个TouchTarget对象
        public TouchTarget next;
}

pointerIdBits怎么理解呢?

比如一个根手指按下时pointerId1=1,二进制表示为0001。第二根手指pointerId2=2,二进制表示为0010。那么要将两根手指按下操作记录到同一个变量中。那这里就是使用的位运算pointerId1&pointerId2 即 0001&0010=0011。将位运算的结果存储到poinerIdBits中。这里只是举例说明pointerIdBits值的运算,并不是代表实际pointerIdBits值。实际pointerIdBits值运算需要==idBitsToAssign==参与。我们下文中会提到。

根据上面TouchTarget类结构分析,mFirstTouchTarget是个链表。==思考:为啥mFirstTouchTarget设计位链表呢?设计成View不就更加明确吗?==

小结:

  1. 子View#requestDisallowInterceptTouchEvent()可以影响上层ViewGroup是否拦截此事件,除了down事件。因为down事件来临时会将mFirstTouchTarget置空和清空requestDisallowInterceptTouchEvent设置的标记位即FLAG_DISALLOW_INTERCEPT。

  2. 每次down事件来临都会询问ViewGroup是否拦截此事件,不管之前有没有view消费事件。

接着往下分析

//检查是否是取消事件
final boolean canceled = resetCancelNextUpFlag(this) 
|| actionMasked == MotionEvent.ACTION_CANCEL;

//是否支持多点触控
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
//标记是否有view消费事件
boolean alreadyDispatchedToNewTouchTarget = false;

resetCancelNextUpFlag()

private static boolean resetCancelNextUpFlag(@NonNull View view) {
    //判断当前View是否包含PFLAG_CANCEL_NEXT_UP_EVENT标记
    if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
        //包含移除后返回
        view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        return true;
    }
    return false;
}

PFLAG_CANCEL_NEXT_UP_EVENT根据View中对该变量的注释描述是view是否暂时分离。而暂时分离听起来很抽象,不过搜索下该变量。该变量在View#onStartTemporaryDetach()被添加到mPrivateFlags变量中。

这里以AbsListView举例,在AbsListView#RecycleBin#addScrapView()最终调用了View#onStartTemporaryDetach()。那根据addScrapView()方法作用不难猜出PFLAG_CANCEL_NEXT_UP_EVENT标记的意思。在ListView中被划出屏幕的itemView会被标记上PFLAG_CANCEL_NEXT_UP_EVENT 标记。在后续dispatchTouchEvent()事件时当控件持有PFLAG_CANCEL_NEXT_UP_EVENT标记时,则清除该标记并返回是取消事件。

接着往下分析

if (!canceled && !intercepted) {
	...
	//如果是down事件,或者第一个手指以外的手指进入屏幕
	if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
	    //获取actionIndex是为了获取手指id即pointerId
		final int actionIndex = ev.getActionIndex(); 
		final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
				: TouchTarget.ALL_POINTER_IDS;
	    //移除和idBitsToAssign相同的触控点,移除后,如TouchTarget不存在其他的触控点记录,则从链表中移除。
		removePointersFromTouchTargets(idBitsToAssign);
    ...
}

什么是pointerId?

pointerId是触控点类型唯一标识。即当屏幕上只有一根手指操作时,pointerId=0。第一根手指未抬起,第二根手指按下时,pointerId=1。也就是说可以根据pointerId来识别是哪根手指在操作。

什么是actionIndex?

actionIndex 是多点触控中记录某个点的索引。比如4个手指A,B,C,D依次按下屏幕对应的索引如下

手指 ActionIndex
A 0
B 1
C 2
D 3

actionIndex是怎么获取的?

MotionEvent中和getActionIndex有关系的代码:

//MotionEvent.java
public static final int ACTION_POINTER_INDEX_SHIFT = 8;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
public static final int ACTION_MASK             = 0xff;

//取getAction()高8位
public final int getActionIndex() {
    return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
            >> ACTION_POINTER_INDEX_SHIFT;
}

//取getAction()低8位
public final int getActionMasked() {
    return nativeGetAction(mNativePtr) & ACTION_MASK;
}

public final int getAction() {
    return nativeGetAction(mNativePtr);
}

由以上代码可知getActionIndex()返回的是getAction()==高8位==字节。==低8位==字节是由getActionMasked()表示。一个int来存储两个信息的做法,在Android源码中比较常见,因为pointerIndex和action的范围都很少,单独给每一个分配一个空间,比较浪费。MeasureSpec就是将Mode和Size整合在一起的例子。到这里,我们就清楚了actionIndex的来历了。

idBitsToAssign取值计算:

如果支持多点触控idBitsToAssign=1 << ev.getPointerId(actionIndex)。 其实就是当一根手指时 1<<0,两根手指时 1<<1。即0001和0010。

removePointersFromTouchTargets()

private void removePointersFromTouchTargets(int pointerIdBits) {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        //包含该触控点
        if ((target.pointerIdBits & pointerIdBits) != 0) {
            //移除该触控点
            target.pointerIdBits &= ~pointerIdBits;
            //移除后如果没有任何触控点记录
            if (target.pointerIdBits == 0) {
                if (predecessor == null) {
                    //清空mFirstTouchTarget
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

ViewGroup事件分发逻辑

if (newTouchTarget == null && childrenCount != 0) {
	final float x = ev.getX(actionIndex);
	final float y = ev.getY(actionIndex);
	final ArrayList<View> preorderedList = buildTouchDispatchChildList();
	//手否自定义顺序默认为false
	final boolean customOrder = preorderedList == null
			&& isChildrenDrawingOrderEnabled();
	final View[] children = mChildren;
	//逆序遍历
	for (int i = childrenCount - 1; i >= 0; i--) {
		final int childIndex = getAndVerifyPreorderedIndex(
				childrenCount, i, customOrder);
		final View child = getAndVerifyPreorderedView(
				preorderedList, children, childIndex);
        ...
		//是否可以接受事件和点击位置是否在view范围内
		if (!canViewReceivePointerEvents(child)
				|| !isTransformedTouchPointInView(x, y, child, null)) {
			ev.setTargetAccessibilityFocus(false);
			continue;
		}
		newTouchTarget = getTouchTarget(child);
		//先在链表中查找,找到了说明触控点作用在同一个View上。
		//如果没找到则有两种情况:
		//1.没有任何触控点时,进行操作
		//2.存在一个触控点时,又新增一个触控点。(多点触摸作用在同一View)
		if (newTouchTarget != null) {
			//添加新的触控点操作,跳出循环
			newTouchTarget.pointerIdBits |= idBitsToAssign;
			break;
		}
		//进行分发
		if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
		    //如果有子View消费事件,则添加到链表头部,并赋值给newTouchTarget和mFirstTouchTarget
			newTouchTarget = addTouchTarget(child, idBitsToAssign);
			//标记已经分发给新的目标
			alreadyDispatchedToNewTouchTarget = true;
			break;
		}
	}
}

canViewReceivePointerEvents()

判断依据是根据View是否VISIBLE&&是否播放动画

isTransformedTouchPointInView()

根据触控的位置判断是否在View范围内。其依据根据该层View来计算子View的点击范围。

dispatchTransformedTouchEvent()

  1. 当child!=null时对child进行事件分发,调用child#dispatchTouchEvent()
  2. 内部在对child分发前将parent的坐标转换为相对于child坐标系的坐标。然后进行分发,分发完后对其进行坐标恢复。具体代码下面给出。
  3. 当child=null时,调用该层View#onTouchEvent()。
  4. 返回值受child#dispatchTouchEvent()和该层View#onTouchEvent()影响,返回true表示消费事件,并将消费事件的View添加到链表头部,跳出循环。返回false表示不消费事件,继续下次循环,直到循环结束。
  5. ViewGroup遍历完子View后未找到需要消费事件的child时,child传递的是null。关于child传递null时方法内部处理,在第3点已经说明。

对child分发前将parent的坐标转换为相对于child坐标系的坐标。这就是我们在getX(),getY()时,获取的是相对于父空间的位置。而不是屏幕的位置的原因。

final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
//分发前要将parent的坐标转换为相对于child坐标系的坐标
event.offsetLocation(offsetX, offsetY);
//分发
handled = child.dispatchTouchEvent(event);
//恢复
event.offsetLocation(-offsetX, -offsetY);

根据以上代码分析我们可以回答下思考问题

为啥mFirstTouchTarget设计位链表呢?设计成单个对象不就更加明确吗?

假如mFirstTouchTarget设置成单个对象,当第一根手指按下A时,mFirstTouchTarget被赋值为A。这时第二根手指按下B, 此时mFirstTouchTarget被赋值为B。那后续的move操作只能作用到B上,而A无响应。这对于游戏开发来说肯定不行,一个手控制人物方向,一个手控制人物行为。

所以不同情况对mFirstTouchTarget链表的长度有所影响:

  • 非多点触控:mFirstTouchTarget链表退化成单个TouchTarget对象。
  • 多点触控,目标相同:同样为单个TouchTarget对象,只是pointerIdBits保存了多个pointerId信息。
  • 多点触控,目标不同:mFirstTouchTarget成为链表。

循环结束后执行以下代码

 if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN){
    for(){
        dispatchTransformedTouchEvent()
    }    
    //将触控类型符加给上一个消费事件的view。
    //我们在下面问题回答中有解释。这里简单理解为为了解决多指下作用在不同View上的特定问题。
    if (newTouchTarget == null && mFirstTouchTarget != null) {
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
        newTouchTarget.pointerIdBits |= idBitsToAssign;
    }
}


 if (mFirstTouchTarget == null) {
    //在介绍dispatchTransformedTouchEvent()时已经说明
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
 } else {
    //遍历分发事件
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        //已经分发过down事件了,所以这里直接标记已处理
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            //是否分发cancel事件
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            //分发move|up|cancel事件        
            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;
    }
}

上面ViewGroup事件分发源码分析可以总结出以下几点:

  1. 子View#requestDisallowInterceptTouchEvent()可以影响上层ViewGroup是否拦截此事件,除了down事件。因为down事件来临时会将mFirstTouchTarget置空和清空requestDisallowInterceptTouchEvent设置的标记位即FLAG_DISALLOW_INTERCEPT。

  2. 每次down事件来临都会询问ViewGroup是否拦截此事件,不管之前有没有view消费事件。

  3. ViewGroup#dispatchTouchEvent()返回值受下层View#dispatchTouchEvent()返回值影响,如果该层是ViewGroup,那么返回值受该层的下层dispatchTouchEvent()影响。依次类推,层层传递,直到找到该事件链上最后一个View。

  4. 如果ViewGroup拦截事件或者没有找到需要分发事件的View,则同一事件序列中的事件都交由该ViewGroup#onTouchEvent处理

  5. 分发给子View时要将相对于父View的坐标转换为相对于子View坐标系的坐标,所以我们拿到的MotionEvent的getX()代表这个相对child的坐标,不然拿到的是相对于屏幕的坐标。

  6. 如果整个事件链上的View都不消费事件,事件会向上抛,直到Activity#onTouchEvent被调起。在 2.1 Activity事件分发 已经说明。

2.3 View事件分发

前面在分析ViewGroup事件分发说当在进行down事件分发时会遍历ViewGroup的子View,并且调用ViewGroup#dispatchTransformedTouchEvent(),其内部会调用子View的dispatchTouchEvent()。那这里就从View#dispatchTouchEvent()开始分析

View#dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent event) {
        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                //设置了setOnTouchListener并且onTouch返回true    
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                //否则result值受onTouchEvent()返回值的影响
                result = true;
            }
        }
        return result;
}

从View#dispatchTouchEvent()看出==View#setOnTouchListener优先级高于onTouchEvent()==。由于setOnTouchListener是外部设置的,所以这里直接分析View#onTouchEvent()

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //view是否可点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //view设置了setEnable(false)
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
             ...
            //一个disabled状态的View仍会消费事件,就是没有效果
            return clickable;
        }
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                     performClickInternal();
                    ...
                    break;
            }
            //省略down,move,cancel事件
            ....
            return true;
        }

        return false;
    }
    
private boolean performClickInternal() {
    ...
    return 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;
    }
    return result;
}

可见在up事件中View如果有设置OnClickListener,会调用OnClickListener#onClick()

根据前面分析View#dispatchTouchEvent()如果外部设置View#setOnTouchListener并且在onTouch()返回true。那么onClick()不会触发。

2.4 事件分发流程图

image

2.5 问题回答

  • 按下A,再按下A(多点触控),为什么释放后A的点击事件只会触发一次?

    因为点击事件只是在actionUp中触发,而不是actionPointerUp中触发。 第一次释放:A收到actionPointerUp事件,第二次触发A才收到actionUp。

  • 按下A,按下ViewGroup(空白区域),为什么先释放A,却无法触发A的点击事件,继续释放ViewGroup,又会触发A的点击事件?

    按下A 收到actionDown事件,A消耗事件,mFirstTouchTarget头部是A 。因为没有从A上抬起接着按下ViewGroup空白区域,收到actionPointDown事件。由于按下ViewGroup空白区域,导致ViewGroup子View都不消耗事件,事件交给ViewGroup消耗。此时ViewGroup在遍历子View循环结束后执行以下代码

if (newTouchTarget == null && mFirstTouchTarget != null) {
        newTouchTarget = mFirstTouchTarget;
        while (newTouchTarget.next != null) {
            newTouchTarget = newTouchTarget.next;
        }
    newTouchTarget.pointerIdBits |= idBitsToAssign;
}

newTouchTarget=null,mFirstTouchTarget!=null。actionPointDown事件id追加给A。当A先释放时,收到的是actionPointerUp事件。 继续释放ViewGroup,收到actionUp事件。

  • 按下按下ViewGroup(空白区域),为什么点击A,B无响应?

    按下ViewGroup收到actionDown,此时点击A,B收到的是actionPointerDown和actionPointerUp。而actionPointerUp是不会响应点击事件的。

2.6 补充说明

2.6.1 int 的高低位获取和组合:

  1. 获取高字节,低字节。
//分别取出int的高字节跟低字节
int value = 515;
int big = (value & 0xFF00) >> 8;
int little = value & 0xFF;
  1. 根据高字节,低字节。组合为一个int
int value;
int a;  int b;
value= (a<< 8) | (b & 0xFF);

2.6.2 位运算

int a = 0x001  
int b = 0x010
int c = 0x100

如果要进行&、|、|=、&= ~ 等操作。确保在每一位上都是独占式。

  • a,b,c组合成d。
int d = a | b | c 
  • d中是否包含a
if(( d & a ) != 0){
  println("d中包含a")
}
  • 从d中移除a
d&= ~a
  • 将e添加到d,并重新赋值给d
d!=e