分享一下我老师大神的人工智能教程。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/46361889
经过前面两篇文章的学习,我们已经对ListView进行了非常深层次的剖析,不仅了解了ListView的源码和它的工作原理,同时也将ListView中常见的一些问题进行了归纳和总结。
那么本篇文章是我们ListView系列三部曲的最后一篇,在这篇文章当中我们将对ListView进行功能扩展,让它能够以瀑布流的样式来显示数据。另外,本篇文章的内容比较复杂,且知识点严重依赖于前两篇文章,如果你还没有阅读过的话,强烈建议先去阅读 Android ListView工作原理完全解析,带你从源码的角度彻底理解 和 Android ListView异步加载图片乱序问题,原因分析及解决方案 这两篇文章。
一直关注我博客的朋友们应该知道,其实在很早之前我就发布过一篇关于实现瀑布流布局的文章,Android瀑布流照片墙实现,体验不规则排列的美感。但是这篇文章中使用的实现算法比较简单,其实就是在外层嵌套一个ScrollView,然后按照瀑布流的规则不断向里面添加子View,原理如下图所示:
虽说功能是可以正常实现,但是这种实现原理背后的问题太多了,因为它只会不停向ScrollView中添加子View,而没有一种合理的回收机制,当子View无限多的时候,整个瀑布流布局的效率就会严重受影响,甚至有可能会出现OOM的情况。
而我们在前两篇文章中对ListView进行了深层次的分析,ListView的工作原理就非常巧妙,它使用RecycleBin实现了非常出色的生产者和消费者的机制,移出屏幕的子View将会被回收,并进入到RecycleBin中进行缓存,而新进入屏幕的子View则会优先从RecycleBin当中获取缓存,这样的话不管我们有多少条数据需要显示,实际上屏幕上的子View其实也就来来回回那么几个。
那么,如果我们使用ListView工作原理来实现瀑布流布局,效率问题、OOM问题就都不复存在了,可以说是真正意义上实现了一个高性能的瀑布流布局。原理示意图如下所示:
OK,工作原理确认了之后,接下来的工作就是动手实现了。由于瀑布流这个扩展对ListView整体的改动非常大,我们没办法简单地使用继承来实现,所以只能先将ListView的源码抽取出来,然后对其内部的逻辑进行修改来实现功能,那么我们第一步的工作就是要将ListView的源码抽取出来。但是这个工作并不是那么简单的,因为仅仅ListView这一个单独的类是不能够独立工作的,我们如果要抽取代码的话还需要将AbsListView、AdapterView等也一起抽取出来,然后还会报各种错误都需要一一解决,我当时也是折腾了很久才搞定的。所以这里我就不带着大家一步步对ListView源码进行抽取了,而是直接将我抽取好的工程UIListViewTest上传到了CSDN,大家只需要点击 这里 进行下载就可以了,今天我们所有的代码改动都是在这个工程的基础上进行的。
另外需要注意的是,为了简单起见,我没有抽取最新版本的ListView代码,而是选择了Android 2.3版本ListView的源码,因为老版本的源码更为简洁,方便于我们理解核心的工作流程。
好的,那么现在将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) { fillGap(down);}
了解了原先的工作原理之后,我们就可以来思考一下怎么将这个逻辑改成适配瀑布流布局的方式。比如说目前ListView中有两列内容,那么获取屏幕中的第一个元素和最后一个元素其实意义是不大的,因为在有多列内容的情况下,我们需要找到的是最靠近屏幕上边缘和最靠近屏幕下边缘的元素,因此这里就需要写一个算法来去计算firstTop和lastBottom的值,这里我先把修改后的trackMotionScroll()方法贴出来,然后再慢慢解释:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == <