Android系统分析之View的基础/滑动和事件

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();
        }
    }
}

Android 模拟触摸动作MotionEvent事件

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事件分发机制详解:史上最全面、最易懂

dispatchTouchEvent源码分析

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

《Android源码设计模式解析与实战》

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值