編輯:關於Android編程
經過前面兩篇文章的學習,我們已經對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[] mColumnViews = new ArrayList[mColumnCount]; protected Map mPosIndexMap = new HashMap ();
其中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 == 0) { return true; } int firstTop = Integer.MIN_VALUE; int lastBottom = Integer.MAX_VALUE; int endBottom = Integer.MIN_VALUE; for (int i = 0; i < mColumnViews.length; i++) { ArrayListviewList = mColumnViews[i]; int size = viewList.size(); if (size == 0) { lastBottom = 0; firstTop = 0; endBottom = 0; } else { int top = viewList.get(0).getTop(); int bottom = viewList.get(size - 1).getBottom(); if (lastBottom > bottom) { lastBottom = bottom; } if (endBottom < bottom) { endBottom = bottom; } if (firstTop < top) { firstTop = top; } } } 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(); if (deltaY < 0) { deltaY = Math.max(-(height - 1), deltaY); } else { deltaY = Math.min(height - 1, deltaY); } if (incrementalDeltaY < 0) { incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); } else { incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); } final int firstPosition = mFirstPosition; if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { // Don't need to move views down if the top of the first position // is already visible return true; } if (firstPosition + childCount == mItemCount && endBottom <= end && deltaY <= 0) { // Don't need to move views up if the bottom of the last position // is already visible return true; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; if (down) { final int top = listPadding.top - incrementalDeltaY; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; 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); } } } } } else { final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; 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; if (count > 0) { detachViewsFromParent(start, count); } tryOffsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } invalidate(); 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(); awakenScrollBars(); return false; }
從第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()方法的參數:
@Override void fillGap(boolean down, int startOffset) { final int count = getChildCount(); if (down) { startOffset = count > 0 ? startOffset + mDividerHeight : getListPaddingTop(); fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { 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可以看到,這裡在makeAndAddView之後並沒有直接使用新增的View來獲取它的bottom值,而是再次使用了一個循環來遍歷瀑布流ListView中的所有列,找出所有列中最靠下的那個子View的bottom值,如果這個值超出了ListView的底部,那就跳出循環。這樣的寫法就可以保證只要在有子View的情況下,瀑布流ListView中每一列的內容都是填滿的,界面上不會有空白的地方出現。viewList = mColumnViews[i]; int size = viewList.size(); if (size > 0) { int bottom = viewList.get(size - 1).getBottom(); if (bottom < lowerBottom) { lowerBottom = bottom; } } else { lowerBottom = 0; break; } } nextTop = lowerBottom + mDividerHeight; if (selected) { selectedView = child; } pos++; } return selectedView; }
接下來makeAndAddView()方法並沒有任何需要改動的地方,但是makeAndAddView()方法中調用的setupChild()方法,我們就需要大刀闊斧地修改了。
大家應該還記得,setupChild()方法是用來具體設置子View在ListView中顯示的位置的,在這個過程中可能需要用到幾個輔助方法,這裡我們先提供好,如下所示:
private int[] getColumnToAppend(int pos) { int indexToAppend = -1; int bottom = Integer.MAX_VALUE; for (int i = 0; i < mColumnViews.length; i++) { int size = mColumnViews[i].size(); if (size == 0) { return new int[] { i, 0 }; } View view = mColumnViews[i].get(size - 1); if (view.getBottom() < bottom) { indexToAppend = i; 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++) { mColumnViews[i].clear(); } }
這三個方法全部都非常重要,我們來逐個看一下。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(); if (p == null) { 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); } else { p.forceAdd = false; 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); } if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } int w = child.getMeasuredWidth(); int h = child.getMeasuredHeight(); if (needToMeasure) { if (flowDown) { 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); } else { 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); } } else { int columnIndex = mPosIndexMap.get(position); if (flowDown) { mColumnViews[columnIndex].add(child); } else { 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() { ...... try { super.layoutChildren(); clearColumnViews(); ...... } finally { 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這樣基本上就算是把所有的工作都完成了。現在重新運行一下UIListViewTest項目,效果如下圖所示:(); } ...... }
恩,效果還是相當不錯的,說明我們對ListView的功能擴展已經成功實現了。值得一題的是,這個功能擴展對於調用方而言是完全不透明的,也就是說在使用瀑布流ListView的時候其實仍然在使用標准的ListView用法,但是自動就變成了這種瀑布流的顯示模式,而不用做任何特殊的代碼適配,這種設計體驗對於調用方來說是非常友好的。
另外我們這個瀑布流ListView並不僅僅支持兩列內容顯示而已,而是可以輕松指定任意列數顯示,比如將mColumnCount的值改成3,就可以變成三列顯示了。不過三列顯示有點擠,這裡我把屏幕設置成橫屏再來看一下效果:
測試結果還是比較讓人滿意的。
最後還需要提醒大家一點,本篇文章中的例子僅供參考學習,是用於幫助大家理解源碼和提升水平的,切誤將本篇文章中的代碼直接使用在正式項目當中,不管在功能性還是穩定性方面,例子中的代碼都還達不到商用產品的標准。如果確實需要在項目實現瀑布流布局的效果,可以使用開源項目 PinterestLikeAdapterView 的代碼,或者使用Android新推出的RecyclerView控件,RecyclerView中的StaggeredGridLayoutManager也是可以輕松實現瀑布流布局效果的。
好的,那麼今天就到這裡了,ListView系列的內容也到此結束,相信大家通過這三篇文章的學習,對ListView一定都有了更深一層的理解,使用ListView時碰到了什麼問題也可以更多從源碼和工作原理的層次去考慮如何解決。感謝大家可以看到最後。
前言 本章繼續完善播放相關播放器的核心功能,為後續擴展打好基礎。系列 1、Android 使用Vitamio打造自己的萬能播放器(1)——准備
Toast英文名為土司,在Android裡面這個類是用來彈出提示信息的,我想sdk作者是認為提示信息片長得就像一塊土司吧。這個理論就不多說什麼了,開始我們的實踐。 第一步
1.寫在前面的話今天我們來學習Android中如何使用Sqlite以及性能優化。2.Android平台下數據庫相關類SQLiteOpenHelper 抽象類:通過從此類繼
前言上一篇我們講到了EventBus3.0的用法,這一篇我們來講一下EventBus3.0的源碼以及它的利與弊。1.構造函數當我們要調用EventBus的功能時,比如注冊