view事件的分发机制:view的核心知识、view的难点、view的另一大难点滑动冲突解决的理论基础。
安卓的view层级: 其实我们平时在xml中写的view都是通过activity的setContentView被一步步加载到window上的,事件的产生也是首先从activity按照层级往下传递,一直到我们CustomView的最内层布局view,中间传递过程有着事件分发处理。
ps:图片来源网络
MotionEvent这个类中对事件进行了封装,我们点击、触摸滑动屏幕所产生的事件都被封装到这个类中。
我们平时操作手机(点击、触摸滑动)是无非就是这三种事件其中的组合:
1、点击事件:ACTION_DOWN -> ACTION_UP
2、触摸滑动事件:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
当MotionEvent产生后,系统需要把这个具体的事件传递给具体的view这个过程就是分发过程。点击事件的分发过程由三个重要的方法共同完成。
1、public boolean dispatchTouchEvent(MotionEvent event)
2、public boolean onInterceptTouchEvent(MotionEvent ev)
3、public boolean onTouchEvent(MotionEvent ev)
dispatchTouchEvent
作用:用来进行事件的分发。
说明:每个View都有此方法,MotionEvent传递到哪个view时,哪个view的dispatchTouchEvent就会被调用。
返回值:true表示消费事件(此view或者子view消耗,拦截事件此view消耗,不拦截传递给子view消耗),事件终止。false表示此view以及子view均不消费事件,将调用父容器的onTouchEvent方法,事件回传。返回值受当前view的onTouchEvent方法和下级view(子view)的dispatchTouchEvent的返回值影响。(参看下文伪代码理解)
ps:如果传递一圈回传回去都没有消费事件,事件由activity消费。
onInterceptTouchEvent
特别说明:此方法为View容器所有,view没有此方法。
作用:事件拦截 。
说明:这个方法在dispatchTouchEvent中被调用,当一个MotionEvent传递到此view容器时,首先调用此view容器的dispatchTouchEvent,在dispatchTouchEvent方法中调用onInterceptTouchEvent方法判断是否拦截事件。
返回值:true拦截事件传递,事件不在向下分发,开始调用本view容器的onTouchEvent进行消费事件。
false不拦截事件传递,事件开始向下传递,事件分发到子view容器的dispatchTouchEvent中。
onTouchEvent
作用:对事件进行消费。
说明:这个方法在dispatchTouchEvent进行调用。
返回值:true表示事件被消费,本次的事件终止。false表示事件没有被消费,将调用父View的onTouchEvent方法。
伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;//事件是否被消费
if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
}else{
consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
}
return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
}
onTouchEvent(MotionEvent ev){
if(消费){
判断,进行 消费处理
}else{// 不消费时
super.onTouchEvent(ev)
}
}
1、可以一目了然看到:onInterceptTouchEvent,onTouchEvent都是在dispatchTouchEvent中被调用。
2、这个返回值也验证了返回值受本view的onTouchEvent,或者子view的dispatchTouchEvent影响。
3、注意这个伪代码是viewGroup的。view收到事件则走onTouchEvent判断是否消费。
4、view容器有dispatchTouchEvent 、onInterceptTouchEvent、onTouchEvent,view只有dispatchTouchEvent 、onTouchEvent。
通过结构图我们知道viewGroup可以向下分发,或者不分发事件。
逻辑流程(图片来自网络)
(1)同一个事件序列是指从手指触摸屏幕那一刻起,到手指离开屏幕那一刻结束,这一过程所产生的事件。这个事件以down开始,中间有不确定的move,以up结束。
(2)正常情况下一个事件序列只能被一个view拦截消耗。因为一旦一个元素拦截此事件,那么这个事件序列就会直接交给他处理,因此同一事件序列不能由两个view处理。通过特殊手段可以做到比如一个view将本该自己处理的onTouchEvent强行转给其他view。
(3)某个view一旦处理事件,如果他不消耗action down事件(onTouchEvent返回false),同一事件序列中的其他事件都不会交给他处理。事件交个他的父控件处理,父元素的onTouchEvent被调用。
(4)如果view不消耗除action down 以外的其他事件,那么点击事件会消失,父元素的onTouchEvent并不会被调用,且当前view可以持续受到后续事件,最终这些消失的点击事件会传递给activity
(5)viewGroup默认不拦截任何事件,viewGroup源码中onInterceptTouchEvent默认返回false
(6)view没有onInterceptTouchEvent,一旦有时间传递给他便走onTouchEvent方法
(7)view的onTouchEvent方法默认消耗事件返回true,除非他是不可点击的(clickable和longclickable同时为false),view的longclickable默认为false,clickable分情况,比如Button默认为true,Textview的默认为false。故当点击事件在view的范围内时Button消耗事件,Textview不消耗。
(8)view的enable不影响OnTouchevent的默认返回值
(9)onClick点击事件可以发生的前提是view可点击,且他收到down和up事件。
(10)事件的传递过程是由外向内的,事件总是先传递给父元素,然后由父元素分发给子view,通过子view的requestDisallowInterceptTouchEvent可以在子view元素中干预父元素的分发过程,但是down事件除外。
MotionEvent的产生,事件最先传递个activity的,由activity的dispatchTouchEvent进行分发,具体的分发工作是由activity内部的window来完成的。window会将事件传递个decorrview ,decorrview 一般就是我们setContentView所设置view的父容器。通过activity的 getWindow().getDecorView()可以获得decorview。
接下来从activity的dispatchTouchEvent开始分析
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 事件交付给了window处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
事件首先交个activity所属的window进分发,如果返回true整个事件循环就结束,返回false意味着事件没人处理,所有view的onTouchEvent都返回了false,那么activity自己消费,调用onTouchEvent。
上面我们知道activity吧事件交个了window进行分发所以我们跟踪代码进入:superDispatchTouchEvent(ev)
/**
* The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*唯一的实现类是:PhoneWindow
*/
public abstract class Window
.....
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
.....
进入方法我们才知道这个方法是个抽象方法,一看window就是个抽象类,所以我们只能看他的实现类了,他的唯一实现类:PhoneWindow(源码注释有说明)所以我们就看PhoneWindow的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
通过源码轻松看出:PhoneWindow将事件分发交给了mDecor(DecorView)
1、我们可以通过: ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);获得setContentView设置的view,这个view就是mDecor(DecorView)的子view。
2、通过activity的 getWindow().getDecorView()可以获得DecorView。
事件传递给DecorView后会传递给他的子view,也就是我们setContentView所设置的view,一般来说这个view都是容器(viewgroup)类型(参看我们平时写的xml,最外层是个容器),这时事件便在容器中开始传递了。
viewGroup的dispatchTouchEvent(MotionEvent ev)局部代码:
拦截事件时:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();//清除FLAG_DISALLOW_INTERCEPT设置,并且mFirstTouchTarget 设置为null
}
// Check for interception.
final boolean intercepted;//默认不拦截
// action down 和 mFirstTouchTarget != null这两种情况下才判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT是子类通过requestDisallowInterceptTouchEvent方法进行设置的
// 判断是否子类干涉
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//调用onInterceptTouchEvent方法判断是否需要拦截
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;
}
从源码可以看出viewGroup在MotionEvent.ACTION_DOWN或者mFirstTouchTarget != null时这两种情况下会判断是否要拦截事件。
ACTION_DOWN还好理解,mFirstTouchTarget != null舍意思呢?
当viewGroup不拦截事件,将事件交给子元素时mFirstTouchTarget 就指向子元素,mFirstTouchTarget 就不为空,如果viewGroup拦截事件mFirstTouchTarget 就为空了。
1、我们前面说过子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外)
为什么ACTION_DOWN除外?通过上述代码我们不难发现。如果事件是ACTION_DOWN,那么ViewGroup会重置FLAG_DISALLOW_INTERCEPT标志位并且将mFirstTouchTarget 设置为null。
2、当事件为ACTION_DOWN 或者 mFirstTouchTarget !=null(即事件由子View处理)时会进行拦截判断。具体规则是如果子View设置了FLAG_DISALLOW_INTERCEPT标志位,那么intercepted =false。否则调用onInterceptTouchEvent方法。
3、如果事件不为ACTION_DOWN 且事件为ViewGroup本身处理(即mFirstTouchTarget ==null)那么intercepted =false,很显然事件已经交给自己处理根本没必要再调用onInterceptTouchEvent去判断是否拦截。
小结:
当ViewGroup决定拦截事件后,后续事件将默认交给它处理并且不会再调用onInterceptTouchEvent方法来判断是否拦截。子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。
所以我们知道了onInterceptTouchEvent并非每次都会被调用。如果要处理所有的点击事件那么需要选择dispatchTouchEvent方法
而FLAG_DISALLOW_INTERCEPT标志位可以帮助我们去有效的处理滑动冲突
viewGroup不拦截事件时:
final View[] children = mChildren;
//对子View进行遍历
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
//如果上述两个条件有一项不满足则continue继续循环下一个View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
//如果有子View处理即newTouchTarget 不为null则跳出循环。
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent第三个参数child这里不为null
//实际调用的是child的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//子View处理了事件,然后就跳出了for循环
break;
}
}
}
viewGroup不拦截时事件传递给子view:
首先遍历viewgroup的所有子元素,判断view是否能够接收击事件。
两个因素:
1、View可见并且没有播放动画。
2、点击事件的坐标落在View的范围内
这两个有一个不满足就继续遍历其他view
可以看到dispatchTransformedTouchEvent实际调用的是child的dispatchTouchEvent方法,
进入dispatchTransformedTouchEvent会发现如下代码,完成了分发过程。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
ViewGroup会遍历所有子View去寻找能够处理点击事件的子View(可见,没有播放动画,点击事件坐标落在子View内部)最终调用子View的dispatchTouchEvent方法处理事件
当子View处理了事件则mFirstTouchTarget 被赋值,并终止子View的遍历。
如果ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件(本质调用View的dispatchTouchEvent去处理)
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
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;
// 如果设置了OnTouchListener,且onTouch返回true ( li.mOnTouchListener.onTouch(this, event))
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result为false调用自己的onTouchEvent方法处理
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;
}
1、上面代码我们可以看到view的dispatchTouchEvent中View会先判断是否设置了OnTouchListener,如果设置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不会被调用。
2、没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false则会调用View自己的onTouchEvent方法。接下来看onTouchEvent方法:
class View:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//1.如果View是设置成不可用的(DISABLED)仍然会消费点击事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
...
//2.CLICKABLE 和LONG_CLICKABLE只要有一个为true就消费这个事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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)) {
//3.在ACTION_UP方法发生时会触发performClick()方法
performClickInternal();//点击进入就是performClick()
}
}
}
...
break;
}
...
return true;
}
return false;
}
1、可以看出即便View是disabled状态,依然不会影响事件的消费,只是它看起来不可用。
2、只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件,就是onTouchEvent返回true。这点也印证了我们前面说的View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。
3、在ACTION_UP方法发生时会触发performClick()方法(如下代码)
performClick:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
// NOTE: other methods on View should not call this method directly, but performClickInternal()
// instead, to guarantee that the autofill manager is notified when necessary (as subclasses
// could extend this method without calling super.performClick()).
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
如果View设置了OnClickListener,那么会回调onClick方法。
1、View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。
2、注意这是默认情况,我们可以单独给View设置clickable属性,但有时候会发现View的setClickable方法失效了。(设置了clickable为false还是可以点击)
3、假如我们想让View默认不可点击,将View的clickable设置成false,我们又给View设置了OnClickListener点击事件,那么你会发现View默认依然可以点击,也就是说setClickable失效了。
原因:
View的setOnClickListener会默认将View的clickable设置成true。
View的setOnLongClickListener同样会将View的longClickable设置成true。
参看如下源码解释:
参看源码:
class View:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
看着书们参考着文章终于总结了一遍,在事件分发机制中感觉viewGroup那块最难,既然总结了一遍就加深了印象,以后多看看慢慢消化
下一篇:
View的事件体系(四)view滑动冲突
参考文章:
一文读懂Android View事件分发机制
ActionMasked
本文来自<安卓开发艺术探索>笔记总结