Android ViewGroup 触摸事件传递机制

引言

上一篇博客我们学习了Android View 触摸事件传递机制,不了解的同学可以查看Android View 触摸事件传递机制。今天继续学习Android触摸事件传递机制,这篇博客将和大家一起探讨ViewGroup的触摸事件传递机制。

示例

示例代码如下:

public class MainActivity extends ActionBarActivity {
    private String TAG = "MainActivity";
    private MyViewGroup parentView;
    private Button childView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        parentView = (MyViewGroup) findViewById(R.id.parent);
        childView = (Button) findViewById(R.id.child);

        childView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "childView=====onClick");
            }
        });

        parentView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "parentView=====onTouch");
                return false;
            }
        });
        parentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "parentView=====onClick");
            }
        });

    }
}

自定义MyViewGroup,并且重写dispatchTouchEvent方法添加打印日志,重写onInterceptTouchEvent方法添加打印日志:

public class MyViewGroup extends LinearLayout {
    private String TAG = "MyViewGroup";

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "MyViewGroup=====dispatchTouchEvent "+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "MyViewGroup=====onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }
}

布局如下:

<com.xjp.viewgrouptouchdemo.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    

分别点击Button按钮和空白区域,打印结果如下:

08-01 17:02:56.792  14706-14706/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 17:02:56.792  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ childView=====onClick
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch1
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onClick

从上面打印可以看出,在ViewGroup嵌套Button布局中,仅仅点击Button按钮时只会执行Button的触摸事件,不会执行ViewGroup的触摸事件。当你点击Button以外的空白区域时,才会执行ViewGroup的触摸事件。那为什么在ViewGroup嵌套View时只会执行View的触摸事件而不执行ViewGroup的触摸事件呢?待着这个疑问,我们来分析下ViewGroup源码中的dispatchTouchEvent方法。为了方便起见,我这里都是分析的Android2.0的源码。

ViewGroup#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;
             }
             // 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);
                             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) {
             // 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) {
             // We don't have a target, this means we're handling the
            // event as a regular view.
             ev.setLocation(xf, yf);
            return super.dispatchTouchEvent(ev);
         }

         // 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)) {
                 // 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;
         }

         if (isUpOrCancel) {
             mMotionTarget = null;
         }

         // 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. 代码3-8行,获取当前手指在屏幕上触摸点击的坐标位置,用于判断当前手指触摸点击的是View区域还是ViewGroup区域。
2. 代码第10行,获得disallowIntercept的值,disallowIntercept指的是是否禁用掉事件拦截功能,默认值是false,你可以调用requestDisallowInterceptTouchEvent方法修改它。
3. 代码第12行,手指触摸手势是先ACTION_DOWN操作,所以条件满足,进入if条件。
4. 代码第13-19行,清除当前手机屏幕上触摸点击对象,也就是将mMotionTarget设置为null。意思是在点击手机屏幕之前是没有任何触摸点击对象的。
5. 代码第22行,由于disallowIntercept默认值是false,所以条件是否满足完全取决于方法onInterceptTouchEvent返回值取反。而我们进入该方法会发现里面的实现仅仅是返回一个false。也就是if条件满足。
6. 代码第31行,通过一个for循环遍历当前ViewGroup下所以子View。
7. 代码第35行,获取遍历子View在屏幕上的坐标位置,然后代码第36行,判断当前屏幕手指触摸点击坐标是否包含遍历的子View在屏幕上的坐标位置范围?如果包含,者表示当前手指触摸点击的地方是该子View,也就是点击了Button。否则表示当前手指触摸并没有点击到ViewGroup中的子View,也就是点击到了空白区域。
8. 代码第41行,调用子View的dispatchTouchEvent方法来处理View的触摸事件分发,这里一步就是我们上一篇博客分析的 Android View 触摸事件传递机制入口。在这篇博客中我们知道,当View是可点击的或者长安点击或者设置了setOnClickListener点击监听事件的,View#dispatchTouchEvent方法一律返回true,否则返回false。所以当条件满足,也就是子View设置了点击事件时ViewGroup#dispatchTouchEvent方法返回true,触摸对象mMotionTarget = child赋值成当前点击的子Viwe执行结束。因此这也验证了上面示例代码,当button设置了点击事件时只执行了Button的onClick事件,并没有执行任何关于ViewGroup的触摸点击事件。
9. 代码第66-72行,假如上面的View#dispatchTouchEvent方法返回false,表示子View不可点击(可以参考上一篇博客),此时mMotionTarget依然为null,那么target==null条件满足。执行父类的dispatchTouchEvnet方法,也就是View的dispatchTouchEvent方法。由于ViewGroup的父类是View,所以此处表示执行了ViewGroup的dispatchTouchEvent方法。言外之意就是,当ViewGroup嵌套的子View不可点击且没有设置setOnClickListener点击监听事件时,点击View先触发子View的触摸事件,然后在触发ViewGroup的触摸事件,执行了ViewGroup#dispatchTouchEvent方法,并且返回了,后面代码不执行。
10. 代码第76-103行,主要是执行子View的ACTION_UP和ACTION_CANCEL手势操作的,逻辑这里就不具体分析了,可以参考上一篇博客。

总结:有上面分析我们知道。

  • onInterceptTouchEvent方法是用于ViewGroup对子View的触摸事件拦截功能,默认返回false,不拦截子View的触摸事件,可以重写该方法,返回true来拦截子View的触摸事件传递。此时只会执行ViewGroup的触摸事件传递。
  • 当子View是不可点击的且没有设置setOnClickListener点击监听事件时,会先执行子View的触摸事件,然后在执行ViewGroup的触摸事件。

现在俩验证以上两个结论。

onInterceptTouchEvent返回true

修改MyViewGroup代码

public class MyViewGroup extends LinearLayout {
    private String TAG = "MyViewGroup";

    public MyViewGroup(Context context) {
        super(context);
        requestDisallowInterceptTouchEvent(false);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "MyViewGroup=====dispatchTouchEvent ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

点击Button按钮,打印结果如下:

08-01 18:36:56.815  29910-29910/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 18:36:56.825  29910-29910/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch1
08-01 18:36:56.825  29910-29910/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onClick

有打印可以看出,当重写onInterceptTouchEvent方法返回true时,是不会执行Button的触摸点击事件的。也正好验证了前面的结论:当ViewGroup重写onInterceptTouchEvent方法返回true时,也就是拦截子View的触摸事件传递,此时只会执行ViewGroup的触摸事件。

子View不可点击且没设置setOnClickListener

代码修改如下:

public class MainActivity extends ActionBarActivity {
    private String TAG = "MainActivity";
    private MyViewGroup parentView;
    private ImageView childView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        parentView = (MyViewGroup) findViewById(R.id.parent);
        childView = (ImageView) findViewById(R.id.child);
        childView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "childView=====onTouch");
                return false;
            }
        });
        parentView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "parentView=====onTouch" +event.getAction());
                return false;
            }
        });
        parentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "parentView=====onClick");
            }
        });

    }
}

将Button换成ImageView,并且不设置setOnClickListener事件。点击ImageView打印日志如下:

08-01 18:52:44.720  31219-31219/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 18:52:44.720  31219-31219/com.xjp.viewgrouptouchdemo E/MainActivity﹕ childView=====onTouch
08-01 18:52:44.730  31219-31219/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch0
parentView=====onClick

有打印可以看出,即执行了子View ImageView的触摸事件,也执行了ViewGroup的触摸事件。由于ImageView默认情况是不可点击的,因此:当子View不可点击或者么有设置setOnClickListener点击事件时,点击子View是先执行View的触摸事件,然后在执行ViewGroup的触摸事件的。这也验证了ViewGroup#dispatchTouchEvent小节的第9点。

最后附带上一幅ViewGroup触摸事件传递流程图

Android ViewGroup 触摸事件传递机制_第1张图片

总结

  1. 可以在ViewGroup里重写onInterceptTouchEvent方法来决定是否拦截子View的传递事件,系统默认返回false,表示不拦截子View的事件分发传递;如果重写返回true,表示拦截子View的触摸事件。
  2. 当子View是可点击的或者设置了setOnClickListener点击事件时,android触摸事件分发是不会传递到ViweGroup的,也就是只会执行View的触摸事件,不会执行ViewGroup的触摸事件。
  3. 当子View不可点击且没有设置setOnClickListener点击事件时,Android触摸事件是先分发到View,View先执行dispatchTouchEvent触摸事件,然后在传递到ViewGroup,ViewGroup执行dispatchTouchEvent触摸事件。
  4. Android事件分发先传递到View,在由View决定是否传递到ViewGroup。

你可能感兴趣的:(andorid,开发)