从小白角度探索Android事件分发机制

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布


概念

所以我们要开始讲解事件分发机制了,说到事件分发机制,这个知识点主要是在自定义view的时候用到,那么什么是事件分发机制呢。

这里我用大白话概述一下:我们在自定义view,或者在使用某个控件,当给这个view或者控件设置事件的时候,比如有setOnTouchListenersetOnClickListener这些方法的时候,这些方法总有一个执行顺序吧,事件分发机制主要就是了解这些方法执行的先后顺序,或者说执行这些事件的顺序和方法之间的关系,比如点击事件,触摸事件,手指上抬下按等等之类的,主要就是要搞清楚这些事件发生的先后顺序和他们之间的关系。

搞清楚这些东西有什么好处呢,首先,即便假设可能由于这些方法名字太像了,所以你还是没有搞清楚这些方法的执行顺序和相互关系,不过在搞清楚的过程中,至少也搞清楚每个方法,就搞清楚这些方法是干嘛的了吧,知道这些方法是干嘛的又能有什么好处呢,至少不仅仅就只会一个setOnClickListener点击事件了吧,如果你搞清楚了setOnTouchListener方法,也许你就可以实现一个view,手指点击按住后拖动,手指放开后,view又回到原来的位置,这种效果,可不是一个简单的setOnClickListener就能够实现的。

如果以上那么大一段文字引起了你阅读这篇文章的兴趣,那你就继续吧。

代码追踪

(p.s. 以下代码追踪基于Android 8.0的源码,即API 26)

不知道有哪些方法跟触摸点击有关啊,那就看源码吧!从我们最熟知的setOnClickListener开始,setOnClickListener做了啥,跳进View.class里面,发现这个方法长这样:

// View.class
	
public void setOnClickListener(@Nullable OnClickListener l) {
	...
	getListenerInfo().mOnClickListener = l;
}

反正就是赋值,给这个接口赋值,所以我们来看看mOnClickListener在什么地方使用的,代码一顿追踪,mOnClickListener在这里performClick()被使用了:

// View.class
	
public boolean performClick() {
	...
	li.mOnClickListener.onClick(this);
	...
}

先不管这里面具体的一个实现流程是啥,知道mOnClickListener在这里被用到就行了,看这个方法名:performClick翻译过来“执行点击”,嗯~靠谱,继续追踪,于是来到了:

// View.class

public boolean onTouchEvent(MotionEvent event) {
	...
	switch (action) {
		case MotionEvent.ACTION_UP:
			...
			performClick();
			...
		break;
			...
	}
...
}

看到这里,我们需要稍微总结一下了。为什么要在这里总结呢,因为不一样了啊,哪里不一样了。首先,前面两个方法setOnClickListenerperformClick,概念单一,就一个设置具体实现类接口,还有一个执行点击事件嘛,但是onTouchEvent好像跟以上两种不太一样,因为他有个参数MotionEvent,翻译过来运动事件或者手势事件,这个事件包含了我们很多手势操作,比如手指上抬下按,在屏幕上拖动等等。所以完整的onTouchEvent方法是如下形状:

// View.class

public boolean onTouchEvent(MotionEvent event) {
	...
        switch (action) {
        	// 手指上抬
            case MotionEvent.ACTION_UP:
        	    ...
                performClick();
                ...
                break;
			// 手指下按
            case MotionEvent.ACTION_DOWN:
                ...
                break;
			// 手指取消
            case MotionEvent.ACTION_CANCEL:
                ...
                break;
			// 手指滑动
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
    ...
    }

所以我们可以很清晰的看到,点击事件只是在手指抬起的这个行为后执行的,只是用到了这么多操作行为中的其中一个而已。那么我们是不是就可以得出一个结论,performClick()方法其实只有在手指上抬的时候执行,也就是当手指接触屏幕到离开屏幕的这个过程中,performClick()只执行了一次,而onTouchEvent可能就执行了多次,至少2次吧,即上抬和下按。
接下来我们开始追踪onTouchEvent方法,然后发现了以下源码。

// View.class
	
public boolean dispatchTouchEvent(MotionEvent event) {
	...
	if (li != null && li.mOnTouchListener != null
		&& (mViewFlags & ENABLED_MASK) == ENABLED
		&& li.mOnTouchListener.onTouch(this, event)) {
			result = true;
		}

	if (!result && onTouchEvent(event)) {
		result = true;
	}
	...
	}

看到此次,有人提问 (问我= =)!为啥这里的源码不写成:

// View.class
	
public boolean dispatchTouchEvent(MotionEvent event) {
	...
	if (!result && onTouchEvent(event)) {
		result = true;
	}
	...
}

因为有个onTouch()方法,待会要讲,主要是由于view有个setOnTouchListener()方法,先不管这些,我们只看上面那个简单的dispatchTouchEvent()方法,这个方法翻译成中文“分发触摸事件”,好像有点谱了,事件分发机制的源头可能就在此处吧。先不管,继续跟踪,我们来到:

// View.class

public final boolean dispatchPointerEvent(MotionEvent event) {
			...
            return dispatchTouchEvent(event);
            ...
    }

继续跟踪:

// ViewRootImpl.class
		
private int processPointerEvent(QueuedInputEvent q) {
	...
	boolean handled = mView.dispatchPointerEvent(event);
	...
}

妈耶!都跑到私有方法去了,不管,继续跟踪:

// ViewRootImpl.class

final class ViewPostImeInputStage extends InputStage {
	...
	@Override
	protected int onProcess(QueuedInputEvent q) {
		...
		return processPointerEvent(q);
		...
	}
	...
}

不管,反正也不知道这个类是干嘛的,继续跟,跟源码死磕到底!

// ViewRootImpl.class

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
	...
 	InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
  	...
}

WTF?都跑到setView设置view那里去了,算了算了,弃坑重练,还是定位到靠谱的dispatchTouchEvent()方法就终止追踪了吧。有朋友可能会问,到这里为啥就不跟了,你说呢,都setView了,这之前的代码连view都没有了,哪还来的事件。

触发顺序

现在我们来总结一下有哪些方法,从上到下的顺一遍,

dispatchTouchEvent()
onTouch()
onTouchEvent()
setOnClickListener

根据源码,我们的猜想是按照这样一个顺序,那我们来验证一下,首先写一个类,并且让这个类实现那些事件方法,所以这个类,基本上就长这样了。

public class EventView extends View {

    private static final String TAG = "EventView";

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

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouch: ");
                return false;
            }
        });

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

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(event);
    }

}

鉴于已经知道触摸行为常见的有3种,按下移动抬起,为了使日志最短,所以我们飞快的点击了屏幕,不给手指在屏幕上的移动的机会,得到了如下日志:

dispatchTouchEvent: 
onTouch: 
onTouchEvent: 
dispatchTouchEvent: 
onTouch: 
onTouchEvent: 
onClick: 

为了看的更加清楚,我们改改源码,打印出具体的行为:

public class EventView extends View {

    private static final String TAG = "EventView";

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

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouch: action = " + event.getAction());
                return false;
            }
        });

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

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: action = " + event.getAction());
        return super.onTouchEvent(event);
    }

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

}

日志:

dispatchTouchEvent:	action = 0
onTouch:		action = 0
onTouchEvent:		action = 0
dispatchTouchEvent:	action = 1
onTouch:		action = 1
onTouchEvent:		action = 1
onClick: 

所以这里面的0和1都是啥意思,还有,这些方法长的太像了,我已经蒙圈了,只认识onClick了。
先看看0和1都是啥意思,看源码呗,不是都说源码是最好的老师吗。

public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;

好了,知道了,手指按下就是0,手指上抬就是1。上面的日志就被改成下面这副模样:

dispatchTouchEvent: 	action = 手指下按
onTouch:		action = 手指下按
onTouchEvent: 		action = 手指下按
dispatchTouchEvent: 	action = 手指上抬
onTouch: 		action = 手指上抬
onTouchEvent: 		action = 手指上抬
onClick: 

然后我们根据方法的名字,将方法名字翻译成中文,上面的日志又被改成以下模样:

分发触摸事件: 	action = 手指下按
触摸: 		action = 手指下按
触摸事件: 	action = 手指下按
分发触摸事件: 	action = 手指上抬
触摸: 		action = 手指上抬
触摸事件: 	action = 手指上抬
onClick: 

大家感受一下这个顺序,给你10秒钟。

接下来我们进入下一个环节。

详细分析

dispatchTouchEvent

首先我们先来到最初的dispatchTouchEvent方法中去寻觅过程。

(当前源码API 26;不用看这些源码,我就摆摆场面= =)

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

某些读者表示,这些源码又多又乱,我在看一篇博客,我要怎么看这些源码,里面的变量是啥意思都不知道,还不能进行变量跟踪,我要怎么看,如果是在IDE里面打开这些源码,兴许我还有几分愿意阅读的兴趣。

以上问题就是我平时看博客的时候脑子里面想到的事情,最讨厌贴上一片源码,然后就开始讲道理了,源码看都看不懂,或者说不想看= =

教大家一个小技巧,看老版本的源码,因为Android源码只会越来越多啊,所以老版本的源码肯定比新版本的少。

目前我这里找到的最老的源码只有API 15 的,所以我们来看看API 15里面的事件分发是怎么写的吧,同一个方法dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
}

是不是觉得少了很多,不过还是挺多的,那我们怎么看呢,就看这里面出现的关键点,源码少了很多,我们就能快速定位我们的关键点在什么地方了。

首先这个方法里面出现了两个很重要的地方,onTouchonTouchEvent方法,所以把跟这些代码无关的地方,我们就都给筛掉,所以就变成了以下模样:

public boolean dispatchTouchEvent(MotionEvent event) {
	...
	ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
		&& li.mOnTouchListener.onTouch(this, event)) {
		return true;
	}

	if (onTouchEvent(event)) {
		return true;
	}
	...
}

好像基本就这样了,也不能再怎么筛了,所以趁着源码才这几行的机会,我们好好来看一下,首先是ListenerInfo类,这个类是干啥的,看名字好像是接口监听信息,瞅瞅源码:

static class ListenerInfo {

        protected OnFocusChangeListener mOnFocusChangeListener;
        
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;

        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
}

果然基本所有的接口都在这里面,当接口很多的时候,用这种方式统一管理接口,真是个不错的方法,学到了,果然看源码还是有很多好处的嘛。

好的,我们继续来看dispatchTouchEvent方法(复制了一遍,免得往上翻)

public boolean dispatchTouchEvent(MotionEvent event) {
	...
	ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
		&& li.mOnTouchListener.onTouch(this, event)) {
		return true;
	}

	if (onTouchEvent(event)) {
		return true;
	}
	...
}

ListenerInfo我们已经知道是怎么回事了,就来看看第一个if,因为第一个if就包含我们其中一个关注点onTouch,这个if条件还挺多的,一共有4个条件,我们一个一个看:

  1. li != null

我想,这个一个不用我说了吧,就判断这个接口管理类是否为null

  1. li.mOnTouchListener != null

判断这个接口是否为null,我们来看看这个值是在哪里赋值的:

public void setOnTouchListener(OnTouchListener l) {
	getListenerInfo().mOnTouchListener = l;
}

仿佛看到常见方法了,或者关键方法了,这个方法是view的。

  1. (mViewFlags & ENABLED_MASK) == ENABLED

说到这里,我要夸夸Google的程序员,确实厉害(Google程序员:还用你夸?)
这个&用的很传神,在API 26 的View.class里面有一群这样的注释:

/**
     * Masks for mPrivateFlags2, as generated by dumpFlags():
     *
     * |-------|-------|-------|-------|
     *                                 1 PFLAG2_DRAG_CAN_ACCEPT
     *                                1  PFLAG2_DRAG_HOVERED
     *                              11   PFLAG2_LAYOUT_DIRECTION_MASK
     *                             1     PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL
     *                            1      PFLAG2_LAYOUT_DIRECTION_RESOLVED
     *                            11     PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK
     *                           1       PFLAG2_TEXT_DIRECTION_FLAGS[1]
     *                          1        PFLAG2_TEXT_DIRECTION_FLAGS[2]
     *                          11       PFLAG2_TEXT_DIRECTION_FLAGS[3]
     *                         1         PFLAG2_TEXT_DIRECTION_FLAGS[4]
     *                         1 1       PFLAG2_TEXT_DIRECTION_FLAGS[5]
     *                         11        PFLAG2_TEXT_DIRECTION_FLAGS[6]
     *                         111       PFLAG2_TEXT_DIRECTION_FLAGS[7]
     *                         111       PFLAG2_TEXT_DIRECTION_MASK
     *                        1          PFLAG2_TEXT_DIRECTION_RESOLVED
     *                       1           PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT
     *                     111           PFLAG2_TEXT_DIRECTION_RESOLVED_MASK
     *                    1              PFLAG2_TEXT_ALIGNMENT_FLAGS[1]
     *                   1               PFLAG2_TEXT_ALIGNMENT_FLAGS[2]
     *                   11              PFLAG2_TEXT_ALIGNMENT_FLAGS[3]
     *                  1                PFLAG2_TEXT_ALIGNMENT_FLAGS[4]
     *                  1 1              PFLAG2_TEXT_ALIGNMENT_FLAGS[5]
     *                  11               PFLAG2_TEXT_ALIGNMENT_FLAGS[6]
     *                  111              PFLAG2_TEXT_ALIGNMENT_MASK
     *                 1                 PFLAG2_TEXT_ALIGNMENT_RESOLVED
     *                1                  PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT
     *              111                  PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK
     *           111                     PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK
     *         11                        PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK
     *       1                           PFLAG2_ACCESSIBILITY_FOCUSED
     *      1                            PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED
     *     1                             PFLAG2_VIEW_QUICK_REJECTED
     *    1                              PFLAG2_PADDING_RESOLVED
     *   1                               PFLAG2_DRAWABLE_RESOLVED
     *  1                                PFLAG2_HAS_TRANSIENT_STATE
     * |-------|-------|-------|-------|
     */

与(&),一个符号巧妙的搞定了判断两个值是否等于相同,好了不吹了,偏题了= =
总之这个判断大概就是判断该View是否可用。

  1. li.mOnTouchListener.onTouch(this, event))

这里,重点环节,回调了onTouch方法,我们就可用在事件onTouchEvent事件执行之前,先一步窥探有什么事件,甚至拦截接下来的事件。
为什么要先一步呢,因为我们在自定义view的时候,可以很方便的重写onTouchEvent方法,但是如果使用的是系统控件,就不能那么方便的得到这些事件了,如果这时候可以巧妙的使用setOnTouchListener,那么就能先一步得到这些事件了。

第一个if的条件分析完了,为了避免再次你们继续翻上去看那个方法,无形增加后摇时间,所以我重新复制一下:

public boolean dispatchTouchEvent(MotionEvent event) {
	...
	ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
		&& li.mOnTouchListener.onTouch(this, event)) {
		return true;
	}

	if (onTouchEvent(event)) {
		return true;
	}
	...
}

第一个if我们只看了条件,内容就一个return true,我们来看看第二个if,条件居然直接就是onTouchEvent方法的返回值,内容体也是return true。

那我们就根据这点代码来总结一下吧!

不过还是有不必要的代码,我再简化一下吧:

public boolean dispatchTouchEvent(MotionEvent event) {
			...
            if (mOnTouchListener.onTouch(this, event)) {
                return true;
            }
            if (onTouchEvent(event)) {
                return true;
            }
            ...
}

这样看的是不是就足够清楚了,代码就是这样,剔除了那些非核心代码后,核心代码其实就短短几句。

第一个if,我们可以看到,其实这里的onTouch方法是我们手动实现的,使用setOnTouchListener,就可以在这个设置的接口里面具体实现onTouch的内容了。

然后由于我们还可以把控onTouch的返回值,如果我们将onTouch的返回值设为true,那么第一个if就结束了,dispatchTouchEvent也就直接结束了,那么第二个if就不会执行了,相当于我们可以通过onTouch的返回值,直接拦截view自己实现的onTouchEvent方法。假设有个自定义的DragView,可以想拖哪就拖到哪,如果给这个类setOnTouchListener,那么这个控件的拖动方式就全凭你管了啊,想想都刺激。

所以使用setOnTouchListener可以拦截onTouchEvent方法,默默记住这个知识点。

然后我们接着看第二个if,好像onTouchEvent也可以拦截这个if下面的代码哈,然后其他的就没啥了,反正这个if下面又没有什么触摸事件了,这里这个onTouchEvent的返回值是true是false应该都没啥关系了吧,如果你这样想,那么你就错了。别忘了,onTouchEvent可再也不是我们实现的了,这是系统实现的,那还不赶紧进来看看,里面长啥样。

onTouchEvent

老规矩,把API 15的源码搬上来:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                       }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
}

好了好了,我知道你们直接跳过来了,知道你们不会看,所以我准备了一份终极简化版:

public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                performClick();
                break;

            case MotionEvent.ACTION_DOWN:
                break;

            case MotionEvent.ACTION_CANCEL:
                break;

            case MotionEvent.ACTION_MOVE:
                break;
        }
        return true;
}

是不是看着眼睛干净多了,去掉的大概都是一些什么view是否可用,能不能点击之类,各种对象的处理和判断啦,大概就这些东西,总之不影响我们研究核心的内容。

可以看到onTouchEvent的返回值直接就是true,也就可以认为是事件分发的终点了。那么我们来看看这个方法里面做了什么事,首先switch区分了触摸事件的类型,上抬下按什么的,然后我们发现手指上抬的时候,执行了一个performClick,关于onTouchEvent方法,其他就没什么好说的了。既然如此,我们就来大概看看performClick里面是些什么东西了,不过我想你们应该猜到了。

performClick

直接上终极简化版吧,一目了然的感觉真好。

public boolean performClick() {
	li.mOnClickListener.onClick(this);
	return true;
}

没错,就是回调了setOnClickListener里面设置的接口。

讲到这里,那view的事件分发基本就算讲完了,顺便一提,关于

public boolean dispatchTouchEvent(MotionEvent event) {
			...
            if (mOnTouchListener.onTouch(this, event)) {
                return true;
            }
            if (onTouchEvent(event)) {
                return true;
            }
            ...
}

现在我们也就知道了,如果onTouchEvent返回false,会影响的就是点击事件了,也就是说,如果我们在重写onTouchEvent的时候,如果返回值是false,那么就没有点击事件了,不过你要把点击事件设置到手指刚刚触碰到屏幕的那一刻也行。

view 事件分发总结

现在我们已经看完了view的整个事件分发的流程源码,重要方法也差不多了解了,那么现在我们来归纳总结一下。

大概总结一下就是:

  1. 事件分发最开始是在dispatchTouchEvent这里,这个方法主要是将触摸事件传给onTouchonTouchEvent
  2. onTouch是一个接口的方法,所以我们可以通过setOnTouchListener来自主实现onTouch里面的内容。
  3. 通过控制onTouch方法的返回值,我们可以决定是否拦截系统实现的onTouchEvent方法。
  4. onTouchEvent方法里面的触摸行为分为很多种,比如手指下按上抬什么的,当手指上抬的时候,onTouchEvent里面会执行点击操作。
  5. 当我们在自定义view的时候,重写onTouchEvent时,如果onTouchEvent的返回值设为false,将不会执行点击操作,不过既然都在重写onTouchEvent了,内部你要怎么实现你的点击事件都可以= =

所以,View的事件分发可以这样说,主要方法:

dispatchTouchEvent
onTouch
onTouchEvent
onClick

顺序就是这样一个顺序,上面的方法可以拦截下面的方法,这里的拦截是指不让下面的方法运行。不过我们主要了解的还是后面3个方法。

ViewGroup

说完了view的事件后,我们来谈谈ViewGroup的触摸事件,ViewGroup的触摸事件跟View的触摸事件大体上都差不多,只是有一个地方不一样,举个例子?
假设我们现在写了这样两个类:

public class TouchViewGroup extends LinearLayout {

    private static final String TAG = "TouchViewGroup";

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

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouch: " + event.getAction());
                return false;
            }
        });

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

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: " + event.getAction());
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent: " + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
}
public class TouchView extends View {

    private static final String TAG = "TouchView";

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

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "onTouch: " + event.getAction());
                return false;
            }
        });

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: " + event.getAction());
        return super.onTouchEvent(event);
    }

}

两个类,一个是ViewGroup,一个是View,就打印下日志,其他啥也没有了。将这个View放进ViewGroup中,我们运算一下试试。打印结果是:

TouchViewGroup:   dispatchTouchEvent: 0
TouchView:        onTouch: 0
TouchView:        onTouchEvent: 0
TouchViewGroup:   dispatchTouchEvent: 1
TouchView:        onTouch: 1
TouchView: 	onTouchEvent: 1
TouchView:        onClick:

有疑问吗??️
View倒是没有啥问题,但是这个ViewGroup就。。。
为啥ViewGroup没有调用onTouchonTouchEventonClick这三个方法,根据view的事件分发机制,我们可以猜测肯定是ViewGroup里面某个方法把onTouchonTouchEventonClick这三个方法给拦截了,既然ViewGroup的dispatchTouchEvent打印出来了,其他的方法却没有打印出来,肯定是dispatchTouchEvent里面做了什么有拦截性质的操作,让我们在源码较少的API 15里面去寻找答案。

public boolean dispatchTouchEvent(MotionEvent ev) {
	...
	// Handle an initial down.
	...
	// Check for interception.
	...
	// Check for cancelation.
	...
	// Update list of touch targets for pointer down, if needed.
	...
}

看源码也不知道是哪里,算了看注释吧,突然发现注释里面有一个注释是Check for interception,拦截检查?听名字靠谱!认真瞧瞧:

public boolean dispatchTouchEvent(MotionEvent ev) {
	...
			// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
	...
}

代码还是有多又看不懂,不过这个intercepted变量肯定是关键,然后看到了在哪里赋值后,这个方法在我眼中已经变成如下模样了:

public boolean dispatchTouchEvent(MotionEvent ev) {
	...
	// Check for interception.
	final boolean intercepted;
	intercepted = onInterceptTouchEvent(ev);
	...
}

onInterceptTouchEvent这个方法翻译过来“拦截触摸事件”,还有返回值?哇,跟View的那些触摸事件很像啊,这个返回值肯定就是控制拦截的,不管三七二十一,我们先看看这个方法的源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
}

是我眼花了吗,还能有这么简单的源码?就返回一个false,根据我们对View的了解,这里返回false,应该是没有拦截才对啊,等等!我们先做个实验。在自定义的ViewGroup里面,重写onInterceptTouchEvent方法,直接返回true,看看效果,用实践出真理。

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

然后运行一下再看看:

TouchViewGroup:   dispatchTouchEvent: 0
TouchViewGroup:   onInterceptTouchEvent: 0
TouchViewGroup:   onTouch: 0
TouchViewGroup:   onTouchEvent: 0
TouchViewGroup:   dispatchTouchEvent: 1
TouchViewGroup:   onTouch: 1
TouchViewGroup:   onTouchEvent: 1
TouchViewGroup:   onClick:

哇,全是ViewGroup的东西,原来onInterceptTouchEvent拦截的是View里面的触摸事件啊!

所以这里的开关就是这个onInterceptTouchEvent的返回值,如果是false就走View的事件,如果是true,就走ViewGroup的事件。

既然View跟ViewGroup的事件分发机制都摸清楚了,那么我们就来总结一下吧!

总结

View 事件分发

首先说说View的事件分发机制,虽然前面已经总结过一次了,不过在这里再总结一次。

dispatchTouchEvent(MotionEvent ev)负责处理MotionEvent这些触摸事件,然后按照顺序,这里有3个方法:

onTouch				setOnTouchListener(xxx)
onTouchEvent		系统实现,或者自定义view的时候,自己实现
onClick				setOnClickListener(xxx)

用动画的方式看怎么样?
图1

正常情况下,程序就跟着这个顺序执行下去了:
图2

我们可以使用setOnTouchListener来实现onTouch,然后可以通过控制onTouch的返回值,来决定是否继续执行下面的两个方法,返回值为true,则不继续执行,为false,则继续执行。像这样?
图3
为false,我就不做图了,跟图2类似。

这里面三个方法,前面执行的方法有决策权,可以决定是否执行他之后的方法,返回值为true,则不执行之后的方法,为false则执行之后的方法。

ViewGroup事件分发

老实说,ViewGroup的事件分发机制跟View基本一样,毕竟ViewGroup继承View嘛。跟事件有关的那几个方法也是一样的,都是:

onTouch				
onTouchEvent		
onClick				

不过如果执行了ViewGroup默认执行View的这三个方法,不会执行ViewGroup的这三个方法,如果想要执行ViewGroup的这三个方法,我们必须修改ViewGroup的onInterceptTouchEvent方法的返回值,为true则可以执行ViewGroup的触摸事件,为false则执行View的触摸事件。

后话

关于Android View的事件分发机制大概就是这些,如果想要了解更多有关事件分发的东西,还是推荐看Android源码,遇到不清楚的地方,大家可以在网上找找相关博客,应该就能解惑了。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值