前面几篇文章主要在介绍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.2滑动相关的变量
ListView之中与滑动相关的变量,主要的作用是用来缓存一些重要的坐标点,如下所示:
mTouchMode: | 当前的触摸模式,1.1节中所列出的八个常量之一,初始值为TOUCH_MODE_REST; |
mMotionCorrection: | 开始滑动之前,手指已经移动的距离; |
mMotionPosition: | 接受到down手势事件的视图对应的item的位置; |
mMotionViewOriginalTop: | 接收到down手势事件的视图的顶部偏移量; |
mMotionX: | down手势事件位置的X坐标; |
mMotionY: | down手势事件位置的Y坐标; |
mLastY: | 上一个手势事件位置的Y坐标(如果存在); |
mVelocityTracker: | 在触摸滚动期间决定速率; |
mTouchSlop
| 当手指滑动一定距离后,才开始产生滑动效果,此变量表示所谓的“一定距离”。 |
1.3滑动变量的相关接口
如果说常量和变量尽可能的来描述一个滑动,那么与滑动相关的接口则是来定义一个滑动在不同的状态,不同的时刻下应该做些什么。此处的接口,除了interface之外还有一些ListView自己定义好了的Runnable。
OnScrollChangeListener接口:
当ListView滚动时,用来回调的接口;该接口主要侧重于滚动状态的改变。接口的内部定义了三个常量,用来描述ListView的3中滚动状态,分别如下:
SCROLL_STATE_IDLE: | 空闲状态,及此状态下ListView没有滚动 |
SCROLL_STATE_TOUCH_SCROLL:
| 滑动状态;即Listview的内容随着手指的移动而移动; |
SCROLL_STATE_FLING: | 抛动状态;即手指离开屏幕,但由于速度过快,ListView的内容会继续滚动一段时间; |
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的滑动流程。
将一些主要的常量、变量、接口及内部类介绍了之后,我们就具体分析一个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的可视范围的算法,如下图所示(以从上往下为例):
如图所示,这个黑线框就是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为例):
如图所示,如果向下滑动的距离超过了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;
......
......
}
......
}
结合上文所述,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,且抛动过程中的抛动位置会越来越大。如图所示
当抛动过程中,抛动的位置越来越大,则表示朝下方滚动,即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个触摸模式的相互转换可如下图所示:
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的滚动机制一文便结束!
当然
由于本人自身水平所限,文章肯定有一些不对的地方,希望大家指出!
愿大家一起进步,谢谢!