【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来处理(此时将ViewGroup当作常规的view来处理,如果view的onTouch返回了false,则传给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验证这个结论的,不想看的,可以到此终止了。





博客原文:http://blog.csdn.net/yelangjueqi/article/details/52525979


下面基于一个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
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center" >

             android:id="@+id/custom_frame_layout"
        android:layout_width="200dip"
        android:layout_height="200dip"
        android:layout_gravity="center"
        android:background="#00ff00" >

                     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" />
    





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


【Android 1.6】View和ViewGroup的touch事件分析和总结_第1张图片


滑动黑色区域打印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,高级编程)