1 View基础知识与滑动
1.1 View基础知识
1.1.1 什么是View
View是Android中所有控件的基类,不光是简单的Button和TextView还是复杂的RelativeLayout和Listview,它们的共同基类都是View。所以说,View是一种界面层的控件的一种抽象,它代表了一个控件,除了View,还有ViewGroup,ViewGroup内部包含了许多个控件,即一组View,ViewGroup也继承了View。
1.1.2 View的位置参数
View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right,bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是有下角纵坐标。
(1)图中屏幕上放了一个ViewGroup布局,里面有个View控件
getTop:获取view自身的顶边到其父布局顶边的距离;
getLeft:获取view自身的左边到其父布局左边的距离;
getRight:获取view自身的右边到其父布局左边的距离;
getBottom:获取view自身的底边到其父布局顶边的距离;
(2)MotionEvent的方法:
getX():获取点击事件相对控件左边的x轴坐标,即点击事件距离控件左边的距离;
getY():获取点击事件相对控件顶边的y轴坐标,即点击事件距离控件顶边的距离;
getRawX():获取点击事件相对整个屏幕左边的x轴坐标,即点击事件距离整个屏幕左边的距离;
getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离;
(3)其他
从Android3.0开始,View增加了额外的几个参数,x,y,translationX,translationY,其中x,y是View左上角的图标,而translationX,translationY是左上角相对父容器的偏移量,这几个参数也是相对于父容器的坐标,并且translationX,translationY的默认值为0;和View的四个基本位置参数一样,View也为我们提供了get/set方法这几个换算关系:
x = left + translationX
y = top + translationY
需要注意的是:View在平移的过程中,top和left表示在原始左上角的位置信息,其值并不会发生什么,此时发生改变的是x、y、translationX、translationY这四个参数。
1.1.3 MotionEvent和TouchSlop
1.1.3.1 MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
ACTION_DOWN一手指刚接触屏幕
ACTION_MOVE一—手指在屏幕上移动
ACTION_UP——手机从屏幕上松开的一瞬间
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:
点击屏幕后离开松开,事件序列为DOWN->UP
点击屏幕滑动一会再松开,事件序列为DOwN > MOVE >…..>MOVE-UP
通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,系统提供了两组方法:getX/gety和 getRawX/getRawY。getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标,如1.1.2图所示。
1.1.3.2 TouchSlop
(1)TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏慕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
(2)通过如下方式即可获取这个常量:ViewConfigurtion.get(getContext()).getScaledTouchSlop()。
(3)这个常量有什么意义呢?当我们在处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到常动距离的临界值,因此就可以认为它们不是滑动。
1.1.3.3 模拟触摸动作MotionEvent事件
public class DeviceOperationUtils {
public static void SendBackDown() {
new SendBackDownThread().start();
}
public static void SendSingleTap(View view,float x, float y) {
view.post(new SendSingleTapThread(view,x, y));
}
public static void SendSingleTapDispatch(View view,float x, float y) {
view.post(new SendSingleTapDispatchThread(view,x, y));
}
private static class SendBackDownThread extends Thread {
@Override
public void run() {
Instrumentation inst = new Instrumentation();
inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
}
}
/**
* 事件立刻传递到view
*/
private static class SendSingleTapThread implements Runnable {
private final View view;
private float x;
private float y;
SendSingleTapThread(View view,float x, float y) {
this.view=view;
this.x = x;
this.y = y;
}
@Override
public void run() {
long downTime = SystemClock.uptimeMillis();
final MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
downTime += 100;
final MotionEvent upEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_UP, x, y, 0);
view.onTouchEvent(downEvent);
view.onTouchEvent(upEvent);
downEvent.recycle();
upEvent.recycle();
}
}
/**
* 事件立刻传递到view
*/
private static class SendSingleTapDispatchThread implements Runnable {
private final View view;
private float x;
private float y;
SendSingleTapDispatchThread(View view,float x, float y) {
this.view=view;
this.x = x;
this.y = y;
}
@Override
public void run() {
long downTime = SystemClock.uptimeMillis();
final MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, view.getLeft() + x, view.getTop() + y, 0);
downTime += 100;
final MotionEvent upEvent = MotionEvent.obtain(downTime, downTime,MotionEvent.ACTION_UP, view.getLeft() + x, view.getTop() + y, 0);
view.dispatchTouchEvent(downEvent);
view.dispatchTouchEvent(upEvent);
downEvent.recycle();
upEvent.recycle();
}
}
}
1.1.4 VelocityTracker,GestureDetector和Scroller
1.1.4.1 VelocityTracker
(1)作用:速度追踪,用于追踪手指在屏幕上滑动的速度,包括水平和竖直方向上的速度。
(2)使用过程:
// 首先在View的onTouchEvent方法里追踪:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
// 接着,当我们先知道当前的滑动速度时,这个时候可以采用如下的方式得到当前的速度:
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
// 最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
(3)注意
①获取速度的之前必须先计算速度,即getXVelocity和getYVelocity这两个方法前面一定要调用computeCurrentVelocity方法;
②这里的速度是指一段时间内手指滑动的屏幕像素,比如将时间设置为1000ms时,在1s内,手指在水平方向手指滑动100像素,那么水平速度就是100,注意速度可以为负数,当手指从右向左滑动的时候为负,这个需要理解一下,速度的计算公式如下表示:
速度 = (终点位置 - 起点位置)/时间段
1.1.4.2 GestureDetector
(1)作用:手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
(2)使用过程:
// 首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我们还可以实现OnDoubleTapListener从而能够监听双击行为
GestureDetector mGestureDetector = new GestureDetector(this);
// 解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
// 接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现:
boolean consum = mGestureDetector.onTouchEvent(event);
return consum;
有选择地实现OnGestureListener和OnDoubleTapListener中的方法:
(3)常用的onSingleTapUp(单击),onFling(快速滑动),onScroll(推动),onLongPress(长按)和onDoubleTap(双击),另外要说明的是,在实际开发中可以不使用GestureDetector,完全可以自己在view中的onTouchEvent中去实现。
1.1.4.3 Scroller
(1)作用:弹性滑动对象,用于实现View的弹性滑动。
(2)当使用View的scrollTo/scrollBy方法来进行滑动的时候,其过程是瞬间完成的,这个没有过度效果的滑动用户体验肯定是不好的。这个时候就可以用Scroller来实现过度效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔去完成的,Scroller本身是无法让View弹性滑动,他需要和view的computScrioll方法配合才能完成这个功能。
(3)典型代码
scroller = new Scroller(getContext());
private void smoothScrollTo(int destX,int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果就是慢慢的滑动
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
1.2 View的滑动
1.2.1 使用scrollTo/scrollBy
(1)作用:View提供了专门的方法来实现实现View的滑动,那就是scrollTo/scrollBy。scrollBy实际上也是调用了scrolrTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
(2)本质:scrolTo和scrollBy只能改变View内容的位置而不能变View在布局中的位置。
(3)分析:View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘。所以,如果从左向右滑动,那么mScrollX负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
// mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离
mScrollX = View左边缘 - View内容左边缘
// mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离
mScrollY = View上边缘 - View内容上边缘
(4)点击事件:不会影响内部元素的点击事件。
1.2.2 使用动画
(1)作用:通过动画我们来让一个View移动,而平移就是一种滑动。使用动画来移动View,主要是操作View的translationX,translationY属性,即可以采用传统的View动画,也可以采用属性动画。
(2)本质:View动画是对View的影像做操作,它并不能真正改变View的位置,包括高宽,并且如果希望动画后的状态得以保存还必须将fillAfter属性设置为true,否则动画完成之后就会消失
(3)实现方式1:采用传统的View动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100"
/>
</set>
(4)实现方式2:采用属性动画
ObjectAnimator.ofFloat(testButton,"translationX",0,100).setDuration(100).start();
(5)点击事件:单击新位置不会触发点击事件,因为button的真身并没有发生任何改变,在新位置上只是View的影像而已。解决方法:
①从3.0开始,使用属性动画可以解决上面的问题;
②在新位置预先创建一个和目标Button一模一样的Button,它们不但外观一样连onClick事件也一样。当目标Button完成平移动画后,就把目标Bution隐藏,同时把预先创建的Button显示出来。
1.2.3 改变布局参数
(1)作用:改变布局参数,即改变LayoutParams。
(2)实现方式1:想把一个Button向右平移100px,我们只需要将这个Bution的LayoutParams里的marginLeft参数的值增加100px即可。
(3)实现方式2:view的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,就自动被挤向右边,即实现了向右平移的效果
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) testButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
testButton.requestLayout();
//或者testButton.setLayoutParams(layoutParams);
(4)点击事件:不会影响内部元素的点击事件。
1.2.4 各种滑动方式的对比
(1)scorllBy/To这种方式:是View提供的原生方式,其作用是专门用于View的滑动,它可以比较方便地实现滑动效果并且不影响内部元素的单击事件。
(2)动画:在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,否则就不太适合。动画有一很明显的优点,那就是一些复杂的效果必须要通过动画才能实现。
(3)改变布局参数的方式:主要适用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画去实现会有问题。
1.3 弹性滑动
知道了View的滑动,我们还要知道如何实现View的弹性滑动,比较生硬地滑动过去这种用户体验实在是太差了,因此我们要实现渐进式滑动。共同的思想:将一次大的滑动分成若干个小的滑动,并且在一个时间段完成,实现方式很多,比如:Scroller,Handler#PostDelayed,以及Thread#Sleep。
1.3.1 使用Scroller
(1)Scroller的典型用法:
Scroller scroller = new Scroller(getContext());
private void smootthScrollTo(int destX,int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 1000ms内滑向destX,效果是慢慢滑动
scroller.startScroll(scrollX,0,deltaX,0,1000); // 保存传递的参数
invalidate(); // 导致View重绘,在View的draw()中又调用了computeScroll()方法
}
@Override // computeScroll方法在View中是一个空实现,因此需要我们自己去实现
public void computeScroll() {
if(scroller.computeScrollOffset()) { // true,滑动还未结束
scrollTo(scroller.getCurrX(),scroller.getCurrY()); // 实现滑动
postInvalidate(); // 又一次重绘
}
}
(2)工作原理
①当View重绘后会在draw方法中调用computescroll,而computeScroll又会去向Scroller获取当前的scrollX 和ScrollY,然后通过 scrolrTo方法实现滑动;
②接着又调用postlnvalidate方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll方法被调用;
③然后继续向 Scroller获取当前的scrollX和scrollY,并通过scrolTTo方法滑动到新的位置;
④如此反复。直到整个滑动过程结束。
(3)Scroller方法分析
Scroller的原理,当我们构建一个scroller对象并且调用它的startScroll方法,scroller内部其实并没有做什么,他只是保存了我们传递的参数:
// startX和startY表示的是滑动的起点,dx和dy表示的是要滑动的距离,而duration表示的是滑动时间,即整个滑动过程完成所需要的时间
// 注意这里的滑动是指View内容的滑动而非View本身位置的改变
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
// 返回true表示滑动还未结束,false表示结束,因此这个方法返回true的时候,继续让View滑动,
public boolean computeScrollOffset() {
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// 根据时间流逝的百分比来计算scrollX和Y,改变的百分比值和,这个过程相当于动画的插值器的概念
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
}
}
return true;
}
1.3.2 通过动画
(1)作用:动画本身就是一种渐进的过程,因此通过他来实现滑动天然就具有弹性效果。
ObjectAnimator.ofFloat(testView, "translationX", 0, 100).setDuration(100).start();
(2)利用动画的特性来实现一些动画不能实现的效果,例如:scrollTo
// 动画本质上没有作用于任何对象上,它只是在1000ms内完成了整个动画过程。
// 利用这个特性,就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离
final int startX = 0;
final int startY = 100;
final int deltaX = 0;
final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float fraction = animator.getAnimatedFraction();
testView.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
1.3.3 使用延时策略
(1)核心思想是:通过发送一系列延时消息从而达到一种渐近式的效果
(2)具体来说:①可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。②对于postDelayed方法来说,可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。
(3)用Handler来做个示例,其他方法思想都是类似的
// 大约1000ms内将View的内容向左移动了100像素
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int count = 1;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_SCROLL_TO:
count++;
if(count <= FRAME_COUNT){
float fraction = count / (float)FRAME_COUNT;
int scrollX = (int)(fraction * 100);
testButton.scrollTo(scrollX,0);
handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
}
break;
}
}
};
4 学习链接
Android艺术开发探索第三章——View的事件体系(上)
Android艺术开发探索第三章————View的事件体系(下)
2 事件分发机制概念
Android事件分发机制是Android开发者必须了解的基础。事件传递虽然算不上某个单独的知识点,但是在实际项目开发中肯定会碰到,如果不明白其中的原理,那在设计各种滑动效果时就会感到很困惑。
2.1 事件分发的对象
(1)事件。当用户触摸屏幕时,View或ViewGroup派生的控件,将产生点击事件,即Touch事件。ps:Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。
(2)事件列:从手指接触屏幕至手指离开屏幕,这个过程产生的任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件,如下图:
(3)即当一个MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理。
2.2 事件分发的本质
将点击事件(MotionEvent)向某个View进行传递并最终得到处理。即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理,这个事件传递的过程就是分发过程。
2.3 事件在哪些对象之间进行传递?
(1)Android的UI界面是由Activity、ViewGroup、View及其派生类组合而成的,传递顺序是:Activity(Window) -> ViewGroup -> View。
(2)ViewGroup是容纳UI组件的容器,即一组View的集合,包含很多子View和子VewGroup。ViewGroup本身是View的子类,是Android所有布局的父类或间接父类,项目用到的布局LinearLayout、RelativeLayout等都继承自ViewGroup;ViewGroup实际上也是一个View,比起View它多了包含子View和定义布局参数的功能。
(3)View是所有UI组件的基类,一般Button、ImageView、TextView等控件都是继承父类View,本文的View主要指ViewGroup容器中的子View。
2.4 事件分发过程由哪些方法协作完成?
2.5 Android事件分发流程
事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成,Android事件分发流程如下:
通俗语言总结一下,事件来的时候,Activity会询问Window,Window这个事件你能不能消耗,Window一看,你先等等,我去问问DecorView他能不能消耗。DecorView一看,onInterceptTouchEvent返回false啊,不让我拦截啊,遍历一下子View吧,问问他们能不能消耗,那个谁,事件按在你的身上了,你看看你能不能消耗。RelativeLayout一看,也没有让我拦截啊,我也得遍历看看这个事件发生在那个子View上面,那个TextView,事件在你身上,你能不能消耗了他。TextView一看,消耗不了啊,RelativeLayout一看TextView消耗不了啊,mFirstTouchTarget==null啊,我自己消耗吧,嗯!一看自己的onTouchEvent也消耗不了啊。那个DecorView事件我消耗不了,DecorView一看自己,我也消耗不了,继续往上传,那个Window啊,事件我消耗不了啊。Window再告诉Activity事件消耗不了啊,Activity还得我自己来啊,调用自己的onTouchEvent,还是消耗不了,不要了。
2.2 用一段伪代码来阐述上述三个方法的关系和点击事件传递规则
// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
// 代表是否消耗事件
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
// 如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
// 则该点击事件则会交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// 如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
// 则该点击事件则会继续传递给它的子元素
// 子元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
return consume;
}
4 Activity的事件分发机制
想充分理解Android分发机制,本质上是要理解:Activity对点击事件的分发机制、ViewGroup对点击事件的分发机制、View对点击事件的分发机制。接下来,我将通过源码分析详细介绍Activity、View和ViewGroup的事件分发机制
4.1 源码分析
当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent()进行事件分发,具体是由Activity的Window来完成。以下是Activity的dispatchTouchEvent()的源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
//关注点1
//一般事件列开始都是DOWN,所以这里基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//关注点2
onUserInteraction();
}
//关注点3
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
4.1.1 关注点1
一般事件列开始都是DOWN(按下按钮),所以这里返回true,执行onUserInteraction()。
4.1.2 关注点2,onUserInteraction()源码
public void onUserInteraction() {
}
从源码可以看出:该方法为空方法。从注释得知:当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法,所以onUserInteraction()主要用于屏保。
4.1.3 关注点3,getWindow().superDispatchTouchEvent(ev)
(1)Window类是抽象类,且PhoneWindow是Window类的唯一实现类
/**
* Used by custom windows, such as Dialog, to pass the touch screen event further down the view hierarchy. Application developers should not need to implement or call this.
* superDispatchTouchEvent(ev)是抽象方法,返回的是一个Window对象
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
(2)PhoneWindow:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// mDecor是DecorView的实例,DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类,
// PhoneWindow将事件直接传递给了DecorView。
}
(3)DecorView:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
// DecorView继承自FrameLayout,那么它的父类就是ViewGroup,
// super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()。
}
4.2 总结
由于一般事件列开始都是DOWN,所以这里返回true,基本上都会进入getWindow().superDispatchTouchEvent(ev)的判断。而DecorView继承自FrameLayout,它的父类就是ViewGroup,super.dispatchTouchEvent(event)方法其实就是ViewGroup的dispatchTouchEvent()。所以,执行Activity.dispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event),这样事件就从Activity传递到了ViewGroup。
4 ViewGroup的事件分发机制
那么,ViewGroup的dispatchTouchEvent()什么时候返回true,什么时候返回false?请继续往下看ViewGroup事件的分发机制。
4.1 例子讲解
(1)布局如下:
(2)结果测试
①只点击Button(onClick()打印“button1/2”)
②再点击空白处(ViewGroup_layout的onTouch()打印“ViewGroup”)
(3)从上面的测试结果发现
当点击Button时,执行Button的onClick(),但ViewGroup_layout注册的onTouch()不会执行。只有点击空白区域时才会执行ViewGroup_layout的onTouch()。
(4)结论
①点击事件没被ViewGroup拦截,Button的onClick()将事件消费,事件向下传递到了View;
②点击事件被ViewGroup拦截,事件在ViewGroup_layout的onTouch()消费了,事件不会再继续向下传递。
4.2 源码分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 第一部分,初始化操作。Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 第二部分,检查是否需要ViewGroup拦截Touch事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is alreadya view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 第三部分开始,intercepted=false没有拦截状态
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 计算Touch事件的坐标
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event. Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 第三部分中,i在ViewGroup不拦截事件下事件会向下分发交由它的子View处理
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 第三部分核心:dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent()
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 第三部分最后,intercepted = true 拦截状态
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already dispatched to it. Cancel touch targets if necessary.
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
4.3 第一部分,初始化操作
private void cancelAndClearTouchTargets(MotionEvent event) {
...
clearTouchTargets();
...
}
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;// mFirstTouchTarget赋值
}
}
当ACTION_DOWN时进行初始化和还原操作。在cancelAndClearTouchTargets( )中将mFirstTouchTarget设置为null,且在resetTouchState()中重置Touch状态标识。
4.4 第二部分,检查是否需要ViewGroup拦截Touch事件(重点)
// 判断是否被拦截当前事件
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { // allowIntercept == 0,进入“询问自己是否要拦截事件”逻辑
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else { // allowIntercept != 0,不拦截
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true; // 拦截
}
}
4.4.1 最外面的判断条件
actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
mFirstTouchTarget != null是什么意思呢?触发按下事件时,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,然后在MOVE和UP事件时mFirstTouchTarget不为null。如果按下事件时,立刻由当前ViewGroup拦截,
那么mFirstTouchTarget为null,MOVE和UP事件到来时mFirstTouchTarget != null为false,ViewGroup的onInterceptTouchEvent不会再被调用,所以同一序列中的其他事件都会默认交给此ViewGroup处理。
4.4.2 子View可调用requestDisallowInterceptTouchEvent()让ViewGroup不再拦截事件
(1)子View中通过requestDisallowInterceptTouchEvent()来设置FLAG_DISALLOW_INTERCEPT标记位,修改mGroupFlags的值,然后设置拦截状态。标记位一旦设置后,ViewGroup将无法拦截除除按下事件以外的其他事件,为什么呢?
(2)因为在dispatchTouchEvent中,每次触发按下事件时,在requsstTouchState()中会将disallowIntercept置为0,源码分析如下:
private void resetTouchState() {
clearTouchTargets();
// ~指非,重置后(mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0,即:把falsedisallowIntercept置为0
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // false
if (!disallowIntercept) { // allowIntercept == 0,进入“询问自己是否要拦截事件”逻辑
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else { // allowIntercept != 0,不拦截
intercepted = false;
}
4.4.3 requestDisallowInterceptTouchEvent(true)如何让ViewGroup不再拦截事件
(1)源码分析
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // true --> disallowIntercept != 0,不拦截
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT; // 第20位是1
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 第20位是0
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
(2)二进制知识和几个按位操作符 & | ~
在ViewGroup中FLAG_DISALLOW_INTERCEPT的值为0x80000,化成二进制是1000000000000000000。
true--不拦截,mGroupFlags |= FLAG_DISALLOW_INTERCEPT操作是:? | 1000000000000000000 = 1(第20位是1);
false--拦截,mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT操作是:? & 01111111111111111111 = 0(第20位是0)。
(3)计算allowIntercept == (mGroupFlags & FLAG_DISALLOW_INTERCEPT),唯一有效的就是第20位,因为FLAG_其他位都是0,&的结果还是0没有意义
true,运算是:1--------------------------- & 1000000000000000000 = 1,allowIntercept != 0,不拦截;
false,运算是:0--------------------------- & 1000000000000000000 = 0,allowIntercept == 0,进入“询问自己是否要拦截事件”逻辑。
(4)学习链接
解惑requestDisallowInterceptTouchEvent
4.4.4 结论
(1)requestDisallowInterceptTouchEvent的调用要写在onTouchEvent方法中,而如果在子view中的构造方法或生命周期方法调用时disallowIntercept重置为0,而导致失效。
(1)当面对按下事件时,即使子View调用requestDisallowInterceptTouchEvent(),ViewGroup总会调用onInterceptTouchEvent方法来询问是否要拦截事件。
(3)当ViewGroup决定拦截事件之后,不再调用他的onInterceptTouchEvent方法,那么后续的点击事件将会默认交给此ViewGroup处理。
4.4.5 根据分析得到的价值
(1)onInterceptTouchEvent不是每次事件都会被调用,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup。
(2)当面对滑动冲突时,我们是不是可以考虑用requestDisallowInterceptTouchEvent()方法去解释问题。
4.5 第三部分,intercepted=false没有拦截状态(重点)
// 第三部分开始,intercepted=false没有拦截状态
if (!canceled && !intercepted) {
// 第三部分中,在ViewGroup不拦截事件下事件会向下分发交由它的子View处理。
for (int i = childrenCount - 1; i >= 0; i--) {
// 首先遍历的是ViewGroup的所有子元素
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex);
// 然后判断子元素是否能接受这个点击事件主要是两点来衡量,子元素是否在播动画和点击是按的坐标是否落在子元素的区域内,
// 如果某子元素满足这两个条件,那么事件就会传递给他处理
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds. Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 第三部分核心:dispatchTransformedTouchEvent实际上大部分调用的是子元素的dispatchTouchEvent()-----(1)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// mFirstTouchTarget真正的赋值过程-----(2)
newTouchTarget = addTouchTarget(child, idBitsToAssign); // -----(3)
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
}
// 第三部分最后,intercepted = true 拦截状态
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
(1)核心逻辑
首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。是否能够接收点击事件主要由两个点来衡量:①子元素是否在播放动画,和②点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。
(2)核心方法:dispatchTransformedTouchEvent方法
如果child不等于null,因此他会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理处理,这就从而完成这一轮事件分发;
如果child等于null,执行的是第(4)步——Viewgroup没有子元素这种情况。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
(3)完成了mFirstTouchTarget的赋值并且并终止对子元素的遍历,如果子元素的dispatchTouchEvent返回true,这时我们暂时不考虑事件在子元素的怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分给下一个子元素。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
(4)mFirstTouchTarget其实是一种单链表的结构,mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截机制,如果mFirstTouchTarget为null,那么ViewGroup的默认拦截下来统一序列中所有的点击事件:
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;// mFirstTouchTarget赋值
return target;
}
(5)如果遍历所有的子元素后事件都没有被合适的处理,mFirstTouchTarget未被赋值,ViewGroup会自己处理点击事件,这包含两种情况:
第一是:Viewgroup没有子元素;
第二是:子元素处理了点击事件,但是在dispatchTouchEvent中返回false,这一般是因为子元素在onTouchEvent中返回了false,具体分析在5.2.2(5);
if (mFirstTouchTarget == null) {
// child为null,实际调用的是super.dispatchTouchEvent(event)。
// ViewGroup继承于View(此View是指父View),ViewGroup的dispatchTouchEvent方法在父类——>View中实现。
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
5 View的事件分发机制
5.1 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
/**
* 核心代码
*/
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
5.2 核心代码分析
View点击事件的处理:因为他只是一个View没有子元素,所以无法向下传递,只能自己处理点击事件。View对点击事件的处理过程:首先会判断你有没有设置onTouchListener,如果onTouchListener中的onTouch为true,那么onTouchEvent就不会被调用,可见onTouchListener的优先级高于onTouchEvent,好处就是方便在外界处理点击事件。
5.2.1 核心代码
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 在onTouch()里返回false,!result=true,执行onTouchEvent(event)方法;
// 在onTouch()返回true,!result==false,不执行onTouchEvent(event)
if (!result && onTouchEvent(event)) {
result = true;
}
5.2.2 详细分析
// 只有以下4个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent(event)方法。
第一个条件:li != null;
第二个条件:li.mOnTouchListener != null;
第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED;
第四个条件:li.mOnTouchListener.onTouch(this, event);
下面,我们来看看下这4个判断条件:
(1)li != null,ListenerInfo li = mListenerInfo所以不会是null。
(2)li.mOnTouchListener!= null;
/**
* 只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
(3)(mViewFlags & ENABLED_MASK) == ENABLED,该条件是判断当前点击的控件是否enable,由于很多View默认是ENABLED的,因此该条件恒定为true。
(而如果是DISABLED的,那么li.mOnTouchListener.onTouch(this, event)将永远得不到执行,如果我们想要监听它的touch事件,就必须通过重写onTouchEvent方法来实现。)
(4)li.mOnTouchListener.onTouch(this, event),回调控件的onTouch方法:
// 手动调用设置
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
在onTouch()里返回false,!result=true,执行onTouchEvent(event)方法;在onTouch()返回true,!result==false,不执行onTouchEvent(event),如下代码:
if (!result && onTouchEvent(event)) {
result = true;
}
(5)在onTouch方法里返回false,执行了onTouchEvent(event),如果该控件是可点击的,onTouchEvent(event)就会返回true;如果该控件是不可点击的,就会返回false。
5.3 onTouchEvent(event)的源码分析
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 当View处于不可用的状态下点击事件的处理过程----(4.3.2.1)
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法----(4.3.2.2)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// onTouchEvent(event)中点击事件的具体处理过程,CLICKABLE,LONG_CLICKABLE----(4.3.2.3)
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
// 如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling performClick directly. This lets other visual state of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
/**
* 关注点1:往下看performClick()的源码分析
*/
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
}
// 如果该控件是可以点击的,就一定会返回true
return true;
}
// 如果该控件是不可点击的,就一定会返回false
return false;
}
5.3.1 当View处于不可用的状态下点击事件的处理过程
(1)当设置setEnable(false)时,我们进入onTouchEvent后,看下面的代码:
// 不可用状态下的View照样会消耗点击事件,尽管他看起来不可用
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
// 这是按钮的状态将会变成不可点击状态,所以这时我们点击也不会有任何反应
setPressed(false);
}
// 接着,但还是消费了这次事件
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
(2)结果:会调用setPressed(false);这是按钮的状态将会变成不可点击状态,所以这时我们点击也不会有任何反应。接着,但还是消费了这次事件,也就是上面代码注释的内容。
5.3.2 View设置有代理,还执行TouchDelegate的onTouchEvent方法,这个onTouchEvent的工作机制看起来和onTouchListener类似
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
5.3.3 onTouchEvent(event)中点击事件的具体处理过程
当我们不对setEnable做处理,调用setClickable(false)后,步骤还是如上,只不过这次代码会接着进入点击状态判断的内容:
(1)mPerformClick = new PerformClick();
// 只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管他是不是DISABLE状态
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
black;
}
return true;
}
}
(2)PerformClick()
// 当ACTION_UP事件发生之后,会触发performClick方法,如果View设置了onClickListener,那么performClick方法内部就会调用他的onClick方法
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
(3)setOnClickListener()
View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体的View有关,确切的说是可点击的View其CLICKABLE为true,不可点击的为false。比如:button是可点击的,textview是不可点击的,通过setOnClik或者setOnLongclik都是可以改变状态的,如下:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
注意:当在setOnClickListener()之前调用setClickable(false),在进入setOnClickListener会对clickable状态进行判断,如果为false,会再次把clickable设置为true。所以要设置起作用,需要把setClickable(false)放在setOnClickListener之后。
问题解析链接:从源码的角度分析Android中setClickable()和setEnable()的区别
5.3.4 得出结论:onTouch()的执行高于onClick()
(1)执行流程图
a、在回调onTouch()里返回true
b、在回调onTouch()里返回false
(2)验证
// 设置OnTouchListener()
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("执行了onTouch(), 动作是:" + event.getAction());
return true;// 结果截图1
//return false;// 结果截图2
}
});
// 设置OnClickListener
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("执行了onClick()");
}
});
(3)总结:onTouch()返回true就认为该事件被onTouch()消费掉,因而不会再继续向下传递,即不会执行OnClick()。
6 View的滑动冲突
6.1 常见的滑动冲突场景
(1)外部滑动方向和内部滑动方向不一致;
(2)外部滑动方向和内部滑动方向一致;
(3)上面两种情况的嵌套;
6.2 滑动冲突的处理规则
(1)对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,比如可以依据滑动路径和水平方向做形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。这里我们通过水平和竖直方向的距离差来判断,比如竖直方向滑动的距离大就判断为竖直滑动,否则判断为水平滑动。
(2)对于场景2,比较特殊,它无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View来响应View的滑动,根据这种业务上的需求我们也能得出相应的处理规则,有了处理规则同样可以进行下一步处理。
(3)对于场景3来说,它的滑动规则就更复杂了,和场景2一样,它也无法直接根据滑动的角度、距离差以及速度差来做判断,同样还是只能从业务上找到突破点,具体方法和场景2一样,都是从业务的需求上得出相应的处理规则。
6.3 滑动冲突的解决方式
6.3.1 外部拦截法
(1)应用模板:外部拦截费是指点击事件都先经过父容器的拦截处理,如果父容器需要这个事件就给给他,还得重写我们的onIterceptTouchEvent,如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
/**
* ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,
* 这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,
* 这个时候事件没法再传递给子元素了
*/
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
/**
* ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false
*/
case MotionEvent.ACTION_MOVE:
if("父容器的点击事件"){
intercepted = true;
}else {
intercepted = false;
}
break;
/**
* ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义考虑一种情况,
* 假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,会导致子元素无法接收到ACTION_UP事件,
* 这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它处理,
* 而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。
*/
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = x;
return intercepted;
}
(2)案例
// 父类HorizontalScrollViewEx的拦截事件
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = true;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastXIntercept;
int deltaY = y = mLastYIntercept;
if (Math.abs(deltax) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
6.3.2 内部拦截法
(1)应用模板:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素要消耗此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。
// 重写子元素的dispatchTouchEvent方法
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// 父类不拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if("父容器的点击事件"){
// 父类拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
// 父元素
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
/**
* 为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并接受FLAG_DISALLOW_DOWN这个标记位的控制,
* 所以一旦父容器拦截,那么所有的事件都无法传递到子元素中,这样额你不拦截就无法起作用了
*/
if(action == MotionEvent.ACTION_DOWN){
return false; // 不拦截
}else {
return true; // 拦截
}
}
(2)案例
// 子类ListViewEx的事件分发
public class ListViewEx extends ListView {
public static final String TAG = "ListViewEx";
private HorizontalScrollViewEx mHorizontalScrollViewEx;
private int mLastX = 0;
private int mLastY = 0;
public ListViewEx(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int delatX = x - mLastX;
int delatY = y - mLastY;
if (Math.abs(delatX) > Math.abs(delatY)) {
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
// 父类HorizontalScrollViewEx的事件分发
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
mLastX = x;
mLastY = y;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
return true;
}
return false;
}else {
return true;
}
}
6.4 小书亭的应用案例
(1)代码
/**
* Author: guan
* Version: 2.0.0
* Date: 2019/2/25.
* Mender:
* Modify:
* Description: 用于解决嵌套Scrollview的时候由于多行而产生的滑动冲突问题
* 参考:https://www.jianshu.com/p/44c2e56a43bf,https://blog.csdn.net/sahadev_/article/details/51211057
*/
public class EditTextWithScrollView extends android.support.v7.widget.AppCompatEditText {
/**
* 滑动距离的最大边界
*/
private int mOffsetHeight;
/**
* 是否到顶或者到底的标志
*/
private boolean mBottomFlag = false;
/**
* 是否可以竖屏滑动
*/
private boolean mCanVerticalScroll;
public EditTextWithScrollView(Context context) {
super(context);
init();
}
public EditTextWithScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public EditTextWithScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mCanVerticalScroll = canVerticalScroll();
LogUtils.i("EditTextWithScrollView", "dispatchTouchEvent + onMeasure:" + mCanVerticalScroll);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// 如果是新的按下事件,则对mBottomFlag重新初始化
mBottomFlag = false;
}
// 如果已经不要这次事件,则传出取消的信号,这里的作用不大
if (mBottomFlag) {
event.setAction(MotionEvent.ACTION_CANCEL);
}
LogUtils.i("EditTextWithScrollView", "dispatchTouchEvent + mBottomFlag:" + mBottomFlag);
return super.dispatchTouchEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = super.onTouchEvent(event);
// 如果edittext的高度很大占满了一个屏幕,点不到edittext区域外的地方,所以Scrollview就不能再滑动了
// 所以,当控件内容高度小于控件高度时,继续让父类拦截,允许Scrollview能再滑动
if (mCanVerticalScroll) {
// onScrollChanged方法调用之后onTouchEvent方法也调用了一次requestDisallowInterceptTouchEvent,并设置的参数还是true,
// 也就是说,刚才在onScrollChanged方法中做的处理被取消了。所以,这时我们需要加个标志mBottomFlag判断,此次不需要处理
if (!mBottomFlag) {
getParent().requestDisallowInterceptTouchEvent(true); // 不拦截
}
} else {
getParent().requestDisallowInterceptTouchEvent(false); // 拦截
}
LogUtils.i("EditTextWithScrollView", "onTouchEvent + mCanVerticalScroll:" + mCanVerticalScroll + " mBottomFlag:" + mBottomFlag);
return result;
}
@Override
protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
if (vert == mOffsetHeight || vert == 0) {
// 这里触发父布局或祖父布局的滑动事件
getParent().requestDisallowInterceptTouchEvent(false); // 拦截
mBottomFlag = true;
LogUtils.i("EditTextWithScrollView", "onScrollChanged + 父布局或祖父布局的滑动事件vert:" + vert + " oldVert:" + oldVert);
}
}
/**
* EditText竖直方向是否可以滚动
*
* @return true:可以滚动 false:不可以滚动
*/
private boolean canVerticalScroll() {
// 滚动的距离
int scrollY = getScrollY();
// 控件内容的总高度
int scrollRange = getLayout().getHeight();
// 控件实际显示的高度
int scrollExtent = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
// 控件内容总高度与实际显示高度的差值
mOffsetHeight = scrollRange - scrollExtent;
LogUtils.i("EditTextWithScrollView", "canVerticalScroll + 滚动的距离 scrollY :" + scrollY);
LogUtils.i("EditTextWithScrollView", "canVerticalScroll + 控件内容的总高度 scrollRange :" + scrollRange);
LogUtils.i("EditTextWithScrollView", "canVerticalScroll + 控件实际显示的高度 scrollExtent :" + scrollExtent);
LogUtils.i("EditTextWithScrollView", "canVerticalScroll + 控件内容总高度与实际显示高度的差值 mOffsetHeight :" + mOffsetHeight);
if (mOffsetHeight == 0) {
return false;
}
return (scrollY > 0) || (scrollY < mOffsetHeight - 1);
}
}
(2)效果
(3)学习链接
从ScrollView嵌套EditText的滑动事件冲突分析触摸事件的分发机制以及TextView的简要实现和冲突的解决办法
ScrollView嵌套EditText联带滑动的解决办法
真正完美解决EditText嵌套ScrollView的滑动冲突
7 参考文章与链接
Android艺术开发探索第三章————View的事件体系(下)
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
《Android源码设计模式解析与实战》