怕自己说的不清不楚,先来一个郭神的文章镇楼:http://blog.csdn.net/guolin_blog/article/details/44996879
github:https://github.com/zarics/ZrcListView
先贴一个自己画的ZrcListView的UML类图(学习ing。。。
)
首先说下他的整个大体的布局
SimpleHeader是依据状态来draw自己的。通常是不画空白。下拉时把setHeadable设置的SimpleHeader给画出来;然后假设你把MainActivity中loadMore凝视掉你拉倒底部你会发现载入很多其它的动画一直在跑,说明它从始至终都在那的并没有须要做什么处理。
这里的SimpleHeader和SimpleFooter有且仅仅有一个;并且他也实现了ListView的HeaderView和FooterView的功能,可是在他这个项目里没实用到(上一篇文章说的有一个就是利用HeaderView来实现下拉刷新动画),这个后面会说到怎么实现的。
滚动的实现
想知道滚动方面的实现要看什么呢?当然是触摸事件的监听啦。而onTouchEvent仅仅有在ZrcAbsListView中才有实现,看来是在这里实现的了。
@Override
public boolean onTouchEvent(MotionEvent ev) {
try {
if (!isEnabled()) {
return isClickable() || isLongClickable();
}
if (!mIsAttached) {
return false;
}
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
final int actionMasked = ev.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev);
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) {
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
final int id = ev.getPointerId(index);
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
mMotionCorrection = 0;
mActivePointerId = id;
mMotionX = x;
mMotionY = y;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
看起来似乎非常复杂,可是事实上仅仅须要关心ACTION_MOVE就可以;
private void onTouchMove(MotionEvent ev) {
if (mTouchMode == TOUCH_MODE_INVALID) {
mTouchMode = TOUCH_MODE_SCROLL;
}
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
if (mDataChanged) {
layoutChildren();
}
final int x = (int) ev.getX(pointerIndex);
final int y = (int) ev.getY(pointerIndex);
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
startScrollIfNeeded(x, y);
break;
case TOUCH_MODE_SCROLL:
scrollIfNeeded(x, y);
break;
}
}
startScrollIfNeeded中最后也还是调用scrollIfNeeded;直接看看scrollIfNeeded
private void scrollIfNeeded(int x, int y) {
final int rawDeltaY = y - mMotionY;
final int deltaY = rawDeltaY - mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ?
y - mLastY
: deltaY;
if (mTouchMode == TOUCH_MODE_SCROLL) {
if (y != mLastY) {
if (Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
if (atEdge) {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
mMotionX = x;
mMotionY = y;
mLastY = y;
}
}
}
trackMotionScroll跟进去;这里就是重点
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
final int firstPosition = mFirstPosition;
final int firstTop = childCount == 0 ? mFirstTop : getChildAt(0)
.getTop();
int lastBottom = childCount == 0 ?
firstTop
: getChildAt(childCount - 1).getBottom();
if (firstPosition + childCount >= mItemCount - 1) {
if (!isRefreshing && !isLoadingMore && isLoadMoreOn
&& onLoadMoreStart != null) {
isLoadingMore = true;
onLoadMoreStart.onStart();
}
}
if (isRefreshing || isLoadingMore) {
if (mZrcFooter != null) {
lastBottom += mZrcFooter.getHeight();
}
}
final int mPaddingBottom = getPaddingBottom();
final int mPaddingTop = getPaddingTop();
final Rect listPadding = mListPadding;
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - mPaddingBottom - mPaddingTop;
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final Headable zrcHeader = mZrcHeader;
final boolean isTooShort = childCount == mItemCount
&& lastBottom - firstTop < getHeight();
final int topOffset = firstTop
- (listPadding.top + mFirstTopOffset + (showHeader ? zrcHeader
.getHeight() : 0));
final int bottomOffset = isTooShort ?
firstTop - listPadding.top
: lastBottom - getHeight() + listPadding.bottom
+ mLastBottomOffset;
final boolean isOutOfTop = firstPosition == 0 && topOffset > 0;
final boolean isOutOfBottom = firstPosition + childCount == mItemCount
&& bottomOffset < 0;
final boolean cannotScrollDown = (isOutOfTop && incrementalDeltaY > 0);
final boolean cannotScrollUp = (isOutOfBottom && incrementalDeltaY <= 0);
if (isTooShort && cannotScrollDown && mTouchMode == TOUCH_MODE_RESCROLL) {
mTouchMode = TOUCH_MODE_FLING;
return true;
}
if (isOutOfTop || isOutOfBottom) {
if (mTouchMode == TOUCH_MODE_SCROLL) {
incrementalDeltaY /= 1.7f;
if (zrcHeader != null && isOutOfTop) {
final int state = zrcHeader.getState();
if (topOffset >= zrcHeader.getHeight()) {
if (state == Headable.STATE_PULL
|| state == Headable.STATE_REST) {
zrcHeader.stateChange(Headable.STATE_RELEASE, null);
}
} else {
if (state == Headable.STATE_RELEASE
|| state == Headable.STATE_REST) {
zrcHeader.stateChange(Headable.STATE_PULL, null);
}
}
}
}
if (mTouchMode == TOUCH_MODE_RESCROLL && false) {
if (isOutOfTop && zrcHeader != null) {
final int state = zrcHeader.getState();
if (topOffset < 10
&& (state == Headable.STATE_SUCCESS || state == Headable.STATE_FAIL)) {
zrcHeader.stateChange(Headable.STATE_REST, null);
removeCallbacks(mResetRunnable);
}
}
}
if (mTouchMode == TOUCH_MODE_FLING) {
if (cannotScrollDown) {
incrementalDeltaY /= 1.7f;
int duration = firstTop - listPadding.top;
if (duration > getHeight() / 6) {
return true;
}
} else if (cannotScrollUp && !isOutOfTop) {
incrementalDeltaY /= 1.7f;
int duration = bottomOffset;
if (duration < -getHeight() / 6) {
return true;
}
}
} else {
if (incrementalDeltaY > 0) {
int duration = firstTop - listPadding.top;
if (duration > getHeight() / 2) {
return true;
}
} else if (incrementalDeltaY < 0 && !isOutOfTop) {
int duration = bottomOffset;
if (duration < -getHeight() / 2) {
return true;
}
}
}
if (onScrollStateListener != null) {
if (mScrollState != OnScrollStateListener.EDGE) {
mScrollState = OnScrollStateListener.EDGE;
onScrollStateListener.onChange(OnScrollStateListener.EDGE);
}
}
} else {
if (zrcHeader != null) {
if (zrcHeader.getState() == Headable.STATE_PULL) {
zrcHeader.stateChange(Headable.STATE_REST, null);
}
}
if (incrementalDeltaY > 5) {
if (onScrollStateListener != null) {
if (mScrollState != OnScrollStateListener.UP) {
mScrollState = OnScrollStateListener.UP;
onScrollStateListener
.onChange(OnScrollStateListener.UP);
}
}
} else if (incrementalDeltaY < -5) {
if (onScrollStateListener != null) {
if (mScrollState != OnScrollStateListener.DOWN) {
mScrollState = OnScrollStateListener.DOWN;
onScrollStateListener
.onChange(OnScrollStateListener.DOWN);
}
}
}
}
//---------------------美丽的切割线-----------------
final boolean down = incrementalDeltaY < 0;
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
int top = -incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top + Math.min(0, bottomOffset)) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount
&& position < footerViewsStart) {
mRecycler.addScrapView(child, position);
}
}
}
} else {
int bottom = getHeight() - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom + Math.max(0, topOffset)) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount
&& position < footerViewsStart) {
mRecycler.addScrapView(child, position);
}
}
}
}
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
if (!awakenScrollBars()) {
invalidate();
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY
|| spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
mFirstTop = getChildCount() == 0 ?
mFirstTop + incrementalDeltaY
: getChildAt(0).getTop();
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;
}
代码虽多可是事实上做了两件事,第一件切割符上面的推断并设置SimpleHeader的状态,SimpleHeader会依据状态做不同的变化;第二件依据须要调用fillGap(down)、offsetTopAndBottom使得ListView滚动起来。贴一下SimpleHeader的draw代码
@Override
public boolean draw(Canvas canvas, int left, int top, int right, int bottom) {
boolean more = false;
final int width = right - left;
final int height = mHeight;
final int offset = bottom - top;
canvas.save();
if (isClipCanvas) {
canvas.clipRect(left + 5, 1, right + 5, bottom - 1);
}
switch (mState) {
case STATE_REST:
break;
case STATE_PULL:
case STATE_RELEASE:
if (offset < 10) {
break;
}
mPaint.setColor(mPointColor);
for (int i = 0; i < mPice; i++) {
int angleParam;
if (offset < height * 3 / 4) {
angleParam = offset * 16 / height - 3;// 每1%转0.16度;
} else {
angleParam = offset * 300 / height - 217;// 每1%转3度;
}
float angle = -(i * (360 / mPice) - angleParam) * PI / 180;
float radiusParam;
if (offset <= height) {
radiusParam = offset / (float) height;
radiusParam = 1 - radiusParam;
radiusParam *= radiusParam;
radiusParam = 1 - radiusParam;
} else {
radiusParam = 1;
}
float radius = width / 2 - radiusParam * (width / 2 - mCircleRadius);
float x = (float) (width / 2 + radius * Math.cos(angle));
float y = (float) (offset / 2 + radius * Math.sin(angle));
canvas.drawCircle(x, y + top, mPointRadius, mPaint);
}
break;
case STATE_LOADING:
more = true;
mPaint.setColor(mPointColor);
for (int i = 0; i < mPice; i++) {
int angleParam = mTime * 5;
float angle = -(i * (360 / mPice) - angleParam) * PI / 180;
float radius = mCircleRadius;
float x = (float) (width / 2 + radius * Math.cos(angle));
float y;
if (offset < height) {
y = (float) (offset - height / 2 + radius * Math.sin(angle));
} else {
y = (float) (offset / 2 + radius * Math.sin(angle));
}
canvas.drawCircle(x, y + top, mPointRadius, mPaint);
}
mTime++;
break;
case STATE_SUCCESS:
case STATE_FAIL:
more = true;
final int time = mTime;
if (time < 30) {
mPaint.setColor(mPointColor);
for (int i = 0; i < mPice; i++) {
int angleParam = mTime * 10;
float angle = -(i * (360 / mPice) - angleParam) * PI / 180;
float radius = mCircleRadius + time * mCircleRadius;
float x = (float) (width / 2 + radius * Math.cos(angle));
float y;
if (offset < height) {
y = (float) (offset - height / 2 + radius * Math.sin(angle));
} else {
y = (float) (offset / 2 + radius * Math.sin(angle));
}
canvas.drawCircle(x, y + top, mPointRadius, mPaint);
}
mPaint.setColor(mTextColor);
mPaint.setAlpha(time * 255 / 30);
String text = mMsg != null ? mMsg : mState == STATE_SUCCESS ?
"载入成功" : "载入失败";
float y;
if (offset < height) {
y = offset - height / 2;
} else {
y = offset / 2;
}
canvas.drawText(text, width / 2, y + top + mFontOffset, mPaint);
} else {
mPaint.setColor(mTextColor);
String text = mMsg != null ? mMsg : mState == STATE_SUCCESS ?
"载入成功" : "载入失败";
float y;
if (offset < height) {
y = offset - height / 2;
mPaint.setAlpha(offset * 255 / height);
} else {
y = offset / 2;
}
canvas.drawText(text, width / 2, y + top + mFontOffset, mPaint);
}
mTime++;
break;
}
canvas.restore();
return more;
}
这里有个问题那么draw这个函数什么时候调用呢,事实上是这种:ZrcListView再画自己的时候会调用draw–>onDraw–>dispatchDraw而在ZrcAbsListView中重写了,在这里调用了header和footer的draw方法。这样ZrcListView在画自己的时候也会去画header和footer了。
另一个关键点是ListView内部是怎么滚动起来的呢?我也是看了上面郭神的文章才了解的,详细大家能够细细去看,我这里写一个我理解的流程。fillGap主要是填充载入View到ListView中fillGap–>fillUp/fillDown–>makeAndAddView–>obtainView(真正把View从无到有的方法。假设缓存里没有就是从这里获得)–>setupChild(这种方法里View已经增加了。这时候就是滚动了)–>offsetTopAndBottom(移动)
最后一点ListView整个控件又是怎么移动的呢?我们知道下拉的时候它须要往下移动(一般移动量是手指移动的一半,这样比較有下拉的感觉)让出一定空间好让SimpleHeader能够展示自己。
关于这个分为两部分,第一部分:ListView尾随手指移动而移动它的二分之中的一个量;第二部分松开手指(可能会播放动画也可能不会)后ListView上移值原始位置
首先看第一部分。找了N久没找到他是怎么移动的;后来我一步步跟踪代码最后我把offsetTopAndBottom这段移动ListView内部的代码凝视掉发现子View动不了(肯定的)整个ListView也不会移动。
于是有了例如以下猜想:
所以事实上ListView内部子view的offsetTopAndBottom就是整个ListView的移动他并没有在外面包一层View。心中一万仅仅某马奔腾而过。。
。待验证?????
再然后说说第二部分:一句话概括就是在ACTION_UP里利用Scroller让ListView自然的飘逸的移动到原来的位置
Adapter
ZrcListView的Adapter须要说下。看上面的类图能够看到一个HeaderViewListAdapter,这个是干嘛的呢?我把getView代码贴出来
public View getView(int position, View convertView, ViewGroup parent) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
if (position < numHeaders) {
return mHeaderViewInfos.get(position).view;
}
// Adapter
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
// Footer (off-limits positions will throw an IndexOutOfBoundsException)
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}
还记得刚開始那个布局图吗,那个结构就是在这里进行处理的。
private final ListAdapter mAdapter;
// These two ArrayList are assumed to NOT be null.
// They are indeed created when declared in ListView and then shared.
ArrayList<ZrcListView.FixedViewInfo> mHeaderViewInfos;
ArrayList<ZrcListView.FixedViewInfo> mFooterViewInfos;
没错,这个就是实现ListView的HeaderView和FooterView功能的地方。HeaderViewListAdapter这个类里的这三个。mAdapter就是ListView的视图仅仅有一个,mHeaderViewInfos和mFooterViewInfos则能够有多个,你能够一直addHeaderView往里面加各种View。效果就是ListView.addHeaderView的效果
小白一枚,学习记录,轻喷