什么是 View
View 是 Android 中所有控件的基类,是一种界面层的控件的一种抽象,代表了一个控件。ViewGroup 也是继承了 View,内部可以包含多个控件,所以 View 本身就可以是 单个控件,也可以是由多个控件组成的一组控件。
View 的位置参数
View 的位置主要由它四个定点来决定,分别对应 View 的四个属性:top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)、bottom(右纵角横坐标),这些坐标是相对于 View 的父容器来说的,因此是一种相对坐标。Android 中,x 轴和 y 轴的正方向分别为右和下。
Android 坐标系.png
View 的宽高与坐标有以下关系。
width = getRight() - getLeft();
height = getBottom() - getTop();
Android 3.0 开始增加额外的几个参数,x、y、translationX 和 translationY,x 和 y 是 View 左上角的坐标,translationX 和 translationY 是 View 左上角相对于父容器的偏移量,这几个参数也是相对于父容器的坐标,并且 translationX 和 translationY 的默认值是 0。
x = getLeft() + getTranslationX();
y = getTop() + getTranslationY();
View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,并不会变化,发生变化的是 x、y、translationX 和 translationY。
View 坐标系.png
触摸事件的类型
安卓触摸事件对应的是 MotionEvent 类,事件的类型主要有如下三种。
- ACTION_DOWN:用户手指的按下操作,一个按下操作标志着一次触摸事件的开始。
- ACTION_MOVE:用户手指按压屏幕之后,在松开之前,如果移动的距离超过一定的阀值,那么会被判为 ACTION_MOVE 操作,一般情况下,手指的轻微移动都会出发一系列的移动事件。
- ACTION_UP:用户手指离开屏幕的操作,一次抬起操作标志着一次触摸事件的结束。
- ACTION_CANCEL: 事件序列非人为的提前结束,ACTION_DOWN 下发下来,但其它事件被父 view 拦截时会触发。比如当用户的手指在屏幕上拖动一个 ListView 或者一个 ScrollView 而不是去按上面的按钮时会触发这个事件。
在一次屏幕触摸操作中,ACTION_DOWN 和 MotionEvent.ACTION_UP 这两个事件是必须的,而 ACTION_MOVE 视情况而定,如果用户仅仅点击了一下屏幕,可能只会监测到按下和抬起动作。
通过 MotionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标,getX/getY 返回的是相对于当前 View 左上角的 x 和 y 坐标,而 getRawX/getRawY 返回的是相对于手机屏幕左上角的 x 和 y 坐标。
TouchSlop
TouchSlop 是系统所能被识别出的被认为是滑动的最小距离,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。我们在处理滑动的时候,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值。
通过下面方式就可以获取这个常量。
ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
View 的事件分发机制
事件传递的三个阶段。
- 分发(Dispatch):事件的分发对应着 dispatchTouchEvent 方法,在 Android 系统中,所有的触摸事件都是通过这个方法来分发的。
public boolean dispatchTouchEvent(MotionEvent ev)
在这个方法中,根据当前视图的具体实现逻辑,来决定是直接消费这个事件还是将事件继续分发给子视图处理,方法返回值为 true 表示事件被当前视图消费掉,不再消费分发事件;方法返回值为 super.dispatchTouchEvent 表示继续分发该事件,如果当前视图是 ViewGroup 及其子类,则会调用 onInterceptHoverEvent 方法判定是否拦截该事件。
- 拦截(Intercept):事件的拦截对应着 onInterceptHoverEvent 方法,这个方法只在 ViewGroup 及其子类中才存在,在 View 和 Activity 中是不存在的。
public boolean onInterceptHoverEvent(MotionEvent event)
同理,这个方法也是通过返回的布尔值来决定是否拦截对应的事件,根据具体的实现逻辑,返回 true 表示拦截这个事件,不继续分发给子视图,同事交由自身的 onTouchEvent 方法进行消费;返回 false 或者 super.onInterceptHoverEvent 表示不对事件进行拦截,需要继续传递给子视图。
- 消费(Consume):事件的消费对应着 onTouchEvent 方法。
public boolean onTouchEvent(MotionEvent event)
该方法返回值为 true 表示当前视图可以处理对应的事件,事件将不会向上传递给父视图;返回值为 false 表示当前视图不处理这个事件,事件会被传递给父视图的 onTouchEvent 方法进行处理。
在安卓系统中,拥有事件传递处理能力的类有以下三种。
- Activity:拥有 dispatchTouchEvent 和 onTouchEvent 两个方法。
- ViewGroup: 拥有 dispatchTouchEvent 、onInterceptHoverEvent 和 onTouchEvent 三个方法。
- View:拥有 dispatchTouchEvent 和 onTouchEvent 两个方法。
结论
- 正常情况下,一个事件序列只能被一个 View 拦截且消耗。
- 某个 View 一旦决定拦截,那么一个事件序列都只能由它处理,并且 onInterceptHoverEvent 不会再被调用。
- 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,即在 onTouchEvent 时返回 false,那么同一事件序列中的其他事件都不会再由它来处理,并将事件事件交由父元素去处理,即父元素的 onTouchEvent 会被调用。
- 如果 View 不消费除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,当前 View 可以持续收到后续的事件,最终这些消失的点击事件传递给 Activity 处理。
- View 的 onTouchEvent 默认都会消费事件,返回 true,除非它是不可点击的(clickable 和 longClickable 同时为 false)。
- 事件传递过程是由外到内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过 getParent().requestDisallowInterceptTouchEvent(true) 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件例外。
网友评论