https://blog.csdn.net/qq_44076155/article/details/121582575
前言
节选自《开发艺术》的事件分发章节,并结合https://blog.csdn.net/qq_44076155/article/details/121582575
做一定总结
问题
在给一个view增加可拖动功能时,IDE给出了黄色警告,如下
初步分析
这里使用了View.setOnTouchListener(OnTouchListener l),而OnTouchListener的实现如下
public interface OnTouchListener {
/**
* Called when a touch event is dispatched to a view. This allows listeners to
* get a chance to respond before the target view.
*
* @param v The view the touch event has been dispatched to.
* @param event The MotionEvent object containing full information about
* the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onTouch(View v, MotionEvent event);
}
而MotionEvent定义了多种输入事件,这里我们常用的几种
ACTION_DOWN | 屏幕上按下时触发 |
---|---|
ACTION_MOVE | 在屏幕上滑动时触发,滑动时会连续触发 |
ACTION_UP | 从屏幕抬起时触发 |
ACTION_CANCEL | 事件被(上层)拦截时触发 |
这里有几个重要方法或者函数需要注意
dispatchTouchEvent | 用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件 |
---|---|
onInterceptTouchEvent | 在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,返回结果表示是否拦截当前事件。如果当前View拦截了某个事件,则交给onTouchEvent继续处理。并且同一个事件序列当中,此方法不会被再次调用 |
onTouchEvent | 同样也会在dispatchTouchEvent内部调用,用来处理Touch事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件 |
mFirstTouchTarget | mFirstTouchTarget是一个TouchTarget对象,通过注释的说明:"触摸目标的链接列表中的第一个触摸目标" |
事件分发过程
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。
伪代码
// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// 父View决定是否拦截事件
if(onInterceptTouchEvent(event)){
// 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
// 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
// 该其他View。
consume = onTouchEvent(event);
}else{
// 调用子View的dispatchTouchEvent(event)方法继续分发事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}
结论
- 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以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,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外
事件消费流程
- 先说上面问题的原因,当事件分发到某个view时,会调用view.dispatchTouchEvent,当我们给view设置了onTouchListener时,会先用listener的onTouch,如果成功消费了此事件,则不会调用到onTouchEvent,而view的onClick方法是在onTouchEvent里面触发的,onTouchEvent都不走了,怎么会调到onClick呢,所以会有上面的警告
- 具体调用顺序 mOnTouchListener.onTouch > onTouchEvent > view.onClick
public boolean dispatchTouchEvent(MotionEvent event) {
...............省略
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)) {
// 如果它的touchListener不为空,先调用listener的onTouch
result = true;
}
if (!result && onTouchEvent(event)) {
// 上面未消费成功才会走到onTouchEvent
result = true;
}
}
...............省略
return result;
}
- 继续看view.onTouchEvent,省略无关代码,在ACTION_UP事件里面发现了performClickInternal()这个方法,就是通过这里调用到view的onClick
public boolean onTouchEvent(MotionEvent event) {
......................
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
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)) {
performClickInternal();
}
}
}
..........................
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
-----
break;
case MotionEvent.ACTION_CANCEL:
----
break;
case MotionEvent.ACTION_MOVE:
-----
break;
}
return true;
}
return false;
}
事件冲突解决
什么叫事件冲突:事件只有一个,多个对象想要处理或者处理的事件对象不是我们想给的对象
一般来说,事件冲突有两种解决方式,外部拦截和内部拦截,写法都是固定的,这里直接粘出来
- 外部拦截
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
isIntercept=false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要此事件){
isIntercept =true
}else{
isIntercept =false
}
break;
case MotionEvent.ACTION_UP:
isIntercept=false;
break;
}
return isIntercept; //返回true表示拦截,返回false表示不拦截
}
- 内部拦截
//子view的代码·
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if () { // 允许父View进行事件拦截,交给父View处理
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
//父viewgroup代码 (要确保down是不拦截,move和up时要拦截)
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
分析
外部拦截很简单,就是由父View来控制,想要事件的时候就调用onInterceptTouchEvent为true进行拦截,看下源码中是怎么进行拦截的
ViewGroup的onDispatchTouchEvent,disallowIntercept是通过子view调用requestDisallowIntercepteTouchEvent来进行控制的,true表示不允许父View拦截,我们这里子view未设置,默认允许拦截,然后通过onInterceptTouchEvent获取是否拦截,假设我们进行了拦截
.............. 省略若干代码
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 为true是,走到了下面的if条件中,mFirstTouchTarget代表的含义就是该事件序列是否已经分发给子view了,下面的if 表示从未分发给子View过,else表示该事件序列以及分发过,这里我们外部拦截,有分发给子View,走else,通过调用dispatchTransformedTouchEvent方法,会将mFirstTouchTarget清空,还会给View发送一个ACTION_CANCER,告诉他事件结束了,然后下一次的MOTION_EVENT来的时候mFirstTouchTarget此时为null,命中if,分发事件给自己
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 没有分发给子View过
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
内部拦截稍微复杂点,不仅要写父View还要些子View,我们上面的处理方式中,父view中ACTION_DOWN中是一定不能拦截的,因为一旦拦截了,事件就再也无法分发给子view了
// 父view中的写法
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
源码,还是这个判断,如果父view在ACTION_DOWN中拦截了,那么mFirstTouchTarget一定是为null的,因为没有子view可以分发下去,而事件序列中只有第一次是ACTION_DOWN,而且在ACTION_DOWN时disallowIntercept标记会被清空,也就是说子View设置的requestDisAllowInterceptTouchEvnet在ACTION_DOWN时不会生效 ,所以这里判断直接走到了else中,绕过了disallowIntercept这个参数的判断,就会导致内部拦截失效
if (actionMasked == MotionEvent.ACTION_DOWN) {
// ACTION_DOWN 事件,清空mFirstTouchTarget 和 disallowIntercept 标记
// 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();
}
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;
}
总结
父View可以抢子view的事件,通过onInterceptTouchEvent方法,而一旦父View拿到了事件,子View就无法再对父View进行任何的干预,毕竟事件是从父亲发下来的,现在父亲都不给你了,你还怎么还给父亲。换句话说,一旦事件交给了某个View进行处理,接来下的事件就不会到该View的子View去了
标签:分发,MotionEvent,AndroidView,onTouchEvent,事件,ACTION,view,View From: https://www.cnblogs.com/terrorists/p/18335008