美文网首页
View事件体系探究

View事件体系探究

作者: LeoFranz | 来源:发表于2019-08-20 16:10 被阅读0次
    • view基础坐标系
    • 事件拦截接口
    • View事件分发简述
    • 事件分发详解
    • MotionEvent

    View 基础坐标体系

    view的left、right、top、bottom坐标
    后来出现的x、y、transLationX、transLationY坐标
    响应view的touch事件可以在ontouchEvent中处理,也可以重写GestureDetector,主要是处理双击、滑动
    实现view的滑动三种方式:
    1、使用ScrollTo/ScrollBy,改变的是mScrollX, mScrollY 方法,只能改变view的内容 (指的是View内容的偏移量,如果是ViewGroup的话作用的就是它的所有子view,如果是TextView的话则作用的就是TextView的内容。) 而非view本身,且不能移动到附近view的位置,次滑动直接进行效果生硬,如果要实现弹性平滑的滑动,需要使用Scroller结合view的computeScroll方法(分割进行),或者通过handler的sendDelay方法反复执行这些骚操作。
    2、动画
    3、设置view的布局参数,然后requestLayout,或者setLayoutParams,比较麻烦

    事件拦截接口使用

    GestureDetector可以用来监听长按、双击等动作
    常见的使用方式如下

    // 1.创建一个监听回调
    SimpleOnGestureListener listener = new SimpleOnGestureListener() {
        @Override public boolean onDoubleTap(MotionEvent e) {
            Toast.makeText(MainActivity.this, "双击666", Toast.LENGTH_SHORT).show();
            return super.onDoubleTap(e);
        }
    };
    
    // 2.创建一个检测器
    final GestureDetector detector = new GestureDetector(this, listener);
    
    // 3.给监听器设置数据源
    view.setOnTouchListener(new View.OnTouchListener() {
        @Override public boolean onTouch(View v, MotionEvent event) {
            return detector.onTouchEvent(event);
        }
    });
    

    有效构造函数只有两个:

    GestureDetector(Context context, GestureDetector.OnGestureListener listener)
    GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler)
    

    有四个接口:

    OnContextClickListener,主要用于外部设备的按击事件
    OnDoubleTapListener, 主要监听双击事件
    OnGestureListener,手势检测,主要有按下、长按、扔等动作
    SimpleOnGestureListener 上述三个接口的空实现,一般情况下用这个比较多
    

    View事件分发简述

    三个核心方法:

    boolean dispatchTouchEvent(MotionEvent event) {
            boolean consume = false;
    if(onInterceptTouchEvent(ev)) {
         consume = onTouchEvent(ev);
    }else {
         consume = child.dispatchTouchEvent(ev);
    }
    return consume;
    }
    

    如果view能传递事件,dispatchTouchEvent 一定会被调用,返回值表示是否消耗当前事件,如果拦截了事件,即onInterceptTouchEvent被调用(返回true在同一事件序列中就不会再调用该方法),就会进入该view的onTouchEvent处理事件,该方法返回是否消耗事件(返回false在同一事件序列中当前view就不能再接受到事件)。

    普遍流程:
    根View
    1、——不拦截事件(onInterceptTouchEvent返回false)——子view——循环直到事件被处理
    2.1、——拦截——处理事件——消耗事件(onTouchEvent返回true)
    2.2、——拦截——处理事件——不消耗事件(onTouchEvent返回false)——父容器的onTouchEvent被调用——依次循环直到被消耗或者传递给Activity处理(其onTouchEvent被调用)
    流程例外:

    当一个view需要处理事件,如果它设置了onTouchListener,其中的onTouch会被回调,返回false,才会继续触发onTouchEvent,而onTouchEvent中如果当前设置有onClicListener,其onClick方法就会被调用。

    事件传递遵循顺序:Activity——Window——顶级View

    同一事件序列:手指接触屏幕那一刻到离开屏幕那一刻结束,包括Down事件、n个move事件、up事件

    当前View如果不消耗actiondown事件,剩余事件会上抛给父view处理;如果当前view不处理除了actiondown以外的事件,它仍然能收到剩余事件序列,但是剩余事件序列会直接传递给activity处理

    viewgroup默认不拦截view,其interceptTouchEvent默认返回false;view没有interceptTouchEvent方法,一旦事件传递给它就能处理,且view的onTouchEvent方法默认返回true,除非其不可点击(clikable和longClickable属性同时为false),其enable属性不影响onTouchEvent事件默认返回值。onClick发生的前提是view可点击,并且它收到了down和up的事件。

    DecorView是当前layout根文件的上层view,主题颜色和标题栏等内容显示在DecorView。
    Window是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴。PhoneWindow是window唯一的实现类,所以重要性不言而喻。上面说的 DecorView 是 PhoneWindow 的一个内部类,其职位相当于小太监,帮忙传达PhoneWindow的消息,下层的事件也通过DecorView传递给PhoneWindow

    view和Activity没有拦截事件的方法,原因在于activity是事件传递的入口,它拦截了事件会导致整个屏幕无法响应触摸;而view作为事件传递的末段,要么处理事件,要么回传,没有必要再拦截了。

    事件传递的顺序是:Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View,如果没有任何view消费掉事件,事件会按照相反的方向回传,直到当前层级能够消费事件或者到activity了。如果最后 Activity 也没有处理,本次事件才会被抛弃。

    判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。

    ACTION_CANCEL 事件 被上层拦截 时触发

    事件分发详解

    View的事件分发
    讲道理,当处理事件的时候各种监听器的优先级应该是onTouchListener > onTouchEvent > onLongClickListener > onClickListener,只有onTouchEvent是view自带的,有这么多事件监听器,也不难理解为何view也要添加dispatchTouchEvent方法了,所以view也有事件分发惹。
    这个过程的伪代码如下所示:

    public boolean dispatchTouchEvent(MotionEvent event) {
      if (mOnTouchListener.onTouch(this, event)) {
          return true;
      } else if (onTouchEvent(event)) {
          return true;
      }
      return false;
    }
    

    onclick和onLongClick的处理在onTouchEvent中了

    public boolean onTouchEvent(MotionEvent event) {
        ...
        final int action = event.getAction();
        // 检查各种 clickable
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    removeLongPressCallback();  // 移除长按
                    ...
                    performClick();             // 检查单击
                    ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    checkForLongClick(0);       // 检测长按
                    ...
                    break;
                ...
            }
            return true;                        // ◀︎表示事件被消费
        }
        return false;
    }
    

    只要view的clickable或者longClickable有一个属性为true,那么它的onTouchEvent就会默认返回true——即是可点击的就返回true
    。此外,事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

    ViewGroup的事件分发
    基本流程之前已经描述,即

    boolean dispatchTouchEvent(MotionEvent event) {
            boolean consume = false;
    if(onInterceptTouchEvent(ev)) {
         consume = onTouchEvent(ev);
    }else {
         consume = child.dispatchTouchEvent(ev);
    }
    return consume;
    }
    

    判断应该把事件传递给哪个view就是遍历一遍子view,在手指触摸点的view就能收到馈赠

    子view有重叠怎么办,后面添加的会覆盖之前的,首先会传递给后添加的,这里有一些规则:
    假设view2覆盖了view1,
    1、只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。
    2、只有 View2 可点击时,事件将会分配给 View2。
    3、View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。
    注意:
    1、上面说的是可点击,可点击包括很多种情况,只要你给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。
    2、另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。
    给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件。

    由以上可以搞明白一些问题:
    1、 ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
    事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。
    2、所有事件都应该被同一 View 消费
    安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件收纳过来,不会再传递给其他 View,除非上层 View 进行了拦截。
    如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。

    滑动冲突
    两种思路:外部拦截和内部拦截
    前提知识:自view确认拦截一个事件序列后,所有的后续事件序列都会被其处理,前提是该这些事件能传递到该view。即父容器还会一直“审核”传递的事件。
    父容器相对特殊,如果其决定拦截事件,那么后续所有事件,只要能传递到该容器,都会被处理,哪怕对应的OnIntercptTouchEvent返回false。

    外部拦截:父容器决定是否拦截
    重写父容器的onInterceptTouchEvent方法,ActionDown一定不能拦截,否则后续事件都不会传递给子view,一般对ActionMove事件做业务处理,父容器要拦截就返回true,否则返回false让子容器处理。

    内部拦截:所有事件都传递到子view,子view不处理的上抛给父容器。
    重写子view的dispatchTouchEvent方法,在ActionDown中设置requestDisAllowInterceptTouchEvent(true),一般对ActionMove做业务处理,对于父容器需要的move事件,通过requestDisAllowInterceptTouchEvent设(false),此外还要将父容器中onInercepToucnEvent方法中ActionDown事件下返回false,并且其他事件都默认拦截,这样当子view的requestDisAllowInterceptTouchEvent设置成false时候,父容器才能拦截事件。
    那为什么父容器中onInterceptTouchEvent里actionDown事件一定不能默认拦截?,因为如果拦截了后续所有事件都不能由子view处理,哪怕requestDisAllowInterceptTouchEvent被重新设置为true(本质上是改变FLAG_DISSALLOW_INTERCEPT标志位)也不能改变这种趋势,可见,父容器对actiondown事件的拦截是强制性的。

    MotionEvent

    现在的motionEvent啊,已经资持触控笔,鼠标,键盘,操纵杆,游戏控制器等输入工具。
    有两种action我们不是很常见:

    • ACTION_CANCEL
      只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 ACTION_CANCEL 事件。

    上层 View 是一个 RecyclerView,它收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL ,并且不会再收到后续事件。

    • ACTION_OUTSIDE
      一个触摸事件已经发生了UI元素的正常范围之外。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。

    比如dialog区域外关闭,或者悬浮窗接受区域外的点击事件。这些操作一般需要一些特殊设置,比如设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH,这样点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE 事件。

    • 多点触控

      当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了。
      为了解决这个问题,官方将手指编号和事件类型整合起来,int类型共32位(0x00000000),他们用最低8位(0x000000ff)表示事件类型,再往前的8位(0x0000ff00)表示事件编号。但这样导致getAction方法获取的值无法与标准事件定义值想对比,所以推出了getActionMasked方法,它能消除index数值,让其变成一个标准的事件类型。
      1、多点触控时必须使用 getActionMasked() 来获取事件类型。
      2、单点触控时由于事件数值不变,使用 getAction() 和 getActionMasked() 两个方法都可以。
      3、使用 getActionIndex() 可以获取到这个index数值。不过请注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。

    定义这个手指的index是ActionIndex,只有在手指按下(down)和抬起(up)时是有用的,在移动(move)时是没有用的,追踪事件流,请认准 PointId,这是唯一官方指定标准。getPointerId(int pointerIndex) 获得。 (参数中的 pointerIndex 就是 actionIndex)

    相关文章

      网友评论

          本文标题:View事件体系探究

          本文链接:https://www.haomeiwen.com/subject/wocprctx.html