首页 > 代码库 > 【Android 1.6】View和ViewGroup的touch事件分析和总结

【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:
zhanghu@winth:~$ 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:
zhanghu@winth:~$ 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

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