深入探讨Android异步精髓Handler


站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础

Android多分辨率适配框架(2)— 原理剖析

Android多分辨率适配框架(3)— 使用指南


自定义View系列教程00–推翻自己和过往,重学自定义View

自定义View系列教程01–常用工具介绍

自定义View系列教程02–onMeasure源码详尽分析

自定义View系列教程03–onLayout源码详尽分析

自定义View系列教程04–Draw源码分析及其实践

自定义View系列教程05–示例分析

自定义View系列教程06–详解View的Touch事件处理

自定义View系列教程07–详解ViewGroup分发Touch事件

自定义View系列教程08–滑动冲突的产生及其处理


PS:如果觉得文章太长,那就直接看视频


在上一篇中已经分析完了View对于Touch事件的处理,在此基础上分析和理解ViewGroup对于Touch事件的分发就会相对容易些。

当一个Touch事件发生后,事件首先由系统传递给当前Activity并且由其dispatchTouchEvent()派发该Touch事件,源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

该段代码主要逻辑如下:

  1. 处理ACTION_DOWN事件

    调用onUserInteraction()该方法在源码中为一个空方法,可依据业务需求在Activity中覆写该方法。
  2. 利用PhoneWindow的superDispatchTouchEvent()派发事件

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
    }

    DecorView的superDispatchTouchEvent()源码如下:

    public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
    }

    此处我们可以看到:在该方法中又将Touch事件交给了DecorView进行派发。

    DecorView继承自FrameLayout它是整个界面的最外层的ViewGroup。

    至此,Touch事件就已经到了顶层的View且由其开始逐级派发。如果superDispatchTouchEvent()方法最终true则表示Touch事件被消费;反之,则进入下一步

  3. Activity处理Touch事件

    如果没有子View消费Touch事件,那么Activity会调用自身的onTouchEvent()处理Touch.

在以上步骤中第二步是我们关注的重点;它是ViewGroup对于Touch事件分发的核心。

关于dispatchTouchEvent(),请看如下源码:

  public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
} if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
} boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
} 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);
} else {
intercepted = false;
}
} else {
intercepted = true;
} if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
} final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus= ev.isTargetAccessibilityFocus()? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS; removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
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 (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) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
} resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
} ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
} if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
} 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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
} if (canceled||actionMasked==MotionEvent.ACTION_UP||actionMasked==MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
} if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

第一步:

清理和还原状态,请参见代码第15-18行

ACTION_DOWN是一系列Touch事件的开端,当Touch为ACTION_DOWN时需要进行一些初始化和还原操作。比如:清除以往的Touch状态(state)和开始新的手势(gesture)。所以在cancelAndClearTouchTargets( )中将mFirstTouchTarget设置为null,且在resetTouchState()中重置Touch状态标识

第二步:

检查是否需要ViewGroup拦截Touch事件,请参见代码第20-31行

在此详细分析该段代码:

  1. 请注意变量intercepted,请参见代码第20行

    该值用来标记ViewGroup是否拦截Touch事件的传递,它在后续代码中起着重要的作用.
  2. 事件为ACTION_DOWN或者mFirstTouchTarget不为null时检查是否需要ViewGroup拦截Touch事件,请参见代码第21行

    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget !=null)

    ACTION_DOWN表示Touch事件是手指按下的事件,那么mFirstTouchTarget又是什么意思呢?mFirstTouchTarget是TouchTarget类的对象,而TouchTarget是ViewGroup中的一个内部类,它封装了被触摸的View及这次触摸所对应的ID,该类主要用于多点触控。比如:三个指头依次按到了同一个Button上。

    我们不必过多的理会TouchTarget,但是要重点关注mFirstTouchTarget。

    mFirstTouchTarget贯穿dispatchTouchEvent(),对于流程的走向发挥着至关重要的作用。

    (1) mFirstTouchTarget不为null

    表示ViewGroup没有拦截Touch事件并且子View消费了Touch

    (2) mFirstTouchTarget为null

    表示ViewGroup拦截了Touch事件或者虽然ViewGroup没有拦截Touch事件但是子View也没有消费Touch。总之,此时需要ViewGroup自身处理Touch事件

    如果ACTION_DOWN事件被子View消费(即mFirstTouchTarget!=null),当处理后续到来的ACTION_MOVE和ACTION_UP时仍会调用该代码判断是否需要拦截Touch事件。

    2.1 判断disallowIntercept(禁止拦截)标志位,请参见代码第22行

    ViewGroup可以拦截Touch事件,但是它的子View可调用 getParent().requestDisallowInterceptTouchEvent(true)禁止其父View的拦截。其实,从这个较长的方法名也可以看出来它的用途——禁止事件拦截;在该方法内部会改变FLAG_DISALLOW_INTERCEPT的值。

    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    所以利用此行代码判断是否禁止拦截(disallowIntercept)。

    在此请注意:

    ViewGroup中的requestDisallowInterceptTouchEvent( )方法可以用来禁止或允许ViewGroup拦截Touch事件,但是它对于ACTION_DOWN是无效的。

    也就是说子View可以禁止父View拦截ACTION_MOVE和ACTION_UP但是无法禁止父View拦截ACTION_DOWN。因为在ACTION_DOWN时会调用resetTouchState()重置了FLAG_DISALLOW_INTERCEPT的值导致子View对该值设置失效。所以,对于ACTION_DOWN事件ViewGroup总会调用onInterceptTouchEvent()判读是否要拦截Touch事件

    2.2 处理disallowIntercept的值为false的情况,请参见代码第23-25行

    若disallowIntercept(禁止拦截)的值为false,所以调用onInterceptTouchEvent()拦截Touch并将结果赋值给intercepted。

    常说ViewGroup的事件传递中的流程是:

    dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent

    其实在这就是一个体现:dispatchTouchEvent()中调用了onInterceptTouchEvent()。

    2.3 处理disallowIntercept的值为true的情况,请参见代码第27行

    若disallowIntercept(禁止拦截)的值为true,表示不拦截Touch事件。

    所以将intercepted设置为false

  3. 将intercepted设置为true,请参见代码第30行

    如果不是ACTION_DOWN事件并且mFirstTouchTarget为null,那么直接将intercepted设置为true,表示ViewGroup拦截Touch事件。

    更加直白地说:如果ACTION_DOWN没有被子View消费(mFirstTouchTarget为null)那么当ACTION_MOVE和ACTION_UP到来时ViewGroup不再去调用onInterceptTouchEvent()判断是否需要拦截而是直接的将intercepted设置为true表示由其自身处理Touch事件

第三步:

检查cancel,请参见代码第38行

第四步:

分发ACTION_DOWN事件,请参见代码第43-117行

if (!canceled && !intercepted)

如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。

在此梳理该阶段的主要逻辑。

  1. 计算Touch事件的坐标,请参见代码第56-57行

    在后续的判断中会依据坐标来判断触摸到了ViewGroup中的哪个子View。
  2. 依据坐标,判断哪个子View接收Touch事件,请参见代码第61-105行

    这部分代码的主要操作为:

    在找到可以接收Touch事件的子View后调用dispatchTransformedTouchEvent()方法将Touch事件派发给该子View。

    第一种情况:

    子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null

    第二种情况:

    子View消费掉了Touch事件那么该方法的返回值为true,然后执行

    newTouchTarget = addTouchTarget(child, idBitsToAssign);

    在addTouchTarget()方法内将该子View添加到mFirstTouchTarget链表的表头,并且为mFirstTouchTarget设值使其不为null。随后将alreadyDispatchedToNewTouchTarget置为true,表示已经将Touch事件分发到了子View,或者说子View消费掉了Touch事件

小总结:

在这个步骤中只有找到了可以消费Touch事件的子View时mFirstTouchTarget才不为null;其余情况比如未找到可以接收Touch事件的子View或者子View不能消费Touch事件时mFirstTouchTarget仍为null

小疑惑:

请参见代码第78-82行:

为什么newTouchTarget!=null就会执行break跳出for循环了呢?

还记得这个for循环的作用是什么吗?——寻找一个可以接受Touch事件的子View。

如果先有个指头按在了子View上(即ACTION_DOWN),然后另一根指头又按在相同的子View上(即ACTION_POINTER_DOWN)。这种多点触摸的情况下两个指头按在了同一个View上,当第一指头按下的时候一个TouchTarget就已经记录了该子View,所以当第二个指头再按下的时候当然还是由这个子View来处理Touch事件,也就是说没有再继续寻找的必要了。

第五步:

继续事件分发,请参见代码第119-147行

在第四步对于ACTION_DOWN事件做了一些特有处理,在此继续进行事件的分发。不论是ACTION_DOWN还是ACTION_MOVE和ACTION_UP均会进入该步骤。

第一种情况:

mFirstTouchTarget==null

它表示Touch事件被ViewGroup拦截了根本就没有派发给子view或者虽然派发了但是在第四步中没有找到能够消费Touch事件的子View。

此时,直接调用dispatchTransformedTouchEvent()方法处理事件

第二种情况:

mFirstTouchTarget != null,表示找到了能够消费Touch事件的子View。

在该处亦有两种不同的情况:

  1. 处理ACTION_DOWN,请参见代码第126-127行

    如果mFirstTouchTarget!=null则说明在第四步中Touch事件已经被消费,所以不再做其他处理
  2. 处理ACTION_MOVE和ACTION_UP,请参见代码第129-143行

    调用dispatchTransformedTouchEvent()将事件分发给子View处理,请参见代码第130行

结合第四步和第五步,在此思考一个问题:

ViewGroup将ACTION_DOWN分发给子View,如果子View没有消费该事件,那么当ACTION_MOVE和ACTION_UP到来的时候系统还会将Touch事件派发给该子View么?

答案是否定的——如果子View没有处理ACTION_DOWN那么它就失去了处理ACTION_MOVE和ACTION_UP的资格。

在第四步中如果子View处理了ACTION事件那么mFirstTouchTarget不为null,当ACTION_MOVE和ACTION_UP到来时会跳过第四步进入到第五步。在第五步中就会判断mFirstTouchTarget是否为null,如果为空那么ViewGroup自身会处理Touch;如果不为空那么继续由mFirstTouchTarget处理Touch事件。

第六步:

清理数据和状态还原,请参见代码第149-155行

在手指抬起或者取消Touch分发时清除原有的相关数据


在分析dispatchTouchEvent()源码时多次调用dispatchTransformedTouchEvent(),在次对其源码做一个简略的分析

    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;
} final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; if (newPointerIdBits == 0) {
return false;
} final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
} 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);
} transformedEvent.recycle();
return handled;
}

这段代码不算很复杂,先来瞅瞅官方文档的介绍

Transforms a motion event into the coordinate space of a particular child view,filters out irrelevant pointer ids, and overrides its action if necessary.If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.

该方法的主要作用是将Touch事件传递给特定的子View(即该方法的第三个输入参数child),由子Viwe继续分发处理Touch事件。

但我们发现:在系统调用dispatchTransformedTouchEvent()时该方法的第三个参数有时候是一个子View(比如dispatchTouchEvent()源码中第85和130行),有时候又是null(比如dispatchTouchEvent()源码中第120行).

那么该方法第三个参数child是否为null对于Touch事件的分发有什么影响呢?

在dispatchTransformedTouchEvent()源码中可见多次对于child是否为null的判断且均做出如下类似的操作:

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

    如果子View没有消费掉Touch事件,那么ViewGroup就将自己动手处理Touch事件,即super.dispatchTouchEvent(event)。此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch;这个过程之前已经分析过了,不再赘述。

  2. child != null

    此时会调用该子View(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent()继续处理Touch,即child.dispatchTouchEvent(event)。

    小结:

    如果ViewGroup拦截了Touch事件或者子View不能消耗掉Touch事件,那么ViewGroup会在其自身的onTouch(),onTouchEvent()中处理Touch

    如果子View消耗了Touch事件父View就不能再处理Touch.

至此我们就明白了:

Touch事件的传递顺序为

Activity–>外层ViewGroup–>内层ViewGroup–>View

Touch事件的消费顺序为

View–>内层ViewGroup–>外层ViewGroup–>Activity

其实,在我们平常的工作中也可以见到类似的场景。

开发任务的派发顺序为

CEO–>CTO–>manager–>developer

开发任务的反馈顺序为

developer–>manager–>CTO–>CEO

公司要做一个APP,CEO会将该任务交给CTO;CTO又找到了项目经理,最后项目经理将该任务分配给了开发人员。

在开发人员分析完项目后发现自己能力无法胜任于是就将该问题抛给了项目经理,项目经理觉得自己时间有限也完成不了又抛给了CTO,CTO同样因为某些因素无法按时完成该任务于是又将该任务抛给了CEO。

经过这么一圈折腾公司就觉得这个开发人员技术不是特别好,于是与该项目有关的后续工作也就不会再让这个开发人员参与了。这就像刚才提到的一样:如果子View没有消费ACTION_DOWN那么ACTION_MOVE和ACTION_UP也就不会再派发给它。

这个过程与ViewGroup对于Touch事件的分发是非常类似的。


至此,ViewGroup对于Touch事件的分发处理的主要流程就分析完了。

为了梳理整个dispatchTouchEvent()的脉络,我又画了两个流程图。

该流程图描述Touch事件的传递和消费顺序。

在Touch事件的传递过程中,如果上一级拦截了Touch那么其下一级就无法在收到Touch事件。

在Touch事件的消费过程中,如果下一级消费Touch事件那么其上一级就无法处理Touch事件。

该流程描述了dispatchTouchEvent( )中对于Touch分发。

这部分源码稍微有些复杂。结合此图,可以将整个流程分为三个阶段:

  1. 判断是否需要拦截(intercepted)
  2. 处理ACTION_DOWN事件
  3. 利用mFirstTouchTarget是否为null继续处理Touch(ACTION_DOWN,ACTION_MOVE,ACTION_UP)

嗯哼,源码分析完了,流程图也有了,我们再通过示例来验证和理解ViewGroup对于Touch事件的分发。

先来瞅瞅布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.stay4it.testtouch1.MainActivity"> <com.stay4it.testtouch1.LinearLayoutSubclass
android:layout_width="match_parent"
android:layout_height="match_parent"> <com.stay4it.testtouch1.ButtonSubclass
android:id="@+id/button"
android:layout_centerInParent="true"
android:layout_width="500px"
android:layout_height="500px"
android:background="#FF3A9120"
android:textSize="50px"
android:text="Touch Me" /> </com.stay4it.testtouch1.LinearLayoutSubclass> </RelativeLayout>

此处,我们在示例的布局文件中放入了一个自定义的LinearLayout和Button。

先来看看这个自定义的线性布局LinearLayoutSubclass

package com.stay4it.testtouch1;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout; /**
* 原创作者
* 谷哥的小弟
*
* 博客地址
* http://blog.csdn.net/lfdfhl
*/
public class LinearLayoutSubclass extends LinearLayout {
public LinearLayoutSubclass(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_UP");
default:
break;
}
return super.dispatchTouchEvent(ev);
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_UP");
default:
break;
}
return super.onInterceptTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_UP");
default:
break;
}
return super.onTouchEvent(ev);
}
}

在这个自定义的线性布局中主要是输出一些便于验证的日志信息,比如在dispatchTouchEvent()和onInterceptTouchEvent()以及onTouchEvent()中对于ACTION_DOWN和ACTION_MOVE以及ACTION_UP均输出对于信息。

再来看看自定义的按钮ButtonSubclass

package com.stay4it.testtouch1;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button; /**
* 原创作者
* 谷哥的小弟
*
* 博客地址
* http://blog.csdn.net/lfdfhl
*/
public class ButtonSubclass extends Button{
public ButtonSubclass(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_UP");
default:
break;
}
return super.dispatchTouchEvent(event);
} @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_UP");
default:
break;
}
return super.onTouchEvent(event);
}
}

在这个自定义Button中的处理和LinearLayoutSubclass非常类似只不过View是没有onInterceptTouchEvent()罢了,故此,不再赘述。

最后请看Activity的代码实现

package com.stay4it.testtouch1;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
/**
* 原创作者
* 谷哥的小弟
*
* 博客地址
* http://blog.csdn.net/lfdfhl
*/
public class MainActivity extends AppCompatActivity {
private ButtonSubclass mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
} private void init(){
mButton= (ButtonSubclass) findViewById(R.id.button);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_UP");
default:
break;
}
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_UP");
default:
break;
}
return super.onTouchEvent(ev);
}
}

在该Activity中除了日志输出亦无其余操作。

当手指轻触Button再抬起后,我们瞅瞅输出日志。



看到这些Log一切都是那么清晰明了

  1. ACTION_DOWN事件由外及里从Activity传递到Button
  2. Button处理了ACTION_DOWN事件
  3. ACTION_UP事件由外及里从Activity传递到Button
  4. Button处理了ACTION_UP事件

在这个过程中把Touch事件从Activity传递到最里层某个子View的过程体现得很清楚和完整。但是对于Touch事件的消费过程怎么没有体现出来呢?不是说Touch事件的消费顺序和传递过程是反过来的么?在这里怎么没有体现呢?

嗯哼,这是因为Button在onTouchEvent()中执行

return super.onTouchEvent(event);

消耗了Touch事件,所以Touch事件就没有回传给LinearLayoutSubclass和Activity。

现在对刚才的代码做一点小小的修改:

  1. 在ButtonSubclass的onTouchEvent()中返回false
  2. 在LinearLayoutSubclass的onTouchEvent()中返回false

现在再次运行代码并且轻触Button后抬起手指,观察一下输出日志:

  1. ACTION_DOWN事件由外及里从Activity传递到Button。
  2. Button没有处理ACTION_DOWN事件,将其回传至LinearLayoutSubclass
  3. LinearLayoutSubclass也没有处理ACTION_DOWN事件,将其回传至Activty
  4. Activity处理ACTION_DOWN
  5. Activity处理ACTION_UP,不再分发

嗯哼,在这里就看明白了如果子View没有处理Touch事件就会回传给父View,一层一层地往上回溯。在刚才这个过程中没有一个子View处理ACTION_DOWN事件造成mFirstTouchTarget为null,所以当ACTION_UP事件发生时Activity不再将其派发给子View而是自己处理了。这个过程在之前分析源码的时候也着重提到过。


至此,关于ViewGroup对于Touch事件的分发就全部分析完了。

PS:如果觉得文章太长,那就直接看视频


who is the next one? ——> demo

自定义View系列教程07--详解ViewGroup分发Touch事件的更多相关文章

  1. 自定义View系列教程06--详解View的Touch事件处理

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  2. 自定义View系列教程01--常用工具介绍

    站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...

  3. 自定义View系列教程08--滑动冲突的产生及其处理

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  4. 自定义View系列教程05--示例分析

    站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...

  5. 自定义View系列教程04--Draw源码分析及其实践

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  6. 自定义View系列教程03--onLayout源码详尽分析

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  7. 自定义View系列教程02--onMeasure源码详尽分析

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  8. [js高手之路] html5 canvas系列教程 - 状态详解(save与restore)

    本文内容与路径([js高手之路] html5 canvas系列教程 - 开始路径beginPath与关闭路径closePath详解)是canvas中比较重要的概念.掌握理解他们是做出复杂canvas动 ...

  9. Android 自定义View修炼-Android开发之自定义View开发及实例详解

    在开发Android应用的过程中,难免需要自定义View,其实自定义View不难,只要了解原理,实现起来就没有那么难. 其主要原理就是继承View,重写构造方法.onDraw,(onMeasure)等 ...

随机推荐

  1. JQuery或JS判断浏览器内核版本号以及是否支持W3C盒子模型

    jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support .在更新的 2.0 版本中,将不再支持 IE 6/7/8. ...

  2. ios那些事之如何在ios5上运行gdb

    为啥要在ios上运行gdb? 这个问题见仁见智喽.对于搞开发的同学们来所, 有了gdb更方便跟踪分析别人的程序,取长补短:)这里不是教大家crack:) 运行环境: Mac OS 10.7.4 Xco ...

  3. HDU 4584

    //也是简单题,因为n太小,故暴力之! #include<stdio.h> #include<math.h> #include<string.h> #define ...

  4. C#多线程之间事件通知

    我有两个线程,线程1接受网络数据,存到队列;线程2取队列,进行各种复杂的处理然后绘制到界面上;想让线程1有数据了通知线程2,线程2再取队列,因为不通知的话,线程2一直在while循环检索队列时候有东西 ...

  5. angular4 路由重用策略 RouterReuseStrategy

    单页面应用现在是主流,随之而来的缺点:页面间切换时不能保存状态 angular4出了一个RouteReuseStrategy路由重用策略可以让组件所有的state和渲染好的html存起来,然后在切回去 ...

  6. MyBatis连接Neo4j问题记录:mapper参数传递(节点标签作为参数)

    MyBatis与Neo4j的连接我在上一篇做了,这是链接:https://blog.csdn.net/qq_34233510/article/details/82496101 上一篇中UserMapp ...

  7. union /union all/ intersect / minus

  8. 计蒜客 Flashing Fluorescents(状压DP)

    You have nn lights, each with its own button, in a line. Pressing a light’s button will toggle that ...

  9. 开始使用Apache弗林克和Mapr Streams

    Introduction MapR Ecosystem Package 2.0 (MEP) is coming with some new features related to MapR Strea ...

  10. Android 程序员不得不收藏的个人博客(持续更新...)

    本文已收录我的 Github ,持续更新中 ,欢迎点赞 ! 每周打开一次收藏夹里的个人博客,已经成为了我的人生一大乐趣. 相比各大博客平台,我一直更加偏爱个人博客.在每个人自己的这一亩三分地里,你能看 ...