Extended Android ListView to achieve high-performance waterfall stream Layout
After studying the previous two articles, we have deeply analyzed ListView, not only understanding the source code of ListView and its working principle, at the same time, some common problems in ListView are summarized and summarized.
So this article is the last article in our ListView series trilogy. In this article, we will extend the functions of ListView so that it can display data in the waterfall stream style. In addition, the content of this article is complex, and the knowledge point is heavily dependent on the first two articles. If you have not read it, it is strongly recommended to read it first.The working principle of Android ListView is completely resolved, which gives you a thorough understanding of the source code.AndAndroid ListView asynchronously loads out-of-order images, causes analysis and solutionsThese two articles.
Friends who have been paying attention to my blog should know that, in fact, I have published an article about Waterfall stream layout a long time ago,Android waterfall stream photo wall to experience the beauty of irregular Arrangement. However, the implementation algorithm used in this article is relatively simple. In fact, it is to nest a ScrollView in the outer layer and add a subview to it according to the waterfall flow rules. The principle is shown in:
Although the function can be implemented normally, there are too many problems behind this implementation principle, because it will only add sub-views to the ScrollView without a reasonable recycling mechanism, when the number of sub-views is infinite, the efficiency of the overall waterfall flow layout will be seriously affected, and even OOM may occur.
In the previous two articles, we performed in-depth analysis on ListView, and the working principle of ListView was very clever. It used RecycleBin to implement a very good mechanism of producers and consumers, the sub-View that is removed from the screen will be recycled and cached in RecycleBin. The sub-View that is new to the screen will first obtain the cache from RecycleBin, in this case, no matter how much data we need to display, the sub-View on the screen actually comes back and forth.
If we use ListView to implement waterfall stream layout, the efficiency problem and OOM problem will no longer exist. It can be said that a high-performance waterfall stream layout is truly realized. The principle is as follows:
OK. After the working principle is confirmed, the next task is to implement it. Because the extension of waterfall stream greatly changes the overall ListView, we cannot simply use inheritance to implement it. Therefore, we can only extract the source code of ListView first, then, modify the internal logic to implement the function. The first step is to extract the source code of the ListView. However, this work is not that simple, because only the ListView class cannot work independently, if we want to extract code, we also need to extract AbsListView, AdapterView, and so on, and then we will report all kinds of errors that need to be solved one by one. At that time, I had a hard time to solve them. So here I will not take you step by step to extract the ListView source code, but directly upload the project UIListViewTest that I extracted to CSDN. You just need to clickHere Download the code. Today, all our code changes are made on the basis of this project.
In addition, for the sake of simplicity, I did not extract the latest ListView code, but chose the source code of the Android 2.3 ListView, because the source code of the old version is more concise, it is easy for us to understand the core workflow.
Okay. Now, import the UIListViewTest project to the development tool and run the program. The effect is shown in:
As you can see, this is a very common ListView. Each ListView sub-View contains an image, a text, and a button. The text length is randomly generated, so the height of each sub-View is also different. Now, we will extend the ListView so that it can display waterfall streams.
First, open the AbsListView class and add the following global variables:
protected int mColumnCount = 2;protected ArrayList
[] mColumnViews = new ArrayList[mColumnCount];protected Map
mPosIndexMap = new HashMap
();
Here, mColumnCount indicates that the waterfall flow layout has several columns. Here we first let it be displayed in two columns, and it can be modified at any time later. Of course, if you want to achieve better scalability, you can also use custom attributes to specify the number of columns to be displayed in XML. However, this function is not covered in this article. MColumnViews creates an array with the length of mColumnCount. Each element in the array is an ArrayList with a generic View, which is used to cache the child views of corresponding columns. MPosIndexMap is used to record the column in which the child View at each position should be placed.
Next, let's recall that the most basic filling methods of ListView are downward filling and upward filling. The corresponding methods are fillDown () and fillUp, the triggering points of these two methods are in the fillGap () method, and the fillGap () method is called by the trackMotionScroll () method based on the position of the child element, this method keeps computation as long as your fingers slide on the screen. When an element outside the screen needs to enter the screen, it will call the fillGap () method for filling. Then, the trackMotionScroll () method may be where we started to modify it.
Here, the most important thing is to modify the time when the sub-View enters the screen for judgment, because the native ListView only has one column of content, and the waterfall stream layout will have multiple columns of content, therefore, the algorithm needs to be modified at this time. Let's take a look at the original judgment logic as follows:
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;
Here firstTop indicates the position of the top edge of the first element on the screen, lastBottom indicates the position of the bottom edge of the last element on the screen, and spaceAbove records the distance between the top edge of the first element on the screen and the edge of the ListView, spaceBelow records the distance between the bottom edge of the last element on the screen and the bottom edge of the ListView. Finally, compare the distance between fingers moving on the screen with spaceabve and spaceBelow to determine whether to call the fillGap () method, as shown below:
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {fillGap(down);}
After learning about the original working principle, we can think about how to adapt the logic to the waterfall flow layout. For example, if ListView currently has two columns of content, obtaining the first and last elements on the screen is of little significance, because when there are multiple columns of content, we need to find the elements closest to the top and bottom of the screen. Therefore, we need to write an algorithm to calculate the values of firstTop and lastBottom, here, I first paste the modified trackMotionScroll () method, and then explain it slowly:
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++) {ArrayList
viewList = 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 visiblereturn 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 visiblereturn 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;}
Starting from row 9th, we use a loop to traverse all columns in the waterfall stream ListView. Each loop obtains the first and last elements of the column, then compare it with firstTop and lastBottom to find the element position closest to the edge of the screen and the element position closest to the edge of the screen. Note that in addition to firstTop and lastBottom, we also calculated an endBottom value, which records the position of the bottom element and is used for border check during sliding.
These are the most important changes, but some minor changes have been made in other places. Observe Row 3. Add the child View removed from the screen to RecycleBin. In fact, this View has been recycled. Do you still remember the global variable mColumnViews we just added? It is used to cache the sub-views of each column. When a sub-View is recycled, it needs to be deleted in mColumnViews. In row 3, call the getTag () method to obtain the column in which the child View is located, and then call the remove () method to remove it. 96th the logic at the row is the same, but one is to move up and the other is to move down, so we will not repeat it here.
Another change is that we added a parameter when calling the fillGap () method in row 115th. The original fillGap () method only receives one Boolean parameter, used to determine whether to slide up or down, and then obtain the first or last element position in the method to obtain the offset value. However, in the waterfall stream ListView, this offset value needs to be calculated cyclically, and we have actually calculated it in the trackMotionScroll () method, therefore, it is more efficient to directly pass this value through parameters.
Now that the content to be modified in the AbsListView is complete, let's go back to the ListView and first modify the parameters of the fillGap () method:
@Overridevoid 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());}}
Only the original obtained value is changed to the value passed directly using the parameter, and there is no major change. Next, let's take a look at the fillDown method. The original logic is to constantly fill the child View in the while loop. When the lower edge of the newly added child View exceeds the bottom of the ListView, it jumps out of the loop, now let's make the following changes:
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();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;}
We can see that, after makeAndAddView, we didn't directly use the newly added View to get its bottom value. Instead, we used a loop to traverse all the columns in the waterfall stream ListView, find the bottom-most sub-View bottom value in all columns. If this value exceeds the bottom of the ListView, it will jump out of the loop. This method ensures that the content of each column in the waterfall stream ListView is filled with no blank content on the interface as long as there is a subview.
The makeAndAddView () method does not need to be modified, but the setupChild () method called in the makeAndAddView () method requires drastic modifications.
You should remember that the setupChild () method is used to set the position of the sub-View displayed in the ListView. Several auxiliary methods may be used in this process. Here we provide the following methods, as follows:
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();}}
All three methods are very important. Let's take a look at them one by one. The getColumnToAppend () method is used to determine the columns to which the subview is added when the ListView slides down. The judgment logic is also very simple. In fact, it is to traverse each column of the waterfall stream ListView and retrieve the bottom element of each column, then, find the column where the element on the top is located. This is the position where the Newly Added Sub-View should be added. The returned value is the subscript of the position column to be added and the bottom value of the bottom child View of the column. The principle is as follows:
Then let's take a look at the getColumnToPrepend () method. The getColumnToPrepend () method is used to determine the columns to which the new Child View should be added when the ListView slides up. However, if you think this is similar or opposite to the getColumnToAppend () method, you are very wrong. Because when sliding up, the new sub-Views on the screen are actually removed from the screen before they are recycled, they do not need to care about the highest sub-View or the lowest sub-View position in each column, instead, you only need to follow the principle that when they are added to the screen for the first time, which column they belong to when sliding up, you must not change columns due to sliding up. The algorithm used is also very simple, that is, to obtain the subscript of the column corresponding to the position value from mPosIndexMap based on the position value of the current sub-View. The value of mPosIndexMap is filled in the setupChild () method, we will see this later. The returned value is the subscript of the position column to be added and the top value of the Child View at the top of the column.
The last clearColumnViews () method is very simple. It is used to clear all the child views cached by mColumnViews.
All auxiliary methods are provided, but we still lack a very important value before setupChild, that is, the column width. Normal ListView does not need to consider this, because the column width is actually the width of the ListView. However, the waterfall stream ListView is different, the number of columns is different, and the width of each column is also different. Therefore, we need to calculate this value in advance. Modify the code in the onMeasure () method as follows:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ...... setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; mColumnWidth = widthSize / mColumnCount;}
In fact, it is very simple. We just added a code in the last line of the onMeasure () method, that is, dividing the width of the current ListView by the number of columns to get the width of each column, assign the column width to the global variable mColumnWidth.
Now that the preparation is complete, let's start to modify the code in the setupChild () method, as shown below:
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); }}
The first change is in row 33rd, when calculating childWidthSpec. Normal ListView because the width of the sub-View is the same as that of the ListView, you can. in the getChildMeasureSpec () method, mWidthMeasureSpec is directly input, but in the waterfall stream ListView, another MeasureSpec is required. makeMeasureSpec is used to calculate the widthMeasureSpec of each column. The input parameter is the global variable mColumnWidth we just saved. After this step is modified, the sub-View width obtained by calling the child. getMeasuredWidth () method is the column width, not the ListView width.
Next, Judge needToMeasure in Row 3. If it is normal filling or ListView scrolling, needToMeasure is true, but if it is to click ListView to trigger the onItemClick event, needToMeasure is false. The processing logic for these two different scenarios is also different. Let's first look at the situation where needToMeasure is true.
In row 49th, if it is sliding down, call the getColumnToAppend () method to obtain the column to which the newly added sub-View is added, and calculate the position of the top left and bottom right of the sub-View, finally, call child. layout () method to complete the layout. If it is sliding up, call the getColumnToPrepend () method to obtain the column to which the newly added sub-View is added, calculate the position of the top left and bottom right of the sub-View, and call child. layout () method to complete the layout. In addition, after the sub-View layout is set, several additional operations are performed. Child. setTag () is used to tag the current sub-View and record which column the sub-View belongs to. In this way, we can call getTag () When trackMotionScroll () to obtain this value. The values in mColumnViews and mPosIndexMap are also filled here.
Next, let's take a look at the case where needToMeasure is false. First, call the get () method of mPosIndexMap in Row 3 to obtain the column to which the View belongs, and then determine whether to slide downward or upward, if you slide down, add the View to the end of the column in mColumnViews. If you slide up, add the View to the top of the column in mColumnViews. The reason for this is that when needToMeasure is false, the positions of all subelements in ListView do not change, so you do not need to call child. the layout () method, but the ListView still goes through the layoutChildren process. layoutChildren is a complete layout process, and all the cache values should be cleared here, therefore, we need to assign a value to mColumnViews again.
When it comes to the layoutChildren process, all the cache values should be cleared. Obviously, we have not performed this step yet. Now, modify the code in the layoutChildren () method as follows:
protected void layoutChildren() { ...... try { super.layoutChildren(); clearColumnViews(); ...... } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } }}
It is very simple. As we have provided a helper method just now, you only need to call the clearColumnViews () method before starting the layoutChildren process.
Note that when defining mColumnViews, we only defined an ArrayList array with the length of mColumnCount. However, each element in the array is still empty, therefore, we need to initialize each element in the array before the ListView starts to work. Modify the code in the ListView constructor as follows:
public ListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); for (int i = 0; i < mColumnViews.length; i++) { mColumnViews[i] = new ArrayList
(); } ......}
In this way, everything is done. Run the UIListViewTest project again, as shown in:
Well, the effect is quite good, indicating that we have successfully implemented the Function Extension of ListView. It is worth noting that this function extension is completely opaque to the caller. That is to say, standard ListView usage is still in use when using the waterfall stream ListView, but it automatically becomes the display mode of the waterfall stream, without any special code adaptation. This design experience is very friendly for the caller.
In addition, the waterfall stream ListView not only supports the display of two columns, but also allows you to easily specify any number of columns for display. For example, you can change the value of mColumnCount to 3 to three columns for display. However, the three columns are a little crowded. Here I will set the screen to a horizontal screen to see the effect:
The test results are satisfactory.
Finally, we need to remind you that the examples in this article are for reference only and are used to help you understand the source code and improve the level, by mistake, the code in this article is directly used in a formal project. In terms of functionality and stability, the code in this example still fails to meet the standards of commercial products. If you really need to implement waterfall flow layout in the project, you can use the open-source projectPinterestLikeAdapterViewOr use the new RecyclerView control of Android. StaggeredGridLayoutManager in RecyclerView can also easily implement waterfall flow layout.
Okay, now we are here, and the content of the ListView series is over. I believe you will have a deeper understanding of ListView through the study of these three articles, if you encounter any problems when using ListView, you can also consider how to solve them from the source code and working principles. Thank you for seeing the conclusion.