Android ListView功能扩展,实现高性能的瀑布流布局

通过前面两篇文章的学习,咱们已经对 ListView 进行了很是深层次的剖析,不只了解了 ListView 的源码和它的工做原理,同时也将 ListView 中常见的一些问题进行了概括和总结。算法

通过前面两篇文章的学习,咱们已经对 ListView 进行了很是深层次的剖析,不只了解了 ListView 的源码和它的工做原理,同时也将 ListView 中常见的一些问题进行了概括和总结。数组

那么本篇文章是咱们 ListView 系列三部曲的最后一篇,在这篇文章当中咱们将对 ListView 进行功能扩展,让它可以以瀑布流的样式来显示数据。另外,本篇文章的内容比较复杂,且知识点严重依赖于前两篇文章,若是你尚未阅读过的话,强烈建议先去阅读 Android ListView 工做原理彻底解析,带你从源码的角度完全理解 和 Android ListView 异步加载图片乱序问题,缘由分析及解决方案 这两篇文章。缓存

一直关注我博客的朋友们应该知道,其实在很早以前我就发布过一篇关于实现瀑布流布局的文章,Android 瀑布流照片墙实现,体验不规则排列的美感。可是这篇文章中使用的实现算法比较简单,其实就是在外层嵌套一个 ScrollView,而后按照瀑布流的规则不断向里面添加子 View,原理以下图所示:markdown

虽然说功能是能够正常实现,可是这种实现原理背后的问题太多了,由于它只会不停向 ScrollView 中添加子 View,而没有一种合理的回收机制,当子 View 无限多的时候,整个瀑布流布局的效率就会严重受影响,甚至有可能会出现 OOM 的状况。异步

而咱们在前两篇文章中对 ListView 进行了深层次的分析,ListView 的工做原理就很是巧妙,它使用 RecycleBin 实现了很是出色的生产者和消费者的机制,移出屏幕的子 View 将会被回收,并进入到 RecycleBin 中进行缓存,而新进入屏幕的子 View 则会优先从 RecycleBin 当中获取缓存,这样的话无论咱们有多少条数据须要显示,实际上屏幕上的子 View 其实也就来来回回那么几个。ide

那么,若是咱们使用 ListView 工做原理来实现瀑布流布局,效率问题、OOM 问题就都不复存在了,能够说是真正意义上实现了一个高性能的瀑布流布局。原理示意图以下所示:函数

OK,工做原理确认了以后,接下来的工做就是动手实现了。因为瀑布流这个扩展对 ListView 总体的改动很是大,咱们没办法简单地使用继承来实现,因此只能先将 ListView 的源码抽取出来,而后对其内部的逻辑进行修改来实现功能,那么咱们第一步的工做就是要将 ListView 的源码抽取出来。可是这个工做并非那么简单的,由于仅仅 ListView 这一个单独的类是不可以独立工做的,咱们若是要抽取代码的话还须要将 AbsListView、AdapterView 等也一块儿抽取出来,而后还会报各类错误都须要一一解决,我当时也是折腾了好久才搞定的。因此这里我就不带着你们一步步对 ListView 源码进行抽取了,而是直接将我抽取好的工程 UIListViewTest 上传到了 CSDN,你们只须要点击 这里 进行下载就能够了,今天咱们全部的代码改动都是在这个工程的基础上进行的。工具

另外须要注意的是,为了简单起见,我没有抽取最新版本的 ListView 代码,而是选择了 Android 2.3 版本 ListView 的源码,由于老版本的源码更为简洁,方便于咱们理解核心的工做流程。oop

好的,那么如今将 UIListViewTest 项目导入到开发工具当中,而后运行程序,效果以下图所示:布局

能够看到,这是一个很是普通的 ListView,每一个 ListView 的子 View 里面有一张图片,一段文字,还有一个按钮。文字的长度是随机生成的,所以每一个子 View 的高度也各不相同。那么咱们如今就来对 ListView 进行扩展,让它拥有瀑布流展现的能力。

首先,咱们打开 AbsListView 这个类,在里面添加以下所示的几个全局变量:

protected int mColumnCount = 2;

protected ArrayList<View>[] mColumnViews = new ArrayList[mColumnCount];

protected Map<Integer, Integer> mPosIndexMap = new HashMap<Integer, Integer>();
复制代码

其中 mColumnCount 表示瀑布流布局一共有几列,这里咱们先让它分为两列显示,后面随时能够对它进行修改。固然,若是想扩展性作的好的话,也可使用自定义属性的方式在 XML 里面指定显示的列数,不过这个功能就不在咱们本篇文章的讨论范围以内了。mColumnViews 建立了一个长度为 mColumnCount 的数组,数组中的每一个元素都是一个泛型为 View 的 ArrayList,用于缓存对应列的子 View。mPosIndexMap 则是用于记录每个位置的子 View 应当放置在哪一列当中。

接下来让咱们回忆一下,ListView 最基本的填充方式分为向下填充和向上填充两种,分别对应的方法是 fillDown() 和 fillUp() 方法,而这两个方法的触发点都是在 fillGap() 方法当中的,fillGap() 方法又是由 trackMotionScroll() 方法根据子元素的位置来进行调用的,这个方法只要手指在屏幕上滑动时就会不停进行计算,当有屏幕外的元素须要进入屏幕时,就会调用 fillGap() 方法来进行填充。那么,trackMotionScroll() 方法也许就应该是咱们开始着手修改的地方了。

这里咱们最主要的就是修改对于子 View 进入屏幕判断的时机,由于原生的 ListView 只有一列内容,而瀑布流布局将会有多列内容,因此这个时机的判断算法也就须要进行改动。那么咱们先来看一下原先的判断逻辑,以下所示:

final int firstTop = getChildAt(0).getTop();

final int lastBottom = getChildAt(childCount - 1).getBottom();

final Rect listPadding = mListPadding;

final int spaceAbove = listPadding.top - firstTop;

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

final int spaceBelow = lastBottom - end;
复制代码

这里 firstTop 表示屏幕中第一个元素顶边的位置,lastBottom 表示屏幕中最后一个元素底边的位置,而后 spaceAbove 记录屏幕第一个元素顶边到 ListView 上边缘的距离,spaceBelow 记录屏幕最后一个元素底边到 ListView 下边缘的距离。最后使用手指在屏幕上移动的距离和 spaceAbove、spaceBelow 进行比较,来判断是否须要调用 fillGap() 方法,以下所示:

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);

if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
复制代码

了解了原先的工做原理以后,咱们就能够来思考一下怎么将这个逻辑改为适配瀑布流布局的方式。好比说目前 ListView 中有两列内容,那么获取屏幕中的第一个元素和最后一个元素其实意义是不大的,由于在有多列内容的状况下,咱们须要找到的是最靠近屏幕上边缘和最靠近屏幕下边缘的元素,所以这里就须要写一个算法来去计算 firstTop 和 lastBottom 的值,这里我先把修改后的 trackMotionScroll() 方法贴出来,而后再慢慢解释:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {

final int childCount = getChildCount();

int firstTop = Integer.MIN_VALUE;

int lastBottom = Integer.MAX_VALUE;

int endBottom = Integer.MIN_VALUE;

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

		ArrayList<View> viewList = mColumnViews[i];

int size = viewList.size();

int top = viewList.get(0).getTop();

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

if (lastBottom > bottom) {

if (endBottom < bottom) {

final Rect listPadding = mListPadding;

final int spaceAbove = listPadding.top - firstTop;

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

final int spaceBelow = lastBottom - end;

final int height = getHeight() - getPaddingBottom() - getPaddingTop();

		deltaY = Math.max(-(height - 1), deltaY);

		deltaY = Math.min(height - 1, deltaY);

if (incrementalDeltaY < 0) {

		incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);

		incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);

final int firstPosition = mFirstPosition;

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 - getFooterViewsCount();

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<View> 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 值。原理示意图以下所示:

而后来看一下 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 构造函数中的代码,以下所示:

public ListView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

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

    	mColumnViews[i] = new ArrayList<View>();
复制代码

这样基本上就算是把全部的工做都完成了。如今从新运行一下 UIListViewTest 项目,效果以下图所示:

恩,效果仍是至关不错的,说明咱们对 ListView 的功能扩展已经成功实现了。值得一题的是,这个功能扩展对于调用方而言是彻底不透明的,也就是说在使用瀑布流 ListView 的时候其实仍然在使用标准的 ListView 用法,可是自动就变成了这种瀑布流的显示模式,而不用作任何特殊的代码适配,这种设计体验对于调用方来讲是很是友好的。

另外咱们这个瀑布流 ListView 并不只仅支持两列内容显示而已,而是能够轻松指定任意列数显示,好比将 mColumnCount 的值改为 3,就能够变成三列显示了。不过三列显示有点挤,这里我把屏幕设置成横屏再来看一下效果:

测试结果仍是比较让人满意的。

最后还须要提醒你们一点,本篇文章中的例子仅供参考学习,是用于帮助你们理解源码和提高水平的,切误将本篇文章中的代码直接使用在正式项目当中,无论在功能性仍是稳定性方面,例子中的代码都还达不到商用产品的标准。若是确实须要在项目实现瀑布流布局的效果,可使用开源项目 [PinterestLikeAdapterView]的代码,或者使用 Android 新推出的 RecyclerView 控件,RecyclerView 中的 StaggeredGridLayoutManager 也是能够轻松实现瀑布流布局效果的。

好的,那么今天就到这里了,ListView 系列的内容也到此结束,相信你们经过这三篇文章的学习,对 ListView 必定都有了更深一层的理解,使用 ListView 时碰到了什么问题也能够更多从源码和工做原理的层次去考虑如何解决。感谢你们能够看到最后。

关注个人技术公众号“郭霖”,优质技术文章推送。

相关文章
相关标签/搜索