Android进阶 - ViewGroup事件分发机制

鸿洋版ViewGroup事件分发机制
View滑动冲突处理方法(外部拦截法、内部拦截法)
Android View 滑动冲突解决方式以及原理

案例

添加一个自定义的LinearLayout:

package com.example.zhy_event03;
 
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
 
public class MyLinearLayout extends LinearLayout
{
	private static final String TAG = MyLinearLayout.class.getSimpleName();
 
	public MyLinearLayout(Context context, AttributeSet attrs)
	{
		super(context, attrs);
	}
 
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev)
	{
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
			break;
		case MotionEvent.ACTION_UP:
			Log.e(TAG, "dispatchTouchEvent ACTION_UP");
			break;
 
		default:
			break;
		}
		return super.dispatchTouchEvent(ev);
	}
 
   @Override
    public boolean onTouchEvent(MotionEvent event)
    {

        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;

            default:
                break;
        }

        return super.onTouchEvent(event);
    }
 
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
			break;
		case MotionEvent.ACTION_UP:
			Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
			break;
 
		default:
			break;
		}
		
		return super.onInterceptTouchEvent(ev);
	}
 
	@Override
	public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
	{
		Log.e(TAG, "requestDisallowInterceptTouchEvent ");
		super.requestDisallowInterceptTouchEvent(disallowIntercept);
	}
 
}

更改xml文件

<com.example.zhy_event03.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
 
    <com.example.zhy_event03.MyButton
        android:id="@+id/id_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click me" />
 
</com.example.zhy_event03.MyLinearLayout>

ViewGroup事件运行顺序为VeiwGroup的dispatchTouchEvent -> VeiwGroup的onInterceptTouchEvent ->View的dispatchTouchEvent ->View的onTouchEvent

源码

为什么会这样呢?我们来看一下ViewGroup的dispatchTouchEvent源码

 @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.
                        }
                    }
                }
            }
        }                                                                                                                                                  ....//other code omitted

代码比较长,决定分段贴出,首先贴出的是ACTION_DOWN事件相关的代码。
16行:进入ACTION_DOWN的处理

17-23行:将mMotionTarget置为null

26行:进行判断:if(disallowIntercept || !onInterceptTouchEvent(ev))

两种可能会进入IF代码段

1、当前不允许拦截,即disallowIntercept =true,

2、当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;
注:disallowIntercept 可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置,后面会详细说;而onInterceptTouchEvent(ev)可以进行复写。

36-57行:开始遍历所有的子View

41行:进行判断当前的x,y坐标是否落在子View身上,如果在,47行,执行child.dispatchTouchEvent(ev),就进入了View的dispatchTouchEvent代码中了,如果不了解请参考:Android View的事件分发机制,当child.dispatchTouchEvent(ev)返回true,则为mMotionTarget=child;然后return true;

ViewGroup的ACTION_DOWN分析结束,总结一下:

ViewGroup实现捕获到DOWN事件,如果代码中不做TOUCH事件拦截,则开始查找当前x,y是否在某个子View的区域内,如果在,则把事件分发下去。

在这里插入图片描述
如何拦截
复写ViewGroup的onInterceptTouchEvent方法:

@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_MOVE:
			//如果你觉得需要拦截
			return true ; 
		case MotionEvent.ACTION_UP:
			//如果你觉得需要拦截
			return true ; 
		}
		
		return false;
	}

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。
原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

如何不被拦截
如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;此时子View希望依然能够响应MOVE和UP时该咋办呢?

Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

@Override
	public boolean dispatchTouchEvent(MotionEvent event)
	{
		getParent().requestDisallowInterceptTouchEvent(true);  
		int action = event.getAction();
 
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
			break;
		case MotionEvent.ACTION_UP:
			Log.e(TAG, "dispatchTouchEvent ACTION_UP");
			break;
 
		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}

getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。

总结

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;

View滑动冲突处理方法(外部拦截法、内部拦截法)

1、外部拦截法 (子view代码无需修改)(符合view事件分发机制)

说明:需要在父ViewGroup,重写onInterceptTouchEvent方法,根据业务需要,判断哪些事件是父Viewgroup需要的,需要的话就对该事件进行拦截,然后交由onTouchEvent方法处理,若不需要,则不拦截,然后传递给子view或子viewGroup。

代码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int y= (int) ev.getY();
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            yDown=y;
            isIntercept=false;
            break;
        case MotionEvent.ACTION_MOVE:
            yMove=y;
            if (yMove-yDown<0){                    //根据业务需求更改判断条件,判断是时候需要拦截
                isIntercept=false;
            }else if(yMove-yDown>0&&getChildAt(0).getScrollY()==0){
                isIntercept=true;
            }else if(yMove-yDown>0&&getChildAt(0).getScrollY()>0){
                isIntercept=false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercept=false;
            break;
    }
    return isIntercept;         //返回true表示拦截,返回false表示不拦截
}

总结

  • 外部拦截法主要是父容器去控制事件的拦截,若事件是父容器需要的,则进行拦截,不需要的则向下传递。
  • 父容器不能拦截DOWN事件或者UP事件。

2.内部拦截法(父viewgroup需要重写onInterceptTouchEvent)(不符合view事件分发机制)

说明:顾名思义就是在子view中拦截事件,父viewGroup默认是不拦截任何事件的,所以,当事件传递到子view时,
子view根据自己的实际情况来,如果该事件是需要子view来处理的,那么子view就自己消耗处理,如果该事件不需要由子view来处理,那么就调用getParent().requestDisallowInterceptTouchEvent()方法来通知父viewgroup来拦截
这个事件,也就是说,叫父容器来处理这个事件,这刚好和view的分发机制相反。

代码:    **(需要注意,要确保MotionEvent.ACTION_DOWN时不拦截)**

//子view的代码·
public boolean dispatchTouchEvent(MotionEvent ev) {
    int y= (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            yDown=y;
            break;
        case MotionEvent.ACTION_MOVE:
            yMove=y;
            Log.e("mes",yMove+"!!!");
            int scrollY = getScrollY();
            if (scrollY == 0&&yMove-yDown>0) {    //根据业务需求判断是否需要通知父viewgroup来拦截处理该事件
                //允许父View进行事件拦截
                Log.e("mes",yMove-yDown+"拦截");
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(ev);
}

总结

  • 内部拦截法是将事件控制权交给子View,若子View需要事件,则对事件进行处理,不需要则将事件传递给父ViewGroup,让父ViewGroup处理。
  • 子View通过调用父ViewGroup的requestDisallowInterceptTouchEvent来干预父ViewGroup对事件的拦截状况
  • 父ViewGroup不能拦截DOWN事件,至于MOVE或者UP事件的拦截状态要根据具体的情景
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值