通過前面兩篇文章的學習,咱們已經對 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 時碰到了什麼問題也能夠更多從源碼和工做原理的層次去考慮如何解決。感謝你們能夠看到最後。
關注個人技術公衆號「郭霖」,優質技術文章推送。