1.View的基础概念
View是Android中控件的基类,是一种界面层控件的一种抽象,它代表的是一个控件。除了View,还有ViewGroup,从名字来看,它可以被翻译成控件组,言外之意就是ViewGroup中可以包含多个控件。而在Android设计中,ViewGroup也是继承了View,这意味着View本身就可以是单个控件也可以是多个控件组成的一组控件,这种关心与Web中DOM叔的概念类似。
2. View的位置参数
View的位置主要由它的四个顶点来决定的,分别对应View的四个属性:left、top、right和right。但是这四种属性都是相对于parentView来定义的,也就是说它们是相对坐标。
从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父组件的偏移量。由此可见:x、y、translationX和translationY也是相对于父组件的相对坐标。它们满足的关系式为:
x = translationX + left
需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变是x、y、translationX和translationY这四个参数。
3.一些基本概念
3.1 MotionEvent
在手指滑动屏幕的过程中,经过Android系统的封装,会产生一系列的事件,一般都描述为:
- ACTION_DOWN 手指接触到屏幕
- ACTION_MOVE 手指在屏幕上移动
- ACTION_UP 手指离开屏幕的一瞬间
正常的情况下,一次手指触摸屏幕的行为会触发一系列的点击事件,一般情况为:
- ACTION_DOWN —> ACTION_UP 手指点击屏幕后立即离开屏幕
- ACTION_DOWN —> ACTION_MOVE —> ACTION_MOVE —> … —> ACTION_UP 手指点击屏幕后滑动屏幕,最后离开屏幕。
这种情况就是典型的时间序列,同时通过MotionEvent对象我们可以得到点击事件发生的坐标(x,y)。同时可以根据Android系统提供getX()/getRawX()、getY()/getRawY()进行相应的判断。
3.2 TouchSlop
TouchSlop是系统中所能识别的被认为是滑动的最小距离。该距离与设备相关,可以通过代码获取:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
3.3 VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平速度和竖直速度。使用方式比较简单,首先在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTraker velocityTracker = VelocityTraker.obtain();
velocityTrcaker.addMovement(event);
此时可以计算出当前的速度:
velocityTracker.computeCurrentVelocity(1000);
int velocityX = (int)velocityTracker.getXVelocity();
int velocityY = (int)velocityTracker.getYVelocity();
这一步需要注意两点:
1.获取速度之前必须先计算速度,获取velocityX或者velocityY之前需要调用computeCurrentVelocity方法;
2.这里的速度是指一段时间内手指所滑过的像素数目,比如将时间间隔设置为1000ms,在1s内手指在水平方向上从左向右滑过100像素,那么水平速度就为100.当然从右到左此时速度就为-100了。
最后当我们不需要velocityTracker时,需要调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
4. View事件机制
4.1 三个重要方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
用于事件分发。如果事件能传递到该View,那么此方法一定会调用。它表示是否消耗该事件(消耗返回true/不消耗返回false)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
用于事件拦截,ViewGroup独有事件。如果该事件返回true,则标识本ViewGroup将会拦截本事件,并将调用ViewGroup的onTouchEvent。正常情况下,onInterceptTouchEvent
在同一个事件序列中只会调用一次。
@Override
public boolean onTouchEvent(MotionEvent event)
用来处理点击事件,返回结果标识是否消耗本事件,如果不消耗,那么在同一个事件序列中,当前View无法再次接受事件。
对于三个事件,一般可以描述为:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result ;
if(onInterceptTouchEvent(event)){
result = onTouchEvent(event);
}else{
result = child.dispatchTouchEvent(event);
}
return result;
}
综合概述为:当事件被传递到ViewGroup时,ViewGroup的dispatchTouchEvent
首先会被调用,如果本ViewGroup中onInterceptTouchEvent
返回值为true,表明需要拦截本事件;那么将会调用ViewGroup的onTouchEvent
。如果ViewGroup表明不拦截本次事件,将会将本次事件传递到子View中,进而继续调用dispatchTouchEvent
,直到消耗事件被处理为止。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch
方法被调用。此时事件如何处理还需要看onTouch的返回值,如果返回false,则当前的View的onTouchEvent
方法将会被调用;如果返回true,那么onTouchEvent
将不会被调用。
4.2 伪代码表示事件分发机制
在3.1中已经描述过,每次手指操作屏幕都会产生一系列的事件,那么我们需要对事件进行拆开分析:
- 在
dispatchTouchEvent
中如果我们的事件是ACTION_DOWN
,那么此时的运行模式是:先询问onInterceptTouchEvent
是否拦截该事件,然后找到将要处理该事件的viewTarget,伪代码为:
public boolean dispatchTouchEvent(MotionEvent ev) {
View targetView = null ;
if(onInterceptTouchEvent(ev)) { //1.是否拦截
return onTouchEvent(ev);
}else {
targetView = findMotionEventTargetView(ev); //2.子view是否消耗
if(null != targetView){
return targetView.dispatchTouchEvent();
}else{
return super.dispatchTouchEvent(ev);
}
}
}
对于拦截的的条件分支,我们看到如下情况:
a. 目标targetView
将保持为null
b. 将直接调用onTouchEvent
方法
对于ViewGroup不拦截本事件,那么我们就需要寻找一个可以处理本事件的View,也就是targetView,具体的查看源码里面有,无非就是遍历所有的子View,然后查找合适的View,当然这个findMotionEventTargetView
方法里面也是别有洞天的,大致伪代码为:
//ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
final boolean handled;
//...代码省略
handled = child.dispatchTouchEvent(transformedEvent);
return handled;
}
如果这个方法dispatchTransformedTouchEvent
返回false
,那么我们的targetView
将不会被找到,此时回到dispatchTouchEvent
伪代码中,将会调用ViewGroup#super.dispatchTouchEvent(ev),而此时将会调用View.dispatchTouchEvent
方法,dispatchTouchEvent
中将会调用onTouchEvent
将会被调用。
- 如果此时MotionEvent为非ACTION_DOWN,比如ACTION_MOVE、ACTION_UP、ACTION_CANCEL事件时,此时用伪代码表示为:
public boolean dispatchTouchEvent(MotionEvent ev) {
if(null == targetView) {
return onTouchEvent(ev);
}else {
return targetView.dispatchTouchEvent();
}
}
此时需要用到ACTION_DOWN事件我们查找出来的targetView
了,如果targetView
不存在,直接还是调用自身的onTouchEvent
,而当我们找到了那个targetView
时,事件处理将会出现在targetView.dispatchTouchEvent
事件中。也许这就是这简单的事件分发机制了,下面来总结一下这个过程和其中发生的过程:
a. View事件分发时,首先会调用dispatchTouchEvent
,在该方法中首先会询问自身的onInterceptTouchEvent
是否会拦截此次事件;如果不拦截则遍历查找ViewGroup
中所有子View,如果找到一个可以处理该事件的View,则会调用View的dispatchTouchEvent
,进而会调用View的onTouchEvent
,如果onTouchEvent
返回值为false,表示将不会消耗该事件,那么说明此次事件不能被消费;如果返回为true,表明此次将会消费此次事件,并将此view标记为targetView
。
b. 如果找到targetView
,接下来的ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
等非ACTION_DOWN
事件将会通过targetView.dispatchTouchEvent
依次调用,并完成相应逻辑;如果没有找到targetView,那么该直接View将不再询问onIntercept
方法,而是直接调用自身的onTouchEvent
方法,完成相应的逻辑。
4.3 滑动冲突解决方式
4.3.1 外部拦截法
所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法主要是重写OnInterceptTouchEvent
方法,在内部做相应的拦截处理即可,一般伪代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercept = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前事件) {
intercept = true;
} else {
intercept = false;
}
break;
default:
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercept;
}
这个是典型的外部拦截逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的条件即可,其他均不需要做修改并且也不能修改。在onInterceptTouchEvent
方法中,首先是ACTION_DOWN
事件,父容器必须返回false,即不拦截ACTION_DOWN
事件。因为父容器一旦拦截了ACTION_DOWN
,那么后续的ACTION_MOVE
和ACTION_UP
事件将会直接交给父容器处理,无法再传递给子元素;其次是ACTION_MOVE
事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则为false;最后是ACTION_UP
事件,这里需要要返回false,因为ACTION_UP事件本身没有太多意义。
4.3.3 内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent
方法才能正常工作,使用起来比外部拦截法显得复杂。
伪代码如下,需要重写子元素dispatchTouchEvent
方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
parentView.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG,"deltaX :" + deltaX + ",deltaY:" + deltaY);
//如果水平距离大于竖直距离,事件将会被父组件拦截
//否则事件还是给与自己使用
if(父类组件需要当前点击事件){
parentView.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x ;
mLastY = y ;
return super.dispatchTouchEvent(ev);
}
这就是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动且不能做改动。除了子元素需要做处理之外,父元素中也要默认拦截ACTION_DOWN以外的事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)
,父元素才能继续拦截所需要的事件。
对于父容器不能拦截ACTION_DOWN
事件?因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT
标记位控制,所以一旦父容器拦截ACTION_DOWN
事件,那么所有的事件都无法传递到子元素中,这样内部拦截就无法起作用了。父元素需要修改的伪代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
mLastX = x ;
mLastY = y ;
return false;
}
return true ;
}
文中部分内容来自经典读物<<Android开发艺术探索>>,也算是自己对View事件的一个简单概括吧。