首页 > 其他分享 >AndroidView事件体系,事件分发机制

AndroidView事件体系,事件分发机制

时间:2024-08-02 10:08:05浏览次数:19  
标签:分发 MotionEvent AndroidView onTouchEvent 事件 ACTION view View

https://blog.csdn.net/qq_44076155/article/details/121582575

 

前言

节选自《开发艺术》的事件分发章节,并结合https://blog.csdn.net/qq_44076155/article/details/121582575
做一定总结

问题

在给一个view增加可拖动功能时,IDE给出了黄色警告,如下
image

初步分析

这里使用了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;
}

结论

  1. 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  3. 某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。
  5. 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  6. ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
  7. View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  8. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
  9. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  10. onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子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

相关文章

  • 代码随想录算法训练营第二十五天|134. 加油站、135. 分发糖果、860.柠檬水找零、406.
    写代码的第二十五天继续贪心!!gogogo!134.加油站思路贪心算法总让我有种脑子知道每次怎么计算,但是写不出来,也想不出贪心贪在哪里了,就只是觉得应该这么做。。。。。本题中大家可以按照自己的计算方法一步一步模拟一下这个过程,然后会发现其实每次都是要计算每站剩余的油量,......
  • JavaScript (八)——JavaScript 作用域和事件
    目录JavaScript 作用域JavaScript局部作用域JavaScript全局变量JavaScript变量生命周期HTML中的全局变量JavaScript 事件HTML事件常见的HTML事件JavaScript可以做什么?JavaScript 作用域作用域是可访问变量的集合。在JavaScript中,作用域为可访问变......
  • 在淘客返利系统中使用Kafka实现事件驱动架构
    在淘客返利系统中使用Kafka实现事件驱动架构大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!今天我们来探讨如何在淘客返利系统中使用Kafka实现事件驱动架构,以提高系统的可扩展性和灵活性。一、什么是事件驱动架构事件驱动架构(Event-DrivenArchit......
  • 事件循环-
    事件循环(EventLoop)是JavaScript运行时(例如浏览器或Node.js)的一种机制,用于处理异步编程。它允许非阻塞操作,即使在某些任务需要等待(如网络请求或定时器),JavaScript也可以继续执行其他代码。事件循环的基本概念调用栈(CallStack):JavaScript是一门单线程语言,这意味着它一次只能......
  • Gartner 魔力象限:安全信息和事件管理 (SIEM) 2024
    GartnerMagicQuadrantforSecurityInformationandEventManagement2024Gartner魔力象限:安全信息和事件管理2024请访问原文链接:https://sysin.org/blog/gartner-magic-quadrant-siem-2024/,查看最新版。原创作品,转载请保留出处。Gartner魔力象限:安全信息和事件管理202......
  • 我必须每秒捕获一帧的时间戳,但无法捕获整个事件。我能得到的最接近日期是“YYYY-MM-DD
    我正在尝试从左上角的一帧速率的视频中获取时间戳。我只能得到日期,不能得到整个时间戳。帮我获取整个时间戳我正在共享一个在预处理视频后得到的窗口。我本来希望获得整个时间戳,但我无法做到。我想要一个json文件中的整个时间戳,例如“2024-03-2916:36:20”,并且每个帧都......
  • 前端——jQuery中的事件与动画
    jQuery事件事件组成在jQuery中,一个事件由事件主体、事件类型、事件处理函数三个部分组成。//实现事件$("#button").click(function(){//...})//调用事件$("#button").click();鼠标事件常用的鼠标事件方法方法                  ......
  • 为什么在使用 pip 警告:忽略无效的分发 -ip 时会收到此消息?
    在过去的几周里,每次我使用pip下载软件包时,我都会得到以下信息:警告:忽略无效的分发-ip(软件包路径)有什么想法为什么我会得到这个吗?看到「警告:忽略无效分发-ip」消息的原因是的Python软件包索引缓存中存在格式错误或损坏的文件。Pip在搜索软件包时发现这些文件无......
  • PyQt:最大化窗口时如何防止处理多个调整大小事件?
    我有一个QMainWindow包含一个子QWidget包含自身aQLabel当窗口最大化时(例如,通过单击窗口上的最大化图标),QLabel.resizeEvent()处理程序被调用多次(据说跟随窗口的逐渐放大,直到占据整个桌面空间)。事件处理程序中的代码调用setPixmap(......
  • Android开发 - setOnTouchListener 监听触控事件解析
    事件解析setOnTouchListener(newOnTouchListener(){});:事件分发解析MotionEvent.ACTION_DOWN:按下MotionEvent.ACTION_MOVE:滑动MotionEvent.ACTION_UP:抬起使用方法//部分区域调用需要对象:view.setOnTouchListener(newview.OnTouchListener(){})setOnTouchListe......