之所以上一篇文章没有讲这些,主要因为这些代码我看起来也是晕,不得不说google的工程师写的代码真的是让人佩服,该片主要记录我在研究ListView的滑动事件时的一些看法。接下来直接看代码吧。
Note:我们需要了解以下几个概念
//表示我们不在触摸手势的中间
static final int TOUCH_MODE_REST = -1;
//提示我们收到一个down事件,判断他是轻拍还是滚动事件。
static final int TOUCH_MODE_DOWN = 0;
//表示触摸已被识别为轻拍,我们现在正在等待是否触摸事件是一个longperss
static final int TOUCH_MODE_TAP = 1;
//表示我们已经等待了所有可以等待的东西,但用户的手指仍在向下
static final int TOUCH_MODE_DONE_WAITING = 2;
//表示触摸手势为滚动
static final int TOUCH_MODE_SCROLL = 3;
//表示视图正在被抛出
static final int TOUCH_MODE_FLING = 4;
//指示触摸手势是一个覆盖滚动-一个滚动超过开始或结束
static final int TOUCH_MODE_OVERSCROLL = 5;
//指示视图被抛出到正常内容范围之外会弹回来
static final int TOUCH_MODE_OVERFLING = 6;
接下来就进入onTouchEvent方法中进一步查看:
@Override
public boolean onTouchEvent(MotionEvent ev) {
//当前视图不可点仍然会消费事件,
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
if (mPositionScroller != null) {
mPositionScroller.stop();
}
//若当前视图正在分离或者还没有依附到窗口,不处理
if (mIsDetaching || !isAttachedToWindow()) {
// Something isn't right.
// Since we rely on being attached to get data set change notifications,
// don't risk doing anything where we might try to resync and find things
// in a bogus state.
return false;
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
return true;
}
//初始化VelocityTracker函数,用于计算手指滑动速度
initVelocityTrackerIfNotExists();
final MotionEvent vtev = MotionEvent.obtain(ev);
//ev.getActionMasked():多点触控时必须使用该方法来获取Action,与单点触碰时的ev.getAction效果一样
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
/**
*
* ACTION_POINTER_UP 有非主要的手指按下(表示还有手指在屏幕上)
*
* ACTION_POINTER_DOWN 有非主要的手指抬起(表示还有手指在屏幕上)
*
*/
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
case MotionEvent.ACTION_UP: {
onTouchUp(ev);
break;
}
case MotionEvent.ACTION_CANCEL: {
onTouchCancel();
break;
}
case MotionEvent.ACTION_POINTER_UP: {
//根据手指离开的主次,重新赋值各参数
onSecondaryPointerUp(ev);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
// Remember where the motion event started
final View child = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = child.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {//新的手指按下接管拖曳任务
//获取当前手指的下标index,和id
final int index = ev.getActionIndex();
final int id = ev.getPointerId(index);
//根据index获取相对于父类的x,y坐标
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
mMotionCorrection = 0;
mActivePointerId = id;
mMotionX = x;
mMotionY = y;
//判断该触摸点位置是否位于父View的某一个子元素内
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
// 记录实际位置,根据Adapter中元素位置
final View child = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = child.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
//追踪当前事件的速度
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
在这里我主要关注的时Action=MOVE时的方法,也就是onTouchMove(ev, vtev);其余的一些方法我也做了一些注解,不在多讲,进入onTouchMove看一看。
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
if (mHasPerformedLongPress) {
// Consume all move events following a successful long press.
return;
}
//根据手指ID获取当前手指Index
int pointerIndex = ev.findPointerIndex(mActivePointerId);
//若当前手指不存在,则默认第一个手指的id和Index
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
//滑动过程中个,数据发生改变时调用
if (mDataChanged) {
// Re-sync everything if data has been changed
// since the scroll operation can query the adapter.
layoutChildren();
}
//根据Index获取当前手指的Y轴坐标相对于当前Parent
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.
final View motionView = getChildAt(mMotionPosition - mFirstPosition);
final float x = ev.getX(pointerIndex);
if (!pointInView(x, y, mTouchSlop)) {
setPressed(false);
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.
final float[] point = mTmpPoint;
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, motionView);
motionView.drawableHotspotChanged(point[0], point[1]);
}
break;
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
通过以上方法可以看出ListView处理了多点触摸事件,当滑动过程中给数据发生改变时,调用layoutChildren方法重新设置数据。在这里根据Action,ListView会调用TOUCH_MODE_OVERSCROLL中的scrollIfNeeded方法进入该方法查看。
/**
* x:当前手指X轴坐标
*
* y:当前手指Y轴坐标
*
* vtev:Event事件
*/
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
int rawDeltaY = y - mMotionY;
int scrollOffsetCorrection = 0;
int scrollConsumedCorrection = 0;
if (mLastY == Integer.MIN_VALUE) {
rawDeltaY -= mMotionCorrection;
}
if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY,
mScrollConsumed, mScrollOffset)) {
rawDeltaY += mScrollConsumed[1];
scrollOffsetCorrection = -mScrollOffset[1];
scrollConsumedCorrection = mScrollConsumed[1];
if (vtev != null) {
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
}
final int deltaY = rawDeltaY;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
int lastYCorrection = 0;
//调用该方法TOUCH_MODE_SCROLL
if (mTouchMode == TOUCH_MODE_SCROLL) {
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (mScrollStrictSpan == null) {
// If it's non-null, we're already in a scroll.
mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
}
//前一个Event事件结束后的Y值,若不相等,代表手指滑动了
if (y != mLastY) {
// 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.
if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
//若存在Parent则请求不拦截触摸滑动事件
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
//根据mMotionPosition获取当前手指所在View位置,若不存在从中间开始
final int motionIndex;
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;
//获取View
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;
//手指滑动距离一点点就会调用该方法
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
...
}
这里的核心代码就是trackMotionScroll该方法,通过我的注释可以看到,当手指在ListView内部滑动,且大于最小滑动距离,有Parent则请求父类不拦截该触摸事件,调用trackMotionScroll实现滑动,咱们进入trackMotionScroll方法看一看。
// deltaY:手指按下时到手指当前的距离,这是一个总距离
// incrementalDeltaY:距离上次触发event事件,手指在Y轴上移动的距离 incrementalDeltaY < 0页面向下滑动
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;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}
// FIXME account for grid vertical spacing too?
/**
* spaceAbove=listPadding.top-getChildAt(0).getTop()
*
* 若不设置child的setPadding,则effectivePaddingTop=firstTop=0,则spaceAbove>=0,默认为0
*
* 最底部可用bottom
*/
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
final int spaceBelow = lastBottom - end;
//实际高度去除padding
final int height = getHeight() - mPaddingBottom - mPaddingTop;
//确保deltaY在一个合理的范围内
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
//确保incrementalDeltaY在一个合理的范围内,比如最小不能小于-(height - 1),最大不能大于height - 1
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
// Update our guesses for where the first and last views are
if (firstPosition == 0) {
mFirstPositionDistanceGuess = firstTop - listPadding.top;
} else {
mFirstPositionDistanceGuess += incrementalDeltaY;
}
if (firstPosition + childCount == mItemCount) {
mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
} else {
mLastPositionDistanceGuess += incrementalDeltaY;
}
final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= listPadding.top && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}
//<0表示手指上滑y坐标减小,页面下滑
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
//firstPosition为第一个可见的View的position,依据Adapter的实际数量
//down=true:向下滑动(手指向上滑动) incrementalDeltaY<0
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);
//child.getBottom() >= top,说明当前child说明当前child依旧在屏幕内
if (child.getBottom() >= top) {
break;
} else {
//child.getBottom() < top说明当前child已经移出屏幕。调用RectcleBin机制将该child添加进入废弃缓存中
//count计数器用于记录有多少个child被移除屏幕
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();
//将移出屏幕的view添加到废弃缓存中,并记录位置.依据Adapter中数量
mRecycler.addScrapView(child, position);
}
}
}
} else {
//向上滑动(手指向下移动)
//这里使用ListView的可用高度-y轴偏移,是为了计算判断各子View是否会超出屏幕
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);
//child.getTop() <= bottom 当前child位于屏幕内
if (child.getTop() <= bottom) {
break;
} else {
//当前child已经移出屏幕,调用RectcleBin机制将该child添加进入废弃缓存中
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;
//count>0表示有child被移出屏幕,调用detachViewsFromParent将所有被移出的child detach调
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
// invalidate before moving the children to avoid unnecessary invalidate
// calls to bubble up from the children all the way to the top
if (!awakenScrollBars()) {
invalidate();
}
//调用该法,让所有的child进行相应的偏移,实现滑动效果
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
//滑动距离在ListView内部,则加载屏幕外数据
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
//加载屏幕外数据
fillGap(down);
}
mRecycler.fullyDetachScrapViews();
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();
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
return false;
}
这里的代码还是比较长的,我没有做删减,从上到下过一遍吧,可以看到首先计算出了一些变量参数,来看一一看什么意思
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;
首先获取ListView的子元素,获取第一个子元素的Top,和最后一个元素的Bottom值。这里的mListPadding属于ListView的Padding值,画个图加深一下理解
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}
然后计算出可用空间的Top值和Bottom值,后面通过incrementalDeltaY的正负值来判断页面上滑还是下滑。注意这里页面的上下滑动与手指的移动正好是相反的,比如手指上移,页面下移,incrementalDeltaY<0.代码中注释写的比较清楚,可以看到当View被移除屏幕时,会调用mRecycler.addScrapView方法将移除View添加到废弃缓存中。ListView会调用detachViewsFromParent方法将移除的Viewdetach掉。然后调用fillGap方法加载屏幕内数据,fillGap方法具体实现位于ListView中
/**
* {@inheritDoc}
*
* down=true 向下滑动(手指向上)
*/
@Override
void fillGap(boolean down) {
final int count = getChildCount();
//底部加载
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
//获取需要加载的View的getTop,count>0,则getTop=最后一个View的Bottom+分割线宽度,反之取paddingTop
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
//顶部加载
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
//获取需要加载的View的getBottom,count>0,则getBottom第一个View的Top+分割线宽度,反之取paddingBottom
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
这里down==true表示当前手指向上滑,屏幕向下移动,获取当前需要加载的View的Top,然后调用fillDown方法。down==false表示手指向下滑动,屏幕向上移动,获取需要加载的View的Bottom,然后调用fillUp方法,这两个方法之前的博客已经分析过这里只看一些不一样的点。
//ListView 初始化时,mRecycler中缓存的View为null
if (!mDataChanged) {
//第二次获取View是从缓存中读取
final View activeView = mRecycler.getActiveView(position);
/**
* 在拉youtchild中第二次调用了recycleBin.fillActiveViews(childCount, firstPosition)缓存了
* 屏幕显示的所有View,因此直接读取缓存复用,调用setupChild方法填充ListView
*/
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
可以看到首先还是会调用mRecycler.getActiveView(position)来获取缓存View,但此时并不会获取到,因为我们之前分析过getActiveView中的View一旦被获取就会被置null,而之前第二次测量已经获取过,所以此时调用以下方法obtainView方法
//从废弃的缓存View列表中获取一个View,mRecycler会根据position获取Type来获取View
final View scrapView = mRecycler.getScrapView(position);
//调用Adapter的getView方法获取一个子View
/**
* 因为第一次调用时mRecycler钟缓存的废弃view==null,所以直接调用mAdapter.getView方法返回一个View
*
* 这里可以看出在我们自定义适配器时要判断convertView是否为null,
*
*
*/
final View child = mAdapter.getView(position, scrapView, this);
//列表滑出屏幕时,判断是否将scrapView添加进废弃View集合
if (scrapView != null) {
if (child != scrapView) {
//view不匹配,重新缓存到废弃View集合中
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
这里还是只看主要代码,调用getScrapView获取指定Type的View,此时肯定能获取到,因为移除的View会被自动添加到该废弃缓存中,此时调用mAdapter.getView(position, scrapView, this)获取View,这个方法就比较熟悉了,就是我在适配器中重写的方法,此时若scrapView为null则直接inflate一个View,否则就复用缓存的废弃View,直接替换数据就可以了,这也就是ListView为什么能够加载成百上千数据的原因。
到现在为止,ListView的源码分析也就告了一个段落,写下这两篇文章只是为了加深自己的印象,为了以后复习比较容易,若有写错的地方,就当权当没看见吧!^_^