Android进阶知识——View的事件体系


本章我们将介绍Android中十分重要的一个概念:View,它的应用十分广泛。比如说自定义控件和解决滑动冲突等,因此学好Veiw的事件体系对于我们开发者而言是十分必要的。

1.View的基础知识

本节我们将主要介绍的内容有:View的位置参数、MotionEvent和TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。

1.1什么是View

首先View是Android中所有控件的基类,无论是Button还是ListView,它们的共同基类都是View。所以,View是一种界面层的控件的一种抽象,它代表了一个控件。而ViewGroup则是一个控件组,也就是说ViewGroup内部包含了许多控件,即一组View。

在Android的设计中,ViewGroup也继承了View,这就意味着View本身就可以是单控件也可以是由多个控件组成的一组控件,通过这种关系就形成了View树的结构。

1.2View的位置参数

View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标。(这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标;在Android中,x轴和y轴的正方向分别为右和下)
在这里插入图片描述
根据上述知识,我们很容易得到View的宽高和坐标的关系:

width = right - left
hight = bottom - top

上述四个参数的获取方式如下:

  • Left = getLeft();

  • Right = getRight();

  • Top = getTop();

  • Bottom = getBottom();

从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这四个参数也是相对于父容器的坐标,并且translationX和translationY的默认值是0。和View的四个基本位置参数一样,View也为它们提供了get/set方法,这些参数的换算关系如下所示:

x = left + translationX
y = top + translationY

注意:View在平移过程中,top和left表示的是原始左上角的位置信息,并且值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。

1.3MotionEvent和TouchSlop

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坐标)

2.TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,也就是说,当在手机屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。

这个常量和设备有关系,在不同设备上这个值可能是不同的,我们可以通过如下方式获取这个常量:ViewConfiguration.get(getContext()).getScaledTouchSlop()。我们可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为它们不是滑动,这样做可以有更好的用户体验。

1.4VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使用过程如下:

  • 首先,在View的onTouchEvent方法中追踪当前单击事件的速度
    VelocityTracker velocityTracker=VelocityTracker.obtain();
    velocityTracker.addMovement(event);
    
  • 接着,调用get…方法获取当前的速度
    velocityTracker.computeCurrentVelocity(1000);
    int xVelocity= (int) velocityTracker.getXVelocity();
    int yVelocity= (int) velocityTracker.getYVelocity();
    

注意:获取当前速度之前必须先计算速度,即getXVelocity和getYVelocity这两个方法的前面必须调用computeCurrentVelocity方法;这里的速度是指一段时间内手指所滑动的像素数,比如将时间间隔设置为1000ms时,在1s内,手指在水平方向从左向右滑动100像素,那么水平速度就是100。注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值。
速度的计算公式如下:

速度 = (终点位置 - 起点位置) / 时间段

另外,compeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,它的单位是毫秒(ms),计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素点。

最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

velocityTracker.clear();
velocityTracker.recycle();

2.GestureDectector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。它的使用过程如下:

  • 首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要我们可以实现OnDoubleTapListener从而能够监听双击行为

    GestureDetector mGestureDetector=new GestureDetector(new GestureDetector.OnGestureListener() {
    	//选择实现以下的方法
        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {
        }
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return false;
        }
        @Override
        public void onLongPress(MotionEvent e) {
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    });
    //解决长按屏幕后无法拖动的现象
    mGestureDetector.setIsLongpressEnabled(false);
    
  • 接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加

    boolean consume=mGestureDetector.onTouchEvent(event);
    return consume;
    

接下来,我们来看看OnGetsureListener和OnDoubleTapListener中我们可以实现的方法:
在这里插入图片描述
上表中的方法有很多,而我们比较常用的方法有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。

另外,关于是使用onTouchEvent还是使用GestureDectector。这里给大家一个建议:如果是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为的话,那么就使用GestureDectector。

3.Scroller

弹性滑动对象,用于实现View的弹性滑动。当使用View的scrollTo/scrollBy方法进行滑动时,其过程是一瞬间完成的,但这个没有过渡效果的滑动用户体验不好。而Scroller可以用来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成。Scroller和View的computeScroll方法配合使用才能共同完成这一功能。使用方法如下:

//注:以下代码都是写在自定义控件中的
Scroller scroller=new Scroller(getContext());

public void smoothScrollTo(int destX,int destY){//缓慢滚动到指定位置
    int scrollX=getScrollX();
    int delta=destX-scrollX;
    scroller.startScroll(scrollX,0,delta,0,1000);
    invalidate();
}

@Override
public void computeScroll(){
    if(scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),scroller.getCurrY());
        postInvalidate();
    }
}

2.View的滑动

滑动在Android开发中具有很重要的作用,不管一些滑动效果多么绚丽,归根结底,它们都是由不同的滑动外加一些特效所组成的。因此,掌握滑动的方法是实现绚丽的自定义控件的基础。通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy方法来实现滑动;第二种是通过动画给View施加平移效果来实现滑动;第三种是通过改变View的LayoutParams使得View重新布局从而实现滑动。

2.1使用scrollTo/scrollBy

scrollBy本质上也是调用了scrollTo方法,它实现基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。

scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置;View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘;mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离;mScrollX和mScrollY的单位为像素;从左向右滑动,那么mScrollX为负值,反之为正值,从上往下滑动,那么mScrollY为负值,反之为正值。(正好和x、y轴的延伸方向相反)

2.2使用动画

通过动画我们能够让一个View进行平移,而平移就是一种滑动。使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。

采用如下的View动画代码,可以在1000ms内将一个View从原始位置向右下角移动100个像素,代码如下:

//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="1000"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100" />

</set>
//给控件应用View动画
Button button=findViewById(R.id.myButton);
Animation myAnimator= AnimationUtils.loadAnimation(this,R.anim.my_animation);
button.startAnimation(myAnimator);

如果采用属性动画的话,就更简单了,以下代码可以将一个View在1000ms内从原始位置向右平移100像素。

ObjectAnimator.ofFloat(button,"translationX",0,100).setDuration(1000).start();

View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失。而使用属性动画并不会存在上述问题。

上面提到View动画并不能真正改变View的位置,这会带来一个很严重的问题。比如我们通过View动画将一个Button向右移动100px,并且这个View设置的有单击事件,然后你会惊奇的发现,单击新位置无法触发onClick事件,而单击原始位置仍然可以触发onClick事件,尽管Button已经不再原始位置了。这个问题还是比较好理解的,因为Button的位置信息(四个顶点和宽高)并不会随着动画而改变,因此在系统眼里,这个Button并没有发生任何改变。

从Android3.0开始,使用属性动画可以解决上面的问题。而在Android3.0以下我们这里给出一个简单的解决方法。针对上述的View动画问题,我们可以在新位置预先创建一个和目标Button一模一样的Button,它们不但外观一样连onClick事件也一样。当目标Button完成平移动画后,就把目标Button隐藏,同时把预先创建好的Button显示出来,通过这种间接的方式我们解决了上面的问题。

2.3改变布局参数

改变布局参数,即改变LayoutParams。比如我们想要把一个Button向右平移100px,我们只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。

还有一种情形,为了达到移动Button的目的,我们可以在Button的左边放置一个空View,这个空View的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,当空View的宽度增大时(假设Button的父容器是水平方向的LinearLayout),Button就自动被挤向右边,即实现了向右平移的效果。而该怎样重新设置一个View的LayoutParams呢?代码如下所示:

Button button=findViewById(R.id.myButton);
ViewGroup.MarginLayoutParams params=(ViewGroup.MarginLayoutParams) button.getLayoutParams();
params.leftMargin+=300;
button.setLayoutParams(params);
//或button.requestLayout();

2.4各种滑动方式的对比

上面我们共介绍了三种不同的滑动方式,它们都能实现View的滑动,接下来我们就来分析一下它们之间的差别。

  • scrollTo/scrollBy这种方式:它可以比较方便的实现滑动效果并且不影响内部元素的单击事件,但是它的缺点也是很明显的:它只能滑动View的内容,并不能滑动View本身。

  • 动画这种方式:如果是使用属性动画,那么这种方式没有明显的缺点;如果是使用View动画或者是在Android3.0以下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,否则就不合适了。但是动画有一个很明显的优点,那就是一些复杂的效果必须要通过动画才能实现。

  • 改变布局这种方式:它除了用起来麻烦点以外,也没有明显的缺点。它的主要适用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画去实现会有问题。

针对上述分析,我们再来总结一下:

  • scrollTo/scrollBy:操作简单,适合对View内容的滑动;

  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;

  • 改变布局参数:操作稍微复杂,适用于有交互的View。

2.5用属性动画来实现跟手滑动的效果

下面我们将用属性动画的方式来实现一个跟手滑动的效果,拖动它可以让它在整个屏幕上随意滑动。代码如下:

public class ScrollerLayout extends LinearLayout {

    int mLastX=0;
    int mLastY=0;

    public ScrollerLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x=(int) event.getRawX();
        int y=(int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastX=x;
                mLastY=y;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX=x-mLastX;
                int deltaY=y-mLastY;
                int translationX=(int) this.getTranslationX()+deltaX;
                int translationY=(int) this.getTranslationY()+deltaY;
                this.setTranslationX(translationX);
                this.setTranslationY(translationY);
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }

        mLastY=y;
        mLastX=x;
        return true;
    }
}

3.弹性滑动

上面我们讲的几种滑动方式都是比较生硬地滑动过去,这种方式的用户体验实在太差,因此我们要来实现渐进式滑动——弹性滑动。实现弹性滑动的方法有很多,但他们都有一个共同思想:将一次大的滑动分成若干次小滑动并在一个时间段内完成。

3.1使用Scroller

Scroller的典型使用方法,代码如下:

//注:以下代码都是写在自定义控件中的
Scroller scroller=new Scroller(getContext());

//缓慢滚动到指定位置
public void smoothScrollTo(int destX,int destY){
    int scrollX=getScrollX();
    int delta=destX-scrollX;
    //1000ms内滑向delta,效果就是慢慢滑动
    scroller.startScroll(scrollX,0,delta,0,1000);
    invalidate();
}

@Override
public void computeScroll(){
    if(scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),scroller.getCurrY());
        postInvalidate();
    }
}owmenx

我们先来看一下它的工作原理:当我们构造一个Scroller对象并且调用它的startScroll方法时,Scroller内部其实什么都没做,它只是保存了我们传递的几个参数。startScroll的源码如下所示:

//规划滑动路线
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;
}

这个方法的参数含义:startX和startY表示的是滑动的起点,dx和dy表示的是要滑动的距离,而duration表示的是滑动时间,即整个滑动过程完成所需要的时间。(注意:这里的滑动是指View内容的滑动而非View本身位置的改变)

通过上面的分析,我们可以看到,仅仅调用startScroll方法是无法让View滑动的,因为它内部并没有做滑动相关的事,而到底是什么让View滑动的呢?

答案就是startScroll方法下面的invalidate方法。invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现,在上面的代码中就已经实现了computeScroll方法。正是因为这个computeScroll方法,View才能实现弹性动画。这么说可能不太好理解,换句话说就是:当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。

我们再来看一下Scroller的computeScrollOffset方法的实现,如下所示:

//循环小幅度移动,从而实现弹性滑动
public boolean computeScrollOffset() {
    ...
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        ...
        }
    }
    return true;
}

该方法会根据时间的流逝来计算出当前的scrollX和scrollY的值,这样就将一次大的滑动分成了若干次小滑动并在一个时间段内完成。这个方法的返回值也很重要,它返回true表示滑动还未结束,false则表示滑动已经结束,因此当这个方法返回true时,我们要继续进行View滑动。

这里我们再来概括一下Scroller的工作原理:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。(Scroller:规划滑动路线;computeScroll:计算出当前的滑动位置;scrollTo:实现滑动)

3.2通过动画

动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。而我们还可以利用动画的特性来实现一些动画不能实现的效果。接下来我们就用动画来模仿Scroller来实现View的弹性滑动,代码如下:

final int startX=0;
final int deltaX=100;

ValueAnimator animator=ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float fraction=animation.getAnimatedFraction();//动画进行的百分比
        button.scrollTo(startX+(int)(deltaX*fraction),0);
    }
});
animator.start();

在上述的代码中,我们的动画本质上没有作用于任何对象,它只是在1000ms内完成了整个动画过程。根据这一特性,我们就可以在动画的每一帧到来时获取动画完成的比例,然后根据这个比例计算出当前View所要滑动的距离。(注意:这里的滑动针对的是View的内容而非View本身)

我们能够发现,这个方法的思想其实和Scroller比较类似,都是通过改变一个百分比配合scrollTo方法来完成View的滑动。需要说明的是,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在onAnimationUpdate方法中加上我们想要的其他操作。

3.3使用延时策略

使用延时策略实现弹性滑动,它的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果。具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。

对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。

下面我们采用Handler来做个示例,其他方法请读者自行尝试,思想都是类似的。代码如下:

private static final int MESSAGE_SCROLL_TO=1;
private static final int FRAME_COUNT=30;
private static final int DELAYED_TIME=33;
private int mCount=0;

@SuppressLint("HandlerLeak")
private Handler mHandler=new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {
        switch (msg.what){
            case MESSAGE_SCROLL_TO:
                mCount++;
                if(mCount<=FRAME_COUNT){
                	//核心思想还是:将一次大的滑动分成若干次小滑动并在一个时间段内完成。
                    float fraction=mCount/(float) FRAME_COUNT;
                    int scrollX=(int) (fraction*100);
                    button.scrollTo(scrollX,0);
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                }
            default:
                break;
        }
    }
};

上面几种弹性滑动的实现方法,在介绍中侧重更多的是实现思想,在实际使用中可以对其灵活的进行扩展从而实现更多复杂的效果。

4.View的事件分发机制

本节将介绍View的一个核心知识点:事件分发机制,事件分发机制不仅仅是核心知识点更是难点。另外,View的另一大难题滑动冲突,它的解决方法的理论基础就是事件分发机制,因此掌握好View的事件分发机制是十分重要的。

4.1点击事件的传递规则

首先我们这里要分析的对象就是MotionEvent,即点击事件。所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我们再来逐一介绍这几个方法。

  • public boolean dispatchTouchEvent(MotionEvent ev):用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

  • public boolean onInterceptTouchEvent(MotionEvent event):在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

  • public boolean onTouchEvent(MotionEvent event):在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接收到事件。

为了更加清楚地描述上面三个方法的区别和关系,我们可以借助如下的伪代码帮助理解:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume=false;
    if(onInterceptTouchEvent(event)){
        consume=onTouchEvent(event);//如果拦截就调用onTouchEvent处理点击事件
    }else {
        consume=child.dispatchTouchEvent(event);//如果不拦截就将点击事件分发给子View
    }
    
    return consume;
}

现在我们来总结一下点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。(由ViewGroup向子View分发)

当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于点击事件传递的尾端。(先执行onTouchListener,再执行onTouchEvent,最后执行OnClick)

当一个点击事件产生后,它的传递顺序遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent就会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。(如果拦截了,但onTouchEvent返回false,那么就会一步步的向上逐级调用onTouchEvent;如果直接就不拦截的话,那么点击事件就会向下(子View)进行分发)

关于事件传递的机制,这里给出一些结论,根据这些结论可以更好地理解整个传递机制,如下所示。

  • (1) 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。(一次滑动即一个时间序列)

  • (2) 正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。(一个事件序列只能被一个View拦截)

  • (3) 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果时间序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。(一个View一旦决定拦截,那么整个事件序列都只能由它处理,就不用再调用这个View之前的所有onInterceptTouchEvent去询问它是否要拦截了)

  • (4) 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。(View的onTouchEvent如果不消耗ACTIN_DOWN的话,那么其他事件就不会再交给它,而是交给其父元素处理)

  • (5) 如果View不能消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。(View的onTouchEvent如果不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失最终由Activity处理,此时不会调用父元素的onTouchEvent)

  • (6) ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。(ViewGroup不拦截任何事件)

  • (7) View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。(View不会判断是否拦截,而是直接执行onTouchEvent;View本身并没有onInterceptTouchEvent方法,ViewGroup继承View之后才添加的onInterceptTouchEvent方法)

  • (8) View的onTouchEvent默认都会消耗时间(返回true),除非它是不可点击的(clickable和longClickable同时为false)。(View的onTouchEvent默认都会执行)

  • (9) View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。

  • (10) OnClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。

  • (11) 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN除外。(事件传递是由外向内的)

4.2事件分发的源码解析

上一节我们分析了View的事件分发机制,本节将会从源码的角度进一步分析、证实上面的结论。

1.Activity对点击事件的分发过程

点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体工作是由Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。我们先从Activity的dispatchTouchEvent开始分析:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction()==MotionEvent.ACTION_DOWN) {
    	onUserInteraction;
    }
    if (getWindow.superDispatchTouchEvent(event)) {//传递给Window进行分发
    	return true;
    }
    return onTouchEvent(event);
}

事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。

接下来我们看Window是如何将事件传递给ViewGroup的(Window类可以控制顶级View的外观和行为策略),由于Window的唯一实现是PhoneWindow,因此我们来看一下PhoneWindow是如何处理点击事件的:

public boolean superDispatchTouchEvent(MotionEvent event) {
	return mDecor.superDispatchTouchEvent(event)//传递给DecorView进行分发
}

到这里逻辑就清晰了,PhoneWindow将事件直接传递给了DecorView,接下来我们就来分析DecorView:

private DecorView mDecor;

@Override
public final View getDecorView() {
	if(mDecor==null) {
		installDecor();
	}
	return mDecor;
}

我们知道,通过((ViewGroup)getWindow.getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。目前事件已经传递到了顶级View了,即在Activity中通过setContentView所设置的View,另外顶级View也叫根View,顶级View一般来说都是ViewGroup。

2.顶级View对点击事件的分发过程

点击事件到达顶级View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供的话,onTouch会屏蔽掉onTouchEvent。如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件分发。(先调用顶级ViewGroup的dispatchTouchEvent方法开始分发,再调用onInterceptTouchEvent方法判断是否拦截该点击事件,若拦截则调用onTouchEvent等方法处理该点击事件;否则传递给它的子View)

我们首先来看一下ViewGroup对点击事件的分发过程,其主要实现在ViewGroup的dispatchTouchEvent方法中,该方法比较长,我们分段说明:

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);
	} else {
		intercepted=false;
	}
} else {
	intercepted=true;
}

上述代码描述的是当前View是否拦截点击事件的逻辑。从上述代码我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null。第一个类型好理解,接下来我们就来讲解一下第二个类型。当事件由ViewGroup的子元素处理时,mFirstTouchTarget会被赋值并指向子元素,换句话说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked==MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。(当子元素处理点击事件后,mFirstTouchTarget != null成立,反之则不成立;当ViewGroup拦截ACtiON_DOWN后,同一序列的所有事件都会由ViewGroup处理)

这里还有个特殊情况需要说明,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,就会导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否需要拦截事件。我们再通过代码来进行分析:

if (actionMasked==MotionEvent.ACTION_DOWN) {
	cancelAndClearTouchTargets(ev);
	resetTouchState();//在该方法中重置FLAG_DISALLOW_INTERCEPT标记位
}

ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。

以上的分析可以帮助我们佐证上一节中的第3点和第11点。同时我们也可以总结出一个值得关注的点:onInterceptTouchEvent不是每次事件都会调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都被调用,当然前提是事件能够传递到当前的ViewGroup。

接着我们再来看当ViewGroup不拦截事件时,事件会向下分发交由它的子View进行处理,源码如下:

final View[] children - mChildren;
for (int i = childrenCount - ! 1; i >= 0; i--) {
	final int childIndex = cust omOrde r
			? getChildDrawingOrder (childrenCount, i) : i;
	final View child 二(preorderedList == null )
			? children [childIndex] : preorderedList.get (childIndex) ;
	if (! canViewReceivePointerEvents (child)
			|| !isTransformedTouchPointInView(x, y, child, null)) {//判断子元素能否接收点击事件
		continue;
	}
	
	newTouchTarget = getTouchTarget (child) ;
	if (newTouchTarget != null) {
		newTouchTarget. pointerIdBits |= idBitsToAssign;
		break;
	}
	
	resetCancelNextUpFlag (child) ;
	if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) ) {//调用子元素的dispatchTouchEvent方法
		mLastTouchDownTime = ev.getDownTime () ;
		if (preorderedList != null) {
			for (int=0; j < childrenCount; j++) {
				if (children [childIndex] == mChildren[j]) {
					mLas tTouchDownIndex = j ;
					break;
				}
			}
		} else {
			mLastTouchDownIndex = childIndex;
		}
		mLastTouchDownX = ev.getx() ;
		mLastTouchDownY = ev.getY() ;
		newTouchTarget = addTouchTarget (child, idBitsToAssign) ;
		alreadyDispatchedToNewTouchTarget=true;
		break;
	}
}

上面这段代码的逻辑是:首先遍历ViewGroup的所有子元素,然后判断子元素是否能接收点击事件。是否能接收点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它处理。我们可以看到,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,而在它的内部是如下的一段代码,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法,这样事件就由子元素处理了,从而完成了一轮事件分发。

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

如果子元素的dispatchTouchEvent返回true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:

newTouchTarget = addTouchTarget (child, idBitsToAssign) ;
alreadyDispatchedToNewTouchTarget=true;
break;

上述的代码处完成了mFirstTouchTarget的肤质并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素。(如果还有下一个子元素的话)

事实上mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,我们来看看addTouchTarget方法:

private TouchTarget addTouchTarget(View child,int pointerIdBits){
	TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
	target.next = mFirstTouchTarget;
	mFirstTouchTarget = target;
	return target;
}

从addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构。而mFirstTouchTarget是否被赋值,将会直接影响到ViewGroup对事件的拦截策略。

如果遍历了所有的子元素后事件都没有被合适地处理,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这种情况下ViewGroup会自己处理点击事件,代码如下:

if (mFirstTouchTarget == null) {
	handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}

这里dispatchTransformedTouchEvent中的第三个参数child为null,从前面的分析可以知道,它会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。(View为ViewGroup的父类)

3.View对点击事件的处理过程

View对点击事件的处理过程稍微简单一些,注意这里的View不包含ViewGroup。我们首先来看看它的dispatchTouchEvent方法,如下所示:

public boolean dispatchTouchEvent (MotionEvent event) (
	boolean result = false;
	...
	if (onFilterTouchEventForSecurity(event)) {
		ListenerInfo 1i = mListenerInfo;
		if (1i != null && 1i。mOnTouchListener !=null
				&& (mViewFlags & ENABLED_ MASK) == ENABLED
				&& l1.mOnTouchListener . onTouch (this, event)) {
			result - true;
		}
		
		if (!result && onTouchEvent (event)) {
			result = true ;
		}
	}
	...
	return result;
}

View(这里不包含ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。从上面的代码我们可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。(View不再向下传递点击事件,而是判断是否有onTouchEvent等处理点击事件的方法,然后选择是否处理点击事件)

接着我们再来分析onTouchEvent的实现。先看当View处于不可用状态下点击事件的处理过程,如下所示:

if ((viewFlags & ENABLED MASK) == DISABLED) {
	if (event .getAction() == MotionEvent.ACTION UP && (mPrivateFlags &PFLAG PRESSED) != 0){
		setPressed(false);
	}
	
	return (( (viewFlags & CLICKABLE) == CLICKABLE ||
			(viewFlags & LONG_ CLICKABLE) == LONG_CLICKABLE)) ;
}

显然,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用。(可以作证上一节的第9点)

接着,如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法,这个onTouchEvent的工作机制看起来和OnTouchListener类似,代码如下:

if (mTouchDelegate != null) {
	if (mTouchDelegate.onTouchEvent(event)) {
		return true;
	}
}

下面我们再来看一下onTouchEvent中对点击事件的具体处理,如下所示:

if (((viewFlags & CLICKABLE) == CLICKABLEI 1
		(viewFlags & LONG_ CLICKABLE) == LONG CLICKABLE) ) {
	switch (event.getAction()) {
		case MotionEvent.ACTION UP:
			boolean prepressed = (mPrivateFlags & PFLAG PREPRESSED) != 0;
			if ( (mPrivateFlags &PFLAG_ PRESSED) != 01 1 prepressed) {
				if (!mHas Per formedLongPress){
					removeLongPressCallback() ;
					if (!focusTaken) {
						if (mPerformClick == null) {
							mPerformClick = new PerformClick() ;
						}
						if (!post (mPerformClick}} (
							performClick() ;
						}
					}
				}
				...
			}
		}
		break;
	}
	...
	return true;
}

从上面的代码来看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true,不管它是不是DISABLE状态(这就佐证了上一节的第8,9,10点)。然后就是当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法,如下所示:

public boolean performClick() t
	final boolean result;
	final ListenerInfo 1i = mListenerInfo; 
	if (li != null && li.mOnClickListener != null) {
		playSoundEf fect (SoundEf fectConstants. CLICK);
		1i.mOnClickListener.onClick(this);//调用OnClick方法
		result = true;
	} else {
		result = false;
	}
	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED) ;
	return result;
}

View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体的View有关,确切来说是可点击的View其CLICKABLE为true,不可点击的View其CLICKABLE为false。通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。另外,setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener则会自动将View的LONG_CLICKABLE设为true,这点我们可以从源码中看出来,如下所示:

public void setOnClickListener (OnClickListener 1){
	if (!isClickable()) {
		setClickable (true) ;//将CLICKABLE设为true
	}
	getListenerInfo().mOnClickListener = 1 ;
}

public void setOnLongClickListener (OnLongClickListener 1) {
	if (!isLongClickable()) {
		setLongClickable (true) ;//将LONG_CLICKABLE设为true
	}
	getListenerInfo().mOnLongClickListener = 1:
}

5.View的滑动冲突

本节我们来介绍View体系中一个深入的话题:滑动冲突。那么滑动冲突是如何产生的呢?其实在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。那么如何解决滑动冲突呢?接下来我们就将在下面的几节内容中进行讲解分析。

5.1常见的滑动冲突场景

比较常见的滑动冲突场景可以简单分为如下三种:

  • 场景1:外部滑动方向和内部滑动方向不一致;

  • 场景2:外部滑动方向和内部滑动方向一致;

  • 场景3:上面两种情况的嵌套。

我们先来说说场景1,主要是将ViewPager和Fragment配合使用所组成的页面滑动效果。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView。本省这种情况是存在滑动冲突的,但是ViewPager内部帮助我们处理了这个滑动冲突,因此我们在采用ViewPager时无须关注这个问题,如果我们采用的不是ViewPager而是ScrollView等,那么我们就必须手动处理滑动冲突了。除了这种典型的情况外,还存在其他情况,比如外部上下滑动、内部左右滑动等,但是它们属于同一类滑动冲突。

我们再来看场景2,这种情况就稍微复杂一点,当内外两层都在同一个方向可以滑动时,显然就存在逻辑问题了。因为当手指开始滑动时,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动的很卡顿。在实际开发中,这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。

最后我们再来说下场景3,场景3是场景1和场景2两种情况的嵌套,因此场景3的滑动冲突看起来就比较复杂了。比如在许多应用中会有这么一个效果:内层有一个场景1中的滑动效果,然后外层又有一个场景2中的滑动效果。具体来说就是,外部有一个SlideMenu的效果,然后内部有一个ViewPager,ViewPager的每一页中又是一个ListView。虽然场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层,中层和外层之间的滑动冲突即可,而具体的处理方法其实是和场景1、场景2相同的。

从本质上来说,这三种滑动冲突场景的复杂度其实是相同的,因为它们的区别仅仅是滑动策略的不同,至于解决滑动冲突的方法,它们几个是通用的。

5.2滑动冲突的处理规则

对于场景1来说,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。这个时候我们就可以根据它们的特征来解决滑动冲突,具体来说就是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。那么又如何根据滑动过程中两个点之间的坐标来得到滑动方向呢?比如说可以依据滑动路劲和水平方向所形成的夹角,也可以根据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来判断。(根据滑动方向进行处理)

对于场景2来说,比较特殊,它无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,你如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View来响应View的滑动,根据这种业务上的需求我们也能得出相应的处理规则,有了处理规则同样可以进行下一步处理。(根据业务逻辑进行处理)

对于场景3来说,它的滑动规则就更复杂了,和场景2一样,它也无法直接根据滑动的角度,距离差以及速度差来做判断,同样还是只能从业务上找到突破点,具体方法和场景2一样,都是从业务的需求上得出相应的处理规则。

5.3滑动冲突的解决方式

上面说过,针对场景1中的滑动,我们可以根据滑动的距离来判断,这个距离差就是所谓的滑动规则。事实上在滑动过程中得到滑动的角度这个是相当简单的,但是到底要怎么做才能将点击事件交给合适的View去处理呢?这时就要用到事件分发机制了。针对滑动冲突,这里给出两种解决滑动冲突的方式:外部拦截法和内部拦截法。

1.外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。这种方法的伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted=false;
    int x=(int) ev.getX();
    int y=(int) ev.getY();
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted=false;
            break;
        case MotionEvent.ACTION_MOVE:
            if(/*父容器需要当前点击事件*/){
                intercepted=true;
            }else {
                intercepted=false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted=false;
            break;
        default:
            break;
    }
    mLastXIntercept=x;
    mLastYIntercept=y;
    return intercepted;
}

上述的代码是外部拦截法的典型案例,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要做修改并且也不能修改。这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太大意义。

考虑这样一种情况,假设事件交由子元素处理,如果父元素在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的OnClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。

2.内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    int x= (int) ev.getX();
    int y= (int) ev.getY();
    
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            parent().requestDisallowInterceptTouchEvent(true);//设置该方法后父容器就无法接收到除ACTION_DOWN之外的其它事件了
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX=x-mLastX;
            int deltaY=y-mLastY;
            if(/*父容器需要此类点击事件*/){
                parent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    
    mLastX=x;
    mLastY=y;
    return super.dispatchTouchEvent(ev);
}

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。(一旦子元素拦截了ACTION_DOWN事件,那么该事件序列中的其它所有事件都会交由其处理,且父容器的onInterceptTouchEvent方法不会再被调用)

为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标志位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用了。父元素所做的修改如下所示:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action=ev.getAction();
    if(action==MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值