您的当前位置:首页正文

一个实例让你透彻理解Android ViewGroupe的事件分发

2024-11-09 来源:个人技术集锦

前言:ViewGroupe的事件分发是一个重点也是一个痛点,许多地方都会用到,尤其是滑动冲突时的解决,下面通过一个实例并结合源码来分析ViewGroupe的事件分发。前提是有一定的View事件分发基础,不熟悉的可以看我之前写的View的事件分发。

自定义的Button:

package com.zjt.touch;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;

public class MyButton extends Button {

    private final static String TAG = "MyButton";

    public MyButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //解决父类拦截事件的调用方法
    //getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
        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;
        case MotionEvent.ACTION_CANCEL:
            Log.e(TAG, "dispatchTouchEvent  ACTION_CANCEL");
            break;

        default:
            break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e(TAG, "onTouchEvent  ACTION_DOWN");
            // return false;
            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);
    }

}

自定义的TextView:

package com.zjt.touch;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;

import com.zjt.view.TestView;

/**
 * Created by hc on 2017/1/23.
 */
public class MyTextView extends TextView {

    private final static String TAG = "MyTextView";

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

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

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
        }
        return super.onTouchEvent(event);
    }
}

自定义的LinearLayout:

package com.zjt.touch;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * Created by hc on 2017/1/23.
 */
public class MyLinearLayout extends LinearLayout {
    private final static String TAG = "MyLinearLayout";
    public MyLinearLayout(Context context) {
        super(context);
    }

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;
//                return true;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
                //return false;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
  //              return false;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
               // return true;
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
        }
        return super.onTouchEvent(event);
    }
}

上面的自定义view都很简单,增加了日志打印而已。

MainActivity如下:

package com.zjt.touch;

import com.example.testactivity2.R;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;

public class TestViewTouchActivity extends Activity implements OnClickListener{

    private final static String TAG = "TestViewTouchActivity";

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

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.btn_touch:
            Log.e(TAG, "btn click......");
            break;

        default:
            break;
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
        }
        return super.onTouchEvent(event);
    }

}

当我们点击button的时候,log如下:

点击TextView的时候,log如下:

点击空白地方的log如下:

为什么会出现如上的结果呢,我们通过ViewGroupe的源码来分析,ViewGroupe的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;
        /**
         * disallowIntercept表示是否允许事件拦截,默认是false,即不拦截事件
         * 此值可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法进行设置
         */
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
            /**
             * 因为从ACTION_DOWN开始要开启新一轮的事件分发,所有要将mMotionTarget(目标)置为空
             */
            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
            /**
             * 当disallowIntercept(默认是false)为true,或者onInterceptTouchEvent(ev)(默认返回为false)方法的返回值
             * 为false,取反则为true,则判断条件成立
             */
            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;
                /**
                 * 遍历当前ViewGroup的所有子View
                 */
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    /**
                     * 如果当前的View是VISIBLE的或者有动画执行
                     */
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {

                        child.getHitRect(frame);
                        /**
                         * 如果子View包含当前触摸的点
                         */
                        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.
                                /**
                                 * 如果子View的dispatchTouchEvent方法的返回值为true,则表示子View已经消费了事件
                                 * 此时将子View赋值给mMotionTarget
                                 */
                                mMotionTarget = child;
                                /**
                                 * 直接返回true,表示down事件被消费掉了
                                 */
                                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
            /**
             * 如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false
             * 因为ACTION_UP或者ACTION_CANCEL表示事件执行完,要将前面设置的值复位
             */
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        /**
         * 当mMotionTarget为空表示没有找到消费事件的View,此时需要调用ViewGroup父类的dispatchTouchEvent方法,
         * ViewGroup的父类即为View
         */
        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
        /**
         * 如果执行到此说明target!=null,然后判断是否允许拦截和是否想要拦截
         * 如果允许拦截(!disallowIntercept=true),并且想要拦截(onInterceptTouchEvent(ev)返回值为true)
         * 则条件成立
         */
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            /**
             * 设置动作为ACTION_CANCEL,此处与ev.getAction相对应
             */
            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;
        }
        /**
         * 将事件分发给target返回target.dispatchTouchEvent(ev)的返回值
         */
        return target.dispatchTouchEvent(ev);
    }

View事件分发是从Activity的DecorView一直往下传递,直到我们的Linearlayout再到我们手指触摸的view,比如我们代码中的Button、TextView以及Linearlayout的空白部分。
知道上面的原则,我们接着往下分析点击Button的Log,首先从Activity传递到我们的布局LinearLayout中,由于LinearLayout的父布局没有设置事件拦截,所以走到代码的61行:
61行的child就表示我们点击的button,button是View所以走View的dispatchTouchEvent,
由于button是可点击的所以消费了ACTION_DOWN事件,所以61行返回true,往下走,到67行:
mMotionTarget = child; //这里的child就是Button
然后在71行返回true,ACTION_DOWN事件执行完毕。

然后是 ACTION_MOVE:
97行:final View target = mMotionTarget;
102行的 target 不为空,120行没有拦截,直接走到165行–target.dispatchTouchEvent(ev); 即走到MyButton的onTouchEvent

ACTION_UP : 和ACTION_MOVE没有拦截的话基本一样就不分析了。

下面分析点击MyTextView的log,
走到61行 ,此时的 child 就是我们点击的 MyTextView了,由于 MyTextView是继承自 TextView ,所以 MyTextView默认也是不可点击的,所以, 61行的child.dispatchTouchEvent(ev)返回的是false,所以我们需要往上看往上看MyLinearLayout的dispatchTouchEvent()方法的61行, 由于由于TextView.dispatchTouchEvent()为false, 导致mMotionTarget没有被赋值,

那么就得继续往下走,走到97行:
final View target = mMotionTarget; //此时由于MyTextView没有消费down事件,所以mMotionTarget = null,所以导致 target 为 null,

所以会走到110行:return super.dispatchTouchEvent(ev); 这里调用的是MyLinearLayout父类View的dispatchTouchEvent()方法,由于MyLinearLayout没有设置onTouchListener,但是重写了onTouchEvent方法,但是由于MyLinearLayout既不是clickable的也是longClickable的,所以其onTouchEvent()方法false,所以dispatchTouchEvent()也是返回false。这也表示 ACTION_DOWN未被消费,从而不能继续执行下面的ACTION_MOVE, ACTION_UP等事件了,具体为什么,代码中没看出来,如果知道原因的小伙伴可以告诉我啊。

点击MyLinearLayout的空白区域和点击MyTextView情况类型,就不作过多分析啦,有问题就留言大家一起讨论。

现在我们看看事件的拦截,我们修改下MyLinearLayout中的onInterceptTouchEvent的方法,现将 ACTION_MOVE中return true,表示拦截move,以及move的后续事件(up等)


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
//                return false;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
//                return false;
               break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
                return true;
//                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

然后我们点击MyButton,我们先分析结果,等下再看看log,由于是点击button而且是在down事件之后的move中拦截事件的,所以MyButton的down事件会完整的走完,我们接着看move事件。

move :走到97行,对 target 进行赋值: final View target = mMotionTarget; mMotionTarget 就是在down事件中进行赋值的 MyButton。

走到120行:if (!disallowIntercept && onInterceptTouchEvent(ev)) ,disallowIntercept 默认是false的,由于在onInterceptTouchEvent的move中 返回true,所以 onInterceptTouchEvent(ev) = true,这个if语句成立,
走到127 行 :ev.setAction(MotionEvent.ACTION_CANCEL); 将action设为 ACTION_CANCEL。

132行:if (!target.dispatchTouchEvent(ev)),就调用MyButton的dispatchTouchEvent方法,由于127行,将将action设为 了ACTION_CANCEL,所以会调用 MyButton的dispatchTouchEvent方法中的ACTION_CANCEL,日志中会提现出来。

140行:mMotionTarget = null; 将 mMotionTarget 置为null。

144行:return true; 表示move事件一被消费掉。

接着看UP事件:

同样是走到97行为target赋值,final View target = mMotionTarget; 但此时的mMotionTarget在move事件中已被置为null,所以target也是null;

102行:if (target == null) ,成立,走到该条件语句的110行:return super.dispatchTouchEvent(ev);

即调用 MyLinearLayout的父类的dispatchTouchEvent方法,即View的dispatchTouchEvent,MyLinearLayout中没有设置onTouchListener,但是重写了 onTouchEvent方法,由于MyLinearLayout不是clickable以及longclickable所以 onTouchEvent 返回false,所以 dispatchTouchEvent也返回false,UP事件未被消费。

现在看看log,log如下:

log和我们的分析是一致的。

现在再看看我们在MyLinearLayout的onInterceptTouchEvent方法的down事件中,进行事件拦截,代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
//                return false;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
                return true;
//               break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
//                return true;
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

我们还是先分析源码再提高log做验证。

DOWN事件:
由于在down事件进行了拦截,所以走到31行:if (disallowIntercept || !onInterceptTouchEvent(ev))中的onInterceptTouchEvent为true,所以该条件不成立。

102行:if (target == null)由于down事件未被消费,所以 target 为null,走到 110行:return super.dispatchTouchEvent(ev); 跟之前的分析一样,由于MyLinearLayout重写了onTouchEvent方法,由于MyLinearLayout不是clickable以及longclickable,所以 onTouchEvent 返回false,所以 dispatchTouchEvent也返回false,Down事件未被消费。

由于Down事件未被消费,所以接下来的MOVE和UP事件不执行了,这个问题上面有提到,我也不知原因。

log如下:

如果MyLinearLayout的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截子View的MOVE以及UP事件;
此时子View希望依然能够响应MOVE和UP时我们应该怎么办呢?

我们可以如下做,比如我们的MyButton中的代码:

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //解决父类拦截事件的调用方法
        getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
        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;
        case MotionEvent.ACTION_CANCEL:
            Log.e(TAG, "dispatchTouchEvent  ACTION_CANCEL");
            break;

        default:
            break;
        }
        return super.dispatchTouchEvent(event);
    }
修改MyLinearLayout的onInterceptTouchEvent(ev)的方法,在move中拦截事件,
 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
                break;
//                return false;
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
//                return true;
               break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
                return true;
//                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

结果代码就可以正常执行下去了,这个源码分析不难,就不做讲解了,log如下:

好了,文章就到这里了,有问题请留言,一起学习,一起进步。

Top