【进阶android】ListView源码分析——ListView的滚动机制

        前面几篇文章主要在介绍ListView的初始化(当然这些方法并不仅仅只在ListView实例化时被调用),这一篇文章我们则主要分析ListView在运动时的情况,即ListView的滚动机制。滚动机制主要分为ListView是如何滚动以及滚动时会引起什么东西变化。

        ListView的滚动机制与ListView的触摸事件息息相关,因此理解其滚动机制就是理解ListView对触摸事件是如何解析、控制的。众所周知,触摸事件分为三个比较主要的动作:down、move、up,本文将按照这个流程来对ListView的滚动机制进行分析,并且在具体分析触摸事件之前,会对ListView的一些与滑动相关的常量、变量及接口做一些解析。因此,本文的目录如下:

       1、与滑动相关的常量、变量及接口;

       2、down——滑动开始前的准备;

       3、move——滑动开始与持续;

       4、up——滑动结束与后序;

       5、ListView的抛动机制

1、滑动相关的常量、变量及接口

    1.1描述滑动状态的常量

            ListView,为一个滚动机制定义了一系列的触摸模式,每一个触摸模式下的滑动都呈现出不同的效果;ListView中,将滚动机制定位为8种不同的触摸模式,分别如下:
TOUCH_MODE_REST:
未处于滚动机制之中;
TOUCH_MODE_DOWN:
接收到down触摸事件,但还没有达到轻触的程度;
TOUCH_MODE_TAP:
触摸事件被标识为轻触事件(轻触Runnable已经执行),且等待一个长按事件;
TOUCH_MODE_DONE_WAITING:
仍为down事件的范畴,等待手指开始移动(即等待move触摸事件的发生);
TOUCH_MODE_SCROLL:
ListView内容随着指尖的移动而移动,此时已经进入move触摸事件之中了;
TOUCH_MODE_FLING:
ListView进入抛动模式,滑动速度过快,手指离开屏幕后,ListView会继续滑动;
TOUCH_MODE_OVERSCROLL:
滑动到ListView边缘的状态;
TOUCH_MODE_OVERFLING:
抛动回弹,抛动到ListView边缘的状态;
        这八个常量的值,从-1到6依次递增;这种递增顺序,也大体表现出了一个ListView滑动的生命周期。而ListView的触摸事件处理函数则根据不同的时刻,来更改当前ListView的触摸模式,并执行当前触摸模式状态下应当呈现的效果。

    1.2滑动相关的变量

       ListView之中与滑动相关的变量,主要的作用是用来缓存一些重要的坐标点,如下所示:
mTouchMode:
当前的触摸模式,1.1节中所列出的八个常量之一,初始值为TOUCH_MODE_REST
mMotionCorrection:
开始滑动之前,手指已经移动的距离;
mMotionPosition:
接受到down手势事件的视图对应的item的位置;
mMotionViewOriginalTop:
接收到down手势事件的视图的顶部偏移量;
mMotionX:
down手势事件位置的X坐标;
mMotionY:
down手势事件位置的Y坐标;
mLastY:
上一个手势事件位置的Y坐标(如果存在);
mVelocityTracker:
在触摸滚动期间决定速率;
mTouchSlop
当手指滑动一定距离后,才开始产生滑动效果,此变量表示所谓的“一定距离”。
         当然,与滑动相关的变量远远不止这9个,在下文分析遇到时,在进行说明;除了这些属性变量外,还有些属于方法的本地变量,在分析时,在做特殊说明。

    1.3滑动变量的相关接口

        如果说常量和变量尽可能的来描述一个滑动,那么与滑动相关的接口则是来定义一个滑动在不同的状态,不同的时刻下应该做些什么。此处的接口,除了interface之外还有一些ListView自己定义好了的Runnable。
OnScrollChangeListener接口:
       当ListView滚动时,用来回调的接口;该接口主要侧重于滚动状态的改变。接口的内部定义了三个常量,用来描述ListView的3中滚动状态,分别如下:
SCROLL_STATE_IDLE:
空闲状态,及此状态下ListView没有滚动
SCROLL_STATE_TOUCH_SCROLL:
滑动状态;即Listview的内容随着手指的移动而移动;
SCROLL_STATE_FLING:
抛动状态;即手指离开屏幕,但由于速度过快,ListView的内容会继续滚动一段时间;
        当ListView的滑动状态在这三种状态之中相互切换时,就会回调该接口的onScrollStateChanged方法;一般而言onScrollStateChanged方法将在Adapter.getView(int, View, ViewGroup)方法之前被调用。OnScrollChangeListener接口之中还定义了另一个方法:onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount),此方法会在滑动完成之后被调用。
CheckForTap内部类:
       轻触事件一般是指,从手指接触到屏幕的一瞬间开始,经过100毫秒之后,会触发一个事件,这个事件就是一个轻触事件。
       轻触事件回调类是指,当产生轻触事件时,进行回调的一个类;它执行了Runnable接口,当进行回调时,会调用run方法。run方法的流程如下:
       1、判断当前模式,因为轻触模式是相对于down触摸事件而言,因此如果当前并不是TOUCH_MODE_DOWN模式,则run方法不会做任何事情;
       2、触摸模式由TOUCH_MODE_DOWN转变为TOUCH_MODE_TAP模式;
       3、轻触事件产生之后,就说明用户的手指已经按在了视图一定时间了,因此需要将对应视图的状态设置为press(调用setPress方法);
       4、判断ListView是否能够具有可长按性,如果具有则post一个异步的长按事件回调消息,一般而言长按事件会在用户按住屏幕后500毫秒触发。
PerformClick内部类:
       执行点击效果,与CheckForTap内部类一致,PerformClick内部类也执行了一个Runnable接口,当执行点击效果时,也会回调此类之中的run方法;此run方法首先会根据mChoiceMode来更新ListView被选择的item,然后 会找到点击效果发送在ListView之中的哪个Item上,最后调用AdapterView. performItemClick方法,performItemClick则会调用OnItemClickListener接口中的 onItemClick 方法( onItemClick 方法应该很熟悉了吧)。
CheckForLongPress内部类:
       执行长按效果,与PerformClick内部类一致,CheckForLongPress内部类也执行了一个Runnable接口;前文曾提过,当调用CheckForTap内部类中的run方法时,会根据ListView是否具有可长按性而向UI线程发送一个异步的回调消息,当处理这个异步消息时,便会调用CheckForLongPress内部类中的run方法。
      run方法首先会判断出作用于长按事件的子视图,然后调用ListView中的performLongPress方法,执行长按事件处理,最后根据performLongPress方法的返回值来确定当前的触摸模式。
      确定作用于长按事件的子视图,则需要借助上文提及的变量mMotionPosition(接受到down手势事件的视图对应的item的位置)。
      performLongPress方法的源代码如下:
boolean performLongPress(final View child,
            final int longPressPosition, final long longPressId) {
        ......
        boolean handled = false;//是否处理了长按事件
        if (mOnItemLongClickListener != null) {
            handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
                    longPressPosition, longPressId);
        }
        if (!handled) {//长按事件创建内容菜单
            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
            handled = super.showContextMenuForChild(AbsListView.this);
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }
      performLongPress方法一共有三个入参:第一个入参是作用于长按事件的子视图;第二个参数是第一个入参对应的item在适配器之中的位置;第三个入参是指第一个入参对应的item的ID。
      根据源代码,ListView之中主要用来处理长按事件的还是OnItemLongClickListener监听器之中的onItemlongClick方法。
      最后返回一个布尔类型的结果。
      回到CheckForLongPress内部类的run方法之中,当run方法调用了performLongPress方法后,会根据performLongPress方法的返回值来设定具体的触摸模式,如果返回true,则表示执行了长按事件,也就是此次触摸流程主要的目的是为了执行长按事件,而非引起滑动,因此将触摸模式还原为TOUCH_MODE_REST;如果performLongPress返回false,则表示未执行长按事件,将触摸模式设置为TOUCH_MODE_DONE_WAITING,即直到此时,用户的手指还处于按住屏幕的阶段——开始等待用户的手指移动。
       将一些主要的常量、变量、接口及内部类介绍了之后,我们就具体分析一个ListView的滑动流程。

2、down——滑动开始前的准备

      ListView的滑动是通过ListView的触摸事件来引起的,那么一个滑动流程的开始是否就是从AbsListView中的onTouchEvent(ListView之中没有onTouchEvent方法)开始的呢?显然不是!
       通过ViewGroup对触摸事件的分配流程来看,在调用onTouchEvent方法之前,会先调用ViewGroup的onInterceptTouchEvent方法来判断是否在将触摸事件分配给子视图的onTouchEvent方法之前,进行拦截。
       众所周知,AbsListView是ViewGroup的一个子类,而在AbsListView类中则重写了ViewGroup的onInterceptTouchEvent方法,即重新定义了拦截规范;AbsListView具体的拦截规范,在此处不详述,我们只需要知道AbsListView的onInterceptTouchEvent方法会在AbsListView的onTouchEvent方法之前接收到触摸事件。
       因此,ListView滑动流程的开始方法就是AbsListView中的onInterceptTouchEvent方法。
       一个触摸事件的开始手势肯定是down,下面我就看看onInterceptTouchEvent方法中对down手势事件处理的源代码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int actionMasked = ev.getActionMasked();
        View v;
        ......
        switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            int touchMode = mTouchMode;
            if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
                mMotionCorrection = 0;//开始滚动之前,手指移动的距离
                return true;
            }

            final int x = (int) ev.getX();
            final int y = (int) ev.getY();
            ......

            int motionPosition = findMotionRow(y);//获取手指按住的这个子视图对应的item在适配器中的位置
            if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
                // User clicked on an actual view (and was not stopping a fling).
                // Remember where the motion event started
                v = getChildAt(motionPosition - mFirstPosition);//手指按住哪一个子视图
                mMotionViewOriginalTop = v.getTop();//更新接收到down手势事件的视图的顶部偏移量
                mMotionX = x;//更新down手势事件位置的X坐标
                mMotionY = y;//更新down手势事件位置的Y坐标
                mMotionPosition = motionPosition;/更新接受到down手势事件的视图的位置
                mTouchMode = TOUCH_MODE_DOWN;//更新解析模式
                clearScrollingCache();
            }
            //因为down手势是滑动的第一个动作,而mLastY表示上一个动作的Y值,
            //因此会在此处让mLastY的值失效
            mLastY = Integer.MIN_VALUE;
            initOrResetVelocityTracker();//初始化速率追踪器
            mVelocityTracker.addMovement(ev);//将当前时间添加到速率追踪器中,以便计算出相应的滑动速率
            ......
            if (touchMode == TOUCH_MODE_FLING) {
                return true;
            }
            break;
        }

        ......

        return false;
    }
       根据代码,可知 onInterceptTouchEvent方法对down手势事件的处理,主要是将上文提及的相关变量进行更新赋值。
       下面就看AbsListView中onTouchEvent对down手势事件处理的源代码:
@Override
    public boolean onTouchEvent(MotionEvent ev) {
     ......
final int actionMasked = ev.getActionMasked();
     ......
switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }
        ......
     }
     ......
}
     直接调用onTouchDown方法,onTouchDown方法的源代码如下:
private void onTouchDown(MotionEvent ev) {
        ......
        if (mTouchMode == TOUCH_MODE_OVERFLING) {//如果已经抛动到ListView的边缘
            // Stopped the fling. It is a scroll.
            mFlingRunnable.endFling();//停止滚动
            if (mPositionScroller != null) {
                mPositionScroller.stop();
            }
            mTouchMode = TOUCH_MODE_OVERSCROLL;
            mMotionX = (int) ev.getX();
            mMotionY = (int) ev.getY();
            mLastY = mMotionY;
            mMotionCorrection = 0;
            mDirection = 0;
        } else {
            final int x = (int) ev.getX();//按住屏幕的X坐标
            final int y = (int) ev.getY();//按住屏幕的Y坐标
            int motionPosition = pointToPosition(x, y);//确定坐标对应的item在适配器中的位置

            if (!mDataChanged) {
                if (mTouchMode == TOUCH_MODE_FLING) {//如果ListView的内容正在抛动中,用户的手指按住了ListView
                    // Stopped a fling. It is a scroll.
                    ......
                    mTouchMode = TOUCH_MODE_SCROLL;//变为滑动模式
                    mMotionCorrection = 0;
                    motionPosition = findMotionRow(y);
                    mFlingRunnable.flywheelTouch();
                } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {//初始模式
                    // User clicked on an actual view (and was not stopping a
                    // fling).                 
                    //用户点击到一个确切的子视图,同时并不是停止一个抛动
                    //It might be a click or a scroll. Assume it is a // click until proven otherwise.
                    //用户的点击可能是一个click也可能是一个滑动,在验证它之前,假定它是一个click
                    mTouchMode = TOUCH_MODE_DOWN; // FIXME Debounce 
                    if (mPendingCheckForTap == null) { 
                           mPendingCheckForTap = new CheckForTap(); 
                    } 
                    mPendingCheckForTap.x = ev.getX(); mPendingCheckForTap.y = ev.getY();
                    //发出一个轻触检测异步消息
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 
                } 
            } 
            if (motionPosition >= 0) { // Remember where the motion event started 
                    final View v = getChildAt(motionPosition - mFirstPosition);               
                    mMotionViewOriginalTop = v.getTop(); 
            } 
            mMotionX = x; 
            mMotionY = y; 
            mMotionPosition = motionPosition; 
            mLastY = Integer.MIN_VALUE;//整个触摸流程之中,down手势事件为第一个事件,所以它的上一个事件的Y坐标无效
        }
        ...... 
}
         onTouchDown方法分别对 TOUCH_MODE_OVERFLING、TOUCH_MODE_FLING以及TOUCH_MODE_RESET模式进行了处理;对于前两者主要是停止抛动,变为对应的滑动模式;而对于后者,则将滑动模式转变为TOUCH_MODE_DOWN模式,并且发送一个异步的轻触消息。
       结合CheckForTap内部类,当轻触消息被处理时,则会调用CheckForTap内部类的run方法,将触摸模式由TOUCH_MODE_DOWN转换为TOUCH_MODE_TAP模式,并发送一个异步的长按消息;当长按消息被处理时,会调用CheckForLongPress内部类的run方法,此方法中根据是否处理了长按事件(是否存在OnItemLongClickListener监听器),分别将触摸模式改变为TOUCH_MODE_RESET模式和TOUCH_MODE_DONE_WAITING模式。
       总体而言,down手势事件,主要做了滑动的一些准备工作,判断区别了额轻触、长按事件是否发生。

3、move——滑动开始与持续

      经历了down手势事件之后,一般来说,当前的触摸模式已经变味了TOUCH_MODE_DONE_WAITING模式,也就是说ListView已经准备就绪,处于等待(滑动)的状态之中。
       与down手势事件相似,在AbsListView的onInterceptTouchEvent方法中,会预先处理move手势事件。
       代码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
        ......
        case MotionEvent.ACTION_MOVE: {
            switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == -1) {
                    pointerIndex = 0;
                    mActivePointerId = ev.getPointerId(pointerIndex);
                }
                final int y = (int) ev.getY(pointerIndex);
                initVelocityTrackerIfNotExists();//初始化速率追踪器
                mVelocityTracker.addMovement(ev);//将此次事件添加到速率追踪器之中,以便计算滑动速率
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
                    return true;
                }
                break;
            }
            break;
        }
        ......
        }

        return false;
    }
      根据代码,onInterceptTouchEvent方法之中,对move手势事件的处理,只限制于TOUCH_MODE_DOWN模式;也就是说,当用户一按下屏幕,就立即move(未经历轻触事件和长按事件)时,会在onInterceptTouchEvent方法之中预先处理。
       onInterceptTouchEvent方法之中,主要是调用了startScrollIfNeeded方法,对于此方法,下文会有进一步的分析。
        对于move手势事件,onInterceptTouchEvent方法中的处理逻辑较为简单;我们继续分析onTouchEvent方法;在onTouchEvent方法之中,对于move手势事件的处理是直接调用onToucheMove方法,相关的源码如下:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
        ......
        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }
        //当前事件的Y坐标
        final int y = (int) ev.getY(pointerIndex);

        switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                //以上三个模式表示滑动的开始
                // Check if we have moved far enough that it looks more like a
                // scroll than a tap. If so, we'll enter scrolling mode.
                //检查是否我们移动的足够远,以至于看起来更像是一个滑动而非一个轻触。
                //如果是这样,我们将进入滑动模式
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                    break;
                }
                // Otherwise, check containment within list bounds. If we're
                // outside bounds, cancel any active presses.
                //如果我们移动的并不远(还是像一个轻触),那么检查我们是否移动到ListView
                //的范围之外,如果是,则取消所有活跃的按下(取消轻触事件和长按事件的发生)
                final View motionView = getChildAt(mMotionPosition - mFirstPosition);//down事件下,按住的子视图
                final float x = ev.getX(pointerIndex);
                if (!pointInView(x, y, mTouchSlop)) {//如果当前移动的点已经离开了motionView视图的范围
                    setPressed(false);//取消ListView的按下状态
                    if (motionView != null) {
                        motionView.setPressed(false);//取消子视图的按下状态
                    }
                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                            mPendingCheckForTap : mPendingCheckForLongPress);//取消相关还未发生的时间
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                    updateSelectorState();
                } else if (motionView != null) {
                    // Still within bounds, update the hotspot.
                    ......
                }
                break;
            case TOUCH_MODE_SCROLL:
            case TOUCH_MODE_OVERSCROLL:
                //以上两个模式表示滑动的持续
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }
       可以看出onTouchMove方法之中主要分为两大部分:一部分是针对TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING,即ListView还未开始滑动的情况;另一部分是针对TOUCH_MODE_SCROLL、TOUCH_MODE_OVERSCROLL;即ListView已经开始滑动的情况。

 3.1TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING模式下的onTouchMove方法

       对于ListView还未开始滑动的情况,主要会进行两个判断,一个判断是当前滑动的距离是否足够大,如果是则进行滑动,如果不是,则进行第二个判断:当前滑动的位置是否离开了down手势事件时,按住的那个子视图的范围,如果离开了,则将ListView及按住的那个子视图的press状态设置为false。
       第一个判断主要是通过startScrollIfNeeded方法来执行的,如果startScrollIfNeeded返回true,则表示已经开始滑动,返回false,则表示还未满足开始滑动的条件;startScrollIfNeeded方法的相关源码如下:
private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
        final int deltaY = y - mMotionY;//与down事件发生时,按住的Y坐标相比,移动了多少距离
        final int distance = Math.abs(deltaY);
        final boolean overscroll = mScrollY != 0;//ListView本身是否存在Y方向上的偏移量
        //如果ListView发生了Y方向的偏移,或者移动的距离达到了一定程度
        if ((overscroll || distance > mTouchSlop) &&
                (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
            createScrollingCache();
            if (overscroll) {
                mTouchMode = TOUCH_MODE_OVERSCROLL;
                mMotionCorrection = 0;
            } else {
                mTouchMode = TOUCH_MODE_SCROLL;//更新触摸模式
                //开始滑动前,手指已经移动了的距离
                mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
            }
            removeCallbacks(mPendingCheckForLongPress);//开始滑动了,自然不是长按事件了
            setPressed(false);
            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
            if (motionView != null) {
                motionView.setPressed(false);
            }
            //调用OnScrollChangeListener接口,表明当前滑动状态改变!
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
            // Time to start stealing events! Once we've stolen them, don't let anyone
            // steal from us
            final ViewParent parent = getParent();
            if (parent != null) {
               //滑动开始了,就不允许ListView的父类中断触摸事件
                parent.requestDisallowInterceptTouchEvent(true);
            }
            scrollIfNeeded(x, y, vtev);//滑动
            return true;
        }

        return false;
    }
      能够进行滑动,需要满足两种条件之一:第一个条件是ListView本身进行了Y轴方向的偏移(滑动);第二个条件是以down手势事件发生时,手指按住屏幕的y坐标为起点,到此时move手势事件发生时,手指按下的屏幕y坐标为终点,这两点之间的距离超过了mTouchSlop。
      对于第一个条件,一般而言,ListView的滑动并不是ListView本身进行滑动(即,ListView的偏移量依旧为0),其滑动的原理为,ListView当前所有的子视图分别朝上(下)移动相同的距离(移动子视图的布局位置),从而实现滑动的效果;因此,一旦ListView本身的偏移量大于了0,则说明滑动到了ListView的底部或者顶部。此时为了实现ListView的回弹效果,则需要先进行一点滑动。
       对于第二个条件之中mTouchSlop的值是配置好了的,默认为8(像素)。
       真正进行滑动处理的是scrollIfNeeded方法,总体而言,scrollIfNeeded方法也分别处理的TOUCH_MODE_SCROLL和TOUCH_MODE_OVERSCROLL。
        其中处理TOUCH_MODE_SCROLL模式的其源码如下:
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        int rawDeltaY = y - mMotionY;//上次触摸事件到此次触摸事件移动的距离
        ......
        if (mLastY == Integer.MIN_VALUE) {
            rawDeltaY -= mMotionCorrection;
        }
        ......
        //如果滑动需要滑动的距离
        final int deltaY = rawDeltaY;
        int incrementalDeltaY =
                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
        int lastYCorrection = 0;

        if (mTouchMode == TOUCH_MODE_SCROLL) {
            ......
            if (y != mLastY) {//此次触摸事件和上次触摸事件的y值发生了改变(需要滑动的距离>0)
                // We may be here after stopping a fling and continuing to scroll.
                // If so, we haven't disallowed intercepting touch events yet.
                // Make sure that we do so in case we're in a parent that can intercept.
                // 当停止一个抛动且继续滑动之后,我们可能会执行此处的代码
                //确保ListView的父视图不会拦截触摸事件
                if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
                        Math.abs(rawDeltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                final int motionIndex;//down手势事件,按住的子视图在ListView之中的位置
                if (mMotionPosition >= 0) {
                    motionIndex = mMotionPosition - mFirstPosition;
                } else {
                    // If we don't have a motion position that we can reliably track,
                    // pick something in the middle to make a best guess at things below.
                    motionIndex = getChildCount() / 2;
                }

                int motionViewPrevTop = 0;//down手势事件,按住的子视图的顶端位置
                View motionView = this.getChildAt(motionIndex);
                if (motionView != null) {
                    motionViewPrevTop = motionView.getTop();
                }

                // No need to do all this work if we're not going to move anyway
                //不需要做所有的工作,如果我们并没有进行移动
                boolean atEdge = false;//是否到达了ListView的边缘
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);//追踪手势滑动
                }

                // Check to see if we have bumped into the scroll limit
                //查看我们是否撞到了滑动限制(边缘)
                motionView = this.getChildAt(motionIndex);
                if (motionView != null) {
                    // Check if the top of the motion view is where it is
                    // supposed to be
                    final int motionViewRealTop = motionView.getTop();
                    if (atEdge) {//到达了ListView的边缘
                        // Apply overscroll
                        //响应的回弹效果实现
                        ......
                    }
                    mMotionY = y + lastYCorrection + scrollOffsetCorrection;//更新
                }
                mLastY = y + lastYCorrection + scrollOffsetCorrection;//更新
            }
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
             ......
        }
    }
      总体而言,这一步也算是一个外壳,真正跟踪滑动运行的是trackMotionScroll方法。 trackMotionScroll方法的逻辑较为复杂;总体而言一个可归纳为以下7个步骤,来实现滑动效果:
      1、确定相关变量的值,以及定义一些临时变量;代码如下:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        final int childCount = getChildCount();//子视图个数
        if (childCount == 0) {
            return true;
        }

        final int firstTop = getChildAt(0).getTop();//第一个子视图的顶部
        //最后一个子视图的底部
        final int lastBottom = getChildAt(childCount - 1).getBottom();

        final Rect listPadding = mListPadding;

        // "effective padding" In this case is the amount of padding that affects
        // how much space should not be filled by items. If we don't clip to padding
        // there is no effective padding.
        int effectivePaddingTop = 0;//paddingTop
        int effectivePaddingBottom = 0;//paddingBottom
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            effectivePaddingTop = listPadding.top;
            effectivePaddingBottom = listPadding.bottom;
        }

         // FIXME account for grid vertical spacing too?
        //第一个子视图的顶部离ListView顶部的距离,即向下滑动此距离,需要调用getView方法重新绑定一个子视图
        final int spaceAbove = effectivePaddingTop - firstTop;
        final int end = getHeight() - effectivePaddingBottom;
        //最后一个子视图的底部离ListView底部的距离,即向上可滑动的距离,需要调用getView方法重新绑定一个子视图
        final int spaceBelow = lastBottom - end;
        //整个ListView的高度(出去padding)
        final int height = getHeight() - mPaddingBottom - mPaddingTop;
        //确保最大的可滚动距离不能超过ListView的高度
        if (deltaY < 0) {
            deltaY = Math.max(-(height - 1), deltaY);
        } else {
            deltaY = Math.min(height - 1, deltaY);
        }
        //确保最大的可滚动距离不能超过ListView的高度
        if (incrementalDeltaY < 0) {
            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
        } else {
            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
        }
        //当前第一个视图对应的item在适配器之中的位置
        final int firstPosition = mFirstPosition;
        ......
}
         2、判断当前滑动是否已经滑动到ListView的顶(低)部边缘位置;
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //是否可以向下滑动
        //当前第一个子视图对应的item在适配器中的位置为0,且
        //第一个子视图整个视图的位置都在ListView之中,且
        //手指滑动的距离大于0
        //以上三个条件同时成立,则不能向下滑动
        final boolean cannotScrollDown = (firstPosition == 0 &&
                firstTop >= listPadding.top && incrementalDeltaY >= 0);
        //是否可以向上滑动
        //当前最后一个子视图对应的item在适配器中的位置为最后一个,且
        //最后一个子视图整个视图的位置都在ListView之中,且
        //手指滑动的距离小于于0
        //以上三个条件同时成立,则不能向上滑动
        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
                lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

        if (cannotScrollDown || cannotScrollUp) {//如果到达了边缘,则返回true
            return incrementalDeltaY != 0;
        }
        ......
}
       如果到达了ListView的边缘位置,且滑动的距离不等于0,则返回true。
       3、如果未达到ListView的边缘位置,则判断当前滑动,是否将一些子视图完全滑出了ListView的可是范围之外;
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //是否从下往上滑动
        final boolean down = incrementalDeltaY < 0;

        ......
        //headerViewsCount与footerViewsStart之间的就是item所在的范围
        //页眉视图的个数
        final int headerViewsCount = getHeaderViewsCount();
        //页脚视图对应的item在适配器中对应的位置
        final int footerViewsStart = mItemCount - getFooterViewsCount();

        int start = 0;//第一个离开了ListView的可见范围的子视图的位置(index)
        int count = 0;//一共有多少个子视图离开了ListView的可视范围

        if (down) {//从下往上移动
            int top = -incrementalDeltaY;//移动的距离
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                //是否有子视图完全被滑动离开了ListView的可见范围
                if (child.getBottom() >= top) {
                    break;
                } else {//当前子视图
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);//回收子视图
                    }
                }
            }
        } else {//从上往下移动
            int bottom = getHeight() - incrementalDeltaY;//移动的距离
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                bottom -= listPadding.bottom;
            }
            for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                //是否有子视图完全滑动离开了ListView的可见范围
                if (child.getTop() <= bottom) {
                    break;
                } else {
                    start = i;
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);//回收子视图
                    }
                }
            }
        }

        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

        mBlockLayoutRequests = true;

        if (count > 0) {//如果存在完全离开了ListView可视范围的子视图
            detachViewsFromParent(start, count);//将这些完全离开了可是范围的子视图全部删掉
            mRecycler.removeSkippedScrap();//从视图重用池中删除需要丢弃的视图
        }
        ......
}
           关于判断是否存在子视图完全离开了ListView的可视范围的算法,如下图所示(以从上往下为例):
【进阶android】ListView源码分析——ListView的滚动机制_第1张图片
        如图所示,这个黑线框就是ListView在滑动之前的可视范围;down手势事件,手指按在了A点,当前move手势事件滑动到了B点,代码之中的incrementalDeltaY本地变量的值=A-B=C-D。其中D是down手势事件时,ListView的底部,而当滑动完成之后C就是ListView的底部,而当前所有顶部在C点下方的子视图,在滑动动作完成之后,都会被滑动到ListView的底部以下,即完全离开了ListView的可视范围。所以,ListView会以incrementalDeltaY本地变量的值来当做判断是否完全离开ListView的标准。
       4、将未从ListView删除的子视图(即没有完全离开ListView可视范围的所有视图)全部朝上(下)移动incrementalDeltaY变量对应的值,单位为像素。代码如下:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //将未删除的所有的子视图朝上(下)移动incrementalDeltaY这么多距离
        offsetChildrenTopAndBottom(incrementalDeltaY);
        //更新第一个子视图对应的item在适配器中的位置
        if (down) {
            mFirstPosition += count;
        }
        ......
}
        通过ViewGroup类的offsetChildrenTopAndBottom方法来实现子视图的移动;而该方法的原理则是同时将一个子视图的mTop变量和mBottom变量加上incrementalDeltaY变量的值。
        5、ListView有可能删除了一些完全离开ListView视图范围的子视图,为了将ListView填充满,需要重新调用适配器的getView方法,绑定相应的item。代码如下:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        //如果还有可移动的范围
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
          //因为有可能有一些子视图完全离开了ListView范围,所有需要重新加载新的item来填充ListView的空白
            fillGap(down);
        }
        ......
}
        spaceAbove变量和spaceBelow变量,是在第一个步骤里被定义赋值的;其具体的含义如下图所示(以 spaceAbove为例):
【进阶android】ListView源码分析——ListView的滚动机制_第2张图片
        如图所示,如果向下滑动的距离超过了spaceAbove变量的值,那么肯定需要重新获取一个新的item,来作为新的第一个子视图。
        fillGap方法的实现请参照【进阶android】ListView源码分析——子视图的七种填充方式一文的分析。
        6、重新设定被选中的item;
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //重新定位被选中的item
        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
            final int childIndex = mSelectedPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(mSelectedPosition, getChildAt(childIndex));
            }
        } else if (mSelectorPosition != INVALID_POSITION) {
            final int childIndex = mSelectorPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(INVALID_POSITION, getChildAt(childIndex));
            }
        } else {
            mSelectorRect.setEmpty();
        }
        ......
}
           7、执行OnScrollListener中的onScroll方法,代码如下:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
       //执行mOnScrollListener中的onScroll方法
        invokeOnItemScrollListener();
        return false;
}
          至此, onTouchMove方法方法对TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING三种模式(即ListView还未开始滑动的情况),做了一个大致的分析。总体而言,onTouchMove方法首先调用startScrollIfNeed方法,startScrollIfNeed方法根据两个条件判断是否继续,这两个条件一个是ListView本身是否发生偏移,一个是滑动的距离是否超过了mTouchSlop变量的值,这两个条件任意一个为true,则调用scrollIfNeed方法;而在scrollIfNeed方法之中,则根据trackMotionScroll方法的返回值判断是否已经滚动到了ListView的边缘,如果是则实现相应的边缘效果。
        trackMotionScroll方法才是真正实现滑动效果的方法。
        最后要特别说明一下mLastY和mMotionY这两个变量,根据android官方的解释,前者的意思是:Y value from on the previous motion event (if any),即上一个手势事件的Y值;后者的意思是:The Y value associated with the the down motion event,即与down手势事件相关的Y值,然而在onTouchMove方法调用的scrollIfNeed方法中,都将当前的move手势的Y值都更新到这两个变量之上,如此而言,总觉得这两个变量的意义都是差不多的,不知道可不可以如此理解,也算是一个存疑点。

3.2TOUCH_MODE_SCROLL、TOUCH_MODE_OVERSCROLL模式下的onTouchMove方法

        前文曾提过,onTouchMove方法对两种情况分别进行了处理;一种是ListView还未开始滑动;一种是ListView正在滑动。onTouchMove方法对两者的处理的最大的不同就是,前者调用了startScrollIfNeed方法,后者直接调用了scrollIfNeed方法。
       而在scrollIfNeed方法的源码中也对两种情况进行分别处理,如下代码所示:
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        ......

        if (mTouchMode == TOUCH_MODE_SCROLL) {//对为滑动或持续滑动的情况的处理
            ......
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {//对滑动到ListView边缘的处理
            ......
        }
        ......    
    }
       对于TOUCH_MODE_SCROLL的处理,在3.1节已经详细叙述;而对TOUCH_MODE_OVER_SCROLL的处理则和TOUCH_MODE_SCROLL分支中,滑动到ListView边缘的处理方式相似。

4、up——滑动结束与后序

        终于来到触摸事件的最后一个阶段:up手势事件;根据down、move的分析方式,我们先看看onInterceptToucheEvent方法之中对up手势事件的处理,代码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        ......

        switch (actionMasked) {
        ......
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            mTouchMode = TOUCH_MODE_REST;
            mActivePointerId = INVALID_POINTER;
            recycleVelocityTracker();
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            ......
            break;
        }

        ......
        }

        return false;
    }
}
        逻辑很简单,将相关值设置为初始值,回收速率控制器,报告滚动状态变更;当然,要执行这段逻辑的前提是,up手势事件还能被传递到onInterceptTouchEvent方法之中。
        对于up手势事件,onTouchUp方法之中分别处理了三大类别的触摸模式:
         1、TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING;
         2、TOUCH_MODE_SCROLL;
         3、TOUCH_MODE_OVERSCROLL;

4.1、TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING模式下的onTouchUp方法

          此三类模式下的onTouchUp方法只做了一件事情,那就是执行点击事件(OnItemClickListener)。源码如下:
private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            //down手势事件,按住的子视图对应的item在adapter之中的位置
            final int motionPosition = mMotionPosition;
            //down手势事件,按住的子视图
            final View child = getChildAt(motionPosition - mFirstPosition);
            if (child != null) {
                if (mTouchMode != TOUCH_MODE_DOWN) {
                    child.setPressed(false);
                }

                final float x = ev.getX();
                //up手势事件对应的x坐标是否还在ListView的视图范围之内
                final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                if (inList && !child.hasFocusable()) {//x坐标还在ListView之中,且子视图不能获取焦点
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }

                    final AbsListView.PerformClick performClick = mPerformClick;
                    performClick.mClickMotionPosition = motionPosition;//更新位置
                    performClick.rememberWindowAttachCount();

                    mResurrectToPosition = motionPosition;
                    //如果当前触摸模式属于TOUCH_MODE_DOWN或者TOUCH_MODE_TAP
                    //则表明还未执行轻触事件或者还未执行长按事件
                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                        //取消轻触事件或者长按事件的触发
                        removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                                mPendingCheckForTap : mPendingCheckForLongPress);
                        mLayoutMode = LAYOUT_NORMAL;
                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            mTouchMode = TOUCH_MODE_TAP;//将触摸模式更改为轻触模式
                            setSelectedPositionInt(mMotionPosition);//设置被选中的item
                            layoutChildren();//重新布局
                            child.setPressed(true);//子视图按下状态为true
                            positionSelector(mMotionPosition, child);//定位选中效果
                            setPressed(true);ListView的按下状态为true
                            if (mSelector != null) {
                                Drawable d = mSelector.getCurrent();
                                if (d != null && d instanceof TransitionDrawable) {
                                    ((TransitionDrawable) d).resetTransition();
                                }
                                mSelector.setHotspot(x, ev.getY());
                            }                                
                            //mTouchModeReset是一个Runnalbe
                            //主要用于将触摸模式恢复为TOUCH_MODE_RESET
                            //取消子视图和ListView的按下状态
                            //在数据未改变的情况下执行item click.
                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }
                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchModeReset = null;
                                    mTouchMode = TOUCH_MODE_REST;
                                    child.setPressed(false);
                                    setPressed(false);
                                    if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                        performClick.run();
                                    }
                                }
                            };
                            //延迟执行mTouchModeReset
                            postDelayed(mTouchModeReset,
                                    ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            updateSelectorState();
                        }
                        return;
                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                        //如果已经调用了长按事件,则直接执行item click
                        performClick.run();
                    }
                }
            }
            mTouchMode = TOUCH_MODE_REST;
            updateSelectorState();
            break;
            ......

            ......
        }

        ......
    }
       执行item click遵循一个规则;如果当前触摸模式,为轻触模式或者轻触模式之前的模式,那么则需要将当前模式强制设定为轻触模式,并且轻触模式与执行item click直接存在一个延迟(一般延迟时间为64毫秒),一般而言轻触模式和执行item click处理时间上差异,还有就是轻触模式下ListView与对应的子视图的press状态为true,item click下ListView与对应的子视图的press状态为false。
       结合上文所述,performClick变量是一个PerformClick内部类,主要目的是调用OnItemClickListener监听器的onItemClick方法,实现item click的效果。

4.2、TOUCH_MODE_SCROLL模式下的onTouchUp方法

        TOUCH_MODE_SCROLL模式,则会根据当前速率,以及是否滑动到item的第一个item或者最后一个item,来判断是否将TOUCH_MODE_SCROLL模式变为TOUCH_MODE_FILING模式;具体的代码如下:
private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......
        case TOUCH_MODE_SCROLL:
            final int childCount = getChildCount();
            if (childCount > 0) {
                //所有子视图的最高位置
                final int firstChildTop = getChildAt(0).getTop();
                //所有子视图的最低位置
                final int lastChildBottom = getChildAt(childCount - 1).getBottom();
                //ListView的top位置
                final int contentTop = mListPadding.top;
                //ListView的bottom位置
                final int contentBottom = getHeight() - mListPadding.bottom;
                //所有的item都完全展示在ListView的可是范围之内
                if (mFirstPosition == 0 && firstChildTop >= contentTop &&
                        mFirstPosition + childCount < mItemCount &&
                        lastChildBottom <= getHeight() - contentBottom) {
                    //不滑动,恢复初始状态
                    mTouchMode = TOUCH_MODE_REST;
                    //滑动状态变为不滑动
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                } else {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    //计算当前滑动的速率
                    //computeCurrentVelocity方法第一个入参表示单位
                    //1000表示每一秒滑过的像素
                    //第二个入参表示computeCurrentVelocity方法能够计算的最大速率
                    //mMaximumVelocity的值默认为8000
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    
                    //getYVelocity方法将返回Y方向上的最后一次计算出的速率
                    //mVelocityScale默认为1
                    final int initialVelocity = (int)
                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                    // Fling if we have enough velocity and we aren't at a boundary.
                    // Since we can potentially overfling more than we can overscroll, don't
                    // allow the weird behavior where you can scroll to a boundary then
                    // fling further.
                    // 如果我们有着足够的速率且在ListViewd的可视范围之内,抛动。
                    // 一旦我们潜在的将抛动回滚的距离多于滑动回滚,则禁止滑动到一个边界,然后
                    // 抛动更远,这一奇怪的行为。
                    // mMinimumVelocity表示可以进行抛动的速率的零界点,默认值为50像素/秒
                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                    // mOverscrollDistance的默认值为0
                    if (flingVelocity &&
                            !((mFirstPosition == 0 &&
                                    firstChildTop == contentTop - mOverscrollDistance) ||
                              (mFirstPosition + childCount == mItemCount &&
                                    lastChildBottom == contentBottom + mOverscrollDistance))) {
                        //进入此分支满足的条件为:速率足够大,并且可以上、下同时滑动
                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
                            if (mFlingRunnable == null) {
                                mFlingRunnable = new FlingRunnable();
                            }
                            //滚动状态变为抛动状态
                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                            mFlingRunnable.start(-initialVelocity);//开始抛动
                            dispatchNestedFling(0, -initialVelocity, true);
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        }
                    } else {//速率不够大,或者不能向上滑动,或者不能向下滑动
                        mTouchMode = TOUCH_MODE_REST;
                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        if (mFlingRunnable != null) {
                            mFlingRunnable.endFling();//结束抛动
                        }
                        if (mPositionScroller != null) {
                            mPositionScroller.stop();
                        }
                        if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) {
                            dispatchNestedFling(0, -initialVelocity, false);
                        }
                    }
                }
            } else {//没有子视图则不进行滑动
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;
            ......
        }
        ......
    }
       ListView的抛动完全是由FlingRunnable内部类控制、实现;关于FlingRunable内部类的抛动机制,下文会详细叙述!。

4.3 TOUCH_MODE_OVERSCROLL模式下的onTouchUp方法

       源码如下:
private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......

        case TOUCH_MODE_OVERSCROLL:
            if (mFlingRunnable == null) {
                mFlingRunnable = new FlingRunnable();
            }
            final VelocityTracker velocityTracker = mVelocityTracker;
            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//计算当前速率
            final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
            //当前滚动状态变为抛动状态
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
            if (Math.abs(initialVelocity) > mMinimumVelocity) {//当前速率超过最低抛动速率
                mFlingRunnable.startOverfling(-initialVelocity);
            } else {
                mFlingRunnable.startSpringback();
            }

            break;
        }

        ......
    }

4.4 onTouchUp方法的结尾部分 

       上文提及,onTouchUp方法会针对三种情况的触摸模式,分别进行处理;而在这三种情况中,无论是哪一种,当onTouchUp分别处理之后,都还会调用一小部分公共的代码,这一小部分代码如下所示:
private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
             //三种情况的不同处理
             ......
        }

        setPressed(false);//ListView无按下状态

        ......

        // Need to redraw since we probably aren't drawing the selector anymore
        //需要重绘,因为我们可能并没有再绘制选择器了
        invalidate();
        removeCallbacks(mPendingCheckForLongPress);//删除长按事件
        recycleVelocityTracker();

        mActivePointerId = INVALID_POINTER;

       ......
    }
        至此,除了关于ListView的抛动机制之外,整个ListView的滑动,在三大触摸手势事件中流程便分析完毕了。

5、ListView的抛动机制

       根据上文的分析,承载ListView抛动效果的是AbsListView之中的内部类FlingRunnable,FlingRunnable是执行了Runnable接口,它是一个抛动行为的响应者,通过start方法,初始化一个抛动,抛动的每一帧都在run方法之中被处理。因为FlingRunnable执行了一个Runnable接口,所以每一个该类的实例,在抛动过程中,都会将自己作为一个消息重复发送给UI线程进行处理。
        FlingRunnable类中有一个较为重要的属性,mScroller,它是一个OverScroller对象,OverScroller与Scroller类似,只不过前者添加了回弹效果的实现,ListView就是通过FlingRunnable类,间接使用OverScroller进行抛动相关的计算,根据计算的结果,来执行trackScrollMotion方法,来实现抛动的效果。
        对于FlingRunnable,start方法是一个抛动的开始,run方法是一个抛动的执行和终结,因此着重分析这两个方法。

5.1FlingRunnable类的start方法。

         由上文可知,当触摸事件到达up阶段时,会调用AbsListView的onTouchUp方法,该方法会根据速率跟踪器,计算出当前滑动的速率,如果速率超过了抛动的最小速率,那么就会调用Flingable类的start方法;其调用过程如下:
private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......
        case TOUCH_MODE_SCROLL:
            final int childCount = getChildCount();
            if (childCount > 0) {
                ......
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    //计算当前速率,单位为像素/秒
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    //获取当前速率,单位为像素/秒
                    final int initialVelocity = (int)
                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                    
                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                    ......
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                    mFlingRunnable.start(-initialVelocity);//当前速率的相反速率
                    ......       
            } else {
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;

            ......
        }

        ......
    }
       在onTouchUp方法之中,通过速率跟踪器,计算出当去Y轴上的速率,从而将此速率,作为mFlingRunnable.start方法的入参;然后onTouchUp方法并未直接将速率作为入参,而是取了入参的相反数,这是什么原因呢?
       我们暂且不提此问题,而是继续看start方法的源代码:
private class FlingRunnable implements Runnable {
        ......
        void start(int initialVelocity) {
            //如果是从上往下抛动,则initialVelocity的值为负数,则上一次抛动的位置为很大            
           //如果是从下往上抛动,则initialVelocity的值为正数,则上一次抛动的位置为0
            int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
            mLastFlingY = initialY;
            //设置插入器,插入器的含义是给定一个对应点,获取此对应点的速率
            mScroller.setInterpolator(null);//使用默认的插入器
            //开始抛动
            //第一个入参表示x方向上的开始点
            //第二个入参表示y方向上的开始点
            //第三个入参表示x方向上的速率
            //第四个入参表示y方向上的速率
            //第五个入参表示x方向上抛动的最小值
            //第六个入参表示x方向上抛动的最大值
            //第七个入参表示y方向上抛动的最小值
            //第八个入参表示y方向上抛动的最大值
            mScroller.fling(0, initialY, 0, initialVelocity,
                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
            //触摸模式变为抛动模式
            mTouchMode = TOUCH_MODE_FLING;
            //持续抛动
            postOnAnimation(this);
            ......
        }
         ......
}
        在高中物理课堂上,我们知道速率的正负符号,代表了此速率的方向;这一定律在此处也适用;而在start方法的源代码中,首先就会根据速率的正负符号来决定抛动的初始位置;如果是负号,则抛动的初始位置为int的最大值,反之则抛动的初始位置为0。
       另一方面,前文已经提过,ListView的抛动本质上则是调用了trackScrollMotion方法;trackScrollMotion的原型如下:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) ;
       第二个入参表示需要滚动的距离。
       回顾一下滚动机制中,调用trackMotionScroll方法时,此参数是如何计算而来的?根据scrollIfNeed方法中的代码:
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        int rawDeltaY = y - mMotionY;
        ......
        final int deltaY = rawDeltaY;
        int incrementalDeltaY =
                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
        ......

        if (mTouchMode == TOUCH_MODE_SCROLL) {
            ......
            if (y != mLastY) {
                ......
                boolean atEdge = false;
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
                }
                ......
            }
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
            ......
        }
    }
       从此代码可以看出,第二个入参incrementalDeltaY是通过两方面得来;一方面,如果mLastY为int类型的最小值,则等于deltaY,在onTouchDown方法中可以得出,当此时触摸事件为down阶段时,mLastY为int类型的最小值,在此情况下, incrementalDeltaY的值依赖于deltaY,而deltaY则等于y-mMotionY,其中y为第一次move时的y坐标,mMotionY为down手势事件,手指按下的y坐标;另一方面,incrementalDeltalY的值等于y-mLastY,其中y表示当前y坐标,mLastY表示上一次move手势事件。
       总体而言,incrementalDeltaY的值等于当前触摸事件的Y位置减去上一次触摸事件的Y位置。这是滑动的情况。
       对于抛动,incrementalDeltaY的值等于上一次抛动后的Y位置减去当前抛动后的Y位置。恰恰与滑动的情况凑成一对相反数。
       因此,滑动与抛动,都是通过速率(位移)的正负,来确定移动的方向;两者不同的是,滑动,通过正号来表示从上往下移动,而抛动,则是通过负号来表示从上往下移动。
       在onTouchUp方法调用Flingable.start方法的过程中,onTouchUp方法获得的速率,是滑动模式下的速率,而start方法中的速率,则是抛动的速率,因此在onTouchUp方法调用Flingable.start方法时,需要对速率取反,再作为start方法的入参。
       在start方法里的具体源码中,首先会根据传来的速率的正负,来设置移动的方向;如果速率是负,则表示手指从上往下抛动,ListView朝上方移动,此时初始的抛动位置为int类型的最大值,且抛动过程中的抛动位置会越来越小;如果速率是正,则表示手指从下往上抛动,ListView朝下方移动,此时初始的抛动位置为0,且抛动过程中的抛动位置会越来越大。如图所示
                                                【进阶android】ListView源码分析——ListView的滚动机制_第3张图片

        当抛动过程中,抛动的位置越来越大,则表示朝下方滚动,即ListView朝下方移动;抛动的位置越来越小,则说明朝上方滚动,即ListView朝上方移动。
       start方法之中,在将抛动的初始位置确定之后,就会调用mScroller(一个OverScroll对象)的fling方法,设置一个抛动的初始状态;接着将触摸模式修改为抛动动模式,最后发送一个异步消息,当异步消息被执行时,会调用Flingable的run方法来计算当前的抛动状态,然后根据抛动状态,调用trackScrollMotion方法来实现ListView移动的效果;实现完移动效果之后,会根据情况,再次调用postOnAnimation方法发送一个异步消息,如此来实现一种持续抛动的效果。

5.2FlingRunnable类的run方法。

        start是一个抛动的开始;而run方法则是一个抛动过程中的每一帧,每抛动一次,就会调用一次run方法;然而,一个抛动过程中的每一帧,可能存在不同的情况,例如是否继续抛动,是否停止抛动等等,而这些不同的情况,往往和ListView的触摸模式息息相关。而run方法则针对不同的触摸模式,做出了不同的处理。
          总的来说,run方法主要处理了TOUCH_MODE_FLING和TOUCH_MODE_OVERFLING这两种触摸模式的情况。
     先看看run方法对TOUCH_MODE_FLING模式的处理,源码如下:
public void run() {
            switch (mTouchMode) {
            ......
            case TOUCH_MODE_FLING: {
                if (mDataChanged) {//如果数据改变了,重新布局
                    layoutChildren();
                }
                //如果item为0,或者子视图为零,结束抛动,并返回
                if (mItemCount == 0 || getChildCount() == 0) {
                    endFling();
                    return;
                }

                final OverScroller scroller = mScroller;
                //计算当前抛动值,其返回值为true,则表示还可以继续滚动
                boolean more = scroller.computeScrollOffset();
                final int y = scroller.getCurrY();//获取当前的Y坐标

                // Flip sign to convert finger direction to list items direction
                // (e.g. finger moving down means list is moving towards the top)
                // 轻抛信号,此信号表示将手指抛动的方向转换成列表item移动的方向
                // 例如,手指朝下移动意味着列表正在往顶部移动
                // 此处计算滚动距离的方式和滑动时计算滚动距离的方式相反;前者是上一次减这一次,后者是这一次减上一次
                int delta = mLastFlingY - y;

                // Pretend that each frame of a fling scroll is a touch scroll
                // 假装将一个抛动滚动的每一帧当做一个触摸滚动
                if (delta > 0) {//从上往下抛动,开始位置在抛动位置的上方
                    // List is moving towards the top. Use first view as mMotionPosition
                    // 列表正朝上方移动,将第一个子视图对应的item作为触摸位置
                    mMotionPosition = mFirstPosition;
                    final View firstView = getChildAt(0);
                    mMotionViewOriginalTop = firstView.getTop();

                    // Don't fling more than 1 screen
                    // 抛动的距离不能超过一屏
                    delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
                } else {
                    // List is moving towards the bottom. Use last view as mMotionPosition
                    // 列表正在朝着底部移动,使用最后一个列表对应的item作为触摸位置
                    int offsetToLast = getChildCount() - 1;
                    mMotionPosition = mFirstPosition + offsetToLast;

                    final View lastView = getChildAt(offsetToLast);
                    mMotionViewOriginalTop = lastView.getTop();

                    // Don't fling more than 1 screen
                    // 抛动的距离不能超过一屏
                    delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
                }

                // Check to see if we have bumped into the scroll limit
                // 检测我们是否撞到了滚动限制之中
                View motionView = getChildAt(mMotionPosition - mFirstPosition);
                int oldTop = 0;
                if (motionView != null) {
                    oldTop = motionView.getTop();
                }

                // Don't stop just because delta is zero (it could have been rounded)
                // atEdge是否到达边界
                final boolean atEdge = trackMotionScroll(delta, delta);//进行滚动
                // 是否停止
                final boolean atEnd = atEdge && (delta != 0);
                if (atEnd) {//如果需要停止
                    if (motionView != null) {
                        // Tweak the scroll for how far we overshot
                        int overshoot = -(delta - (motionView.getTop() - oldTop));
                        overScrollBy(0, overshoot, 0, mScrollY, 0, 0,
                                0, mOverflingDistance, false);//回滚
                    }
                    if (more) {//还能继续抛动
                        edgeReached(delta);//已经到达边界的情况下的抛动处理
                    }
                    break;
                }

                if (more && !atEnd) {//还能继续抛动,且不需要停止
                    if (atEdge) invalidate();//如果到达边界,重绘
                    mLastFlingY = y;//更新上一次抛动点的y坐标
                    postOnAnimation(this);//持续抛动
                } else {//如果不能继续抛动,或者需要停止
                    endFling();//停止抛动
                    ......
                }
                break;
            }

            .......
       }
        一般而言,调用了start方法时,就将ListView当前的触摸模式更改为TOUCH_MODE_FLING,同时,也发送了一个异步消息。不出意外,这个消息被执行时,会调用上面这段源代码。
       这段代码中,首先会根据mScroller计算出当前抛动的信息,主要是当前抛动的位置,然后将上一次位置(mLastY)减去当前抛动的位置,来获取偏移量,调用trackScrollMotion方法来实现滚动效果。
       然后,根据trackScrollMotion的返回值,判断是否滚动到了ListView的边缘位置。
       如果到达了边缘位置,且上一次位置和当前位置不同,则需要进行停止,首先调用ListView的overScrollBy方法,滑动ListView本身,如果是ListView朝上抛动,则调用了overScrollBy方法后,会继续向上移动一点(mScrollY为负数),当然不会继续移动太多(最多移动的距离为6dp)。随后,判断是否还会继续移动ListView,如果时,因为此时已经到达了边缘,所以会继续调用edgeReached方法,绘制边缘效果,并且将触摸模式改为TOUCH_MODE_OVERFLING模式。
       如果还未到达边缘位置,且还能继续移动,则再次发生一个异步消息,此消息被执行时,会持续调用run方法。
       如果还未到达边缘位置,且不能继续移动,则调用endFing方法,停止抛动。
       前文曾述,edgeReached方法会在达到ListView的边缘位置,且抛动还能继续的情况被调用,该方法中会绘制边缘效果,修改触摸模式,而在完成这两件事情之后,会继续发送一个异步消息,而此异步消息被执行时,会再次调用run方法;只不过,此时,由于edgeReached方法已经将触摸模式修改为TOUCH_MODE_OVERFLING模式,所以会执行run方法中的TOUCH_MODE_OVERFLING模式对应的分支。
       run方法中的TOUCH_MODE_OVERFLING模式对应的分支源码如下:
@Override
        public void run() {
            switch (mTouchMode) {
            ......
            case TOUCH_MODE_OVERFLING: {
                final OverScroller scroller = mScroller;
                if (scroller.computeScrollOffset()) {//计算当前滚动速率
                    final int scrollY = mScrollY;
                    final int currY = scroller.getCurrY();
                    final int deltaY = currY - scrollY;
                    if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
                            0, mOverflingDistance, false)) {
                        final boolean crossDown = scrollY <= 0 && currY > 0;//从上往下
                        final boolean crossUp = scrollY >= 0 && currY < 0;//从下往上
                        if (crossDown || crossUp) {
                            int velocity = (int) scroller.getCurrVelocity();
                            if (crossUp) velocity = -velocity;

                            // Don't flywheel from this; we're just continuing things.
                            scroller.abortAnimation();
                            start(velocity);
                        } else {
                            startSpringback();//开始回弹
                        }
                    } else {
                        invalidate();
                        postOnAnimation(this);
                    }
                } else {//如果不能继续抛动
                    endFling();//停止抛动
                }
                break;
            }
            }
        }
         至此,ListView的抛动机制就大致分析完了。

6、ListView滚动机制的总结

        总体而言,ListView的整个滚动机制的生命周期可以分为8个阶段,对应着8个不同的触摸模式;理清这8个不同的触摸模式的转换,就大致明白了整个滚动机制。
       ListView的8个触摸模式的相互转换可如下图所示:
【进阶android】ListView源码分析——ListView的滚动机制_第4张图片
             如图所示,图中一共标出了16个转换,将ListView中的8个触摸模式大致的转换标记出来(还有些细枝末节没有列出 )。这16个转换中1-5表示一次完整的滑动过程(未到达边际),9、11表示在已经滑动的基础上一次完成的抛动过程(未达到边际)。
        1-5的过程如下:1)手指按住屏幕,触发down手势事件,发出轻触检测事件;2)轻触检测事件被触发,发出长按检测事件;3)长按检测事件被促发,且没有执行长按事件;4)手指移动屏幕,触发move手势事件,且手指移动的距离超过最小敏感距离(mTouchSlop变量);5)手指离开屏幕,触发up手势事件,且当前滑动的速率没有达到最小抛动速率。
        9、11的过程如下:9)手指离开屏幕,触发up手势事件,且当前滑动的速率达到或超过最小抛动速率;11)当抛动不能继续下去时,则停止抛动。
        余下的转换过程在一下时刻发生:
         6)当手指按住屏幕,触发down手势事件之后,轻触检测事件触发之前,这段时间中,触发了move手势事件;
         7)此情况有两个场景:第一个场景是当手指按住屏幕,触发down手势事件之后,长按检测事件触发之前,这段时间中,触发了up手势事件(如果此时轻触检测事件还未促发,则将触摸模式修改为TOUCH_MODE_TAP,等待一定时间后,再变为TOUCH_MODE_RESET模式);第二个场景是,触发了长按检测事件,并且长按事件被成功执行;
         8)与7)的第一种场景一致;
       10)当抛动时,达到了ListView的边缘(edgeReached方法);
       12)完成抛动边缘效果及回填之后;
       13)正在抛动过程中,又一次发生了down手势事件;
       14)滑动到ListView的边缘;
       15)滑动到ListView的边缘,又继续产生move手势事件;
       16)正抛动到ListView边缘时,又产生了一次down手势事件。
       总体而言,ListView的滑动、抛动两大滚动机制的原理,还是将ListView中所有的子视图进行朝上(下)位移,如果存在子视图在滚动之后,完全离开了ListView的可视范围,则将这些子视图完全回收;如果存在滚动之后,ListView的可视范围中还余有足够的空间,则重新绑定子视图和数据,重用一个子视图,直到余下的空间全部被重用的子视图填充完毕。
        而滑动与滚动最显著的区别则是手指是否还在屏幕之上;具体而言,前者是借助触摸事件的三个过程(down、move、up),尤其是move手势事件来进行与手指的实时联动;而后者,则主要借助OverScroller类,计算抛动的每一帧结果,根据计算结果来更新ListView的当前效果。
        至此ListView的滚动机制一文便结束!
         当然 由于本人自身水平所限,文章肯定有一些不对的地方,希望大家指出! 愿大家一起进步,谢谢!

你可能感兴趣的:(视图框架,android,ListView)