View事件分发机制

View的事件分发机制

事件分发主要由三个方法共同完成:

public boolean dispatchTouchEvent(MotionEvent ev)

public boolean onInterceptTouchEvent(MotionEvent ev)

public boolean onTouchEvent(MotionEvent ev)

三个方法的关系:

<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
</pre>

对于一个根ViewGroup来说,点击事件产生后,首先会传递给他,这时他dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent的返回值为true表示它要拦截当前事件,事件就会交给这个ViewGroup的onTouchEvent处理,如果onInterceptTouchEvent的返回值为false表示不拦截当前事件,当前事件会继续传递给他的子元素,子元素的dispatchTouchEvent会被调用,如此反复直到事件被最终处理。

当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener中的OnTouchEvent会被调用,如果返回true,那么OnTouchEvent方法不会被调用,否则会被调用。如果当前设置的有OnclickListener,它的onClick方法在onTouchEvent里被调用。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,在传递给Window,最后Window传递给顶级的View,顶级的Viw接收到事件后按照事件分发的机制去分发事件。

事件分发的结论

  1. 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中产生的一些列的事件,这个事件以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列知只能被一个View拦截且消耗
  3. 一旦一个View拦截了某事件,那么同一个事件序列的所有事件都会直接交给他处理,因此同一个事件序列的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent 返回了false),那么同一事件序列中的其他事件都不会再交给他处理,会调用父元素的onTouchEvent
  5. 如果View不消费除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理 ??
  6. ViewGroup默认不拦截任何事件。Android源码中的ViewGroup的onInterceptTouchEvent默认返回false
  7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
  8. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 同时为false),clickable属性要分情况,比如Botton的clickable属性默认为true,而Textview的clickable属性默认为false。
  9. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  10. onClick发生的前提是当前View是可点击的,并且它收到了down和up事件。
  11. 事件传递过程是由外向内的,事件总是先传递给父元素,然后再由父元素传递给子元素。子元素可以通过requestDisallowInterceptTouchEvent方法干预父元素的分发过程。

事件分发的源码解析

Activity对事件的分发过程

当一个点击操作发生时,事件最先传递给Activity,由Activity的dispatchTouchEvent进行事件派发,具体工作由Activity内部的Window来完成。
<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
</pre>

事件交给Activity所属的Window进行分发,如果返回了true,整个事件循环就结束了,返回false意味事件没人处理,所有的View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。

getWindow().superDispatchTouchEvent(ev),这里的Window是SDK里window的唯一实现类PhoneWindow
<pre>
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
</pre>

这里的逻辑非常的清晰,PhoneWindow直接将事件传递给了DecorView,随后DecorView进行事件的分发。

ViewGroup对事件的分发过程

ViewGroup onInterceptTouchEvent返回true,事件由自己处理,如果ViewGroup setOnTouchListener则onTouch会被调用,否则onToucheEvent会被调用,简言之,如果两个都提供的话onTouch会屏蔽onTouchEvent,如果setOnClickListener了,在onTouchEvent中,onClick会被调用?这里作者说的有些问题,onTouch返回为true才会屏蔽。如果ViewGroup不拦截事件,则事件会传递给它所在的点击事件上的子view。
<pre>final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
这里调用onInterceptTouchEvent
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;
}
</pre>

上面的代码表示,ViewGroup在两种情况下会判断是否拦截当前事件,事件类型为ACTION_DOWN或者mFirstTouchTarget!=null; 当事件由ViewGroup的子view成功处理时,mFirstTouchTarget会被赋值并指向子view这段代码按照我的理解:事件开始时ACTION_DOWN,ViewGroup进入判断是否拦截,如果拦截mFirstTouchTarget就为null,然后event 为ACTION_MOVE,ViewGroup进入判断ACTION==DOWN false,mFirstTouchTarget!=null false,然后直接进入else intercepted = true,结果为onInterceptTouchEvent不会再被调用,并且同一事件序列里除ACTION_DOWN的事件都由ViewGroup处理。

FLAG_DISALLOW_INTERCEPT标记位,这个标记位通过requestDisallowInterceptTouchEvent方法设置,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup无法拦截除了ACTION_DOWN其他的点击事件。因为在ViewGroup分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位。

ViewGroup不拦截事件时,事件会向下分发给它的子View,首先遍历ViewGroup的所有子元素,然后判断子元素是否能狗接收到点击事件。是否接收到点击事件由两点衡量:子元素是否在播放动画,点击事件的坐标是否在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会交给他处理,这时会调用子View的dispatchTouchEvent。如果子元素的dispatchTouchevent返回true那么mFirstTouchTarget就会被赋值同时跳出for循环。

遍历所有的子元素事件没有被合适地处理到,有两种情况:1.ViewGroup没有子元素 2.子元素处理了事件但是在dispatchTouchEvent中返回了false,一般是在onTouchEvent里返回了false,这两种情况ViewGroup自己处理事件

View的事件分发过程

<pre>
public boolean dispatchTouchEvent(MotionEvent event) {
..

    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;
        }
    }

   ..
    return result;
}

</pre>

从上面的代码看,首先会判断是否设置了OnTouchListener,如果OnTouchListener里的OnTouch返回了true,OnTouchevent就不会被执行,OnTouch的优先级要高于OnTouchEvent,DispatchTouchEvent也会返回true

然后是OnTouchEvent里的具体处理:

<pre>
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;
}

</pre>

挑重点看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件不管他是不是DISABLE状态,View的LONG_CLICKABLE默认为false,而CLICKABLE是否为false和具体的View有关,例如:Button是可点击的CLICKABLE为true,Imageview、TextView为false。这里我有了一个疑问?通过前面的分析知道如果OnClickListener.onClick在onTouchEvent里调用,如果CLICKABLE和LONG_CLICKABLE直接返回了false,其他逻辑根本没有走?那么为什么TextView 设置了点击事件可以正常调用呢?
原因是:setOnClickListener会自动讲View的CLICKABLE置为true,setOnLongClickListener会自动讲LONG_CLICKABLE置为true

<pre>
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
</pre>

View的滑动冲突

外部拦截法

指事件都先经过父容器的拦截处理,外部拦截法需要重写父容器的onInterceptTouchEvent 伪码表示:

<pre>
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean isInterceped = false;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isInterceped = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要拦截当前事件){
isInterceped = true;
}else {
isInterceped = false;
}
break;
case MotionEvent.ACTION_UP:
isInterceped = false;
break;
}
return isInterceped;
}
</pre>

在onInterceptTouchEvent中,首先是ACTION_DOWN,父容器必须返回false,即不拦截ACTION_DOWN事件,因为一旦父容器拦截了ACTION_DOWN事件那么后续的ACTION_MOVE和ACTION_UP事件都直接交给了父容器处理,这个时候事件就没法传递给子元素了,然后是ACTION_MOVE事件,这个事件可以根据需求考虑是否拦截,如果拦截就返回true,否则返回false。最后是ACTION_UP事件必须返回false,因为ACTION_UP本身没有什么意义,假设事件交给子元素处理,如果ACTION_UP返回了true,子元素无法接收ACTION_UP事件,这时候子元素中的onCLick事件就无法触发。

内部拦截

指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方式需要配合requestDisallowInterceptTouchEvent使用
在子View的dispatchTouchEvent方法里处理,同时父容器要默认拦截除ACTION_DOWN的其他事件

eg:ListView嵌套ViewPager 如果手指是侧这滑动的话,就会造成事件冲突,这里我的策略是:判断横向还是竖向距离更多一些,横向的话就交给ViewPager处理,竖向的话交给ListView处理。

外部拦截方式:

<pre>
PointF lastPont = new PointF();
PointF currentPoint = new PointF();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
currentPoint.x = ev.getX();
currentPoint.y = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float dX = Math.abs(currentPoint.x - lastPont.x);
float dY = Math.abs(currentPoint.y - lastPont.y);
if (dY/dX>1){
return super.onInterceptTouchEvent(ev);
}else {
return false;
}
case MotionEvent.ACTION_UP:
break;
}
lastPont.x = currentPoint.x;
lastPont.y = currentPoint.y;
return super.onInterceptTouchEvent(ev);
}
</pre>

内部拦截方式:
在ViewPager onTouchEvent中
<pre>
if ((y>1||x>1)&&x/y>1) {
横向滑动,不让父布局拦截
getParent().requestDisallowInterceptTouchEvent(true);
}else {
getParent().requestDisallowInterceptTouchEvent(false);
}
</pre>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容