一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Android的View事件传递及传递问题 事件传递机制

时间:2015-08-11 编辑:简简单单 来源:一聚教程网

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 不处理事件流程图

20150811162712052.jpeg


(2) View 处理事件流程图
20150811162712052.jpeg


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。

热门栏目