Android开发艺术探索 | View的事件体系

第三章 View的事件体系

学习清单:

  • View的事件体系

    • View的位置参数

    • View的触控参数

    • View的滑动

  • View的事件分发机制

    • 点击事件传递规则
  • View的滑动冲突

    • 产生原因

    • 常见的滑动冲突场景

    • 处理规则

    • 解决方案


简介

在Android的世界中View是所有控件的基类,其中也包括ViewGroup在内,ViewGroup是代表着控件的集合,其中可以包含多个View控件

从某种角度上来讲Android中的控件可以分为两大类:View与ViewGroup。通过ViewGroup,整个界面的控件形成了一个树形结构,上层的控件要负责测量与绘制下层的控件,并传递交互事件

在每棵控件树的顶部都存在着一个ViewParent对象,它是整棵控件树的核心所在,所有的交互管理事件都由它来统一调度和分配,从而对整个视图进行整体控制

Android开发艺术探索 | View的事件体系_第1张图片
image-20200605111552941.png

一. View 的事件体系

1. View的位置参数

a. Q:如何确定一个View的位置?

A: View的位置主要通过它的四个顶点来决定, 分别是:

  • top: 左上角纵坐标

  • left: 左上角横坐标

  • right: 右下角横坐标

  • bottom: 右下角纵坐标

    Android开发艺术探索 | View的事件体系_第2张图片
    image-20200605112730079.png

b. View的宽高和坐标的关系:

width = right - left;
height = bottom - top;

 // 获取这四个参数的方法
 Left = getLeft();
 Right = getRight();
 Top = getTop()
 Bottom = getBottom();

c. 从Android3.0开始, View增加了额外的四个参数: x, y, translationX 和 translationY, 其中x 和 y 是View左上角坐标, 而translationX 和 translationY 是View左上角相对于父容器的偏移量, 这几个参数也是相对于父容器的坐标, 关系如下图:

Android开发艺术探索 | View的事件体系_第3张图片
image-20200605113942812.png
  • 换算关系: x = left + translationX, y = top + translationY
  • X由此可见, x和left不同体现在:left是View的初始坐标, 在绘制完毕后就不会再改变;而x是View偏移后的实时坐标, 是实际坐标. y和top的区别同理

2. View的触控参数

a. MotionEven 和 TouchSlop:
  • MotionEven的触摸事件:

    • ACTION_DOWN : 手指放接触屏幕

    • ACTION_MOVE : 手指在屏幕上移动

    • ACTION_UP : 手指从屏幕上松开的一瞬间

    正常情况下, 一次手指触碰屏幕的行为可能触发一系列点击事件, 如:

    • 点击屏幕后立刻松开: DOWN -> UP;
    • 点击屏幕后一会再松开: DOWN -> MOVE -> ... -> MOVE -> UP;
    • 通过MotionEven对象我们可以得到事件发生的 xy 坐标:

      • getX() / getY(): 返回相对于当前View左上角的 x, y 坐标

      • getRawX() / getRawY(): 返回相对于手机屏幕左上角的 x , y 坐标


  • TouchSlop的使用:

    • TouchSlop: 是系统所能识别出的滑动最小距离, 是一个常量, 不同的设备上这个值可能是不同的

    • 通过 ViewConfiguration.get(getContext()).getScaledTouchSlop()可以获得这个常量

      使用建议:

      • 通过TouchSlop, 可以对用户的一些操作进行过滤, 提高用户使用体验

b. VelocityTracker 和 GestureDetector:
  • VelocityTracker速度追踪:

    • 功能: 用于追踪手指在滑动过程中的速度, 包括水平和竖直方向的速度

    • 使用:

      • 在View的onTouchEvent()方法中追踪当前单击事件的速度
       VelocityTracker velocityTracker = VelocityTracker.obtain();
       velocityTracker.addMovement(event);
      
      • 获取滑动速度
       int xVelocity = (int) velocityTracker.getXVelocity();
       int yVelocity = (int) velocityTracker.getYVelocity();
      

      注意:

      • 获取速度之前必须调用computerCurrentVelocity方法
      • 获取到的速度指的是在规定时间内划过的像素数
      • 获取到的速度可以为负数, 从右向左 / 从下向上 获取到的就是负数
      • 速度的公式 : 速度 = (终点位置 - 起点位置) / 时间段
      • 不使用VelocityTracker的时候需要调用clear方法来重置并回收内存, recycle方法重新调用

  • GestureDetector手势检测

    • 功能: 用于检测用户的单击, 滑动, 长按, 双击等行为

    • 使用:

      • 创建一个GestureDetector对象

      • 根据不同的需求实现OnGestureListener接口或OnDoubleTapListener接口

        方法名 描述 所属接口
        onDown 手指轻触屏幕的一瞬间, 由1个ACTION_DOWN触发 OnGestureListener
        onShowPress 手指轻触屏幕尚未松开或移动 OnGestureListener
        onSingleTapUp 手指轻触屏幕后松开, 伴随1个ACTION_UP触发 OnGestureListener
        onScroll 手指轻触屏幕并拖动 OnGestureListener
        onLongPress 长按屏幕不放 OnGestureListener
        onFling 触摸屏幕快速滑动后松开 OnGestureListener
        onDoubleTap 双击, 不能和onSingleTapConfirmed共存 OnDoubleTapLinstener
        onSingleTapConfirmed 严格单击行为, 指不能是双击中的一次单击 OnDoubleTapLinstener
        onDoubleTapEvent 发生了双击行为, 在双击期间移动也会触发 OnDoubleTapLinstener
    • 注意: 在实际开发中, 如果需要监听双击事件, 则使用GestureDetector, 否则可以在View的onTouchEvent方法中实现


3. View的滑动

a. 通过View本身的scrollTo / scrollBy实现:
  • 方法:

    • scrollTo: 基于所传递参数的绝对滑动

    • scrollBy: 实际上是通过调用scroolTo方法实现, 传递的是偏移量

  • 注意: 通过scrollTo / scrollBy只能改变View的内容, 不能改变View在当前布局中的位置


b. 使用动画
  • 使用xml文件的方式:

    • xml代码:
     
     
      
     
    
    • java代码:
     Animation animation = AnimationUtils.loadAnimation(this, R.anim.translate);
     view.startAnimation(animation);
    

    推荐阅读: Animation补间动画

  • 注意: 这种动画只能改变View的内容所在的位置, 真身仍在原来的位置

    关于动画的内容, 会在第7章详细说明

c. 改变布局参数
  • 说明: 通过改变LayoutParams来实现, 或例如在Button旁边放置一个View, 通过改变这个View的大小来实现

  • 注意: 在修改了LayoutParams后记得使用requestLayout()方法更新

d. 三种方式的优缺点:
  • scrollTo / scrollBy : 操作简单, 适合对View内容的滑动;

  • 动画: 操作简单, 主要适用于不与用户交互, 复杂的动画效果

  • 改变布局参数: 操作稍微复杂, 但适用与有交互的View


4.View的弹性滑动

View的滑动效果显得太过生硬, Android中还提供了许多弹性滑动的方法, 下面记录一下Android中的弹性滑动

a. 使用Scroller
  • 使用:

    • 创建Scroller的实例

    • 调用startScroll()方法来初始化滚动数据并刷新界面

    • 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑

  • 惯用代码:

     ​
     private void smoothScrollTo(int dstX, int dstY) {
      int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
      int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
      int deltaX = dstX - scrollX;//x方向滑动的位移量
      int deltaY = dstY - scrollY;//y方向滑动的位移量
      scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
      invalidate(); //刷新界面
     }
     ​
     @Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
     public void computeScroll() {
      if (scroller.computeScrollOffset()) { 
      scrollTo(scroller.getCurrX(), scroller.getCurY());
      postInvalidate();//通过不断的重绘不断的调用computeScroll方法
      }
     }
    

    其中startScroll源码如下,可见它并没有进行实际的滑动操作,而是通过后续invalidate()方法去做滑动动作

      public void startScroll(int startX,int startY,int dx,int dy,int duration){
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;//滑动时间
        mStartTime = AnimationUtils.currentAminationTimeMills();//开始时间
        mStartX = startX;//滑动起点
        mStartY = startY;//滑动起点
        mFinalX = startX + dx;//滑动终点
        mFinalY = startY + dy;//滑动终点
        mDeltaX = dx;//滑动距离
        mDeltaY = dy;//滑动距离
        mDurationReciprocal = 1.0f / (float)mDuration;
      }
    

    具体过程:在MotionEvent.ACTION_UP事件触发时调用startScroll方法->马上调用invalidate/postInvalidate方法->会请求View重绘,导致View.draw方法被执行->会调用View.computeScroll方法,此方法是空实现,需要自己处理逻辑。具体逻辑是:先判断computeScrollOffset,若为true(表示滚动未结束),则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。如图所示:

Android开发艺术探索 | View的事件体系_第4张图片
image-20200609142617908.png
  • 原理: 原理:Scroll的computeScrollOffset()根据时间的流逝动态计算一小段时间里View滑动的距离,并得到当前View位置,再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。

b. 使用动画:

动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动

  • 代码:

     ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
    

c. 使用延时策略:
  • 描述: 通过Handler / View的postDelayed方法发送一系列延时消息从而打到一种渐进式的效果, 也可以用线程的sleep方法

  • 注意: 对弹性滑动完成总时间有精确要求的使用场景下, 使用延时策略是一个不太合适的选择


二. View的事件分发机制

事件分发机制是View的核心知识点, 通过学习事件分发机制可以解决滑动冲突难题, 巩固我们对View的掌握

1. 点击事件传递规则

a. 事件分发本质:

  • 就是对MotionEvent事件分发的过程。即当一个MotionEvent产生了以后,系统需要将这个点击事件传递到一个具体的View上

  • 传递顺序:

    • Activity(Window) -> ViewGroup -> View

    补充: 如果所有元素都不处理这个事件, 那么这个事件最终会由Activity处理, 即Activity的onTouchEvent方法会被调用

Android开发艺术探索 | View的事件体系_第5张图片
image-20200609165829185.png

b. 核心方法:

  • public boolean dispatchTouchEvent(MotionEvent ev):

    用于进行事件的分发, 如果事件能传递给当前View, 则此方法一定调用. 返回结果受到当前View的onTouchEvent和下级dispatchTouchEvent影响, 表示是否消耗当前事件

  • public boolean onInterceptTouchEvent(MotionEvent event):

    dispatchTouchEvent方法中调用, 用于判断是否拦截当前事件, 如果当前View拦截了某个事件, 则同一个任务序列中此方法不会再被调用(只有ViewGroup有这个方法)

  • public boolean onTouchEvent(MotionEvent event):

    dispatchTouchEvent方法中调用, 用于处理点击事件, 返回结果表示是否消耗当前事件, 如果不消耗, 则在同一个任务序列中, 当前View无法再次接受到事件

  • 补充阅读: Android事件分发机制(源码)


三. View的滑动冲突

a. 产生原因:
  • 一般情况下,在一个界面里存在内外两层可同时滑动的情况时,会出现滑动冲突现象
b. 常见的滑动冲突场景:
  1. 外部滑动方向和内部滑动方向不一致;

  2. 外部滑动方向和内部滑动方向一致;

  3. 上述两种情况的嵌套;

c. 处理规则:
  • 对于场景一: 左右滑动时, 让外部View拦截事件. 上下滑动时, 让内部View拦截事件

  • 对于场景二: 根据相应的业务情景做出相应的操作

  • 对于场景三: 将组合问题根据场景拆分成若干个小问题, 逐一解决

Q: 如何判断是左右滑动还是上下滑动:

  • 根据滑动路径与水平方向上的夹角
  • 根据水平和竖直方向上的速度差
  • 根据水平和竖直方向上的距离差
d. 解决方案:
  • 外部拦截法:

    • 含义: 先经过父容器, 如果需要就拦截, 不需要再分发到子View

    • 方法: 重写父容器的onInterceptTouchEvent方法, 在内部做相应的拦截

    public boolean onInterceptTouchEvent(MotionEvent event) {
            boolean intercepted = false;
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                //对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
                case MotionEvent.ACTION_DOWN:
                    intercepted = false;
                    break;
                //对于ACTION_MOVE事件根据需要决定是否拦截
                case MotionEvent.ACTION_MOVE:
                    if (父容器需要当前事件){
                    intercepted = true;
                } else{
                    intercepted = flase;
                    break;
                }
                //对于ACTION_UP事件必须返回false,一旦拦截子View的onClick事件将不会触发
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
                default:
                    break;
            }
    
            mLastXIntercept = x;
            mLastYIntercept = y;
            return intercepted;
    }
    
  • 内部拦截法

    • 含义: 父容器不拦截任何事件, 如果子元素不需要就交由父容器处理

    • 方法: 重写子元素的dispatchTouchEvent方法, 再配合requestDisallowInterceptTouchEvent方法,

   public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // parent.requestDisallowInterceptTouchEvent()可以理解为:
                // 告诉(request)父容器(parent)
                // 不再(disallow)拦截(intercept)触摸事件(touchEvent)吗(boolean)
                // 当requestDisallowInterceptTouchEvent(ture)时
                // 父容器不再拦截接下来的一系列事件
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父View需要重写onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

你可能感兴趣的:(Android开发艺术探索 | View的事件体系)