介绍
当我们手指在屏幕上进行操作时,系统根据我们的操作进行反应,系统为了更好的记录我们的操作,将我们的手指的所有操作分为三类操作,统一封装在MotionEvent类中。
把大象装进冰箱只要三部:1.打开冰箱 2.把大象扔进去 3.关闭冰箱
操作分类只有三类: 1.手指按下屏幕 2. 按下之后在屏幕各种滑动 3. 手指从屏幕离开
将我们日常生活中所能进行的所有操作进行抽象化,执行抽象成两种结果
1. 手指按下屏幕—–>手指离开屏幕 (相当于点击点击,没有任何滑动)
2. 手指按下屏幕—–>无数个手指滑动——>手指离开屏幕
如果滑动1cm是一个滑动,那么滑动10cm就是十个滑动,这也是为什么 抽象结果2中是 无数个滑动
记录手指操作
上面说到过我们的所有手指操作都被记录在MotionEvent这个类中,除了记录我们是按下屏幕还是滑动屏幕还是离开屏幕,还记录了
触摸点坐标 ——-通过getX()和getRawX() 可以活动手指的触摸坐标
重要函数介绍
- dispatchTouchEvent(MotionEvent event)
如果一个View能够接收到我们的触摸事件,那么这个方法就会被执行
- onInterceptTouchEvent(MotionEvent event)
是否拦截这个触摸事件(ViewGroup有这个函数,单一View没有)
- onTouchEvent(MotionEvent event)
对系统记录的触摸实践进行处理的逻辑写在这里面
例子
MyLayout ——-最外层的布局 继承自LinearLayout
MyLayoutB ——-第二次布局 在最外层布局的包裹内,继承自LinearLayout
MyView ——-一个具有点击能力的控件继承自Button
布局如下
重写MyLayout和MyLayoutB 的三个函数
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TAG","MyLayoutB"+"------dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TAG","MyLayoutB"+"------onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TAG","MyLayoutB"+"------onTouchEvent");
return super.onTouchEvent(event);
}
对于MyView只用重写两个函数
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("TAG","MyView----dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TAG","MyView----onTouchEvent");
return super.onTouchEvent(event);
}
接下来我们点击一下MyView看一下Log输出
从Log 可以看出 最先处理我们触摸的最外层的控件,从MyLayout—-MyLayoutB—-MyView。
但是呢,对于每个ViewGroup中又都是先调用dispatchTouchEvent,然后调用onInterceptTouchEvent
对于每个单一View而言是 先调用dispatchTouchEvent然后调用onTouchEvent
总结:
1. 对于ViewGroup(拥有子控件的ViewGroup)他们只用考虑,自己是否接收这个触摸事件(dispatchTouchEvent)和自己是否要拦截这个触摸事件,不让自己的子控件接触这个触摸事件(onInterceptTouchEvent)
2. 对于单一View(也就是没有子控件的View)他们只能考虑,自己接收接收这个触摸事件(dispatchTouchEvent),自己是否要处理这个触摸事件(onTouchEvent)
3. 从Log可以看出从第七行开始Log把上面输出的Log又输出的一遍,也就是流程走了两遍。这是因为按下屏幕是一个触摸事件,离开屏幕又是一个触摸事件,所以Log打印了两遍
onInterceptTouchEvent()返回值探究
我们再看onInterceptTouchEvent这个函数,这个函数拥有一个boolean的返回值。
如果返回true :这个控件要拦截这个触摸事件,不让子控件获得触摸事件
如果返回false:把这个触摸事件传给子控件,让他先进行处理
我们现在就让MyLayoutB 拦截我们的触摸事件,看看MyView能不能进行对触摸事件的响应(能不能打印出MyView的Log)
将MyLayoutB的onInterceptTouchEvent函数进行修改,然后进行点击
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TAG","MyLayoutB"+"------onInterceptTouchEvent");
// return super.onInterceptTouchEvent(ev);
return true;
}
可以看到这里完全没有MyView的事了,也就是MyView没有收到点击事件,即使我们是在MyView上进行点击
总结:
如果onInterceptTouchEvent如果返回了true就代表当前控件要拦截触摸事件,那么这个控件的所有的子控件都没有机会对当前的触摸事件进行响应
dispatchTouchEvent()返回值探究
dispatchTouchEvent()当控件一接收到触摸事件,这个方法就会被调用
这里比较麻烦,我们先上结论
验证子View的dispatchTV返回ture,子控件处理触摸事件
- 将上面对MyLayoutView 的onInterceptTouchEvent方法返回子改回false
- 对MyView进行如下修改
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("TAG","MyView----dispatchTouchEvent");
//super不能省略,因为如果该控件具有点击能力便会调用onTouchEvent函数
//否则直接返回false
super.dispatchTouchEvent(event);
return true;
}
结果如下
可以看到这里值调用了MyView.onTouchEvent,并没有调用MyLayout、MyLayoutB的OnTouchEvent方法,证明了上图右下角的结论:当child.dispatchTouchEvent返回true是事件传递结束。下面为了更加深入的证实这个结论,我们将阅读源码
源码验证dispatchTouchEvent
我们最上面的结论得知
1.触摸事件是从顶层View传递到底层View
2.当触摸时间传递到一个View时,就会调用当前View的dispatchTouchEvent方法。
所以在最底层的视图是单一View,单一View的父级视图大多都是ViewGroup,所以这里我们查看ViewGroup的dispatchTouchEvent的源码
public boolean dispatchTouchEvent(MotionEvent ev) {
...
/*
disallowIntercept 子View可以通过调用requestDisallowInterceptTouchEvent进行赋值
onInterceptTouchEvent(ev) 返回值默认为false
所以如果我们没有对onInterceptTouchEvent(ev)为false也就是不拦截事件,就会遍历当前控件的所有子视图,并根据点击位置判断点击的是哪一个子视图,然后调用被点击的子视图的dispatchTouchEvent
*/
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
//通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
//判断当前遍历的View是不是正在点击的View
//如果是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
//条件判断的内部调用了该View的dispatchTouchEvent()方法
if (child.dispatchTouchEvent(ev)) {
//调用子View的dispatchTouchEvent返回值如果为true那么当前控件的dispatchTouchEvent就会返回true,并且不执行任何操作
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
/*
一下两种情况会执行下面的代码
1. onInterceotTouchEvent 返回值为true
2. onInterceotTouchEvent 返回值为false,但是子视图的dispatchTouchEvent返回值为false
如果是上面两种情况就会调用ViewGroup的父类View的dispatchTouchEvent(),从而调用当前控件的onTouch()、onTouchEvent()
*/
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
验证单一视图View的dispatchTouchEvent
因为View的dispatchTouchEvent源码比较简单,所以这里以源码进行解释
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
1.如果我们对当前的控件设置了Touch事件监听,那么mOnTouchListener != null就成立为true
2. (mViewFlags & ENABLED_MASK) == ENABLED 一般都为true
3.mOnTouchListener.onTouch(this, event) 如果监听器的onTouch方法返回true的话,View的dispatchTouchEvent就返回true然后执行结束。
*如果上面是哪个条件有一个不满足就会执行当前View自身的onTouchEvent方法
结论
将总结论用一个伪代码来表示就是
public boolean dispatchTouchEvent(MontionEvent){
boolean consume = false ;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume ;
}