手把手教你实现 ListView

时间:2023-01-23 07:49:27

前言

上一篇博客 ListView 源码分析 大概分析了一下 ListView 的复用机制的源码。那么紧跟上篇,这篇用大概 400 行代码,实现一个继承自ViewGroup 的 ListView。

我们自己实现的 ListView,只包含测量、布局以及滑动时的回收复用机制,其他东西,一概忽略。因为 ListView 是一个牛逼的控件,撇开时间不谈,我也没那能力整个去实现一遍。
麻雀虽小,五脏也只有半个,但是也实现了最主要的复用机制,手动实现一个小型 ListView 对于我们学习 ListView也是非常有用的。原理性的东西我上一篇已经分析完了,这里就不重复了,直接贴代码,相关的地方已经做出了注释。

导读: 对测量以及布局不感兴趣的,可以直接从 onTouchEvent 开始,一步步跟下去就好。

public class MyListView extends ViewGroup {

public static final String TAG = "MyListView";

private RecycleBin mRecycler;

private ListAdapter mAdapter;

private boolean[] mIsScrap = new boolean[1];

private int mFirstPosition;

private int mWidthMeasureSpec;

public MyListView(Context context) {
this(context, null);
}

public MyListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mRecycler = new RecycleBin();
}


// ListView 的测量逻辑,如果是 match_parent,那就按 match_parent 的大小
// 如果是 wrap_content,就循环获得 view(obtain),累加子 view 的高度,但最大高度不能超过可用高度
// obtain 获得的view 需要存进废弃数组中,以供复用。
// 注意,其实 onMeasure是没必要对 子 View 进行测量的,只有当测量模式为 wrap_content,才需要 obtainView以获得需要的高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e("cat", "onMeasure");
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = heightSize;

int itemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
int childHeightMeasureSpec;
if (itemCount > 0) {
//有数据的时候,测量模式为 wrap_content 或者 unspecified 时,高度为包含 view 的高度且不超过最大高度
heightSize = 0;
for (int i = 0; i < itemCount; i++) {
View child = obtainView(mFirstPosition + i, mIsScrap);
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeight = p.height;
if (childHeight > 0) {// exactly
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
} else {
// wrap_content||其他情况
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightMeasureSpec);
heightSize += child.getMeasuredHeight();
mRecycler.addScrapView(child);
if (heightSize > maxHeight) break;
}
} else {
//如果没有数据,且测量模式为 wrap_content 或者 unspecified 时,高度为边距
heightSize = getPaddingTop() + getPaddingBottom();
}
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.e("cat", "onLayout");
View oldFirst = getChildAt(0);
int childCount = getChildCount();
mRecycler.fillActiveViews(childCount, mFirstPosition);

detachAllViewsFromParent();

fillDown(mFirstPosition, oldFirst == null ? getPaddingTop() : oldFirst.getTop());

}


private View obtainView(int pos, boolean[] isScrap) {
isScrap[0] = false;
View scrap = mRecycler.getScrapView();
Log.e("cat", "retrieve from " + (scrap != null ? "scrap" : "inflate") + ",pos:" + pos);
View child = mAdapter.getView(pos, scrap, this);
if (scrap != null) {

if (scrap != child) {
Log.e("cat", "unbind convert view");
mRecycler.addScrapView(scrap);
} else {
isScrap[0] = true;
}
}


//设置 child 的 layoutParams,主要是对 layoutParams 进行校验
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
LayoutParams lp;
if (vlp == null) {
lp = generateDefaultLayoutParams();
} else if (!(vlp instanceof AbsListView.LayoutParams)) {
lp = generateLayoutParams(vlp);
} else {
lp = vlp;
}
if (lp != vlp) {
child.setLayoutParams(lp);
}
return child;


}

private View makeAndAddView(int pos, int y, boolean flow) {
View activeView = mRecycler.getActiveView(pos);
if (activeView != null) {
setupChild(activeView, y, flow, true);
Log.e("cat", "retrieve from activeView ");
return activeView;
}
View view = obtainView(pos, mIsScrap);
setupChild(view, y, flow, mIsScrap[0]);
return view;
}


private void fillDown(int startPos, int top) {
int end = getBottom() - getTop();
while (top < end && startPos < mAdapter.getCount()) {
// Log.e("cat", "fill down pos:" + startPos + ",top:" + top);
View child = makeAndAddView(startPos, top, true);
startPos++;
top = child.getBottom();
}
}

private void fillUp(int startPos, int bottom) {
while (bottom > getPaddingTop() && startPos >= 0) {
View child = makeAndAddView(startPos, bottom, false);
startPos--;
// Log.e("cat", "bottom:" + bottom);
bottom = child.getTop();
}
mFirstPosition = startPos + 1;
}


//当 flow 为 true 时,代表参数 y 为 child 的 top值。为 false 时,y 为参数 y 的 bottom 值
private void setupChild(View child, int y, boolean flow, boolean recycled) {
boolean needToMeasure = !recycled;
//如果 child 是利用的缓存 view,则重新进行关联即可
if (recycled) {
// 与 parent 进行关联,-1 代表添加到尾部
attachViewToParent(child, flow ? -1 : 0, child.getLayoutParams());
} else {
// recycled 为 false 表示为重新 inflate 出来的 view,需要添加到布局中
addViewInLayout(child, flow ? -1 : 0, child.getLayoutParams(), true);
}
//根据 recycled 决定是否对 view 进行测量
if (needToMeasure) {
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
getPaddingLeft() + getPaddingRight(), p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
// 根据 recycled 决定是否对 view 进行 layout
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flow ? y : y - h;
int childLeft = getPaddingLeft();
if (needToMeasure) {
child.layout(childLeft, childTop, childLeft + w, childTop + h);
} else {
child.offsetLeftAndRight(childLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (recycled) {
child.invalidate();
}
}

private boolean trackMotionScroll(int incrementalDeltaY) {
if (getChildCount() <= 0) {
return true;
}
View firstChild = getChildAt(0);
View lastChild = getChildAt(getChildCount() - 1);
int childCount = getChildCount();
//回收开始的位置
int start = 0;
//总共回收的数量
int count = 0;
//是否上滑
boolean down = incrementalDeltaY < 0;

// 判断是否滑动到了边界
final boolean cannotScrollDown = (mFirstPosition == 0 &&
firstChild.getTop() >= getPaddingTop() && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (mFirstPosition + childCount == mAdapter.getCount() &&
lastChild.getBottom() <= getHeight() - getPaddingBottom() && incrementalDeltaY <= 0);
if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}
int spaceAbove = firstChild.getTop() - getPaddingTop();
int spaceBelow = lastChild.getBottom() - getHeight() + getPaddingBottom();
spaceAbove = Math.min(Math.abs(spaceAbove), incrementalDeltaY);
spaceBelow = Math.max(spaceBelow, 0);
if (mFirstPosition + childCount == mAdapter.getCount() && incrementalDeltaY < 0) {
Log.e("cat", "incrementalDeltaY:" + incrementalDeltaY + ",spaceBelow:" + spaceBelow);
incrementalDeltaY = Math.max(-spaceBelow, incrementalDeltaY);

} else if (mFirstPosition == 0 && incrementalDeltaY > 0) {
incrementalDeltaY = Math.min(spaceAbove, incrementalDeltaY);
}
//如果为上滑
if (down) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//事实上,此时是还没有进行真正的滑动的,只是预先计算出是否需要回收
if ((child.getBottom() < getPaddingTop() - incrementalDeltaY)) {
//能进入此条件说明 child 的底部偏移后已经移出屏幕了,那么回收他
count++;
mRecycler.addScrapView(child);
} else {
// 下标为 i 的下标都没偏移出屏幕,那么后续的 view 肯定也都在屏幕内,直接结束循环
break;
}
}

} else {
int end = getBottom() - getTop();
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (child.getTop() > end - incrementalDeltaY) {
//能进入此条件说明 最后一个 child 的顶部偏移后已经移出屏幕了,那么回收他
count++;
start = i;
mRecycler.addScrapView(child);
} else {
break;
}
}
}
// count>0 代表有回收掉的 view,解除关联关系。
if (count > 0) {
// Log.e("cat", "recycle start:" + start + ",count:" + count);
detachViewsFromParent(start, count);
}
// 这里就是 ListView 的滑动方法了。
offsetChildrenTopAndBottom(incrementalDeltaY);

// 最后,再填充 ListView
int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
if (down) {
mFirstPosition += count;
fillDown(mFirstPosition + getChildCount(), lastChild.getBottom());
} else {
fillUp(mFirstPosition - 1, firstChild.getTop());
}
}
return false;
}


private int mLastY;

@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int y = (int) event.getY();
int incrementalDeltaY = y - mLastY;
// Log.e("cat", "incrementY:" + incrementalDeltaY);
if (y != mLastY) {
if (incrementalDeltaY != 0) {
trackMotionScroll(incrementalDeltaY);
}
}
mLastY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}

public void offsetChildrenTopAndBottom(int offset) {
final int count = getChildCount();

for (int i = 0; i < count; i++) {

final View v = getChildAt(i);
v.offsetTopAndBottom(offset);
}
}

public void setAdapter(ListAdapter adapter) {
mAdapter = adapter;
requestLayout();
Log.e("cat", "setAdater");
}

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new AbsListView.LayoutParams(p);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new AbsListView.LayoutParams(getContext(), attrs);
}

class RecycleBin {

private int mFirstActivePos;

private View[] mActiveViews = new View[0];
private ArrayList<View> mCurrentScrap;

public RecycleBin() {
mCurrentScrap = new ArrayList<>();
}

public void fillActiveViews(int childCount, int firstPosition) {
mFirstActivePos = firstPosition;
if (childCount > mActiveViews.length) {
mActiveViews = new View[childCount];
}
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
mActiveViews[i] = view;
}
}

public View getActiveView(int pos) {
int index = pos - mFirstActivePos;
if (index >= 0 && index < mActiveViews.length) {
View match = mActiveViews[index];
mActiveViews[index] = null;
return match;
}
return null;

}

public View getScrapView() {
if (mCurrentScrap.size() > 0) {
return mCurrentScrap.remove(mCurrentScrap.size() - 1);
}
return null;
}

public void addScrapView(View view) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) view.getLayoutParams();
if (lp == null) {
return;
}
mCurrentScrap.add(view);
}
}
}

用法跟 ListView 一样,就是不支持多种 viewTypeCount 以及 header footer 等。