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

第三章 View的事件体系 学习清单:

  • View的事件体系
    • View的位置参数
    • View的触控参数
    • View的滑动
  • View的事件分发机制
    • 点击事件传递规则
  • View的滑动冲突
    • 产生原因
    • 常见的滑动冲突场景
    • 处理规则
    • 解决方案
简介
在Android的世界中View是所有控件的基类,其中也包括ViewGroup在内,ViewGroup是代表着控件的集合,其中可以包含多个View控件
从某种角度上来讲Android中的控件可以分为两大类:View与ViewGroup。通过ViewGroup,整个界面的控件形成了一个树形结构,上层的控件要负责测量与绘制下层的控件,并传递交互事件
在每棵控件树的顶部都存在着一个ViewParent对象,它是整棵控件树的核心所在,所有的交互管理事件都由它来统一调度和分配,从而对整个视图进行整体控制
Android开发艺术探索|Android开发艺术探索 | View的事件体系
文章图片
image-20200605111552941.png 一. View 的事件体系 1. View的位置参数 a. Q:如何确定一个View的位置?
A: View的位置主要通过它的四个顶点来决定, 分别是:
  • top: 左上角纵坐标
  • left: 左上角横坐标
  • right: 右下角横坐标
  • bottom: 右下角纵坐标

    Android开发艺术探索|Android开发艺术探索 | View的事件体系
    文章图片
    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开发艺术探索|Android开发艺术探索 | View的事件体系
文章图片
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对象我们可以得到事件发生的 x 和 y 坐标:
      • getX() / getY(): 返回相对于当前View左上角的 x, y 坐标
      • getRawX() / getRawY(): 返回相对于手机屏幕左上角的 x , y 坐标
  • 【Android开发艺术探索|Android开发艺术探索 | View的事件体系】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开发艺术探索|Android开发艺术探索 | View的事件体系
文章图片
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开发艺术探索|Android开发艺术探索 | View的事件体系
文章图片
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; } }

    推荐阅读