View事件体系总结

1.1.1 View的概念

View是android中所有控间的基类,不论是简单的Button,TextView还是复杂的RelativeLayout、ListView。

1.1.2 View的位置参数

view的四个属性:top,left,right,bottom。
注意:1.这些坐标的都是相对坐标,相对于View的父容器。
2.View在平移过程中,top和left表示的是原始左上角的位置信息,所以不会发生改变,发生改变的是x,y,translationX,translationY
这里写图片描述
中间的小矩形是你的View,大矩形是父容器,红色是坐标轴。
所以可以得出View的宽、高。
宽:Width = right - left
高:Height = bottom - top
从Android3.0开始,View增加了几个参数:x、y、translationX和translationY。
其中,x,y是相对坐标,在View不移动的时候,x的值等于left的值,y的值等于right的值。translationX、translationY都是移动距离,表示View移动的x距离和y距离,默认值为0,0。

x= left + translation
y = right +translation
1.1.3 MotionEvent和TouchSlop
1.MotionEvent

典型的事件类型:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE: 手指在屏幕上移动
ACTION_UP: 手指离开屏幕的一瞬间

举几个事件序列的例子:
1)点击屏幕后离开,事件序列为:DOMN->UP
2)点击屏幕移动一会再松开,时间序列为:DOWN->MOVE->MOVE->…->MOVE->UP
其中,1)出现的场景如手势判断,单击,双击,长按;2)出现的场景如手势判断:放缩图片的手势判断,跟手移动,翻书动作,滑动冲突的处理等等。
注意:1.我们可以通过MotionEvent对象获得点击事件发生的x,y坐标:getX/getY和getRawX/getRawY。getX/getY返回的是相对于当前View左上角点击的x,y坐标,getRawX和getRawY返回的是相对于手机屏幕左上角点击的x,y坐标。

2.TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小滑动距离。即,如果手指在屏幕上两次滑动之间的距离小于这个常量就被认为未发生滑动。(注意:这个常量的值和设备有关,设备不同这个值可能也不同)。
意义/用途:做一些事件过滤,使用户有更好的用户体验,比如用户在玩2048时,点击屏幕后稍稍移动了一点松开了,结果卡片就移动了,这样用户体验就很不好,当然这个可以自己设定滑动的判断值。

1.1.4 VelocityTracker、GestureDetector和Scroller
1.VelocityTracker(速度追踪)

用于追踪手指在滑动过程中的速度,包括水平速度和竖直速度,如下为它的使用过程:
使用场景:如快速下滑的时候收起键盘
在View的onTouchEvent方法中:
追踪当前点击事件的速度

VelocityTracker velocityTracker = VelcoityTracker.obtaion();
velocityTracker.addMovement(event);

追踪滑动速度

VelocityTraker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

注意:
1)在获取滑动速度时,必须先调用computeCurrentVelocity方法计算滑动速度,才能调用getXVelocity/getYVelocity方法获取速度。
2)computeCurrentVelocity这个方法中的参数是一个时间间隔单元,如1000ms,在1秒中假若水平滑过100px(像素)即认为水平速度为100。

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

3)用完要回收,调用它的clear方法来重置回收

velocityTracker.clear();
velocityTracker.recycler();
2.GestureDetector(手势检测)

用于辅助检测用户的单击、滑动、长按、双击等行为。
首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,然后给我们提供了这些方法满足我们的需求:
这里写图片描述
我们可以根据我们的需要来实现这些方法,下面是这些方法的介绍:
这里写图片描述
如果你还想要处理双击事件的处理,你可以实现OnDoubleTapListener中的方法:
这里写图片描述
方法介绍如下:
这里写图片描述

上面两个表里的方法在日常开发中并不是都要实现的,你只用实现你需要的,常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。
另外注意一下,实际开发中可以不使用GestureDeterctor,可以自己在View的onTouchEvent方法中实现所需的监听,看个人习惯。我一般是滑动在onTouchEvent中直接处理,点击事件就实现onClickListener接口,双击才使用GestureDeterctor。

3.Scroller(弹性滑动对象)

用于实现View的弹性滑动。由于在用View的scollerTo/scollerBy方法滑,动时,这个过程是瞬间,用户体验很不好,所以我使用们可以Scroller对象来实现滑动的过渡过程。它本身是无法让View滑动的,它需要和View的computeScroll方法配合使用才能完成这个功能。
这是它的典型代码:

Scroller scroller  = new Scroller(mContext);

//缓慢滚动到指定位置,使用的时候只需要调用此方法即可
private void smoothScrollerTo(int destX, int destY){    
    //这只是一个简单的水平方向的移动
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    //1000ms内滑向destX,效果就是慢慢滑动
    mScroller.startScroll(scrollX, 0, delta, 0,1000);
    invalidate();
}
//重写View的方法
@Override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}
1.2 View的滑动
1.2.1使用scrollTo/scrollBy

这是View专门提供的方法来实现滑动功能。下面是它们的实现:
这里写图片描述
这里写图片描述
从源码中可以看出,scrollBy实际上也是调用了ScrollTo方法,它是在当前位置上在移动传入的参数值,而scrollTo是滑动到参数位置。
注意:scrollTo和scrollBy方法只能使View的内容移动
关于mScrollX、mScrollY这两个参数:
初识值:mScrollX=0,mScrollY=0
当mScrolX值为正值时表示View的内容向左移动,负值表示View内容向右移动
当mScrolX值为正值时表示View的内容向上移动,负值表示View内容向下移动

1.2.2 使用动画

使用动画来移动View,主要是操作View的translationX和translationY属性,既可以使用传统的View动画,也可以采用属性动画。
比如实现这样一个效果:在100ms内将一个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="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>      

使用属性动画:

ObjectAnimator.ofFloat(targetView,"translationX",0,100)
.ofFloat(targetView,"translationY",0,100)
.setDuration(100)
.start();

注意:View动画是对View的影像做操作,它并不能真正的,改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画效果会消失(会一下子回到原位置)。并且这还会带来一个严重问题:假如有一个Button并给他设置了点击监听,移动后点击新位置Button将不能触发点击事件,而原位置可以,因为新位置只是Button的影像,而Button本身大小位置等等都没变。
解决方案:从Android3.0开始,使用属性动画可以解决上面的问题,但,Android2.2上无法使用属性动画。或者也可以在新位置创建一个和原位置一模一样的Button包括点击事件的处理,在移动前隐藏新位置Button,移动后隐藏原位置Button,显示新位置Button。

1.2.3 改变布局参数(LayoutParams)

比如:一个Button向右平移100px
我们只用让Button的LayoutParams里的marginLeft参数的值增加100px。
实现代码如下:

MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width+=100;
params.leftMargin+=100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);
1.2.4 各种滑动方式的对比

1)scrollTo/scrollBy: 操作简单,适合对View的内容滑动
2)动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
3)改变布局参数:操作稍微复杂,适用于有交互的View

1.3 弹性滑动

也是三种方式:使用Scroller、通过动画、使用延时策略。

1.使用Scroller(View的内容滑动

它的用法我在上面的1.1.4也说过,但是这次我们分析一下它是如何完成View内容的滑动。首先创建一个Scroller对象,它会调用它的startScroll方法,但是Scroller本身不会使View发生滑动,它需要和View的computeScroll方法一起使用。下面是用到的代码:

Scroller scroller  = new Scroller(mContext);

//缓慢滚动到指定位置,使用的时候只需要调用此方法即可
private void smoothScrollerTo(int destX, int destY){    
    //这只是一个简单的水平方向的移动
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    //mScroller调用它的startScroll方法
    mScroller.startScroll(scrollX, 0, delta, 0,1000);
    //重绘1
    invalidate();
}
//重写View的方法
@Override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        //重绘2
        postInvalidate();
    }
}
--------------------------------------------------------
    //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;
    }

我们在使用的时候先调用smoothScrollTo方法,这个方法里
第一行获得的scrollX为View内容左边缘和View左边缘之间的距离。
第二行是计算出X方向需要移动的距离deltaX。
第三行mScroller调用startScroll方法,它的是实现看上面代码,会发现它只是做了一些变量的保存操作,并没有进行滑动的操作。
第四行调用View的重绘方法。这其实才是实现滑动的关键,因为View重绘后会在View的draw方法中调用computeScroll方法。
computeScroll方法:这个方法中会先用mScroller调用computeScrollOffset方法判断是否滑动结束,若未结束,又会去向mScroller获取当前的scrollX和scrollY,并通过scrollTo方法完成View的移动,最后调用postInvalidate完成二次重绘。
总结:
第一次重绘的作用:调用View中draw方法的computeScroll方法,这是启动这个弹性滑动循环的开始。
第二次重绘的作用:调用View中draw方法的computeScroll方法,导致循环。
循环终止判断:mScroller.computeScrollOffset方法。
实质:ScrollTo配合百分比(通过时间百分比,具体逻辑在判断那个方法中),每次移动一点点,最终实现滑动效果。

1.3.2 使用动画

动画本省就是一个渐进的过程,因此通过它来实现的滑动天然就具有弹性效果,下面,给个简单的动画使用,这个上面也有提过,这里复习一下:
注意:这里移动的是View

//在0.1s内,向右平移100px
ObjectAnimator.ofFloat(targetView,"translationX",0,100)
.setDuration(100)
.start();

我们也可以用动画模仿Scroller来实现View的动画效果。
注意:这里移动的是View的内容

final int startX = 0;
final int delatX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(100);
animator.addUpdateListener(new AnimatorUpdateListener(){    
    float fraction = animator.getAnimation;
    mButton1.scrollTo(startX +(int)(deltaX * fraction),0));
    }
});
animator.start();

在上述代码中,我们的动画本质上没有作用于任何对象上,mButton也只是调用了scrollTo方法,而这个方法是作用在View内容上的。这个动画的思想和Scroller也比较类似,也是通过一个百分比配合ScrollTo来完成的。还有一点就是,采用这种方法除了可以完成弹性滑动以外,还可以实现其他动画效果,在onAnimationUpdate方法中。

1.3.3 使用延时策略

核心思想:通过发送一系列延时消息从而达到一种渐进式的效果,具体来说就是可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。
1)View的postDelayed方法:我们可以通过它发送一个延时消息,然后再消息中进行View滑动。
2)线程的sleep方法:通过while循环中不断地滑动View和sleep。
3)使用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(){
    public void handleMessage(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);
                mButton1.scrollTo(scrolloX,0);
                mHandler.sendEmptyMessageDelayed(MESSAGE_SCROOL_TO,DELAYED_TIME);
                break;
            } 
        }
    }
}

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

1.4 View事件的分发机制
1.4.1 点击事件的传递规则

首先,我们要知道事件分发的对象:MotionEvent,这个在上面也已经提到过。所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个过程就是分发过程。点击事件的分发过程由三个重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
这三种方法介绍:
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,其结果受下面俩个方法的影响,返回结果表示是否消耗当前事件,具体原因看下面三者关系的介绍。
public boolean onInterceptTouchEvent(MotionEvent event)
用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

三种方法的关系:
//伪代码
public void dispatchTouchEvent(MotionEvent  ev){
    //初始化consume为false
    Boolean consume = false;
    //通过onIntrceptTouchEvent判断是否拦截事件
    if(onIntrceptTouchEvent(ev)){
        //若拦截则consume的值取决于当前View的onTouchEvent返回值
        consume = onTouchEvent(ev); 
    }else{
        //若不拦截,则consume的值取决于子View的dispatchTouchEvent返回值
        consume = child.dispatchTouchEvent(ev)
    }
    //返回最终的consume值
    return consume;
}

由此我们可以得出点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它就会按上面伪代码的调用顺序一样,先调用它的dispatchTouchEvent方法,如果这个ViewGroup拦截此事件,则它的onTouchEvent方法就会调用;若它不拦截,则调用它的子View的dispachTouchEvent方法,然后依次传递。判断拦截用当前View的onInterceptTouchEvent。
当一个View需要处理事件的时候,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法就会被回调,它的优先级高于onTouchEvent。若它返回false才会调用onTouchEvnet,若它返回true则onTouchEvent不会被调用。还有平常我们用的OnClicklistener优先级最低。
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->顶级View
总结:
1)一个事件序列以down事件开始,中间有数量不定的move事件,最终以up事件结束。
2)正常情况下,一个事件序列只能被一个View拦截且消耗,因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会交给它处理并且也只能由它处理,也就是说一个事件序列不能分别由两个View同时处理。并且它的onInterceptTouchEvent不会再被调用。
3)某个View开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件也不会交给它处理,会交给他的父元素处理即父元素的onTouchEvent会被调用。
4)如果View不消耗除ACTION_DOWN以外的其他事件,那么,这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前View可以持续收到后续的事件,而那些消失的点击事件最终会传递给Activity处理。
5)View没有onInterceptTouch方法,一旦有点击事件传递给它,它的onTouchEvent方法就会被调用,并且默认返回true,除非他是不可点击的(clickable和longClickable同时为false)。View的longClickable默认为false,clickable要分情况,如Button为true,TextView为false。
6)View的enable属性不影响onTouchEvent的默认返回值,因为哪怕View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
7)onClick会发生的前提是当前View是可点击的,并且让收到了down和up事件。
8)事件传递过程是有外向内的,即事件总是先传递给父元素,然后再由父元素传递给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的时间分发过程,除了ACTION_DOWN事件。

1.4.2 事件分发的源码解析
1.Activity对点击事件的分发过程

当一个点击操作发生时,时间最想传递给Activity,然后调用它的dispatchTouchEvent方法,代码如下:

  public boolean dispatchTrackballEvent(MotionEvent ev) {
        onUserInteraction();
        if (getWindow().superDispatchTrackballEvent(ev)) {
            return true;
        }
        return onTrackballEvent(ev);
    }

在这个方法中,onUserInteraction()是个空方法,然后下来到if判断里把这个时间就传递给了Window,如果返回true则循环结束,若返回false则表示所有的View的onTouchEvent都返回了false即没有View处理这个事件,则会调用Activity的onTouchEvent。
下来我们来看下Window是如何把事件传递给ViewGroup的,搜索发现Window是个抽象类,superDispatchTouchlEvent也是一个抽象方法,因此我们需要找到Window的实现类,而Window的实现类只有唯一的一个PhoneWindow。
下面给出PhoneWindow是如何实现superDispatchTouchEvent这个方法:

2.window向mDecor传递事件
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

其中,mDecor的类型是DecorView,这个类名是不是有点熟悉,对看下面代码:

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

就这样Window就把事件传递给了DecirView,也就是那个ViewGroup。补充一点,我们可以通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);这句话获取我们Activity设置的那个View,其中哪个getWindow().getDecorView()方法明显是返回了一个mDecor对象,类型为ViewGroup,获取mDecor的代码如下:

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

再详细我们就不去分析了,要想看看具体是怎么设置生成的就点开installDecor方法来分析,下来我们需要分析顶级View又是如何对点击事件进行分发。

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

这个过程就回到时间分发机制了,过程是一模一样的。也就是先调用ViewGroup的dispatchTouchEvent然后判断是否拦截此事件,若拦截则由当前View也就是ViewGroup处理,否则调用子View的dispatchTouchEvent方法,以此类推,注意View是没有onInterceptTouchEvent方法,这个在上面分发机制的总结也有提到过,还有如果事先设置了OnTouchListener则先看onTouch的返回值,因为它的优先级高于onTouchEvent,所以它会屏蔽onTouchEvent方法,并且只有onTouch返回false时才会调用onTouchEvent方法。
下面给出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;
            }

在上述代码中,发现ViewGroup在两种情况下会询问自己是否拦截当前事件,第一个是传来的是MotionEvent.ACTION_DOWN事件,第二个是mFirstTouchTarget != null。第一种情况好说就是它传来的事件是DOWN事件,第二种情况是怎么来的呢?第二种情况是当所有子View都不去处理这个事情时就为null了,因为从下面代码可以看出当一个子View成功处理了事件,这个mFirstTouchTarget就等于该子View。所以当ViewGroup拦截时,mFirstTouchTarget为null,也是就是mFirstTouchTarget!=null不成立。那么当ACTION_MOVE、ACTION_UP时间来的时候这个if条件为false,导致ViewGroup的onInterceptTouchEvent不会被调用,并且给同一序列中的个其他事件都会默认交给它处理。
还有一个特殊情况,就是这里的一个标志FLAG_DISALLOW_INTERCEPT,它一般是子View通过requsetDisallowInterceptTouchEvent方法设置的,一旦设置,ViewGroup将无法拦截除了ACTION_DOWN的所有点击事件。为什么除了ACTION_DOWN(事件序列的开始)?因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置这个标记,使子View的设置无效。并且总会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。
下面代码是ViewGroup对ACTION_DOWN的处理:

  // Handle an initial down.
  if (actionMasked == MotionEvent.ACTION_DOWN) {
          // Throw away all previous state when starting a new touch gesture.
          // The framework may have dropped the up or cancel event for the previous gesture
          // due to an app switch, ANR, or some other state change.
          cancelAndClearTouchTargets(ev);//取消并清除所有触摸目标
          resetTouchState();//重置FLAG_DISALLOW_INTERCEPT标志位
      }

上面代码更加清晰的说明了子View设置的标志,对ACTION_DOWN事件无效,并且会被清除。
注意:
1)onInterceptTouchEvent方法并不是每次都会调用的,比如ViewGroup决定拦截事件后,所有的事件都默认交由ViewGroup处理,并会调用它的onInterceptTouchEvent方法。而dispatchTouchEvent方法会每次都调用,只要事件能传到ViewGroup中。
2)FLAG_DISALLOW_INTENCEPT这个标志的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件。
上面过程就完成了ViewGroup对一个事件来的分发,当事件传到它的子Veiw时,又是怎么处理的呢?看下面代码:

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    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);
    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;
    }

    // The accessibility focus didn't handle the event, so clear
    // the flag and do a normal dispatch to all children.
    ev.setTargetAccessibilityFocus(false);
}

上面这段代码,开始遍历所有子View,然后判断子元素是否能够接收到点击事件,判断条件有两个:①子元素是否在播动画②点击事件坐标能否落到子元素的区域内。若某个元素满足以上两个条件,那么事件就会传递给它处理。dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,在dispatchTransformedTouchEvent的内部有如下一段代码:

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

而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent,这样事件就交由子元素处理了,从而完成了一轮事件的分发。
如果子元素的dispatchTouchEvent返回true,那么就会被赋值同时跳出for循环,如下所示:

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

---------------------------------------------
private TouchTarget addTouchTarget(View child, int pointerIdBits){
    //这里采用头插法
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
return target;
}  

mFirstTouchTarget:是一种单链表结构,它是否被赋值会影响到ViewGroup对事件的拦截策略,若它为null,则ViewGroup就会默认拦截接下来同一序列的所有点击事件。
如果遍历所有子元素后时间都没有被合适的处理,这包含两种情况:①ViewGroup没有子元素;②子元素处理了点击事件,但在dispatchTouchEvent返回了false。这两种情况下ViewGroup都会自己处理点击事件。代码如下:

//Dispatch to touch targets
if(mFirstTouchTarget == null){
    //No touch targets so treat this as an ordinary view
    handled = dispatchTransformTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}

dispatchTransformTouchEvent这个方法上面有提到过,看它的第三个参数为null所以,它会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent方法,即开始了View对点击事件的处理。

4.View(不包含ViewGroup)对点击事件的处理过程

首先,接着上面的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;
    }

View(不包含ViewGroup)对点击事件的处理过程就比较简单了,因为它不用向下传递,所以它只能自己处理事件,从上面的代码可以看出,View对点击事件的处理会先判断有没有设置OnTouchListener,OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,这里是第三次说明了,因为OnTouchListener的优先级高于OnTouchEvent,这样做的好处是方便在外界处理点击事件。
接下来分析onTouchEvent的实现,先看当View处于不可用状态下点击事件的处理过程,如下所示,不可用状态下的View照样会消耗点击事件:

final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    //翻译:可点击的禁用视图仍然会使用触摸事件,只是不会对它们做出响应。
    return clickable;
}

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

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

下面再看一下onTouchEvent中对点击事件的具体处理:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
     switch (action) {
             case MotionEvent.ACTION_UP:
                   mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                   if ((viewFlags & TOOLTIP) == TOOLTIP) {
                       handleTooltipUp();
                   }
                   if (!clickable) {
                       removeTapCallback();
                       removeLongPressCallback();
                       mInContextButtonPress = false;
                       mHasPerformedLongPress = false;
                       mIgnoreNextUpEvent = false;
                       break;
                   }
                   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) {
                           // The button is being released before we actually
                           // showed it as pressed.  Make it show the pressed
                           // state now (before scheduling the click) to ensure
                           // the user sees it.
                           setPressed(true, x, y);
                       }

                       if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           // 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;
...
return ture;

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

 public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

View的LONG_CLICKABLE属性默认为false,CLICKABLE属性与具体View有关,这个前面也有提到过,而且一个View通过setOnClickListener,它的CLICKABLE属性会被OnCLickListener自动设置为ture,setOnLongClickListener也是一样。如下:

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

到这里,事件分发机制就完了。

1.5View的滑动冲突
1.5.1常见的滑动冲突

常见的滑动冲突场景可以简单的分以下三类:
①外部滑动方向和内部滑动方向不一致
②外部滑动方向和内部滑动方向一致
③上面两种情况的嵌套
这里写图片描述
举个例子:
①场景一:ScrollView + ListView(ViewPager+ListView不存在这种冲突,ViewPager内部处理了)
②场景二:ListView +ListView
③场景三:SlideMenu + ViewPager + ListView

1.5.2 滑动冲突的处理规则

①场景一:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。
如下图:若dx>dy则外部拦截,若dx’<’dy则内部拦截,否则不拦截。也可以通过角度判断拦截。这里写图片描述
②场景二:根据业务需求,比如:但出于某种状态时需要外部View响应用户的滑动,而处于另一状态时需要内部View来响应。
③场景三:它的滑动规则就更复杂了,它和场景二一样,它也无法直接根据滑动的角度、距离差以及速度差来做判断,同样还是只能从业务上找出突破点。

1.5.3 滑动冲突的解决方式

这里给出两种方法:外部拦截法和内部拦截法。
1.外部拦截法
所谓外部拦截法是指:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。
外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下:

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:
        intercepet = 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;
}

这是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的条件即可。下来分析一下这个代码,首先是遇到ACTION_DOWN事件,这个必须返回false,因为一旦拦截了ACTION_DOWN,就必须拦截同一时间序列的所有事件,下来是ACTION_MOVE事件,这个根据需求拦截即可,最后是ACTION_UP,这里必须返回fasle,因为ACTION_UP这个事件本身没有太多意义。

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

//子元素的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event){
    int x = (int) evnet.getX();
    int y = (int) event.getY();

    switch(event.getAction()){
    case MotionEvent.ACTION_DOWN: 
        parent.requestDisallowInterceptTouchEvent(true);
        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.disapatTouchEvent(event);
    }
}
--------------------------------------------------------------
//父容器的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent event){
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return ture;
    }
}

我们来分析一下上面代码,首先子元素在事件的时候调用parent.requstDisallowInterceptTouchEvent(true),使父容器不能拦截除了ACTION_DOWN以外的所有事件,原因是父容器拦截事件受FLAG_DISALLOW_INTERCEPT标志影响,除了ACTION_DOWN事件,ACTION_DOWN事件会重置这个标志使子元素的设置无效,这个前面也有说到过。然后在子元素收到ACTION_MOVE事件的时候进行判断父容器是否需要此事件,若需要则将FLAG_DISALLOW_INTERCEPT标志设置为false,也就是parent.requestDisallowInterceptTouchEvent(false)的作用。除了子元素要做处理以外,父容器也要做一些处理,它要默认拦截除了ACTION_DOWN事件以外的所有事件,这样当子元素调用parent.requstallowInterceptTouchEvent(fasle)方法时,父元素可以继续拦截所需要的事件。

到此滑动冲突问题也完结了,希望大家多多练习。本来还有练习的Demo但由于太长,而且基本都是代码所以就不再展现了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值