终于到了 View 这一关卡了,以前也有实践过自定义 View:圆弧刻度温度进度条,可是对于 View 底层的东西没什么了解,只是会用而已,抱着“知其然知其因此然”的心态,不少时候都会先去尝试使用,而后才来究其缘由。此次会分两个部分来叙述本篇:事件分发机制、滑动冲突;本身自己对源码也不熟悉,因此本篇主要是理论概述,尽可能不出现源码的东西。java
首先要声明这里用来分析的对象是 MotionEvent,即点击事件。git
所谓点击事件的事件分发其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了之后,系统须要把这个事件传递给一个具体的 View,而这个传递过程就是分发机制。github
了解了分发机制后就来了解另外一个概念,同一个事件序列:从手指接触屏幕的那一刻起到手指离开屏幕的那一刻结束,在这个过程当中所产生的一系列事件就叫同一个事件序列。这个事件序列以 down 事件开始,中间含有 n 个 move 事件,最终以 up 事件结束。面试
知道什么是同一个事件序列对后面的分析有很大的帮助,由于后续不少都是针对同一个事件序列来进行分析的。接下来看看点击事件的分发过程当中三个很重要的方法:spa
public boolean dispatchTouchEvent(MotionEvent event)code
public boolean onInterceptTouchEvent(MotionEvent event)对象
public boolean onTouchEvent(MotionEvent event)事件
这三个方法的关系就以下面的伪代码:get
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}复制代码
经过这个伪代码能够很清晰的知道这三个方法之间的关系。它们的传递规则:对于一个根 ViewGroup 来讲,点击事件产生后,首先会传递给 ViewGroup 自己,此时 ViewGroup 的 dispatchTouchEvent() 方法就会被调用,接着会调用 onInterceptTouchEvent() 方法,若是 onInterceptTouchEvent() 方法返回 true 就表示 ViewGroup 要拦截当前事件;接着再调用 onTouchEvent() 方法来处理该事件;可是若是 onInterceptTouchEvent() 方法返回 false 就表示 ViewGroup 不拦截当前事件,此时 ViewGroup 的 onTouchEvent() 方法就不会被调用,而是调用子 View 的 dispatchTouchEvent() 方法,如此反复直到事件最终被处理。源码
一般来讲那三个方法的执行流程就如上所说的,可是还会有一些比较特殊的状况,好比设置 OnTouchListener、OnClickListener。
当一个 View 须要处理事件时,若是 View 设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch() 方法就会被调用。若是 onTouch() 方法返回 false,则当前 View 的 onTouchEvent() 方法就会被调用;若是返回 true,当前 View 的 onTouchEvent() 方法将不会被调用。
在 onTouchEvent() 方法中,若是有设置 OnClickListener,那么 onClick() 方法是必定会被调用的。
曾经见过一道面试题,详细描述记不清了,大概意思是这样的:onTouch() 和 onTouchEvent() 谁先执行 ?有评论者说若是对 View 进行过比较深刻的了解是接触不到这些的。
那么经过前面几段叙述,对事件分发机制应该有个比较清晰的理解了,还有如下几点须要注意:
在界面中只要内外两层同时能够滑动,这个时候就会产生滑动冲突。常见的滑动冲突场景能够简单的分为如下三种:
- 外部滑动方向和内部滑动方向不一致;
- 外部滑动方向和内部滑动方向一致;
- 上面两种状况的嵌套。
场景一: 主要是 ViewPager 和 Fragment 配合使用所组成的页面滑动效果。在这种效果中能够经过 ViewPager 提供的左右滑动来切换页面,而每一个页面内部每每又是一个 RecyclerView。按说这种效果是会出现滑动冲突的,但却不用咱们去处理,缘由是 ViewPager 内部已经处理了滑动冲突,因此咱们使用的时候无须关注这个问题。
场景二: 若是在一个 ScrollView 中嵌套一个 RecyclerView,那么内外两层都在同一个方向能够滑动,当手指开始滑动的时候就会出现问题,由于系统不知道咱们想要滑动的是哪一层。同理,若是内外两层均可以在左右方向滑动也会出现这种状况。
场景三: 场景三是场景一和场景二两种状况的嵌套,这种状况更为复杂。
比较常见的滑动冲突就是上面那三种,那么该怎么来解决呢?其实能够根据滑动类型是水平滑动仍是竖直滑动来判断由谁来拦截事件。至于如何判断滑动类型就有比较多的方式了:能够依据滑动路径和水平方向所造成的夹角;也能够依据水平方向和竖直方向上的距离差来判断 … 。
滑动类型已经肯定了,接下来就是肯定滑动的接收者,到底是谁来响应这个滑动类型?下面介绍两种具体的解决方法:
所谓外部拦截就是指点击事件都先通过父容器的拦截处理,若是父容器须要该事件就拦截,不然就不拦截而是交给子容器。外部拦截须要重写父容器的 onInterceptTouchEvent() 方法,在内部作响应的拦截便可。这种方法的伪代码以下:
public boolean oonInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器须要当前点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}复制代码
在 onInterceptTouchEvent() 方法中,首先是 ACTION_DOWN 事件,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,这是由于一旦父容器拦截了 ACTION_DOWN 事件,那么后续的 ACTION_MOVE 和 ACTION_UP 事件都会直接交由父容器处理,此时事件就不能传递给子元素了;其次是 ACTION_MOVE 事件,这个事件能够根据须要来决定是否拦截,若是父容器须要拦截就返回 true,不然就返回 false;最后是 ACTION_UP 事件,这里必须返回 false,由于 ACTION_UP 事件自己没有太多意义。
内部拦截是指父容器默认不拦截任何事件,全部的事件都传递给子元素,若是子元素须要此事件就直接消耗处理掉,不然就返回给父容器进行处理。这种方法和外部拦截恰好相反,须要配合 requestDisallowInterceptTouchEvent() 方法才能正常工做,此时须要重写子元素的 dispatchTouchEvent() 方法。伪代码以下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotinEvent.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);
}复制代码
使用内部拦截除了须要对子元素进行修改之外,父元素也要修改成拦截除了 ACTION_DOWN 之外的其余事件。事件父元素所作的修改以下:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}复制代码
为何父元素不能拦截 ACTION_DOWN 事件呢?那是由于经过 requestDisallowInterceptTouchEvent() 方法会自动设置一个 FLAG_DISALLOW_INTERCEPT 标记,该标记会致使父元素没法拦截除了 ACTION_DOWN 之外的其余点击事件,而 ACTION_DOWN 事件会重置 FLAG_DISALLOW_INTERCEPT 标记,使之无效。
目前所理解的 View 事件分发机制和滑动冲突就这么多了,只是一些理论概述,接下来还须要好好的实践一番才能进一步掌握,同时也才能发现一些细节上的问题。