第3章 View的事件体系

第3章 View的事件体系

[TOC]

3.1 View基础知识

1. View的位置参数

首先来认识一下View的位置参数,因为这关系到View的测量,这跟View的绘制有关系,因此认识View的位置参数是很有必要的。

简单来说有这么8个,可以分为3类:

  1. left,top,right,bottom
  2. x,y
  3. translationX,translationY

注:
这里需要提前声明一下,这些位置参数都是相对于父控件的,因此不要混淆,下面具体介绍一下。

  1. left,top,right,bottom这四种从名字就能知道这是View控件相对于父控件的左上右底四个边缘的坐标
  2. x,y是左上角相对于父控件的坐标
  3. translationX,translationY是左上角相对于父控件的偏移量,一般为0,那为什么要有这两个属性?因为有时View会移动,但是父控件没有移动,此时translationX和translationY有助于获取到移动后的左上角的XY坐标,由此可得到换算公式:
x = left + translationX
y = top + translationY

然后这几个位置参数都是可以通过方法获取到的,如下所示。

Log.i(TAG, "(left " + v.getLeft() 
        +", top " + v.getTop() 
        +", right " + v.getRight() 
        +", bottom " + v.getBottom() 
        +", tranxlationX " + v.getTranslationX() 
        +", x " + v.getX() 
        +", translationY " + v.getTranslationY() 
        +", y " + v.getY() 
        * ")");

然后通过实际效果更能清楚的认识这几个类,从下图中可以看出来上面那些位置参数都是相对于父控件的。

View的位置参数.png

2. MotionEvent和TouchSlop

2.1 MotionEvent
MotionEvent是点击事件中不可缺少的类,所有关于点击事件的信息都集成在该类上,因此需要来了解该事件类,先总结一下从MotionEvent中可以获得到什么,主要有两个。

  1. 事件类型信息ACTION:ACTION_DOWN,ACTION_MOVE,ACTION_UP,通过event.getAction()获得
  2. 坐标信息:(x,y)相对点击的控件左上角的坐标,(rawX,rawY)绝对坐标,分别通过event.getX()和event.getRawX()获得

首先最主要的是MotionEvent中的三个事件类型:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指从屏幕上松开的一瞬间

然后MotionEvent可以获取到点击的位置位于所点击的控件的相对坐标以及绝对坐标:
x:相对x坐标,相对于点击控件左上角的x坐标,比如点击在Button的左上角时,x == 0
y:相对y坐标,相对于点击控件左上角的y坐标,比如点击在Button的左上角时,y == 0
rawX:绝对x坐标,相对于屏幕左上角
rawX:绝对y坐标,相对于屏幕左上角

2.2 TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离常量,意思就是当手指在屏幕上滑动时,如果滑动的距离小于该常量,则不认为在进行滑动操作。

不同的屏幕对应有不同的TouchSlop的值


3.2 View的滑动

在Android中View的滑动时很重要的,掌握滑动的方法是实现绚丽的自定义控件的基础。实现View的滑动的方式总共有三种:

  1. 通过View本身提供的scrollTo/ScrollBy方法
  2. 通过动画给View施加平移效果
  3. 通过改变View的LayoutParams使得View重新布局

1. 使用scrollTo/scrollBy

scrollTo(x,y)是绝对滑动
scrollBy(x,y)是相对于当前位置的滑动,即相对滑动,其实内部也是用scrollTo(x,y)实现的

下面是源码:

/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
 */
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();
        }
    }
}

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

这里需要知道的是,scrollTo和scrollBy滑动的只是View内容的位置,View处于布局中的位置并没有改变。
由源码中可以发现,有两个变量在滑动中有重要的作用,即mScrollX和mScrollY,它们的意思是view的边缘与view内容边缘的距离。

mScrollX = View的左边缘 - View内容的左边缘
mScrollY = View的上边缘 - View内容的上边缘

也就是说,在内容移动之前,mScrollX和mScrollY都是0,当内容偏向左边100单位的时候,mScrollX== 0 - (-100) == 100,当内容偏向右边100单位的时候,xScrollX == -100,总而言之,当View的内容往屏幕正方向(包括x和y方向)移动时,mScrollX和mScrollY就会减小,如图所示。

mScrollX和mScrollY的变换规律示意图.PNG

因此可得出结论:

  1. scrollTo给定(0,0)参数时,内容返回原位,给定负数的参数时,内容往相对于原点的正方向移动变化
  2. scrollBy给定负数的参数时,内容往屏幕正方向移动;反之往负方向移动
  3. 记住,scroll的移动只是移动View的内容,而不是移动View

下面是简单使用scrollTo和scrollBy的方法,使用效果是,每点击一次按键,btn1的内容就会向下移动1单元,移动到10单元的时候就会回到原位置。

@Override
public void onClick(View v) {
    // TODO Auto-generated method stub
    switch (v.getId()) {
    case R.id.btn_move:
        
        Log.i("MainActivity", btn1.getScrollY()+"");
        
        if(btn1.getScrollY() == -10)
        {
            btn1.scrollTo(0, 0);
        }else
        {
            btn1.scrollBy(0, -1);
        }
        
        break;

    default:
        break;
    }
}

2. 使用动画

另一种使View滑动的方式是使用动画,而动画的本质是通过改变View的translationX和translationY位置参数来达到移动的目的的。还记得在第一节讲过的关于translationX(Y)的意义和计算公式吗,这里再复习一遍。

x = left + translationX
y = top + translationY

其中x,y为View相对于父控件的位置,left和top是View相对于父控件的左边缘和上边缘,原始原始情况下translationX和translationY为0,如果这两个变量改变的话,x和y就会被改变,导致View的位置发生改变从而发生滑动。

而动画分为两种,分为传统的View动画和属性动画,属性动画不兼容3.0以下的,需要采用开源动画库nineoldandroid。

关于动画需要注意的是,动画移动的只是影像,“真身”还是留在了原来的位置,意思就是,比如使用动画将一个Button移动到了下方100px,但是会发现点击移动后的Button没有点击效果,反而点击原来的位置才有效。这个问题使用属性动画可以解决。

3. 改变布局参数

这就跟View的绘制有关系了,View的绘制都是通过获取View的布局参数来将View绘制到屏幕上的,那么我只要将View的布局参数改变了,然后再重新绘制它就可以造成移动的效果了。所以改变布局参数步骤上大概可以分成3步:

  1. 先获取到View的布局参数对象(LayoutParams)
  2. 修改该对象的参数
  3. 重绘View或者View重新设置布局参数

下面示例一个向右移动100px的实例

MarginLayoutParams marginLayoutParams = (MarginLayoutParams) btn1.getLayoutParams();
marginLayoutParams.leftMargin += 100;
btn1.requestLayout();
//或者 
//btn1.setLayoutParams(marginLayoutParams);

4. 3种滑动方式的比较

scrollTo/By:
优点:对View内容实现滑动效果操作简单
缺点:只能滑动View的内容,并不能滑动View本身

动画:
优点:操作简单,主要适用于没有交互的View和实现复杂的动画效果
缺点:使用传统动画的话会造成不能改变View本身的属性

改变布局参数:
优点:适用于有交互的View滑动
缺点:操作稍微复杂


3.3 View的事件分发

1. 点击事件的传递规则

事件分发的传递过程:Activity(Decor View)->Windows->View

事件分发机制是View的一个核心知识点。首先要明白,事件的分发指的就是MotionEvent对象的分发。分发的过程主要由这3个方法来完成:

  1. dispatchTouchEvent(MotionEvent ev):用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用。
  2. onInterceptTouchEvent(MotionEvent ev):用于进行事件的拦截。如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用。
  3. onTouchEvent(MotionEvent ev):用于进行事件的消费。如果不消耗,则在同一事件序列中,当前View无法再次接收到事件(效果和onInterceptTouchEvent返回false一样)。

下面通过一段伪代码来表现这三个方法的关系:

public boolean dispatchTouchEvent(MotionEvent ev)
{
    boolean consume = false;
    if(onInterceptTouchEvent(ev))
    {
        //其实还有个onTouch()在onTouchEvent的前面
        //if(!onTouch(ev))
        consume = onTouchEvent(ev);
    }else
    {
        consume = child.dispatchTouchEvent(ev);
    }

    return consume;
}

其实在onTouchEvent之前还有个onTouch方法,如果onTouch返回false时onTouchEvent才会被执行。

这里先提前说一个规律,便于后面的理解,那就是如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用。这个过程可以这么理解,假设点击事件是一个难题,难题被领导分给了程序员处理(事件分发),如果程序员处理不了(onTouchEvent返回了false),就会将难题分发给领导(上级的onTouchEvent被调用)。

下面给出一些重要的结论,根据这些结论可以更好地理解整个传递机制。

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束时这一过程中所产生的一系列事件。即事件序列以down事件开始,经过move,到up事件结束。
  2. 一个事件序列只能被一个View拦截并消耗。这里的拦截有两种方式,第一种是从上之下分发事件的过程中被某个View拦截,另一种是底层View不消耗事件,将事件重新返回给父容器处理。
  3. 某个View一旦决定拦截,那么整个事件序列都只由它处理,并且它的onInterceptTouchEvent不会再被调用。
  4. 如果View不消耗除ACTION_DOWN以外的其他事件(即View在onTouchEvent中拦截消耗了DOWN事件,对MOVE,UP事件都返回false),对于其他的事件(MOVE,UP)此时父容器的onTouchEvent并不会被调用(毕竟事件序列已经被View拦截了),而是会传递给Activity的onTouchEvent处理。(这点有点难理解)
  5. ViewGroup在事件从上至下分发过程中默认不拦截任何事件,即onInterceptTouchEvent返回false。View没有onInterceptTouchEvent方法。
  6. (可点击的)View的onTouchEvent默认消耗事件(返回true),不可点击的View比如TextView的onTouchEvent是不消耗事件的(返回false)
  7. onClick发生的前提是View是可点击且收到down和up事件。
  8. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ANTION_DOWN除外(这点后面会根据源码解释)。

下面是事件分发的规律图。

事件分发机制.PNG

*2. 事件分发的源码解析

接收到事件的首先是Activity,Activity不是View,但是具有和View差不多的事件传递的方法,该方法之中其实是由Activity中的decor view来进行事件的分发的,以下先来分析Activity对事件的传递。

源码:Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看到里面用了getWindow().superDispatchTouchEvent(ev)将事件分发下去,里面其实就是通过一个Decor View来进行事件的分发的。这里为了说明主要问题,所以不展开介绍。

Activity将事件传递下去,一般接收到事件的就是ViewGroup,因此接下来看ViewGroup的事件分发源码。在看源码之前,我们先来回忆一下,ViewGroup分发源码的一些结论:

  1. dispatchTouchEvent中
    返回true时,丢弃事件;
    返回false时,表示不分发,将事件返回给父容器的onTouchEvent处理;
    返回super.dispatchTouchEvent时,将事件传给onInterceptTouchEvent;
  2. onInterceptTouchEvent中
    返回true时,表示拦截,触发onTouchEvent;
    返回false时,表示不拦截,将事件分发给子View;
    返回super.onInterceptTouchEvent时,跟返回false一样(默认不拦截事件)
  3. onTouchEvent中
    返回true时,表示消耗事件;
    返回false时,表示不处理,将事件返回给父容器的onTouchEvent;
    返回super.onTouchEvent,跟true一样

然后我们带着结论看源码,从源码中找出得出该结论的原因:

源码片段:ViewGroup#dispatchTouchEvent

// Check for interception.
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;
}

从源码中可以看出:

  1. 注意intercepted标志位后面是ed结尾的,因此它表示的意思是,已经被拦截
  2. 有两种情况来判断是否要拦截当前事件:
    ACTION_DOWN:按下事件时;
    mFirstTouchTarget != null:这里mFirstTouchTarget是一个很重要的变量,可以理解为处理事件的子元素(后继有人),即当之前(注意是之前)往下传递的事件被某个View成功处理之后,就知道“后继有人”了,即mFirstTouchTarget被赋值。也就是说当后继有人时,就会进入判断需不需要拦截。
    如果两个条件都不能满足时,则说明已经被当前对象拦截,intercepted设成true。
  3. FLAG_DISALLOW_INTERCEPT标志位决定了要不要触发onInterceptTouchEvent;
    当该标志位为0时,表示拦截,触发onInterceptTouchEvent处理事件ev;
    当该标志位为1时,表示不拦截,设置intercepted为false;
    (其实onInterceptTouchEvent默认返回false,因此只要FLAG_DISALLOW_INTERCEPT被设置了,就不会进行拦截)
  4. 当面对ACTION_DOWN事件时,ViewGroup总是会调用onInterceptTouchEvent方法来询问自己是否要拦截事件

然后可以得出上面所说的两个结论:

  1. (结论3)当某个View(包括ViewGroup)决定拦截(DOWN事件被消费),那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用,而且它的父元素对同一序列事件不再拦截。
    原因:在dispatchTouchEvent之中的mFirstTouchTarget为null,不会再向下分发事件,然后onInterceptTouchEvent也执行不到,因此在拦截之后onInterceptTouchEvent只会执行一次。对于父元素来说,由于mFirstTouchTarget不为null,因此对后面的事件都不拦截(注意,每个ViewGroup都有一个mFirstTouchTarget)
  2. (结论8)在子元素中可以干预父元素的事件分发,但ACTION_DOWN除外。
    原因:子元素通过requestDiallowInterceptTouchEvent可以改变父元素的FLAG_DISALLOW_INTERCEPT标志位,而该标志位决定了父元素要不要执行onInterceptTouchEvent。而ACTION_DOWN除外的原因在于在每个DOWN事件到来时,标志位就会复位。

接着再看当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View,这里就不贴出源码了,直接把源码中的特点列出来:

  1. ViewGroup会遍历ViewGroup内部的子View,然后找出符合点击条件的子View
    条件:子View是否在播动画
    点击事件的坐标是否落在子View的区域内
  2. 通过dispatchTransformedTouchEvent分发处理事件
  3. 如果事件被子View处理,mFirstTouchTarget就会在addTouchTarget中被赋值,此时父元素对后面的事件都不会拦截(结论3)
  4. 如果事件不能被子View处理(包括子View为空或者子View的onTouchEvent返回false),mFirstTouchTarget为空,则会将事件传递给作为一个View的自身(就是ViewGroup本身)

源码片段:ViewGroup#dispatchTransformedTouchEvent

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

注:这里不要混淆了super和父容器的概念,super并不是表示父容器,super的意思是父类对象,指的还是本身,只不过引用变了,因此上面super.dispatchTouchEvent的意思是ViewGroup作为一个View(View是ViewGroup的父类)对象来调用dispatchTouchEvent方法来处理事件。

从上面我们已知mFirstTouchTarget会决定了父容器要不要对之后的事件进行拦截,这很重要,而且已知是在addTouchEvent中被赋值的,因此我们从源码中看一下实现过程:

源码片段:ViewGroup#dispatchTouchEvent

/*
    当事件分发到child中被处理之后就会触发addTouchTarget方法
 */
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    // Child wants to receive touch within its bounds.
    mLastTouchDownTime = ev.getDownTime();
    mLastTouchDownIndex = childIndex;
    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}

源码片段:ViewGroup#addTouchTarget

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    //在这里mFirstTouchTarget被赋值
    mFirstTouchTarget = target;
    return target;
}

还是用流程图比较清晰地显示ViewGroup的dispatchTouchEvent处理流程。

ViewGroup的dispatchTouchEvene处理流程.PNG

上面说明了ViewGroup的对事件的分发处理过程,总结来说就是,看是否拦截事件,不拦截的话就遍历子View将事件传递下去,若事件被处理了则返回,若事件没有被处理则ViewGroup自行处理该事件;若拦截事件也是自行处理该事件。
下面来介绍View对点击事件的处理过程。(注意ViewGroup也是个View,当ViewGroup拦截事件时,ViewGroup是作为一个View来处理事件的,因此View的点击事件的处理也是很重要的)

由于View的dispatchTouchEvent不用再往下分发事件了,只需要看看需不需要消耗该事件,因此比ViewGroup的dispatchTouchEvent简单得多。

源码片段:View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {

    ...

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    ...

    return false;
}

可以发现,View的dispatchTouchEvent主要有两步:

  1. 判断有没有设置OnTouchListener,如果有则调用OnTouchListener的onTouch方法
  2. 调用onTouchEvent方法

有以下结论:

  1. 当onTouch方法返回true的时候,onTouchEvent不会被调用
  2. onTouchEvent返回true时,View的dispatchTouchEvent返回true,表明View消耗了该事件,此时返回到父元素ViewGroup的dispatchTouchEvent中(因为子类的dispatchTouchEvent是在父元素的dispatchTouchEvent中调用的),mFirstTouchTarget会被设置;反之返回false,表明View并不消耗该事件

看完View的事件分发流程之后,来看一下View对事件的处理流程,即onTouchEvent。

源码片段:View#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {

    ...

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                ...

                if (!mHasPerformedLongPress) {
                    // This is a tap, so remove the longpress check
                    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)) {
                            //触发点击事件
                            performClick();
                        }
                    }
                }

                ...
            }
            break;
            ...
        }
        //只要View是可点击的(CLIKABLE或者LONG_CLICKABLE),就会消耗掉事件
        return true;
    }

    //默认返回false,即不消耗事件
    return true;
}

在View的onTouchEvent中主要关注以下几点:

  1. 如果View是可点击的(CLIKABLE或者LONG_CLICKABLE),就会消耗掉事件,即返回true。比如Button是可点击的,TextView是不可点击的。
    如果View不是可点击的,默认返回false,即不消耗事件
  2. 点击事件onClick会在onTouchEvent的UP事件中被触发,因此如果onTouchEvent不能被执行的话,onClick也不会被执行。

View事件分发总结
View事件分发我们从ViewGroup将事件往下分发,因为Activity中接收到的事件也是由顶级View分发的,而这顶级View也是一个ViewGroup。
在总结事件分发之前,一定要明白,一般分析的是一个事件序列的事件分发,即从DOWN事件到UP事件的分发,因为拦截一般是对DOWN以后的事件进行的,因为往往是由于DOWN被消耗之后就决定了该View会拦截以后的所有事件。

  1. ViewGroup的dispatchTouchEvent
    首先ViewGroup接收到事件,调用dispatchTouchEvent分发事件。首先有两个判断条件决定要不要拦截,一个是否为DOWN事件,另一个是mFirstTouchTarget对象是否为空
    如果为DOWN事件或者mFirstTouchTarget不为空就会触发onInterceptTouchEvent方法(中间省略了FDD标志位这一步),而onInterceptTouchEvent方法默认情况下返回false,也就是不拦截,将intercepted变量设为false;
    如果不为DOWN事件并且mFirstTouchTarget为空的话,表示拦截,将intercepted变量设为true

然后如果决定不拦截,则遍历ViewGroup里面的子View,找到满足条件的子View(没有在进行动画和事件位置落在控件区域内),调用子View的dispatchTouchEvent处理事件,完成事件的分发。此时如果子View的dispatchTouchEvent返回true,则说明事件被子View拦截处理了,此时mFirstTouchTarget会被赋值,如果返回false,说明事件没有被处理,mFirstTouchTarget还是为空。

最后判断mFirstTouchTarget是否为空,如果为空则说明事件没有被子类处理,所以此时ViewGroup调用super.dispatcherTouchEvent作为View来处理事件

这就是ViewGroup的dispatchTouchEvent流程,简单总结就是两个过程:
从上至下:判断事件需不需要拦截,如果不需要则将事件分发给子View处理,如果需要拦截则自行处理事件();
从下至上:如果之前事件不拦截交与子View处理,并且子View并没有处理(dispatchTouchEvent返回false),此时ViewGroup本身会作为View来调用dispatchTouchEvent自行处理事件

  1. View的dispatchTouchEvent
    上面说到了ViewGroup不拦截事件时会调用子View的dispatchTouchEvent方法,或者子View不处理事件时自行作为View处理事件,因此View的dispatchTouchEvent是很重要的。
    在View的dispatchTouchEvent中,首先会判断View是否设置了OnTouchListener以及调用onTouch方法,如果onTouch方法返回true,则dispatchTouchEvent会返回true表明事件被消费,并且不会执行onTouchEvent方法;
    如果onTouch方法返回false,则onTouchEvent会被调用,此时如果onTouchEvent返回true,则dispatchTouchEvent也会返回true。
    最后View的dispatchTouchEvent默认返回false

  2. View的onTouchEvent
    在事件的最后肯定是onTouchEvent在处理,而onTouchEvent中也比较简单,主要有两点,一是可点击的View默认返回true,不可点击的View默认返回false;二是onClick方法会在UP事件中被调用,因此onClick方法调用的前提是onTouchEvent方法被执行。

到此事件分发便完成了,主要从ViewGroup的dispatchTouchEvent,View的dispatchTouchEvent和View的onTouchEvent三个方法来进行分析的。


3.4 View的滑动冲突

滑动冲突产生原因:内外两层同时可以滑动,这个时候会产生滑动冲突。
滑动冲突是一个核心章节,前面几节的内容都是为此节服务的。

总的来说View的滑动冲突有两种解决方法,一种是外部拦截法,一种是内部拦截法。

外部拦截法比较简单也比较容易理解,因此只介绍外部拦截法。这里拿ViewPager来举例,比如微信的界面,左右滑是切换界面,上下滑是滑动列表,因此这里需要解决滑动冲突。冲突的解决目的很明确,就是横向滑动时滑动的就是界面,纵向滑动时滑动的就是列表,因此根据目的我们可以知道,只要将不同的事件分发给不同的控件就行了。而这里假设外层是一个横向的LinearLayout,界面里面是一个ListView,因此我们需要把横向滑动的事件给LinearLayout处理,把纵向滑动事件给ListView处理。而ListView是位于LinearLayout里面的,因此LinearLayout是ListView的父容器,所以整个问题就转化成了,在事件分发的过程中,如果判定为横向滑动,则LinearLayout把事件拦截处理,否则将事件分发给ListView。

在前面一节中可以知道,在ViewGroup的onInterceptTouchEvent中默认返回的是false,因此我们可以重写onInterceptTouchEvent来达到拦截事件的目的。需要注意的点主要有:

  1. DOWN和UP事件都需要返回false,因此如果返回true的话,事件会被拦截导致子类直接收不到事件而不是经过判断之后才决定拦不拦截。
  2. 在MOVE事件中判断是否需要拦截,也就是判断是否为横向滑动,如果是横向滑动则返回true,反之返回false。
  3. 最后不要忘了在onTouchEvent中进行事件的处理。

以下是简单的代码,只要是为了体现外部拦截法的写法。

public class HorizontalScrollViewEx extends ViewGroup
{
    ...

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                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;
            default:
                break;    
        }

        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

    @Override
    public boolean onTouchEvene(MotionEvent event)
    {
        ...

        return true;
    }


    ...
}

推荐阅读更多精彩内容