《Android 开发艺术探索》学习笔记之View的事件体系

一、View基础知识

1、View的位置参数

image

  • 在Android中,左上角为坐标原点,x、y的正方向分别是右和下
  • View的位置主要由四个顶点(相对父容器)决定
    • top:左上角纵坐标
      • top = getTop();
    • left:左上角横坐标
      • left = getLeft();
    • right:右下角横坐标
      • right = getRight();
    • bottom:右下角纵坐标
      • bottom = getBottom();
    • width = riht - left
    • height = top - bottom
  • Android3.0之后,新增(相对父容器
    • x、y是View左上角的坐标
    • translationX、translationY是View左上角相对于父容器的偏移量
  • View在平移过程中,top和left表示的是初始左上角位置信息,并不会改变,发生改变的是x、y、translationX、translationY。
    • x = left + translationX
    • y = top + translationY
2、MotionEvent和TouchSlop
  • MotionEvent
    • 手指接触屏幕后典型事件类型
      • ACTION_DOWN:手指刚接触屏幕
      • ACTION_MOVE:手指在屏幕上移动
      • ACTION_UP:手指从屏幕上松开的一瞬间
    • 通过==MotionEvent对象==可以获得点击事件发生的x、y坐标
      • getX/getY返回的是相对于当前View左上角的x和y坐标
      • getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标
  • TouchSlop
    • TouchSlop是系统所能识别出的被认为是滑动的最小距离,是一个常量。不同设备上的这个值可能不同。
    • 通过 ViewConfiguration.get(Context()).getScaledTouchSlop() 获取
3、VelocityTracker、GestureDetector和Scroller
  • VelocityTracker 速度追踪

    //创建VelocityTracker对象并加入追踪对象
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);
    
    //设置追踪时间间隔(此处为1000ms)
    velocityTracker.computeCurrentVelocity(1000);
    
    //获取横向速度
    int xVelocity = (int) velocityTracker.getXVelocity();
    //获取纵向速度
    int yVelocity = (int) velocityTracker.getYVelocity();
    
     //不使用时重置并回收内存
    velocityTracker.clear();
    velocityTracker.recycle();
    • 速度指的是设置的时间间隔内划过的像素点数。速度可为负,手指逆着坐标系的正方向滑动时速度即为负。
      • 速度 = (终点位置 - 起点位置) / 时间段
  • GestureDetector 手势检测

    • 辅助检测用户的单击、滑动、长按、双击等行为
    • 使用

      • 首先创建一个GestureDetector对象并实现OnGestureListener接口,或根据需要实现OnDoubleTapListener接口从而能够监听双击行为

        GestureDetector mGestureDetector = new GestureDetector(this);
        //解决长按屏幕后无法拖动的现象
        mGestureDetector.setIsLongpressEnabled(false);
      • 接着在待监管目标View的onTouchEvent方法中添加如下方法

        boolean consume = mGestureDetector.onTouchEvent(event);
        return consume;
      • 然后可以有选择的实现接口中的方法

        • OnGestureDetectorListener
        方法 说明
        onDown 手指轻触的一瞬间,由一个ACITON_DOWN触发
        onShowPress 手指轻触,尚未松开或拖动,由一个ACTION_DOWN触发
        onSingleTapUp 手指轻触屏幕后松开(单击行为),伴随一个ACTION_UP触发
        onScroll 手指按下屏幕并拖动(拖动行为),由一个ACITON_DOWN和多个ACTION_MOVE触发
        onLongPress 长按
        onFiling 用户按下触摸屏、快速滑动后松开(快速滑动行为),由一个ACTION
        • OnDoubleTapListener
        方法 说明
        onDoubleTap 双击,由两次连续单击行为组成
        onSingleTapConfirmed 严格的单击行为
        onDoubleTapEvent 发生了双击行为,ACTION_DOWN/MOVE/UP都会触发此回调
  • Scroller 弹性滑动对象

    • 实现有过渡效果的滑动
    • 与 View的computeScroll() 方法配合使用实现

二、View的滑动

1、使用View本身提供的scrollTo/scrollBy(操作简单,适合对View内容的滑动)
  • View边缘是指View的位置,由四个顶点组成。View的内容边缘是指View中的内容边缘。
    • mScrollX:
      • 单位为像素
      • 在滑动过程中,该参数总是等于View左边缘和View内容左边缘在水平方向上的距离
      • 通过getScrollX()方法得到
      • 当View左边缘在View内容左边缘的右边时,该参数为正(从右向左划),反之为负
    • mScrollY:
      • 单位为像素
      • 在滑动过程中,该参数总是等于View上边缘和View内容上边缘在垂直方向上的距离
      • 通过getScrollY()方法得到
      • 当View上边缘在View内容上边缘的右边时,该参数为正(从下向上划),反之为负
  • 两个方法都只能改变View内容的位置而不是改变View在布局中的位
    • scrollTo():实现了基于所传递参数的绝对滑动
    • scrollBy():调用了scrollTo方法,实现了基于位置的相对滑动
2、通过动画给View施加平移效果(操作简单,主要适用于没有交互的View和实现复杂的动画效果)
  • View动画是对View 的影像做操作,并不能真正的改变View的位置参数(包括宽高)
    • 使用fillAfter标签设置为true使完成后的动画状态的以保留,如果为false的话会在动画完成的瞬间恢复初始状态
  • 使用属性动画则不存在这样的问题(Android3.0以下的系统通过nineoldandroids实现)
3、通过改变View的LayoutParamas使得View重新布局从而实现滑动(操作稍微复杂,适用于有交互的View)
  • 改变布局参数

    //eg:
    
    MarginLayoutParamas paramas = (MarginLayoutParams) mButton1.getLayoutParamas();
    paramas.width += 100;
    paramas.leftMargin += 100;
    mButton1.requestLayout();
     //或者mButton1.setLayoutParams(params);

三、弹性滑动

共同思想:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成

1、使用Scroller
  • (1)Scroller scroller = new Scroller(mContext);
  • (2)调用 scroller.startSroll() 方法
    • 该方法只是保存了5个传入的参数
      • startX、startY:滑动的起点
      • dx、dy:要滑动的距离
      • duration:滑动时间
    • 该方法在源码的 smoothScrollTo() 方法中
        private void smoothScrollTo(int destX, int destY) {
            ···
            scroller.startScroll(···);
            invalidate();
        }
  • (3)调用 invalidate() 方法,导致View重绘
  • (4)View重绘中的draw方法调用 computScroll()方法,computScroll()方法是实现弹性滑动的核心方法。该方法在View中是一个空实现,需要自己实现
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {     
        //判断条件中的方法会根据时间的流逝计算出当前的scrollX和scrollY的值
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
  • (5)computeScroll又会向Scroller获取当前的scrollX和scrollY,然后通过scrollTo()方法滑动,接着又去调用 postInvalidate() 方法来进行第二次重绘,然后再次调用computeScroll方法
2、通过动画
  • 思想与Scroller类似(差值器)
  • 除了能完成弹性滑动之外,还可以实现其他的动画效果,在onAnimationUpdate方法中实现
3、使用延时策略
  • 核心思想:通过发送一系列的延时消息从而达到一种渐进式的效果
    • 使用Handler或View的postDelayed方法
    • 使用线程的sleep方法

四、View的事件分发机制

1、点击事件传递规则
  • 三大方法:

    • (1)dispatchTouchEvent()
      • 返回值表示是否消耗当前事件
    • (2)onInterceptTOuchEvent()
      • 返回值表示是否拦截当前事件
      • 如果当前的View拦截了某个事件,那么在同一个事件序列当中,此方法不会再次被调用
      • 此方法仅存在与ViewGroup当中,View中无此方法
    • (3)onTouchEvent()
      • 返回值表示是否消耗当前事件
      • 如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件
    //点击事件分发过程三大方法关系伪代码分析
    
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;   //事件未被消费
        if (onInterceptTouchEvent(ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
    
    return consume;
    }
  • View处理事件时,如果设置了OnTouchListener,则回调onTouch()方法
    • 如果返回false,则当前View的onTouchEvent方法被调用
    • 如果返回true,则onTouchEvent不会被调用
    • 优先级排序为OnTouchListener > OnTouchEvent > OnClickListener
  • 点击事件传递顺序为:Activity -> Window -> View
    • 如果所有元素都不处理某一点击事件,最终会调用 Activity的onTouchEvent() 方法
  • 总结
    • 同一个事件序列是指以down事件开始,中间含有数量不等的move事件,最终以up事件结束
    • 正常情况下,一个事件序列只能被一个View拦截且消耗。
      • 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给他的话),并且它的onInterceptTouchEvent不会再被调用
      • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent方法返回了false),那么同一事件序列中的其他时间都不会在交给它处理,并将事件重新交与父元素处理,即调用父元素的onTouchEvent()
      • 如果View 不消耗除ACTION_DOWN以外的其它事件 ,那么这个点击事件会消失。此时父类元素的onTouchEvent并不会调用,并且当前的View可以持续的收到后续事件,最终这些消失的点击事件会交与Activity处理
    • ViewGroup默认不拦截任何事件,即onInterceptTouchEcent方法默认返回false
      • View没有onInterceptTouchEvent方法,一旦有事件传递给他,那么他的onTouchEvent方法就会被调用
    • View的onTouchEvent方法默认消耗事件(返回true)
      • 除非它是不可点击的(clickable和longClickable同时为false)
        • View的longClickable默认都为false
        • clickable分情况
      • View的enable属性不影响onTouchEvent的默认返回值。
        • 即使View是disable状态的,只要clickable或longclickable中有一个为true,那么它的onTouchEvent就返回true
    • onClick会发生的前提是当前的View是可点击的,并且它收到了down和up的事件
    • 事件传递的过程是由外向内的
      • 但通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程
        • ACTION_DOWN事件除外
2、源码解析
  • Activity调用 dispatchTouchEvent 进行事件分发。
    • 具体的工作是由Activity内部的Window完成的,Window会将事件传递给decor view
      • Window类可以控制顶级View的外观和行为策略
      • 顶级View(根View):即在Activity中通过setContentView设置的View。顶级View一般来说都是ViewGroup
  • 顶级View(ViewGroup)对点击事件的分发过程

    //check for interception
    
    final boolean intercepted;
    if(actionMasked == MOtionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT != 0);
        //FLAG_DISALLOW_INTERCEPT一旦设置后,标记位不为零,disallowIntercept赋值为true
        //进入else,在else中intercept返回true
        //ViewGroup将无法拦截除了ACTION_DOWN之外的其他点击事件
        if(!disallowIntercept) {   
            //如果disallowIntercept为false才会进入此代码块
            //即子View不请求该标记位为不能拦截事件
            intercept = onInterceptTouchEvent(ev);
            ev.setAction(action);
        } else {
    
            intercept = false;
        }
    } else {       
        //如果ViewGroup拦截了DOWN事件,mFirstTouchTarget = 0
        //非ACTION_DOWN的后续系列事件默认交给ViewGroup处理,即不会调用onInterceptTouchEvent
        intercept = true 
    }
    • mFirstTouchTarget:一种单链表结构,赋值在 addTouchTarget 中完成。当事件由ViewGroup的子元素成功处理时,该参数会被赋值并指向子元素,即mFirstTouchTarget != null。ViewGroup拦截时值为空
    • FLAG_DISALLOW_INTERCEPT:通过 requestDisallowInterceptTouchEvent 方法来设置。一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN之外的其他点击事件
      • 因为ViewGroup在分发事件时,如果是ACTION_DOWN就会在 resetTouchState 方法中重置FLAG_DISALLOW_INTERCEPT标记位,导致子View中的设置无效
  • View对点击事件的处理
    • 只要View的CLICKABLE和LONG_CLICKABLE中有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true,不管是不是DISABLE状态
    • 当ACTION_UP事件发生时,触发 performClick() 方法,如果View设置了OnClickListener,则performClick方法内部会调用它的 onClick() 方法
      • setOnClickListener或setOnLongClickListener会将CLICKABLE和LONG_CLICKABLE设置为true

五、View的滑动冲突

1、常见的滑动冲突场景
  • 场景1——外部滑动方向和内部滑动方向不一致
    • 外部拦截法
    • 内部拦截法
  • 场景2——外部滑动方向和内部滑动方向一致
    • 具体情况具体分析
  • 场景3——场景一、二两种情况的嵌套
    • 具体情况具体分析
2、滑动冲突的处理规则
  • 依据滑动路径和水平方向所形成的夹角
    • dx和dy
  • 依据水平方向和竖直方向上的距离差
    • dx和dy
  • 依据水平和竖直方向的速度差
    • VelocityTracker
3、滑动冲突的处理方法
  • (1)外部拦截法(推荐)
    重写父容器的onInterceptTouchEvent方法,父容器根据自己的需要进行事件拦截。

    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACION_DOWN: {
                intercepted = false;
                break;
            }
    
            case MotionEvent.ACTION_MOVE: {
                if (父容器需要拦截当前点击事件) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;
            }
    
    
            case MotionEvent.ACTION_UP: {
                intercept = false;
                break;
            }
    
            default:
                break;
        }
    
        //赋值记录位置
        mLastXIntercept = x;
        mLastYIntercept = y;
    
        return intercept;
    }
    • 对ACTION_DOWN事件,父容器必须返回false。否则后续事件都会交给父容器处理
    • 对ACTION_MOVE事件,可以根据需要来决定是否拦截
    • 对ACTION_UP事件,父容器必须返回false。避免View的onClick不能触发
  • (2)内部拦截法
    父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器处理。重写子容器的dispatchTouchEvent方法

    • 需要配合requetDisallowInterceptTouchEvent方法才能正常工作
    • 父元素要默认拦截除了ACTION_DOWN之外的其他事件。因为DOWN事件会重置requestDisallowIntereptTouchEvent设置的标记位
      public boolean dispatchTouchEvent(MotionEvent event) {
          int x = (int) event.getX();
          int y = (int) event.getY();
      
          switch(event.getAction()){
              case MotionEvent.ACTION_DOWN: {
                  parent.requestDisallowIntereptTouchEvent(true);
                  break;
              }
      
              case MotionEvent.ACITON_MOVE: {
                  int deltaX = x - LastX;
                  int deltaY = y - LastY;
                  if (父容器需要此类点击事件) {
                      parent.requestDisallowIntereptTouchEvent(false);
                  }
                  break;
              }
      
              case MotionEvent.ACTION_UP: {
                  break;
              }
      
              default:
                  break;
          }
      
          //赋值记录位置
          mLastX = x;
          mLastY = y;
      
          //保持原始的事件分发逻辑
          return super.dispatchTouchEvent()
      }

你可能感兴趣的:(Android开发艺术探索)