Android ListView功能扩展,实现高性能的瀑布流布局,androidstudio汉化插件

if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {

if (firstPosition + childCount == mItemCount && endBottom <= end && deltaY <= 0) {

final boolean down = incrementalDeltaY < 0;

final boolean inTouchMode = isInTouchMode();

final int headerViewsCount = getHeaderViewsCount();

final int footerViewsStart = mItemCount - get

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

FooterViewsCount();

final int top = listPadding.top - incrementalDeltaY;

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

final View child = getChildAt(i);

if (child.getBottom() >= top) {

int position = firstPosition + i;

if (position >= headerViewsCount && position < footerViewsStart) {

mRecycler.addScrapView(child);

int columnIndex = (Integer) child.getTag();

if (columnIndex >= 0 && columnIndex < mColumnCount) {

mColumnViews[columnIndex].remove(child);

final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;

for (int i = childCount - 1; i >= 0; i–) {

final View child = getChildAt(i);

if (child.getTop() <= bottom) {

int position = firstPosition + i;

if (position >= headerViewsCount && position < footerViewsStart) {

mRecycler.addScrapView(child);

int columnIndex = (Integer) child.getTag();

if (columnIndex >= 0 && columnIndex < mColumnCount) {

mColumnViews[columnIndex].remove(child);

mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

mBlockLayoutRequests = true;

detachViewsFromParent(start, count);

tryOffsetChildrenTopAndBottom(incrementalDeltaY);

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);

if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {

fillGap(down, down ? lastBottom : firstTop);

if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {

final int childIndex = mSelectedPosition - mFirstPosition;

if (childIndex >= 0 && childIndex < getChildCount()) {

positionSelector(getChildAt(childIndex));

mBlockLayoutRequests = false;

invokeOnItemScrollListener();

从第 9 行开始看,这里我们使用了一个循环,遍历瀑布流 ListView 中的所有列,每次循环都去获取该列的第一个元素和最后一个元素,然后和 firstTop 及 lastBottom 做比较,以此找出所有列中最靠近屏幕上边缘的元素位置和最靠近屏幕下边缘的元素位置。注意这里除了 firstTop 和 lastBottom 之外,我们还计算了一个 endBottom 的值,这个值记录最底部的元素位置,用于在滑动时做边界检查的。

最重要的修改就是这些了,不过在其它一些地方还做了一些小的改动。观察第 75 行,这里是把被移出屏幕的子 View 添加到 RecycleBin 当中,其实也就是说明这个 View 已经被回收了。那么还记得我们刚刚添加的全局变量 mColumnViews 吗?它用于缓存每一列的子 View,那么当有子 View 被回收的时候,mColumnViews 中也需要进行删除才可以。在第 76 行,先调用 getTag() 方法来获取该子 View 的所处于哪一列,然后调用 remove() 方法将它移出。第 96 行处的逻辑是完全相同的,只不过一个是向上移动,一个是向下移动,这里就不再赘述。

另外还有一点改动,就是我们在第 115 行调用 fillGap() 方法的时候添加了一个参数,原来的 fillGap() 方法只接收一个布尔型参数,用于判断向上还是向下滑动,然后在方法的内部自己获取第一个或最后一个元素的位置来获取偏移值。不过在瀑布流 ListView 中,这个偏移值是需要通过循环进行计算的,而我们刚才在 trackMotionScroll() 方法中其实已经计算过了,因此直接将这个值通过参数进行传递会更加高效。

现在 AbsListView 中需要改动的内容已经结束了,那么我们回到 ListView 当中,首先修改 fillGap() 方法的参数:

void fillGap(boolean down, int startOffset) {

final int count = getChildCount();

startOffset = count > 0 ? startOffset + mDividerHeight : getListPaddingTop();

fillDown(mFirstPosition + count, startOffset);

correctTooHigh(getChildCount());

startOffset = count > 0 ? startOffset - mDividerHeight : getHeight() - getListPaddingBottom();

fillUp(mFirstPosition - 1, startOffset);

correctTooLow(getChildCount());

只是将原来的获取数值改成了直接使用参数传递过来的值,并没有什么太大的改动。接下来看一下 fillDown 方法,原先的逻辑是在 while 循环中不断地填充子 View,当新添加的子 View 的下边缘超出 ListView 底部的时候就跳出循环,现在我们进行如下修改:

private View fillDown(int pos, int nextTop) {

View selectedView = null;

int end = (getBottom() - getTop()) - mListPadding.bottom;

while (nextTop < end && pos < mItemCount) {

boolean selected = pos == mSelectedPosition;

View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

int lowerBottom = Integer.MAX_VALUE;

for (int i = 0; i < mColumnViews.length; i++) {

ArrayList viewList = mColumnViews[i];

int size = viewList.size();

int bottom = viewList.get(size - 1).getBottom();

if (bottom < lowerBottom) {

nextTop = lowerBottom + mDividerHeight;

可以看到,这里在 makeAndAddView 之后并没有直接使用新增的 View 来获取它的 bottom 值,而是再次使用了一个循环来遍历瀑布流 ListView 中的所有列,找出所有列中最靠下的那个子 View 的 bottom 值,如果这个值超出了 ListView 的底部,那就跳出循环。这样的写法就可以保证只要在有子 View 的情况下,瀑布流 ListView 中每一列的内容都是填满的,界面上不会有空白的地方出现。

接下来 makeAndAddView() 方法并没有任何需要改动的地方,但是 makeAndAddView() 方法中调用的 setupChild() 方法,我们就需要大刀阔斧地修改了。

大家应该还记得,setupChild() 方法是用来具体设置子 View 在 ListView 中显示的位置的,在这个过程中可能需要用到几个辅助方法,这里我们先提供好,如下所示:

private int[] getColumnToAppend(int pos) {

int bottom = Integer.MAX_VALUE;

for (int i = 0; i < mColumnViews.length; i++) {

int size = mColumnViews[i].size();

return new int[] { i, 0 };

View view = mColumnViews[i].get(size - 1);

if (view.getBottom() < bottom) {

bottom = view.getBottom();

return new int[] { indexToAppend, bottom };

private int[] getColumnToPrepend(int pos) {

int indexToPrepend = mPosIndexMap.get(pos);

int top = mColumnViews[indexToPrepend].get(0).getTop();

return new int[] { indexToPrepend, top };

private void clearColumnViews() {

for (int i = 0; i < mColumnViews.length; i++) {

这三个方法全部都非常重要,我们来逐个看一下。getColumnToAppend() 方法是用于判断当 ListView 向下滑动时,新进入屏幕的子 View 应该添加到哪一列的。而判断的逻辑也很简单,其实就是遍历瀑布流 ListView 的每一列,取每一列的最下面一个元素,然后再从中找出最靠上的那个元素所在的列,这就是新增子 View 应该添加到的位置。返回值是待添加位置列的下标和该列最底部子 View 的 bottom 值。原理示意图如下所示:

Android ListView功能扩展,实现高性能的瀑布流布局,androidstudio汉化插件

然后来看一下 getColumnToPrepend() 方法。getColumnToPrepend() 方法是用于判断当 ListView 向上滑动时,新进入屏幕的子 View 应该添加到哪一列的。不过如果你认为这和 getColumnToAppend() 方法其实就是类似或者相反的过程,那你就大错特错了。因为向上滑动时,新进入屏幕的子 View 其实都是之前被移出屏幕后回收的,它们不需要关心每一列最高子 View 或最低子 View 的位置,而是只需要遵循一个原则,就是当它们第一次被添加到屏幕时所属于哪一列,那么向上滑动时它们仍然还属于哪一列,绝不能出现向上滑动导致元素换列的情况。而使用的算法也非常简单,就是根据当前子 View 的 position 值来从 mPosIndexMap 中获取该 position 值对应列的下标,mPosIndexMap 的值在 setupChild() 方法当中填充,这个我们待会就会看到。返回值是待添加位置列的下标和该列最顶部子 View 的 top 值。

最后一个 clearColumnViews() 方法就非常简单了,它就是负责把 mColumnViews 缓存的所有子 View 全部清除掉。

所有辅助方法都提供好了,不过在进行 setupChild 之前我们还缺少一个非常重要的值,那就是列的宽度。普通的 ListView 是不用考虑这一点的,因为列的宽度其实就是 ListView 的宽度。但瀑布流 ListView 则不一样了,列数不同,每列的宽度也会不一样,因此这个值我们需要提前进行计算。修改 onMeasure() 方法中的代码,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(widthSize, heightSize);

mWidthMeasureSpec = widthMeasureSpec;

mColumnWidth = widthSize / mColumnCount;

其实很简单,我们只不过在 onMeasure() 方法的最后一行添加了一句代码,就是使用当前 ListView 的宽度除以列数,得到的就是每列的宽度了,这里将列的宽度赋值到 mColumnWidth 这个全局变量上面。

现在准备工作都已经完成了,那么我们开始来修改 setupChild() 方法中的代码,如下所示:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,

boolean selected, boolean recycled) {

final boolean isSelected = selected && shouldShowSelector();

final boolean updateChildSelected = isSelected != child.isSelected();

final int mode = mTouchMode;

final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&

mMotionPosition == position;

final boolean updateChildPressed = isPressed != child.isPressed();

final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();

p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,

ViewGroup.LayoutParams.WRAP_CONTENT, 0);

p.viewType = mAdapter.getItemViewType(position);

if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&

p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {

attachViewToParent(child, flowDown ? -1 : 0, p);

if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {

p.recycledHeaderFooter = true;

addViewInLayout(child, flowDown ? -1 : 0, p, true);

if (updateChildSelected) {

child.setSelected(isSelected);

if (updateChildPressed) {

child.setPressed(isPressed);

int childWidthSpec = ViewGroup.getChildMeasureSpec(

MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);

childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);

childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

child.measure(childWidthSpec, childHeightSpec);

cleanupLayoutState(child);

int w = child.getMeasuredWidth();

int h = child.getMeasuredHeight();

int[] columnInfo = getColumnToAppend(position);

int indexToAppend = columnInfo[0];

int childTop = columnInfo[1];

int childBottom = childTop + h;

int childLeft = indexToAppend * w;

int childRight = indexToAppend * w + w;

child.layout(childLeft, childTop, childRight, childBottom);

child.setTag(indexToAppend);

mColumnViews[indexToAppend].add(child);

mPosIndexMap.put(position, indexToAppend);

int[] columnInfo = getColumnToPrepend(position);

int indexToAppend = columnInfo[0];

int childBottom = columnInfo[1];

int childTop = childBottom - h;

int childLeft = indexToAppend * w;

int childRight = indexToAppend * w + w;

child.layout(childLeft, childTop, childRight, childBottom);

child.setTag(indexToAppend);

mColumnViews[indexToAppend].add(0, child);

int columnIndex = mPosIndexMap.get(position);

mColumnViews[columnIndex].add(child);

mColumnViews[columnIndex].add(0, child);

if (mCachingStarted && !child.isDrawingCacheEnabled()) {

child.setDrawingCacheEnabled(true);

第一个改动的地方是在第 33 行,计算 childWidthSpec 的时候。普通 ListView 由于子 View 的宽度和 ListView 的宽度是一致的,因此可以在 ViewGroup.getChildMeasureSpec() 方法中直接传入 mWidthMeasureSpec,但是在瀑布流 ListView 当中则需要再经过一个 MeasureSpec.makeMeasureSpec 过程来计算每一列的 widthMeasureSpec,传入的参数就是我们刚才保存的全局变量 mColumnWidth。经过这一步修改之后,调用 child.getMeasuredWidth() 方法获取到的子 View 宽度就是列的宽度,而不是 ListView 的宽度了。

接下来在第 48 行判断 needToMeasure,如果是普通情况下的填充或者 ListView 滚动,needToMeasure 都是为 true 的,但如果是点击 ListView 触发 onItemClick 事件这种场景,needToMeasure 就会是 false。针对这两种不同的场景处理的逻辑也是不一样的,我们先来看一下 needToMeasure 为 true 的情况。

在第 49 行判断,如果是向下滑动,则调用 getColumnToAppend() 方法来获取新增子 View 要添加到哪一列,并计算出子 View 左上右下的位置,最后调用 child.layout() 方法完成布局。如果是向上滑动,则调用 getColumnToPrepend() 方法来获取新增子 View 要添加到哪一列,同样计算出子 View 左上右下的位置,并调用 child.layout() 方法完成布局。另外,在设置完子 View 布局之后,我们还进行了几个额外的操作。child.setTag() 是给当前的子 View 打一个标签,记录这个子 View 是属于哪一列的,这样我们在 trackMotionScroll() 的时候就可以调用 getTag() 来获取到该值,mColumnViews 和 mPosIndexMap 中的值也都是在这里填充的。

接着看一下 needToMeasure 为 false 的情况,首先在第 72 行调用 mPosIndexMap 的 get() 方法获取该 View 所属于哪一列,接着判断是向下滑动还是向上滑动,如果是向下滑动,则将该 View 添加到 mColumnViews 中所属列的末尾,如果是向上滑动,则向该 View 添加到 mColumnViews 中所属列的顶部。这么做的原因是因为当 needToMeasure 为 false 的时候,所有 ListView 中子元素的位置都不会变化,因而不需要调用 child.layout() 方法,但是 ListView 仍然还会走一遍 layoutChildren 的过程,而 layoutChildren 算是一个完整布局的过程,所有的缓存值在这里都应该被清空,所以我们需要对 mColumnViews 重新进行赋值。

那么说到 layoutChildren 过程中所有的缓存值应该清空,很明显我们还没有进行这一步,那么现在修改 layoutChildren() 方法中的代码,如下所示:

protected void layoutChildren() {

if (!blockLayoutRequests) {

mBlockLayoutRequests = false;

很简单,由于刚才我们已经提供好辅助方法了,这里只需要在开始 layoutChildren 过程之前调用一下 clearColumnViews() 方法就可以了。

最后还有一个细节需要注意,之前在定义 mColumnViews 的时候,其实只是定义了一个长度为 mColumnCount 的 ArrayList 数组而已,但数组中的每个元素目前还都是空的,因此我们还需要在 ListView 开始工作之前对数组中的每个元素进行初始化才行。那么修改 ListView 构造函数中的代码,如下所示:
layoutChildren() {

if (!blockLayoutRequests) {

mBlockLayoutRequests = false;

很简单,由于刚才我们已经提供好辅助方法了,这里只需要在开始 layoutChildren 过程之前调用一下 clearColumnViews() 方法就可以了。

最后还有一个细节需要注意,之前在定义 mColumnViews 的时候,其实只是定义了一个长度为 mColumnCount 的 ArrayList 数组而已,但数组中的每个元素目前还都是空的,因此我们还需要在 ListView 开始工作之前对数组中的每个元素进行初始化才行。那么修改 ListView 构造函数中的代码,如下所示:

上一篇:高并发编程-队列-BlockingQueue-LinkedBlockingQueue


下一篇:PyQt5基础学习-QVBoxLayout(垂直布局)