关于android事件分发之ViewGroup

本文详细解析了Android中ViewGroup如何通过dispatchTouchEvent方法传递touch事件至其内部的View,以及如何通过重写onInterceptTouchEvent方法来控制事件的分发。通过实例演示,展示了如何在特定条件下拦截事件,避免其传递给子View,从而影响用户的交互体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

感觉现在对touch事件的分发运用的越来越熟练,赶紧记录下来,免得以后忘记了。上一篇博客已经详细的讲解了关于View的touch事件的,感觉还不过瘾,赶紧把ViewGroup事件的传递也总结一下吧。

还是通过具体的实例进行总结吧。

实例中自定义MyLayout继承自LinearLayout。请看布局代码。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
   <com.example.toucheventdemo.MyLayout 
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical"
       android:id="@+id/ll"
       >
       
       <Button 
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="click 1"
           android:id="@+id/btn1"
           />
       
       <Button 
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="click 2"
           android:id="@+id/btn2"
           />
       
   </com.example.toucheventdemo.MyLayout>
    

</LinearLayout>

有两个button。我们分别注册click和touch事件。请看代码。

btn1.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d(tag, "button 1 click");
			}
		});
		
		btn2.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d(tag, "button 1 click");
			}
		});
		
		layout.setOnTouchListener(new OnTouchListener() {
			
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d(tag, "touch");
				return false;
			}
		});

请两个button注册了单击事件,和布局注册了touch事件。

我现在分别点击button1和button2,并且触摸了button以外的区域。输出结果如下:

06-04 23:34:48.096: D/MainActivity(19416): button 1 click
06-04 23:34:50.646: D/MainActivity(19416): button 1 click
06-04 23:34:51.496: D/MainActivity(19416): touch

点击button1和button2时仅仅是执行了onclick,却没有执行ontouch。那我们就有一个疑问了,这个touch事件到底是从ViewGroup传递给View呢,还是从View传递到ViewGroup。关于这个问题,我们可以先放一放。先看看ViewGroup中的onInterceptTouchEvent方法的源码吧。

 /**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

这个方法的作用就是判断是否拦截touch事件的分发,如果返回true,表示拦截,这时候就不会传递给子View,如果返回false,表示不拦截,正常传递给View即可。所有说touch事件的传递应该是从ViewGroup传递给View,需要层层传递的。那我们不妨在MyLayout方法中重写onInterceptTouchEvent方法,让它返回true,拦截一下试试,拦截以后,button应该是不能响应onclick事件才对,这时候MyLayout就不会讲touch事件传递给Button了,点击时只会执行MyLayout的touch事件才对。不妨拭目以待吧。

public class MyLayout extends LinearLayout {

	public MyLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return true;
	}

}

输出结果为:

06-04 23:50:28.736: D/MainActivity(23257): touch
06-04 23:50:30.306: D/MainActivity(23257): touch
06-04 23:50:31.916: D/MainActivity(23257): touch

我点击了button1和button1,和其他的区域。这说明MyLayout拦截了事件,没有传递到Button。上一篇我们就说过不管是View还是ViewGroup事件的分发都是从dispatchTouchEvent开始的,我们不妨看看ViewGroup代码吧。

 /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }

        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);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            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);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            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;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            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);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        return target.dispatchTouchEvent(ev);
    }
代码比较长,我们重点看29行, if (disallowIntercept || !onInterceptTouchEvent(ev)),这个if是或的关系,只要有一个满足条件即可。其中disallowIntercept,表示是否禁止事件的分发,默认为false,我们可以通过getParent().requestDisallowInterceptTouchEvent(true)修改这个值,这个方法非常有用,我会在下一篇博客中进行介绍。而第二个onInterceptTouchEvent方法就行我们刚刚重写的方法了,默认返回false,那么这个表达式就为true,就会执行下边的for循环了,找到你触摸区域的View,然后调用View的dispatchTouchEvent方法,这时候流程就我们上一篇介绍的View的事件分发过程一样了。当我们重写onInterceptTouchEvent方法,返回true时,就不会执行for循环了,就会执行76行的代码,最终调用super.dispatchTouchEvent(ev)的方法,那就View的分发了,所有最终我们看到只执行touch事件,而没有执行onclick。

今天的总结就要告一段落,我们再回顾一下ViewGroup事件分发的知识点吧。

1. touch事件的传递是从ViewGroup经过层层传递到View的。

2. 对于ViewGroup事件的传递也是从dispatchTouchEvent方法开发分发。

3.如果某一个ViewGroup想要拦截事件分发到子view上去,可以重写ViewGroup中的onInterceptTouchEvent方法。

4. 如果想要让ViewGroup不拦截事件,让touch事件顺利的传递到View,让View来处理这个手势的话,有两个方法,requestDisallowInterceptTouchEvent(true),而另一个就是重写方法onInterceptTouchEvent,即可解决。


下一篇将要总结touch事件在我工作中遇到的问题,以及是如何解决的。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值