Android事件分发机制——ViewGroup(二)

版权声明:本文为博主原创文章,欢迎转载但需注明出处谢谢! https://blog.csdn.net/dongxianfei/article/details/83826817

上一篇文章我们已经分析了Android事件分发机制——View(一),今天给大家带来ViewGroup事件分发的源码解析。

案例

public class MyLinearLayout extends LinearLayout {
    private static String TAG = MyLinearLayout.class.getSimpleName();

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

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @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 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 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, "disallowIntercept = " + disallowIntercept);
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.dispatchevent.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.dispatchevent.MyButton
        android:id="@+id/my_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click me"/>

</com.example.dispatchevent.MyLinearLayout>

MyLinearLayout中包含一个MyButton,MyButton在上篇博客中已经出现过,这里就不再贴代码了,看一下输出Log。

01-07 19:32:44.385 6515-6515/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_DOWN
01-07 19:32:44.385 6515-6515/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
01-07 19:32:44.386 6515-6515/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 19:32:44.386 6515-6515/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 19:32:44.386 6515-6515/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_DOWN

01-07 19:32:44.420 6515-6515/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_MOVE
01-07 19:32:44.420 6515-6515/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_MOVE
01-07 19:32:44.420 6515-6515/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
01-07 19:32:44.421 6515-6515/com.example.dispatchevent E/MyButton: onTouch ACTION_MOVE
01-07 19:32:44.421 6515-6515/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_MOVE

01-07 19:32:44.428 6515-6515/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_UP
01-07 19:32:44.428 6515-6515/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_UP
01-07 19:32:44.428 6515-6515/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
01-07 19:32:44.429 6515-6515/com.example.dispatchevent E/MyButton: onTouch ACTION_UP
01-07 19:32:44.429 6515-6515/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_UP

可以看到大体的事件流程为:
MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent->Mybutton的onTouch ->Mybutton的onTouchEvent

可以看出,在View上触发事件,最先捕获到事件的为View所在的ViewGroup,然后才会到View自身。

源码解析
当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

由于MyLayout中没有会一直向上寻找,最终发现ViewGroup.dispatchTouchEvent。
ViewGroup.dispatchTouchEvent

ViewGroup.dispatchTouchEvent

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) {
            mMotionTarget = null;
        }
        //(1)是否需要拦截
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            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)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        //(2)调用子View.dispatchTouchEvent()方法
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;
    //(3)如果子View.dispatchTouchEvent返回false,调用Group的OnTouch相关方法
    if (target == null) {
        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);
    }
	//...

1、进入ACITON_DOWN操作后,首先将mMotionTarget=null,然后进行判断if(disallowIntercept || !onInterceptTouchEvent(ev))。

(1)当前不允许拦截,即disallowIntercept =true;
(2)当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;
(3)disallowIntercept 可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean)进行设置,而onInterceptTouchEvent(ev)可以进行复写;

2、循环遍历子View,通过坐标定位事件的子View,执行child.dispatchTouchEvent(ev),之后的操作请参考Android事件分发机制——View(一)

(1)调用子View的dispatchTouchEvent后是有返回值的,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true;
(2)当返回true时,导致条件判断成立,于是ViewGroup.dispatchTouchEvent方法直接返回了true,导致后面代码无法执行,也就会把MyLayout的touch事件拦截掉;

if (child.dispatchTouchEvent(ev))  {
	mMotionTarget = child;
	return true;
}

3、当我们点击的不是按钮,而是空白区域呢?此时将不会调用child.dispatchTouchEvent(ev),所以target==null,将会调用后面的super.dispatchTouchEvent(ev),由于ViewGroup的父类就是View,因此MyLayout中注册的onTouch方法也会得到执行。

现在再来看一下整个ViewGroup事件分发过程的流程图:
ViewGroup事件分发

如何拦截事件
复写ViewGroup的onInterceptTouchEvent方法:

//group
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 true;
        //return super.onInterceptTouchEvent(ev);
    }

//子View事件全部被拦截
01-07 04:28:05.588 15966-15966/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_DOWN
01-07 04:28:05.588 15966-15966/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
01-07 04:28:05.590 15966-15966/com.example.dispatchevent E/MyLinearLayout: onTouchEvent ACTION_DOWN
//group
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");
                return true;
                //break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    
//子View的Move之后事件全部被拦截
01-07 04:31:51.459 17533-17533/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_DOWN
01-07 04:31:51.459 17533-17533/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
01-07 04:31:51.459 17533-17533/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 04:31:51.461 17533-17533/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 04:31:51.461 17533-17533/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_DOWN
01-07 04:31:51.481 17533-17533/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_MOVE
01-07 04:31:51.481 17533-17533/com.example.dispatchevent E/MyLinearLayout: onInterceptTouchEvent ACTION_MOVE
01-07 04:31:51.497 17533-17533/com.example.dispatchevent E/MyLinearLayout: dispatchTouchEvent ACTION_UP
01-07 04:31:51.498 17533-17533/com.example.dispatchevent E/MyLinearLayout: onTouchEvent ACTION_UP

默认是不拦截的,即返回false,如果你需要拦截,只要return true就行了。这样该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件,如果你在MOVE return true,则子View在MOVE和UP都不会捕获事件。

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

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

//group
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");
                return true;
                //break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    
//child
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事件。

注:但是如果ViewGroup.onInterceptTouchEvent(ev)在ACTION_DOWN里面直接return true了,那么子View是没有办法捕获事件的。

总结

  1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的;
  2. 当onTouch()返回true时,onTouchEvent()将不会被执行;
  3. 当设置了longClick并且返回true时,onClick将不被执行,当没有设置longClick或者返回false时,onClick仍将被执行;
  4. 在ViewGroup中可以通过onInterceptTouchEvent()对事件进行拦截,当返回true时表示事件不允许向子View传递(事件交给自己的onTouchEvent处理),返回false代表不对事件进行拦截,默认返回false;
  5. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件;
  6. 子View可通过调用getParent().requestDisallowInterceptTouchEvent(true)来阻止ViewGroup对其MOVE/UP事件进行拦截;

猜你喜欢

转载自blog.csdn.net/dongxianfei/article/details/83826817