android事件处理,最复杂的就是对Touch事件的处理,因为Touch事件包括:down, move, up, cancle和多点触摸等多种情况,多点触摸的情况先不讨论,因为Touch有这么多的状态,所以Touch相对来说是最难处理的,下面就来讨论一下android系统是如何处理Touch事件的
先来张参照图:
Paste_Image.png1.说到事件处理,首先我们要明白,为什么要处理事件,要了解android系统本身对事件的一个处理过程.在实际的开发中,我们如果都用系统的基本控件,那是不需要去处理事件的,但是如果我们用复杂的布局嵌套去做一些特殊的需求,例如:ScrollView中嵌套ListView,ScrollView嵌套ViewPager等,则会产生事件冲突,所以,由于事件冲突的存在,我们要去处理这些冲突,只有了解android的事件处理机制,才能有效的去处理事件冲突.还有就是如果我们要新开发一个组件,则组件的所有事件都要我们自己去做处理,这种情况也需要我们去处理事件.所以:由于存在以上说到的两种情况,我们要自己处理事件.
2.有了处理事件的动机后,接下来就要了解android系统本身是如何处理复杂的事件的.android系统为所有的事件提供了三个相关的方法,以下只以Touch事件为例说明.
这三个方法分别是:
dispatchTouchEvent(MotionEvent ev); (Activity, ViewGroup, View都有此方法)
onInterceptTouchEvent(MotionEvent ev); (只有ViewGroup有)
onTouchEvent(MotionEvent); (Activity, ViewGroup, View都有此方法)
要想了解android系统是如何对事件进行一步一步的处理,这三个方法是必须要掌握的.其中: dispatchTouchEvent(MotionEvent ev);方法是用来对事件进行分发的,即将事件分发到目标控件,onInterceptTouchEvent(MotionEvent ev)是用来过滤事件的,即进行事件的拦截,也就是是否要向下传递事件,onTouchEvent(MotionEvent ev)才是最终用来处理事件的,也就是说我们平常重写onTouchEvent时,其实,系统已经默认帮我们调用了前两个方法.下面就来详细分析一下三个方法.
首先要提的是,android系统对本件的处理是一层一层向下传递处理(树形处理).那这棵树是从那来的呢..就是我们的布局树,一个布局,无论是代码编写的布局还是xml生成的布局,android系统对它进行解析时都是将其组装成一棵UI树,最外层布局是整个UI树的根.知道这个以后,再来分析事件的处理.
处理流程:当我们的手指触摸到手机屏幕时,当前处于onStart()状态的Activity最先接收到此Touch事件下的ACTON_DOWN,然后开始调用它自己dispatchTouchEvent()开始进行DOWN事件分发,如果此方法返回true,则Activity不向下分发事件,则整个布局都不会收到DOWN事件,TouchEvent直接到Activity的onTouchEvent()方法进行事件处理.如果返回false,则表示DOWN是要被分发到下层的,此时DOWN事件被直接分发(因为没有过滤方法)到UI树的根布局(即最外层的布局),根布局拿到DOWN事件时,执行自己的dispatchTouchEvent方法,返回true,则事件直接交到根布局的onTouchEvent()中进行处理,false则表示还得向下分发,此时事件被传递到根布局的onInterceptTouchEvent()方法中,如果此方法返回true,表示要对此事件进行过滤,则此DOWN事件又直接进行到根布局的onTouchEvent()方法直接处理,false则,要根布局不对事件进行过滤,DOWN事件继续向下传递,直到达到目标组件后,目标组件调用自己的dispatchTouchEvent()方法,由于是目标组件,直接分发事件到自己的onTouchEvent方法中,目标组件如果处理完这个DOWN事件后返回true,表示该事件被消费完毕,不再向上层传递,如果返回false,则表示没有消费完这个DOWN事件,DOWN向上传递到自己的父组件中,父组件再进行DOWN事件的处理.一直向上传递直到事件被扔到虚拟机.DOWN事件才算处理完成,接着调用MOVE,MOVE完了UP,整个流程与DOWN是一樣的.
这里要强调一点的是:如果一个组件没有接收到DOWN事件,那么一定接收不到MOVE,UP事件。
通过以上的流程,我们可以明白:android系统对任何一个事件的处理都是这样的,分发事件,过滤事件,处理事件,下一个事件, 分发事件,过滤事件,处理事件……一直这样循环去处理所有的事件的。即:事件的分发,过滤是从根到叶的,处理则是从叶再到根的
Paste_Image.png从图上看,我们可以更直观的感受整个Touch事件的处理流。
总结:onInterceptTouchEvent负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent返回true的那个view进行处理。
看到这里如果你还是有点困惑,请想象一下生活中常见的场景:假如你所在的公司,有一个总经理,级别最高,它下面有个部长,级别次之,最底层就是干活的你,没有级别。现在总经理有一个任务,总经理将这个业务布置给部长,部长又把任务安排给你,当你完成这个任务时,就把任务反馈给部长,部长觉得这个任务完成的不错,于是就签了他的名字反馈给总经理,总经理看了也觉得不错,就也签了名字交给董事会,这样,一个任务就顺利完成了。这其实就是一个典型的事件拦截机制。
在这里我们先定义三个类:
一个总经理—MyViewGroupA,最外层的ViewGroup
一个部长—MyViewGroupB,中间的ViewGroup
一个你—MyView,在最底层
根据以上的场景,我们可以绘制以下流程图:
Paste_Image.png从图中,我们可以看到在ViewGroup中,比View多了一个方法—onInterceptTouchEvent()方法,这个是干嘛用的呢,是用来进行事件拦截的,如果被拦截,事件就不会往下传递了,不拦截则继续。
如果我们稍微改动下,如果总经理(MyViewGroupA)发现这个任务太简单,觉得自己就可以完成,完全没必要再找下属,因此MyViewGroupA就使用了onInterceptTouchEvent()方法把事件给拦截了,此时流程图:
Paste_Image.png我们可以看到,事件就传递到MyVewGroupA这里就不继续传递下去了,就直接返回。
如果我们再改动下,总经理(MyViewGroupA)委托给部长(MyViewGroupB),部长觉得自己就可以完成,完全没必要再找下属,因此MyViewGroupB就使用了onInterceptTouchEvent()方法把事件给拦截了,此时流程图:
Paste_Image.png我们可以看到,MyViewGroupB拦截后,就不继续传递了,同理如果,到干货的我们上(MyView),也直接返回True的话,事件也是不会继续传递的,如图:
Paste_Image.png源码
分析Android View事件传递机制之前有必要先看下源码的一些关系,如下是几个继承关系图:
Paste_Image.png Paste_Image.png看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。
从View的dispatchTouchEvent方法说起
在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中),所以我们先来看下View的dispatchTouchEvent方法,如下:
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)) {
//noinspection SimplifiableIfStatement
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;
}
}
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;
}```
dispatchTouchEvent的代码有点长,但可以挑几个重点讲讲,if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住等,然后定义ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句就是重点,首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}```
li.mOnTouchListener是不是null取决于控件(View)是否设置setOnTouchListener监听,在上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null,接着通过位与运算确定控件(View)是不是ENABLED 的,默认控件都是ENABLED 的,接着判断onTouch的返回值是不是true。通过如上判断之后如果都为true则设置默认为false的result为true,那么接下来的if (!result && onTouchEvent(event))就不会执行,最终dispatchTouchEvent也会返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句有一个为false则if (!result && onTouchEvent(event))就会执行,如果onTouchEvent(event)返回false则dispatchTouchEvent返回false,否则返回true。
这下再看前面的实例部分明白了吧?控件触摸就会调运dispatchTouchEvent方法,而在dispatchTouchEvent中先执行的是onTouch方法,所以验证了实例结论总结中的onTouch优先于onClick执行道理。如果控件是ENABLE且在onTouch方法里返回了true则dispatchTouchEvent方法也返回true,不会再继续往下执行;反之,onTouch返回false则会继续向下执行onTouchEvent方法,且dispatchTouchEvent的返回值与onTouchEvent返回值相同
dispatchTouchEvent总结 :
在View的触摸屏传递机制中通过分析dispatchTouchEvent方法源码我们会得出如下基本结论:
触摸控件(View)首先执行dispatchTouchEvent方法。
在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。
onTouchEvent方法 :
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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) {
// 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) {
// 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)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}```
首先地6到14行可以看出,如果控件(View)是disenable状态,同时是可以clickable的则onTouchEvent直接消费事件返回true,反之如果控件(View)是disenable状态,同时是disclickable的则onTouchEvent直接false。多说一句,关于控件的enable或者clickable属性可以通过java或者xml直接设置,每个view都有这些属性。
接着22行可以看见,如果一个控件是enable且disclickable则onTouchEvent直接返回false了;反之,如果一个控件是enable且clickable则继续进入过于一个event的switch判断中,然后最终onTouchEvent都返回了true。switch的ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,接着到手抬起来ACTION_UP时你会发现,首先判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。具体如下:
public boolean performClick() {
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);
return result;
}```
这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。
onTouchEvent小结 :
onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听。
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action。
通过以上总结,Android中的事件拦截机制,其实跟我们生活中的上下级委托任务很像,领导可以处理掉,也可以下发给下属员工处理,如果员工处理的好,领导才敢给你下发任务,如果你处理不好,则领导也不敢把任务交给你,这就像在中途把下发的任务的中途拦截掉了。通过流程和源码的分析,相信大家能比较容易了解事件的分发、拦截、处理事件的流程。学习过程中,保持好奇心是很重要的。
网友评论