【Android 1.6】View和ViewGroup的touch事件分析和总结

ENV: android 1.6

目前Android版本已经到了7.0(nougat)了,Android 随着版本升级,touch事件的源码也在跟随着系统的升级而写得越来越复杂,加入了很多旁枝末节,这些旁枝末节,对于分析流程是一种干扰;由于Android的版本升级是向下兼容的,万变不离其宗,研究Android早期的版本,可以更容易理解touch事件的分发,本篇以Android1.6版本的源码进行讲解,由简及繁,理解了早期的源码,再进入高版本的研究也会更容易许多。

前言:

View事件的派发其实非常简单,不是想象的那么复杂,你也别把它看得那么复杂,你简单看它,它就很简单;你复杂看它,它就较复杂。在我看来,它其实很简单,无非就是java里面的接口,抽象类,继承,方法覆写这些概念。

View分为两种类型:View和ViewGroup,怎么理解这两种类型?

首先,假象你自己就是Google工程师中的一员,接到一个任务:让你去设计一个android系统出来,你该如何如考虑这个设计?

其实系统完全可以只存在一个View类型,而不需要ViewGroup类型,View类型完全可以实现成既当作原子对象(最小单元),也可以实现成容纳其他的同类对象。但是由于View这个类本身就比较庞大了,已经有8千多行的代码,如果再容纳其他同类对象,那么单个类的代码逻辑势必更加庞大冗杂,维护起来就会非常困难。为了后期维护容易,于是将容纳其他同类对象的逻辑单独抽取出来,继承View,重新定义一个新类,取名为ViewGroup。

顾名思义,View的群组,可以容纳View的对象。

因此,在看源码的时候,你要时时刻刻站在Google工程师的角度思考问题,站在一个设计者的角度去思考,时刻思考Google工程师写这样一行代码,他想做什么?他的初衷是什么?他是怎么想的?你要把他的大脑打开,进入他的想法里,一探究竟;而不是把自己当作观众看客。

一切的概念源自于生活,你可以将View的这两种类型联系到生活中来,站在生活中去寻找相应的物体与之对应起来,这样更容易理解。

我把View比作实体的砖头,把ViewGroup比作空心的砖头,这个空心的砖头里面可以继续容纳其他实体的砖头,即ViewGroup可以容纳View,而View是最小的单元,不能再容纳其他东西,砖头都具备共同的性质,都是泥土制作的,即他们都有共同的行为和属性。将共同的行为和属性抽取出来定义一个类叫做View类,这个View类是一个实体的砖头;除了共同的行为和属性之外,还有其他的特点,比如是空心的特点,可以继续容纳其他东西,将这个特点区别开来重新定义,叫做ViewGroup类。

上面这个比喻,你有没有理解都不要紧。这个只是个人的理解。下面从源码角度进入分析 >>>

一 View事件的分析和总结

1 dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {

//如果当前view或目标view已经消费了touch事件,则直接返回true,否则调用onTouchEvent方法进行后续点击事件的判断处理

if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&

mOnTouchListener.onTouch(this, event)) {

return true;

}

return onTouchEvent(event);

}

示例下:

TextView mTextView = (TextView) findViewById(R.id.text);

mTextView.setOnTouchListener(new View.OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

Log.d(TAG, "setOnTouchListener");

// 1.此处返回false,则先响应touch事件,再响应click事件

// 2.此处返回true,则只响应touch事件,不再响应click事件

return false;

}

});

mTextView.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Log.d(TAG, "setOnClickListener");

}

});

说明:

setOnTouchListener方法如果返回false,则打印如下log:

[email protected]:~$ adb logcat -s kkkkMainActivity

--------- beginning of system

--------- beginning of main

09-12 13:52:51.147 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.175 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.192 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.210 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.227 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.237 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.238 11059 11059 D kkkkMainActivity: setOnTouchListener

09-12 13:52:51.250 11059 11059 D kkkkMainActivity: setOnClickListener

setOnTouchListener方法如果返回true,则打印如下log:

[email protected]:~$ adb logcat -s kkkkMainActivity

--------- beginning of main

--------- beginning of system

09-12 13:57:12.019 11333 11333 D kkkkMainActivity: setOnTouchListener

09-12 13:57:12.037 11333 11333 D kkkkMainActivity: setOnTouchListener

09-12 13:57:12.055 11333 11333 D kkkkMainActivity: setOnTouchListener

09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener

09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener

总结:View如果既设置了touch监听,又设置了click监听,见上示例,click监听是否执行,取决于touch监听是否返回false。即:如果touch监听消费了事件(返回了true),则click监听不会再执行。

2 dispatchTouchEvent方法中,如果touch监听返回了false,则继续执行onTouchEvent方法,下面看onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {

final int viewFlags = mViewFlags;

// 如果当前View diable了,直接返回不进行事件处理,假如当前view属于可点击的,则返回true,标志应该消费事件;否则返回false

if ((viewFlags & ENABLED_MASK) == DISABLED) {

// A disabled view that is clickable still consumes the touch

// events, it just doesn‘t respond to them.

return (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));

}

// 给当前view的touch代理对象一个机会去处理touch事件,如果当前view的touch代理对象的onTouchEvent方法返回了true,

// 则此处直接返回true

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

return true;

}

}

//当是点击事件或长点击事件,则进入循环,处理完之后,返回true

if (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

case MotionEvent.ACTION_UP:

if ((mPrivateFlags & PRESSED) != 0) {//当没有被ACTION_CANCEL时,进入循环

// take focus if we don‘t have it already and we should in

// touch mode.

boolean focusTaken = false;

if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {

//请求focus,需要聚焦的控件,第一次点击是聚焦,第二次点击才响应点击事件,所以此处一般返回false

focusTaken = requestFocus();

}

if (!mHasPerformedLongPress) {

//如果是轻轻的点击,则移除长按事件的检查

// This is a tap, so remove the longpress check

if (mPendingCheckForLongPress != null) {

removeCallbacks(mPendingCheckForLongPress);

}

// 如果我们已经按压了,且没有focusTaken时,执行点击事件

// Only perform take click actions if we were in the pressed state

if (!focusTaken) {

performClick();//执行点击事件

}

}

if (mUnsetPressedState == null) {

//构建取消按压的Runnable对象,提供一个取消按压的能力

mUnsetPressedState = new UnsetPressedState();

}

// 如果取消按压的操作没有执行成功,则立即执行取消按压的动作

if (!post(mUnsetPressedState)) {

// If the post failed, unpress right now

mUnsetPressedState.run();

}

}

break;

case MotionEvent.ACTION_DOWN: //按下事件

mPrivateFlags |= PRESSED; //添加标志,表示已经按压了

refreshDrawableState();//更新drawable state

if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {//如果是长按事件,则处理长按操作

postCheckForLongClick();

}

break;

case MotionEvent.ACTION_CANCEL:

mPrivateFlags &= ~PRESSED;//添加标志,表示按压要取消掉

refreshDrawableState();//更新drawable state

break;

case MotionEvent.ACTION_MOVE:

final int x = (int) event.getX();

final int y = (int) event.getY();

// Be lenient about moving outside of buttons

int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();

if ((x < 0 - slop) || (x >= getWidth() + slop) ||

(y < 0 - slop) || (y >= getHeight() + slop)) {

// 移动到button区域外面了

// Outside button

if ((mPrivateFlags & PRESSED) != 0) {//当没有被ACTION_CANCEL掉时

// Remove any future long press checks

if (mPendingCheckForLongPress != null) {

//移除长按事件的检查操作

removeCallbacks(mPendingCheckForLongPress);

}

// 将状态切换成not pressed,并更新drawable state

// Need to switch from pressed to not pressed

mPrivateFlags &= ~PRESSED;

refreshDrawableState();

}

} else {

// 移动到button区域内部了

// Inside button

if ((mPrivateFlags & PRESSED) == 0) {//之前没有按压当前buttion,移动过来才按压上的

// Need to switch from not pressed to pressed

mPrivateFlags |= PRESSED;

refreshDrawableState();

}

}

break;

}

return true;

}

return false;

}

说明:

上面代码已经比较详细的注释说明了,此处不再赘述。总结一条:

View对象(如TextView,Buttion,etc.)如果既设置了touch监听,又设置了click监听:

A 如果touch事件监听返回false,则先响应touch事件,再响应click事件

B 如果touch事件监听返回true, 则只响应touch事件,不再响应click事件

二 ViewGroup事件的分析和总结

1 dispatchTouchEvent方法

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

final int action = ev.getAction();

final float xf = ev.getX();

final float yf = ev.getY();

final float scrolledXFloat = xf + mScrollX;

final float scrolledYFloat = yf + mScrollY;

final Rect frame = mTempRect;

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

if (action == MotionEvent.ACTION_DOWN) {

if (mMotionTarget != null) {

// this is weird, we got a pen down, but we thought it was

// already down!

// XXX: We should probably send an ACTION_UP to the current

// target.

mMotionTarget = null;

}

// 当前ViewGroup被要求不允许拦截事件,或当前ViewGroup允许去拦截但是并没有真正拦截事件

// 能否进入if判断去寻找能够处理事件的子view,分为两种情况:

// 一.disallowIntercept为false,表示子View没有明确要求父View不拦截事件,此时事件是否往子View传递,取决于onInterceptTouchEvent的返回值

// 1.onInterceptTouchEvent返回false,则进入if判断,表示事件会传递到子View,进入if判断后去寻找处理事件的目标View,如果找到了处理事件的目标view,则将该view赋值给mMotionTarget。

// 2.onInterceptTouchEvent返回true,则不会进入if判断,也就不会去寻找目标View(此时mMotionTarget不会被赋值,即为null),表示父View会拦截事件,不会往子View传递事件。

// 二.disallowIntercept为true,表示子View明确要求父View不拦截事件,不论onInterceptTouchEvent返回true或false,都会进入if判断,去寻找处理事件的目标View。

// If we‘re disallowing intercept or if we‘re allowing and we didn‘t

// intercept

if (disallowIntercept || !onInterceptTouchEvent(ev)) {

// reset this event‘s action (just to protect ourselves)

ev.setAction(MotionEvent.ACTION_DOWN);

// We know we want to dispatch the event down, find a child

// who can handle it, start with the front-most child.

final int scrolledXInt = (int) scrolledXFloat;

final int scrolledYInt = (int) scrolledYFloat;

final View[] children = mChildren;

final int count = mChildrenCount;

for (int i = count - 1; i >= 0; i--) {

final View child = children[i];

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE

|| child.getAnimation() != null) {

child.getHitRect(frame);

if (frame.contains(scrolledXInt, scrolledYInt)) {

// offset the event to the view‘s coordinate system

final float xc = scrolledXFloat - child.mLeft;

final float yc = scrolledYFloat - child.mTop;

ev.setLocation(xc, yc);

// child如果是ViewGroup类型,则继续递归调用本类中的dispatchTouchEvent方法

// child如果是View类型,则调用View类的dispatchTouchEvent方法,具体见View类的dispatchTouchEvent方法分析

if (child.dispatchTouchEvent(ev))  {

// Event handled, we have a target now.

mMotionTarget = child;

return true;

}

// The event didn‘t get handled, try the next view.

// Don‘t reset the event‘s location, it‘s not

// necessary here.

}

}

}

}

}

boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||

(action == MotionEvent.ACTION_CANCEL);

if (isUpOrCancel) {

// 之前已经将FLAG_DISALLOW_INTERCEPT添加到mGroupFlags了,此处将mGroupFlags中的FLAG_DISALLOW_INTERCEPT

// 取消掉,以重置mGroupFlags

// Note, we‘ve already copied the previous state to our local

// variable, so this takes effect on the next event

mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;

}

// The event wasn‘t an ACTION_DOWN, dispatch it to our target if

// we have one.

final View target = mMotionTarget;

if (target == null) {//如果没有找到处理事件的目标View

// We don‘t have a target, this means we‘re handling the

// event as a regular view.

ev.setLocation(xf, yf);

// 没有找到处理事件的目标View,则直接调用父类的super.dispatchTouchEvent方法

// 此处可以理解成:

// FrameLayout内有三个子view,但是这三个子view都没有去处理事件,因此就把FrameLayout当成一个常规的View

// 然后将touch事件交给它去处理,例如:当为FrameLayout设置touch事件的时候,这个FrameLayout就去响应touch事件

return super.dispatchTouchEvent(ev);

}

// 如果我们有一个子view处理事件,且没有明确要求ViewGroup不拦截事件,再且onInterceptTouchEvent返回true,则当前的ViewGroup传递一个ACTION_CANCEL事件给子View,

// 并清除子view对象(mMotionTarget置为null),当下一个move事件进来时,由于target已经被mMotionTarget置null了,所以直接调用上一步的super.dispatchTouchEvent(ev),

// 将当前的ViewGroup来当作常规的View类型来处理。

// 举例理解:

// 一个自定义的FrameLayout内有包裹着三个子view,当这个自定义的FrameLayout被允许去拦截事件时,就传给之前处理Down事件的这个子view一个ACTION_CANCEL事件,

// 并把处理Down事件的这个子view对象给清除掉,使这个子view不再处理Down后续的事件,将后续的事件交给这个自定义的FrameLayout来处理(传给onTouchEvent方法)。

// if have a target, see if we‘re allowed to and want to intercept its

// events

if (!disallowIntercept && onInterceptTouchEvent(ev)) {

final float xc = scrolledXFloat - (float) target.mLeft;

final float yc = scrolledYFloat - (float) target.mTop;

ev.setAction(MotionEvent.ACTION_CANCEL);

ev.setLocation(xc, yc);

if (!target.dispatchTouchEvent(ev)) {//子View去处理ACTION_CANCEL事件

// target didn‘t handle ACTION_CANCEL. not much we can do

// but they should have.

}

// clear the target

mMotionTarget = null;

// Don‘t dispatch this event to our own view, because we already

// saw it when intercepting; we just want to give the following

// event to the normal onTouchEvent().

return true;

}

// 如果是up或cancel事件,则将mMotionTarget置null,清除目标view

if (isUpOrCancel) {

mMotionTarget = null;

}

// 子View继续处理后续的move,up或cancel事件

// finally offset the event to the target‘s coordinate system and

// dispatch the event.

final float xc = scrolledXFloat - (float) target.mLeft;

final float yc = scrolledYFloat - (float) target.mTop;

ev.setLocation(xc, yc);

return target.dispatchTouchEvent(ev);

}

总结:

1.不论是View类型,还是ViewGroup类型,其事件都是首先从dispatchTouchEvent开始分发,除非系统的默认设计不能满足事件的分发处理,一般情况下不建议覆写dispatchTouchEvent方法

2.事件的传递流向永远是:父view-->子view,总结如下:

A 当且仅当:onInterceptTouchEvent返回true,且disallowIntercept为false时(默认值即为false,即子View没有明确要求父View不拦截事件),事件不会往子View传递,完全交由父View去处理事件,此时把父view(ViewGroup类型)当成常规的View(View类型)去处理事件。

B 当且仅当:disallowIntercept为true时(即子View明确要求父View不拦截事件),不论onInterceptTouchEvent返回true还是返回false,Down事件都会传递给子View,若找到了处理事件的目标子View(mMotionTarget),后续的事件(move,up)是否继续传递给给目标子View.

取决于在此期间:有没有改变disallowIntercept的值,使disallowIntercept为false及onInterceptTouchEvent是否能够返回true,如果满足这两个条件,则父view会拦截后续的事件(move,up),不会再往目标子View传递。如果不满足,则后续的事件(move,up)继续传递给目标子View。

C 如果进入了 if (disallowIntercept || !onInterceptTouchEvent(ev)) 判断去寻找目标子view,但是没有找到能够处理事件的目标子view,则事件又会回到父View,此时会把ViewGroup当成常规的View类型,调用super.dispatchTouchEvent(ev)去进行常规view事件的处理。

D 能否走到ViewGroup的onTouchEvent方法,得看是否会将ViewGroup当作常规的view类型来处理,如满足if (target == null) 这个条件或target就是ViewGroup(此时ViewGroup就是处理事件的目标view,此时把它当作常规的view)

以上ABCD已经说明了ViewGroup事件处理包含的所有情况,我觉得已经说得比较清楚了,如果你觉得还不是很清楚,那我强烈建议你看源码再结合demo验证下所得到的结论,一切都清楚了。因为答案就在源码中,看源码反倒更容易理解,因为文字总是不能够表达的尽如人意。

事件分析和总结讲述完毕!!!!!

到此可以止步了,后面的内容主要是通过demo验证这个结论的,不想看的,可以到此终止了。

下面基于一个demo列举几个示例验证下所得到的结论:

示例一 通过requestDisallowInterceptTouchEvent(true)明确要求父view不拦截事件,而父CustomFrameLayout的onInterceptTouchEvent方法返回了true,表达的意图是想要拦截事件,但是由于disallowIntercept为true了,即使父CustomFrameLayout的onInterceptTouchEvent方法返回了true,也不会执行onInterceptTouchEvent方法的。下面验证下:

CustomTextView.java

package com.android.touchtest;

import android.content.Context;

import android.support.annotation.Nullable;

import android.util.AttributeSet;

import android.util.Log;

import android.view.MotionEvent;

import android.widget.TextView;

public class CustomTextView extends TextView {

private static final String TAG = "kkkkkkkk-CustomTextView";

public CustomTextView(Context context, @Nullable AttributeSet attrs,

int defStyleAttr, int defStyleRes) {

super(context, attrs, defStyleAttr);

}

public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

this(context, attrs, defStyleAttr, 0);

}

public CustomTextView(Context context, @Nullable AttributeSet attrs) {

this(context, attrs, 0);

}

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

Log.d(TAG, "dispatchTouchEvent " + codeToString(event.getAction()));

return super.dispatchTouchEvent(event);

}

private String codeToString(int code) {

switch (code) {

case MotionEvent.ACTION_DOWN:

return "ACTION_DOWN";

case MotionEvent.ACTION_UP:

return "ACTION_UP";

case MotionEvent.ACTION_MOVE:

return "ACTION_MOVE";

case MotionEvent.ACTION_CANCEL:

return "ACTION_CANCEL";

default:

return "";

}

}

}

CustomFrameLayout.java

package com.android.touchtest;

import android.content.Context;

import android.support.annotation.Nullable;

import android.util.AttributeSet;

import android.util.Log;

import android.view.MotionEvent;

import android.widget.FrameLayout;

public class CustomFrameLayout extends FrameLayout {

private static final String TAG = "kkkkkkkk-CustomFrameLayout";

public CustomFrameLayout(Context context, @Nullable AttributeSet attrs,

int defStyleAttr, int defStyleRes) {

super(context, attrs, defStyleAttr);

}

public CustomFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

this(context, attrs, defStyleAttr, 0);

}

public CustomFrameLayout(Context context, @Nullable AttributeSet attrs) {

this(context, attrs, 0);

}

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

Log.d(TAG, "onInterceptTouchEvent " + codeToString(ev.getAction()));

// return super.onInterceptTouchEvent(ev); //super.onInterceptTouchEvent(ev) = false

return true;//想要拦截事件,具体最后是否会拦截事件,取决于子对父的disallowIntercept的值

}

@Override

public boolean onTouchEvent(MotionEvent event) {

Log.d(TAG, "onTouchEvent " + codeToString(event.getAction()));

return super.onTouchEvent(event);

}

private String codeToString(int code) {

switch (code) {

case MotionEvent.ACTION_DOWN:

return "ACTION_DOWN";

case MotionEvent.ACTION_UP:

return "ACTION_UP";

case MotionEvent.ACTION_MOVE:

return "ACTION_MOVE";

case MotionEvent.ACTION_CANCEL:

return "ACTION_CANCEL";

default:

return "";

}

}

}

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:layout_gravity="center" >

<com.android.touchtest.CustomFrameLayout

android:id="@+id/custom_frame_layout"

android:layout_width="200dip"

android:layout_height="200dip"

android:layout_gravity="center"

android:background="#00ff00" >

<com.android.touchtest.CustomTextView

android:id="@+id/custom_text_view"

android:layout_width="130dip"

android:layout_height="100dip"

android:layout_gravity="center"

android:background="#000000"

android:text="CustomTextView"

android:textColor="#FFFFFF" />

</com.android.touchtest.CustomFrameLayout>

</FrameLayout>

MainActivity.java

package com.android.touchtest;

import android.app.Activity;

import android.os.Bundle;

import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

public class MainActivity extends Activity {

private static final String TAG = "kkkkkkkk-MainActivity";

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

setCustomTextViewListener();

// setCustomFrameLayoutListener();

}

private void setCustomFrameLayoutListener() {

CustomFrameLayout mCustomFrameLayout = (CustomFrameLayout) findViewById(R.id.custom_frame_layout);

mCustomFrameLayout.setOnTouchListener(new View.OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

// TODO

return false;

}

});

mCustomFrameLayout.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Log.d(TAG, "mCustomFrameLayout setOnClickListener");

}

});

}

private void setCustomTextViewListener() {

CustomTextView mCustomTextView = (CustomTextView) findViewById(R.id.custom_text_view);

// 明确要求父view不拦截事件,此处置为true了,系统会在up或cancel事件时重置为false,所以

// 如果希望父view一直不拦截事件,则不应该依赖于此处。

mCustomTextView.getParent().requestDisallowInterceptTouchEvent(true);

mCustomTextView.setOnTouchListener(new View.OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

// TODO

return true;

}

});

mCustomTextView.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Log.d(TAG, "mCustomTextView setOnClickListener");

}

});

}

private String codeToString(int code) {

switch (code) {

case MotionEvent.ACTION_DOWN:

return "ACTION_DOWN";

case MotionEvent.ACTION_UP:

return "ACTION_UP";

case MotionEvent.ACTION_MOVE:

return "ACTION_MOVE";

case MotionEvent.ACTION_CANCEL:

return "ACTION_CANCEL";

default:

return "";

}

}

}

滑动黑色区域打印log:

10035:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_DOWN

10036:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10037:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10038:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10039:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10040:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10041:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10042:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10043:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10044:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10045:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE

10046:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_UP

示例二 子View没有调用requestDisallowInterceptTouchEvent(true)方法去明确要求父View不拦截事件,事件是否会传递给子view,由onInterceptTouchEvent方法返回值决定,上面示例中的onInterceptTouchEvent返回的是true,所以,事件不会往子view传递。验证下:

基于示例一,注释掉MainActivity类中的mCustomTextView.getParent().requestDisallowInterceptTouchEvent(true);这一句,打印log如下:

693:D/kkkkkkkk-CustomFrameLayout( 1586): onInterceptTouchEvent ACTION_DOWN

694:D/kkkkkkkk-CustomFrameLayout( 1586): onTouchEvent ACTION_DOWN

示例三 初始时调用了requestDisallowInterceptTouchEvent(true)方法去明确要求父View不拦截事件,且父onInterceptTouchEvent方法返回true(父view表达的意图是想拦截事件),但是子view又在接下来的move事件中调用了getParent().requestDisallowInterceptTouchEvent(false)去取消不拦截的请求,会满足if
(!disallowIntercept && onInterceptTouchEvent(ev)) 这个条件,因此会传递一个cancel事件给子view,后续事件交由父view去处理,此时将父view(ViewGroup类型)当成常规的View。

基于示例一,CustomTextView类的dispatchTouchEvent方法修改如下:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent " + codeToString(event.getAction()));
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

打印Log如下:

970:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_DOWN

971:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_MOVE

972:D/kkkkkkkk-CustomFrameLayout( 1687): onInterceptTouchEvent ACTION_MOVE

973:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_CANCEL

974:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

975:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

976:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

977:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

978:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

979:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

980:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

981:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

982:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE

983:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_UP

继续修改下,将MainActivity类的setCustomFrameLayoutListener();的注释放开,mCustomFrameLayout.setOnTouchListener的onTouch方法返回true,即父view要消费touch事件,如下修改:

mCustomFrameLayout.setOnTouchListener(new View.OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

Log.d(TAG, "mCustomFrameLayout setOnTouchListener");

return true;

}

});

打印log如下:

768:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_DOWN

769:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_MOVE

770:D/kkkkkkkk-CustomFrameLayout( 1755): onInterceptTouchEvent ACTION_MOVE

771:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_CANCEL

772:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener

773:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener

774:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener

775:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener

从上面log可以看到,示例三刚好验证了满足 dispatchTouchEvent的 if (!disallowIntercept && onInterceptTouchEvent(ev)) 这个条件及下一个move事件的if (target == null) 这个条件,所以走到了CustomFrameLayout的onTouch方法里面,如果CustomFrameLayout的onTouch方法返回了false,则会走onTouchEvent方法(见View的dispatchTouchEvent方法)

上面示例了交互的情况,其他情况都比较简单。

Android 高版本中,调用requestDisallowInterceptTouchEvent可能失效,见 探究requestDisallowInterceptTouchEvent失效的原因

查看源码:

Android 1.6 View.java

Android 1.6 ViewGroup.java

时间: 2024-10-29 10:46:45

【Android 1.6】View和ViewGroup的touch事件分析和总结的相关文章

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

自定义View系列教程01–常用工具介绍 自定义View系列教程02–onMeasure源码详尽分析 自定义View系列教程03–onLayout源码详尽分析 自定义View系列教程04–Draw源码分析及其实践 自定义View系列教程05–示例分析 自定义View系列教程06–详解View的Touch事件处理 自定义View系列教程07–详解ViewGroup分发Touch事件 PS:如果觉得文章太长,那就直接看视频吧 在上一篇中已经分析完了View对于Touch事件的处理,在此基础上分析和理

Android精通:View与ViewGroup,LinearLayout线性布局,RelativeLayout相对布局,ListView列表组件

UI的描述 对于Android应用程序中,所有用户界面元素都是由View和ViewGroup对象构建的.View是绘制在屏幕上能与用户进行交互的一个对象.而对于ViewGroup来说,则是一个用于存放其他View和ViewGroup对象的布局容器! Android为我们提供了View和ViewGroup的两个子类的集合,提供常用的一些输入控件(比如按钮,图片和文本域等)和各种各样的布局模式(比如线程布局,相对布局,绝对布局,帧布局,表格布局等). 用户界面布局 在你APP软件上的,用户界面上显示

Android中Preference的使用以及监听事件分析

> 在Android系统源码中,绝大多数应用程序的UI布局采用了Preference的布局结构,而不是我们平时在模拟器中构建应用程序时使用的View布局结构,例如,Setting模块中布局.当然,凡事都有例外,FMRadio应用程序中则使用了View布局结构(可能是该应用程序是marvel公司提供的,如果由google公司做,那可说不准).归根到底,Preference布局结构和View的布局结构本质上还是大同小异,Preference的优点在于布局界面的可控性和高效率以及可存储值的简洁性(每个

Android之UI View与ViewGroup

1.基本概念 View:所有可视化控件的父类,Android App屏幕上用户可以交互的对象(例如 按钮 下拉框 文本框等). ViewGroup:View的子类,存放View和ViewGroup对象的布局容器(线性布局.相对布局等). 简单理解,view就是各种控件(按钮.文本),而ViewGroup提供各种布局模式. App中任一组件都是使用View和ViewGroup对象组成的层次结构.如下图,viewgroup用于所属的view对象组织布局模式,每个view代表输入控件或者UI部件. 2

Android中自定义View、ViewGroup理论基础详解

Android自身提供了许多widgets,但是有时候这些widgets并不能满足我们的需求,这时我们就需要自定义View,本文会详细说明自定义View的各种理论基础,只有理解了这些知识,我们才能更好地实现各种功能的控件. 我觉得自定义View中最重要的部分就是绘图和交互,自定义的绘图使得你的View与众不同,交互使用户可以与你的View进行交互,而绘图的前提是View的量算与布局,交互的基础是触摸事件,所以量算.布局.绘图.触摸事件这些是自定义View的核心. 除此之外,一个设计友好的自定义V

Android View系统分析之二View与ViewGroup

目录 在Android View系统分析之从setContentView说开来(一)一文中,我们从setContentView开始阐述了Android中的视图层次,从设置内容布局到整个视图层次的建立的过程.并且对View和ViewGroup的关系进行了简单的介绍,今天我们继续来深入的了解Android中的View和ViewGroup. ViewGroup与View的关系 我们在定义一个布局时,在它的顶层通常都是使用LinearLayout或者RelativeLayout等组件来包装一些子控件,例

Android Touch事件传递机制全面解析(从WMS到View树)

转眼间近一年没更新博客了,工作一忙起来.非常难有时间来写博客了,因为如今也在从事Android开发相关的工作,因此以后的博文也会很多其它地专注于这一块. 这篇文章准备从源代码层面为大家带来Touch事件的传递机制.我这里分析的源代码时Android4.4的. 说到分析源代码,光看肯定是不行的,一定要亲自去跟,而且要边跟边思考,所以在下一篇中.会有一个Demo来为大家详细分析源代码的走向. 以下进入正题,先来看下Android中事件的分类: 1.键盘事件:主要是指按下虚拟键盘的某个按键.或者机身的

Android 编程下 Touch 事件的分发和消费机制

Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev).onInterceptTouchEvent(MotionEvent ev).onTouchEvent(MotionEvent ev):能够响应这些方法的控件包括:ViewGroup 及其子类.Activity.方法与控件的对应关系如下表所示: Touch 事件相关方法   方法功能     ViewGroup         Activity        public b

Android Touch事件分发过程

虽然网络上已经有非常多关于这个话题的优秀文章了,但还是写了这篇文章,主要还是为了加强自己的记忆吧,自己过一遍总比看别人的分析要深刻得多.那就走起吧. 简单演示样例 先看一个演示样例 : 布局文件 : <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id=&q