前面几篇文章主要在介绍ListView的初始化(当然这些方法并不仅仅只在ListView实例化时被调用),这一篇文章我们则主要分析ListView在运动时的情况,即ListView的滚动机制。滚动机制主要分为ListView是如何滚动以及滚动时会引起什么东西变化。
ListView的滚动机制与ListView的触摸事件息息相关,因此理解其滚动机制就是理解ListView对触摸事件是如何解析、控制的。众所周知,触摸事件分为三个比较主要的动作:down、move、up,本文将按照这个流程来对ListView的滚动机制进行分析,并且在具体分析触摸事件之前,会对ListView的一些与滑动相关的常量、变量及接口做一些解析。因此,本文的目录如下:
1、与滑动相关的常量、变量及接口;
2、down——滑动开始前的准备;
3、move——滑动开始与持续;
4、up——滑动结束与后序;
5、ListView的抛动机制
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边缘的状态; |
mTouchMode: |
当前的触摸模式,1.1节中所列出的八个常量之一,初始值为TOUCH_MODE_REST; |
mMotionCorrection: |
开始滑动之前,手指已经移动的距离; |
mMotionPosition: |
接受到down手势事件的视图对应的item的位置; |
mMotionViewOriginalTop: |
接收到down手势事件的视图的顶部偏移量; |
mMotionX: |
down手势事件位置的X坐标; |
mMotionY: |
down手势事件位置的Y坐标; |
mLastY: |
上一个手势事件位置的Y坐标(如果存在); |
mVelocityTracker: |
在触摸滚动期间决定速率; |
mTouchSlop
|
当手指滑动一定距离后,才开始产生滑动效果,此变量表示所谓的“一定距离”。 |
SCROLL_STATE_IDLE: |
空闲状态,及此状态下ListView没有滚动 |
SCROLL_STATE_TOUCH_SCROLL:
|
滑动状态;即Listview的内容随着手指的移动而移动; |
SCROLL_STATE_FLING: |
抛动状态;即手指离开屏幕,但由于速度过快,ListView的内容会继续滚动一段时间; |
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。
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手势事件的处理,主要是将上文提及的相关变量进行更新赋值。
@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模式,并且发送一个异步的轻触消息。
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方法之中预先处理。
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已经开始滑动的情况。
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。
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个步骤,来实现滑动效果:
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。
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的可视范围的算法,如下图所示(以从上往下为例):
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
......
//将未删除的所有的子视图朝上(下)移动incrementalDeltaY这么多距离
offsetChildrenTopAndBottom(incrementalDeltaY);
//更新第一个子视图对应的item在适配器中的位置
if (down) {
mFirstPosition += count;
}
......
}
通过ViewGroup类的offsetChildrenTopAndBottom方法来实现子视图的移动;而该方法的原理则是同时将一个子视图的mTop变量和mBottom变量加上incrementalDeltaY变量的值。
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为例):
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的边缘,如果是则实现相应的边缘效果。
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边缘的处理方式相似。
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方法之中。
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;
......
......
}
......
}
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内部类的抛动机制,下文会详细叙述!。
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;
}
......
}
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的滑动,在三大触摸手势事件中流程便分析完毕了。
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方法并未直接将速率作为入参,而是取了入参的相反数,这是什么原因呢?
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。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) ;
第二个入参表示需要滚动的距离。
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手势事件。
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,同时,也发送了一个异步消息。不出意外,这个消息被执行时,会调用上面这段源代码。
@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的抛动机制就大致分析完了。