Android的View 事件传递
1、基础知识
(1) 所有 Touch 事件都被封装成了 MotionEvent 对象,包括 Touch 的位置、时间、历史记录以及第几个手指(多指触摸)等。
(2) 事件类型分为 ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每个事件都是以 ACTION_DOWN 开始 ACTION_UP 结束。
(3) 对事件的处理包括三类,分别为传递——dispatchTouchEvent()函数、拦截——onInterceptTouchEvent()函数、消费——onTouchEvent()函数和 OnTouchListener
2、传递流程
(1) 事件从 Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的 View(ViewGroup)开始一直往下(子 View)传递。子 View 可以通过 onTouchEvent()对事件进行处理。
(2) 事件由父 View(ViewGroup)传递给子 View,ViewGroup 可以通过 onInterceptTouchEvent()对事件做拦截,停止其往下传递。
(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子 View 没有消费事件,事件会反向往上传递,这时父 View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到 Activity 的 onTouchEvent()函数。
(4) 如果 View 没有对 ACTION_DOWN 进行消费,之后的其他事件不会传递过来。
(5) OnTouchListener 优先于 onTouchEvent()对事件进行消费。
上面的消费即表示相应函数返回值为 true。
附上两张原文中流程图:
(1) View 不处理事件流程图
(2) View 处理事件流程图
3、最后说几句
Android Touch事件
假设布局层次为
Layout0
Layout1
Layout2
Layout3
如果谁都没有去interceptTouch,同时谁都没有处理onTouch事件。
那么Layout0->intercept Layout1->intercept Layout2->intercept Layout3->intercept
Layout3->onTouch Layout2->onTouch Layout1->onTouch Layout0->onTouch
由于谁都没有消费ACTION_DOWN事件,后续的MOVE,UP事件将不会传进来。
如果Layout2 intercept了,但是不消费onTouch
那么Layout0->intercept Layout1->intercept Layout2->intercept
Layout2->onTouch Layout1->onTouch Layout0->onTouch
后续事件不会传入
如果Layout2 intercept了,同时消费了。
那么 0->intercept 1->intercept 2->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
如果Layout2 intercept了,不消费,Layout1消费了。
那么0->intercept 1->intercept 2->intercept
2->onTouch 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch
总结一下。规律就是
如果当前Layout intercept了,那么子View和子ViewGroup都没有机会去获得Touch事件了。如果当前Layout并不消费事件的话,这个事件会一直向上冒泡,直到某个父Layout的onTouchEvent消费了这个事件。如果没有任何一个父Layout消费这个事件,那么后续的事件都不会被接受。
如果在冒泡过程中有某个Layout消费了这个事件。那么这个Layout的所有父Layout的intercept仍然会被调用。但是当前Layout的intercept不会再被调用了。直接调用onTouch事件。
另外,对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action。在实践过程中发现ListView在滚动的时候会调用这个方法。使得action不能被拦截。
android上view touch事件的传递问题
项目中要实现拖拽功能,即在scrollview里面放的imageview长按后,有影子被拖走的感觉。实现思路基本是这样的:
1、在布局文件里把imageview放到scrollview中
2、为imageview注册touch监听
3、重写scrollview的onTouchEvent函数。
4、创建由一张imgeview生成的popupwindow
5、通过touch的move更新所创建的popupwindow的位置。实现imageview上的图片被拖走的感觉。
开始时,由imageview响应touch事件,但随着手移出imageview,touch事件就不一定会还是由imageview接收了!那touch事件该传给谁呢?imageview上的touch事件是从哪步溜走了呢?经过尝试发现是经过imageview的cancel事件之后,后面的touch事件都交给了其父类操作,这里就是scrollview。touch事件从imageview消失后直接将后续的move事件和up事件交由了scrollview。我们要实现拖走imageview上图片的效果就可以通过创建popupwindow的方式来实现,所创建的popupwindow只有一张图片组成。通过更新popupwindow显示的位置就可以实现拖拽imageview上图片的效果了。
Android的事件传递机制
Android的事件传递机制分为按键事件和触摸事件,而这里的事件指的是touchevent,即触摸事件。
一个touchevent一般是由多个motionevent(有DOWN,UP,MOVE,CANCEL四种)构成,合理的分配这些motionevent到达指定的控件,这些控件才能够接收到相应的touchevent,然后做出处理。关于motionevent请参考我转的另一篇博文。
一.相关类和方法
1.与触摸事件有关系的类是view,viewgroup,activity。
1)这里view我们表示的是那些继承自view不能再容纳其他控件的类,比如textview,imageview。其中下面两个方法是三者都有的,且与touchevent相关的。
2)这里的viewgroup表示的是那些继承自viewgroup的类,它们的共同点是可以继续包含view。比如各种layout以及上面说到的恶心的listview。
3)这里的activity表示的就是那些继承自activity的类。
所以下面没有特殊描述,均用一个类代表它们整个群体。
2.与触摸事件有关系的方法是dispatchTouchEvent,onTouchEvent以及onInterceptTouchEvent,
public boolean dispatchTouchEvent(MotionEvent event) - 用于事件分发,三个类都有该方法
public boolean onTouchEvent(MotionEvent event) - 用于事件消费,三个类都有该方法
public boolean onInterceptTouchEvent(MotionEvent ev) - 用于拦截事件,只有viewgroup有该方法
这三个方法在三个类中的用途是一样的,但是详细的处理过程却不同。这我们将在下一部分去说明。
二.motionevent的dispatchTouchEvent流程
1.Activity部分
对于正常的理解来说,应该是activity拿到某一个motionevent,然后开始事件分发,所以我们来看看activity的dispatchTouchEvent源码
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
1)Activity处理事件
对于一个手机程序来说,最先拿到手势的我觉得应该就是window了,所以首先用superDispatchTouchEvent分配motionevent到activity的内部控件,superDispatchTouchEvent实际做的事情就是FrameLayout.dispatchTouchEvent(处理流程如同下文的ViewGroup.dispatchTouchEvent),它将会去查找有没有view可以处理该事件。如果activity没有内部控件或者内部控件无法处理该motionevent时,superDispatchTouchEvent返回false,然后接收该motionevent的就是activity本身了,我们可以在Activity的onTouchEvent方法里做详细的处理。
2)Activity不处理事件
刚才1)中说到,Activity没有内部控件或者内部控件无法处理该motionevent时superDispatchTouchEvent会返回false,但是如果有内部控件切可以处理该motionevent时,将返回true,这时Activity的dispatchTouchEvent也会返回true告知系统我有一个控件接收了motionevent。
2.ViewGroup部分
superDispatchTouchEvent将事件进行分发,首先接到的当然是该Activity的Layout控件,它继承自ViewGroup。当它接收到了之后显然也要先考虑事件的分发。我们来看看ViewGroup的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; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { 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 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; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); 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. mMotionTarget = child; 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 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; 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 if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 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; } return target.dispatchTouchEvent(ev); }
1)ViewGroup被当成普通的View
整体代码的意思是说有一个motionevent传递过来了,ViewGroup首先看自己的内部View能不能处理或者说哪个View能够处理,判断的方法是看看该motionevent是不是DOWN,如果是则看看View是否可见,如果可见再看看焦点是不是在View的内部,如果在其内部,那么我们说找到了一个View可能处理该motionevent,将其命名为target(We know we want to dispatch the event down, find a child who can handle it, start with the front-most child.),如果说没有一个View可以处理(We don't have a target, this means we're handling the event as a regular view.),此时ViewGroup将被当做普通的View来处理这个motionevent,那么将调用 super.dispatchTouchEvent(ev)方法,这里就是View的dispatchTouchEvent了。return语句返回的就是super.dispatchTouchEvent(ev)
2)ViewGroup内部有View可以处理
如果说ViewGroup内部有View可以处理,假设为target,那么将调用target.dispatchTouchEvent方法。return语句返回target.dispatchTouchEvent(ev)
3)onInterceptTouchEvent
这里有一个非常特殊的方法,就是onInterceptTouchEvent了,它可以让ViewGroup对motionevent进行拦截,意思就是我们发现某个target可以获得DOWN的焦点,但是ViewGroup不想让它内部的View处理事件,则进行拦截,此时dispatchTouchEvent返回true。
3.View部分
View部分在相对就简单一些了,在上面的target.dispatchTouchEvent之后,motionevent被传递到了View的dispatchTouchEvent中,看到这里应该也就明白了motionevent也是类似的从Activity传递到ViewGroup中的,在superDispatchTouchEvent里Framelayout.dispatchTouchEvent找到了某个View(实际是某个Layout的ViewGroup),调用了它的dispatchTouchEvent。
看View源代码中的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
因为View表示没有任何其他内部的控件了,所以它只有两种选择,这里首先将使用我们开发人员定义的OnTouchListener进行处理,如果可以处理,那么将返回true,如果不能处理将使用默认的onTouchEvent来处理。
三.motionevent的onTouchEvent流程
最底层的View的dispatchTouchEvent会调用onTouchListener来进行处理motionevent,或者使用onTouchEvent来处理motionevent,不论哪种都默认会返回true。所以这时ViewGroup的dispatchTouchEvent返回值为true,所以Activity的dispatchTouchEvent的返回值是true。
如果我们没定义自己的onTouchListener,并且重写了onTouchEvent,返回一个false,那么ViewGroup的dispatchTouchEvent返回为false,Activity将会调用它的onTouchEvent方法。
四.后续的motionevent
如果motionevent为DOWN的时候View没有处理,即在它的dispatchTouchEvent内返回了false,那么该View的容器ViewGroup不会再调用该View的dispatchTouchEvent了,即它将无法接收到后续的MOVE,UP。只有DOWN的时候被View处理了(在dispatchTouchEvent返回true),后续的MOVE,UP才会传递到该View。
五.ListView的问题
想给ListView实现左右滑动翻页的功能,正常是想着使用ViewFillper,但是不用ViewFlipper动态改变adapter的内容再刷新ListView的话应该如何实现呢。有下面的想法
1.给ListView定义一个手势对象gestureDector,重写它的onTouchEvent,在里面使用return gestureDector.onTouchEvent。gestureDector的手势监听器默认在onDown的时候返回false,所以一个DOWN的motionevent传过来,onTouchEvent返回false,根据上面的说法,dispatchTouchEvent也将返回false,后续motionevent将不会再传递到ListView,失败。
2.重写手势监听器的onDown返回值为true,这时可以实现左右翻页,但是ListView本身的onItemClickListener将没办法正常工作。失败。
3.重写dispatchTouchEvent,调用super.dispatchTouchEvent,然后始终返回true。重写onTouchEvent,首先调用gestureDector.onTouchEvent,如果返回为false,说明gestureDector.onTouchEvent没有处理该事件,我们的左右滑动也没有触发,那么return super.onTouchEvent处理,包括它的onItemClickListener等等都可以正常运行。不管super.onTouchEvent返回何值,因为dispatchTouchEvent返回了true,所以后续的动作都会传来。如果返回为true,说明gestureDector.onTouchEvent处理了左右滑动事件(前提是在手势监听器里面fling动作返回了true),此时return true。成功。
4.重写dispatchTouchEvent,一开始就调用gestureDector.onTouchEvent,然后同样的处理方式,最终保证能够return true。
以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索view
, 控件
事件
view的事件传递机制、android 事件传递机制、android view事件传递、ios事件传递机制、事件传递机制,以便于您获取更多的相关知识。