VIew的事件包括什么
View的事件其实指的就是MotionEvent,也就是我们对屏幕的点击,滑动,抬起等一系的动作,它有以下四种事件类型
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指从屏幕上松开的瞬间
- ACTION_CANCEL:事件被上层拦截时触发
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,主要有以下两种情况:
- 点击屏幕后松开,事件序列为:ACTION_DOWN -> ACTION_UP
- 点击屏幕滑动一会再松开,事件序列为:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
我们可以编写如下代码用于观察
public class MotionEventActivity extends AppCompatActivity {
private static final String TAG = "MotionEvent";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_motion_event = findViewById(R.id.btn_motion_event);
btn_motion_event.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "MotionEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "MotionEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "MotionEvent: ACTION_UP");
break;
}
return false;
}
});
btn_motion_event.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "onClick: button被点击");
}
});
}
}
那么,ACTION_UP一定是一个事件的结束吗?其实不一定。经过上面代码的测试,如果我们意外结束了点击事件,比如按下菜单键主页键或者锁屏,都不会有ACTION_UP。
事件分发规则
三个事件分发方法
当一个MotionEvent产生了以后,系统需要把这一个事件传递给一个具体的View,这个传递的过程就是分发的过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent()、 onInterceptTouchEvent()和onTouchEvent(),下面我们一一介绍。
- public boolean dispatchTouchEvent(MotionEvent event)
用来进行事件的分发,如果事件能够传递给当前View,那么dispatchTouchEvent方法一定会被调用。 返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法 - public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent()方法内部调用,用来判断是否拦截某个事件,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有拦截方法 返回值:是否拦截当前事件,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。 - public boolean onTouchEvent(MotionEvent ev)
在dispatchTouchEvent进行调用,用来处理点击事件 返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法。同时在同一个事件序列中,当前View无法再次接收到事件。
上面的三个方法可以用以下的伪代码来表示其之间的关系:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
这里或许会有两个疑问:
- 为什么View会有dispatchTouchEvent方法?
- 为什么View没有onInterceptTouchEvent方法?
关于第一个问题, 一个View 可以注册很多监听器吧,例如单击,长按,触摸事件(onTouch),并且View 本身也有 onTouchEvent 方法,那么问题来了,这么多事件相关的方法应该由谁管理,所以View也会有dispatchTouchEvent这个方法。 至于第二个,我们先来看一张图理清View的结构 我们可以看到,ViewGroup下是可以包含ViewGroup和View的,而View不可以,也就是说,如果一个事件分发给了一个具体的View,它只需要选择处理还是不处理,不用进行拦截,因为它肯定不需要向下分发。
传递规则
对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时他的 dispatchTouEvent 就会调用,如果这个 ViewGroup 的 onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法会被调用,如果这个 ViewGroup 的onInterceptTouchEvent 方法返回false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的 子元素,接着子元素 dispatchTouEvent 方法就会被调用。如此反复直到事件最终被处理。 用一张事件分发流程图来说明一下: 当一个View需要处理事件时,如果它设置了 OnTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回false,则当前View 的onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给View设置 onTouchListener,其优先级比 onTouchEvent 还要高。在 onTouchEvent 方法中,如果当前设置有 onClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 onClickListener,其优先度最低,即处于事件传递的尾端.
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View ,事件总是先传递给Activity,Activity在传递给Window,最后Window在传递给顶级View,顶级View接受到事件后,就会按照事件分发机制去分发事件,这里我们在考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,如果所有元素都不处理这个事件,那么这个事件最终会交给Activity去处理,就是Activity的OnTouchEvent方法会被调用。
关于事件传递机制,我们可以总结出以下结论,根据这些结论能更好的理解整个传递机制:
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的的一系列时间,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
- 正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条原因可以参考(3),因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件 通过 onTouchEvent 强行传递给其他View处理。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的 onInterceptTouchEvent 去询问它是否要拦截了。
- 某个View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTOuchEvent 返回了 false ) ,那么同一事件序列中的其他事件都不会交给 它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序猿一件事,如果事情没有处理好,短期内上级就不敢再把事情交给程序猿去做了。二者道理差不多。
- 如果View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。Android 源码中ViewGroup 的 onInterceptTouchEvent 方法默认返回false.
- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
- View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false).View 的 longClickable 属性默认为 false,clickable 属性要分情况,比如Button 的clickable 属性默认为true,而 TextView 的 clickable 属性默认为 false.
- View 的 enable 属性不受影响 onTouchEvent 的 默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个 为 true,那么它的 onTouchEvent 就返回 true.
- onClick 会发生的前提是当前View 是可点击的,并且它收到了 down 和 up 的事件。
- 事件传递过程是由外而向的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
事件分发源码
Activity和Window的分发
当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作是由Activity内部的Window来完成的
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
现在分析上面的代码。首先事件开始交给 Activity所附属的 Window进行分发,如果返回true,整个事件循环就结束了,返回 false意味着事件没人处理,所有View的on Touchevent都返回了 false,那么 Activity的 on Touchevent就会被调用。
Window是一个抽象类,而Window的superDispatchTouchEvent方法是抽象方法,它的唯一实现类是PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow将事件直接传递给DecorView。 我们可以通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)即通过Activity来得到内部的View。这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。总之,现在事件传递给了顶级ViewGroup。
ViewGroup的分发
事件到达顶级ViewGroup后,会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果底层ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理。如果顶层ViewGroup不拦截事件,则事件会传递给它的在点击事件链上的子View,这个时候,子View的dispatchTouchEvent会被调用。如此循环,完成整个事件派发。 另外要说明的是,ViewGroup默认是不拦截点击事件的,其onInterceptTouchEvent返回false。
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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
从上面的代码可以看出,ViewGroup在如下两种情况下会判断是否拦截当前事件:事件类型为down或者mFirstTouchTarget != null。mFirstTouchTarget可以这样理解,当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget会被赋值也就是mFirstTouchTarget不为空。一旦事件由当前ViewGroup拦截,mFirstTouchTarget始终为空,当move和up事件到来,此ViewGroup就不会判断是否需要拦截,onInterceptTouchEvent()方法也不会调用,同一序列的其他事件都由它处理。 这里还有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过的requestDisallowInterceptTouchEvent这个方法来设置,一般在子View中。即使事件已经分发下去,子元素仍然可以调用父元素的requestDisallowInterceptTouchEvent方法来置位FLAG_DISALLOW_INTERCEPT标志位,从而从父元素判断是否拦截事件。但是down事件除外。因为ViewGroup在分发事件时,如果是down事件就会重置FLAG_DISALLOW_INTERCEPT这个标志位,将导致子View中设置的这个标志位无效。
接下来再看当ViewGroup不拦截事件时,事件向下分发的处理
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
首先遍历ViewGroup的所有子元素,然后判断子元素是否能够收到点击事件。是否能够收到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它处理,dispatchTransformedTouchEvent这个方法实际上就是调用子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,如下所示,由于上面传递child不是null,因此它会直接调用子元素的 dispatchTouchEvent方法,这样事件就交由子元素处理,从而完成了一轮事件分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent返回true,表示子元素已经处理完事件,那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
如果遍历完所有的子元素事件没有被合适处理,有两种情况,ViewGroup没有子元素;子元素处理了点击事件,但是dispatchTouchEvent返回false,一般是因为子元素在onTouchEvent返回了false。这时ViewGroup会自己处理点击事件。
View对点击事件的处理
先看View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
……
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
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;
}
}
……
return result;
}
View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。View首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会被调用,所以OnTouchListener的优先级要高于onTouchEvent,这样做的好处是方便子啊外界处理点击点击事件。
接下来再分析onTouchEvent方法
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
……
if (!mHasPerformedLongPress) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
……
}
break;
}
……
return true;
}
可以看出只要CLICKABLE和LONG_ CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true。当up事件发生时,会触发performClick方法,如果View设置OnClickListener,那么performClick就会调用它的onClick方法。 View的 LONG_ CLICKABLE属性默认为false,而CLICKABLE属性默认为true,不过具体的View的CLICKABLE又不一定,确切来说是可点击的View其CLICKABLE属性true,比如Botton,不可点击的View的CLICKABLE为false,比如TextView。。通过setClickable和setLongClickable可以设置这两个属性。另外setOnClickListener和setOnLongClickListener会自动将View的这两个属性设为true。
问题探索
- 当ViewGroup里有一个View,如果ViewGroup只在ACTION_MOVE拦截,说说各个action是如何分发的
先是down事件会经过 ViewGroup的dispatchTouchEvent,再到 ViewGroup的onInterceptTouchEvent,最后到View的dispatchTouchEvent,此时 mFirstTouchTarget不为空,紧接着到了move首先到 ViewGroup的dispatchTouchEvent,再到 ViewGroup的onInterceptTouchEvent,由于在move过程中拦截了,紧接着走view的 dispatchTouchEvent的 action_ cancel,此时接着把 mFirstTouchTarget置为nul,因此后续的move和up事件只会走ViewGroup的dispatchTouchEvent和onTouchEvent。
- View在onTouchEvent中消费,然后拖动手指从其他ViewGroup上挪开
事件已经在View的onTouchEvent中消费,所以mFirstTouchTarget不为空,因此不管手指移动到什么地方,其他事件都会在这个View中消费
- View的onTouch和onTouchEvent的区别
onTouch是setOnTouchListener中的回调方法,它优先于onTouchEvent。如果onTouch方法返回true,那么就不会触发onTouchEvent方法
- View的onClick方法是在什么时候触发,和onTouch有什么区别
onClick是在onTouchEvent消费事件中的ACTION_UP触发,onTouch是在dispatchTouchEvent中触发,所以onTouch要优先于onClick事件,我们可以通过onTouch返回true来屏蔽掉onClick方法。
|