After the study of the first two articles, we have carried out a very deep analysis of ListView, not only understand the ListView source code and its working principle, but also some common problems in ListView were summarized and summarized.
one
After the study of the first two articles, we have carried out a very deep analysis of ListView, not only understand the ListView source code and its working principle, but also some common problems in ListView were summarized and summarized.
In this article, the last in our three-part ListView series, we will extend the ListView to display data as a waterfall stream. In addition, the content of this article is more complex, and knowledge heavily depends on the first two articles, if you have not read it, I strongly recommend to read the Android ListView working principle completely analysis, take you from the source perspective of a thorough understanding and Android ListView asynchronous loading picture out of order problem, Cause analysis and solution of the two articles.
For those of you who have been following my blog, I actually posted a post a long time ago about implementing waterfall flow layout. Android Waterfall Flow photo wall implementation, experiencing the beauty of irregular arrangement. However, the implementation algorithm used in this paper is relatively simple. In fact, it is to nest a ScrollView in the outer layer, and then continuously add sub-views to it according to the rules of waterfall flow, as shown in the following figure:
Although the function can be implemented normally, there are too many problems behind the implementation principle, because it only keeps adding child views to ScrollView without a reasonable recycling mechanism. When the number of child views is infinite, the efficiency of the waterfall flow layout will be seriously affected. There may even be OOM.
And we conducted a deep analysis of ListView in the first two articles, the working principle of ListView is very clever, it uses RecycleBin to achieve a very good producer and consumer mechanism, the child View removed from the screen will be recycled, And then RecycleBin for caching, and then RecycleBin for caching for kids that are new to the screen, so that no matter how many pieces of data we need to display, there are actually only a few kids on the screen going back and forth.
So, if we use the ListView working principle to achieve waterfall flow layout, efficiency problems, OOM problems are no longer exist, can be said to be a true sense of the implementation of a high-performance waterfall flow layout. The schematic diagram of the principle is as follows:
two
OK, after the working principle is confirmed, the next work is hands-on implementation. Because the waterfall stream extension to the ListView as a whole changes very large, we can not simply use inheritance to achieve, so can only first extract the ListView source, and then modify its internal logic to achieve the function, so our first step is to extract the ListView source. But this work is not that easy, because only the ListView class can not work independently, if we want to extract code, we also need to extract the AbsListView, AdapterView, etc., and then report various errors need to be solved one by one. It took me a long time to get it right. So here I will not take you step by step to ListView source code extraction, but directly I extract good project UIListViewTest uploaded to CSDN, you just need to click here to download it, today all of our code changes are on the basis of this project.
It should also be noted that, for the sake of simplicity, I did not extract the latest version of the ListView code, but chose the Android 2.3 version of the ListView source code, because the old version of the source code is more concise, easy to understand the core workflow.
Ok, so now import the UIListViewTest project into the development tool, and run the application, as shown in the following image:
As you can see, this is a very ordinary ListView, and each child of the ListView has an image, a paragraph of text, and a button. The length of the text is randomly generated, so each child View has a different height. So let’s now extend the ListView to have waterfall display capability.
First, we open the AbsListView class and add a few global variables as shown below:
protected int mColumnCount = 2;
protected ArrayList<View>[] mColumnViews = new ArrayList[mColumnCount];
protected Map<Integer, Integer> mPosIndexMap = new HashMap<Integer, Integer>();
Copy the code
Columncount (mColumnCount) specifies the number of columns in the waterfall flow layout. Of course, if you want to scale well, you can specify the number of columns to display in the XML using custom attributes, but that is beyond the scope of this article. MColumnViews creates an array of length mColumnCount, in which each element is an ArrayList of generic View, used to cache the child views of the corresponding column. MPosIndexMap is used to record in which column the child views of each location should be placed.
The fillDown() and fillUp() methods are the fillDown() and fillUp() methods, which trigger the fillGap() method. The fillGap() method is called by the trackMotionScroll() method based on the position of the child element. This method will evaluate as long as the finger is sliding on the screen, and fillGap() will be called when an off-screen element needs to enter the screen. So, the trackMotionScroll() method might be a good place to start.
The most important thing here is to change the timing of the sub-view entering the screen, because the original ListView has only one column of content, and the waterfall layout will have multiple columns of content, so the timing algorithm will need to be changed. So 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;
Copy the code
Here firstTop represents the position of the top edge of the first element on the screen, lastBottom represents the position of the bottom edge of the last element on the screen, and spaceAbove records the distance from the top edge of the first element on the screen to the upper edge of the ListView, SpaceBelow records the distance from the bottom edge of the last element on the screen to the bottom edge of the ListView. Finally, the distance of finger movement on the screen is compared with spaceAbove and spaceBelow to determine whether fillGap() needs to be called, as shown below:
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
Copy the code
Now that we know how the original works, we can think about how to adapt this logic to fit the waterfall flow layout. For example, if we have two columns in the ListView, it doesn’t really make sense to get the first and last element on the screen, because if we have multiple columns, we want to find the element closest to the top edge of the screen and the element closest to the bottom edge of the screen, So here we need to write an algorithm to calculate firstTop and lastBottom. I’ll post the modified trackMotionScroll() method and then explain it more slowly:
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();Copy the code
Starting at line 9, we’re using a loop that iterates through all the columns in the waterfall stream ListView. Each loop gets the first and last element of the column and compares it to firstTop and lastBottom. Find the position of the element closest to the upper edge of the screen and the element closest to the lower edge of the screen in all columns. Note that in addition to firstTop and lastBottom, we also calculate an endBottom value, which records the position of the bottom element and is used for boundary checking when sliding.
These are the most important changes, but there are a few other minor changes. Look at line 75 and add the child View that was removed from the screen to the RecycleBin. So remember the global variable mColumnViews we just added? It is used to cache the child views of each column, so when the child views are reclaimed, the mColumnViews need to be deleted. At line 76, the getTag() method is called to get which column the child View is in, and the remove() method is called to remove it. The logic at line 96 is exactly the same, except that one moves up and one moves down, which I won’t go into here.
One other change is that we add an argument to the fillGap() method at line 115. The original fillGap() method only takes a Boolean argument to determine whether to slide up or down, and then inside the method itself gets the position of the first or last element to get the offset. However, in the waterfall ListView, the offset value is computed through a loop, which we have computed in the trackMotionScroll() method, so it is more efficient to pass this value directly through a parameter.
Now that the changes in the AbsListView are over, let’s go back to the ListView and change the parameters of the fillGap() method first:
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());
Copy the code
We just changed the value of the fetch to the value passed directly from the parameter, not much changed. The fillDown method is used to fill the while loop with child views, and when the bottom edge of the newly added child View goes beyond the bottom of the ListView, the loop is broken.
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;
Copy the code
You can see that instead of using the new View directly after the makeAndAddView to get its bottom value, you use a loop again to go through all the columns in the waterfall ListView and find the bottom value of the child View that is the lowest in all the columns. If the value goes beyond the bottom of the ListView, jump out of the loop. This ensures that, as long as there are subviews, each column of the waterfall ListView is filled and there is no blank space in the interface.
There is nothing to change about the makeAndAddView() method next, but the setupChild() method called in the makeAndAddView() method needs to be changed significantly.
SetupChild () : setupChild() : setupChild() : setupChild() : setupChild() : setupChild
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++) {
Copy the code
three
All three are very important, so let’s take a look at each one. The getColumnToAppend() method is used to determine to which column the newly entered child View should be added when the ListView slides down. The logic of judgment is also very simple, in fact, is to traverse each column of the waterfall stream ListView, take the bottom element of each column, and then find the column of the element closest to the top, which is the location of the new child View should be added to. The return value is the subscript of the position column to be added and the bottom value of the bottommost child View of that column. The schematic diagram of the principle is as follows:
Then look at the getColumnToPrepend() method. The getColumnToPrepend() method is used to determine to which column the newly entered child View should be added when the ListView slides up. But if you think this is a similar or opposite process to getColumnToAppend(), think again. Because when you swipe up, the new child views that come into the screen are actually recycled after they’ve been removed from the screen, and they don’t care about the position of the highest or lowest child views in each column, they just follow the principle of which column they were in when they were first added to the screen, So when you slide up, which column are they still in, you can’t slide up and change columns. SetupChild () : setupChild() : setupChild() : setupChild() : setupChild() : setupChild() : setupChild(); We’ll see that in a minute. The return value is the subscript of the position column to be added and the top value of the topmost child View of that column.
The final clearColumnViews() method is very simple, and is responsible for removing all child views from the mColumnViews cache.
All helper methods are provided, but there is one very important value missing before setupChild, and that is the width of the column. A normal ListView does not take this into account because the width of the column is the width of the ListView. But the waterfall ListView is different. The width of each column is different depending on the number of columns, so 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;
Copy the code
In the last line of the onMeasure() method, divide the width of the current ListView by the number of columns to obtain the width of each column. Assign the column width to the mColumnWidth global variable.
Now that everything is ready, let’s start by modifying the code in the setupChild() method as follows:
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);Copy the code
The first change is in line 33, when childWidthSpec is computed. Ordinary ListView due to the width of the View and the width of the ListView is consistent, so it can be ViewGroup. GetChildMeasureSpec () method of direct incoming mWidthMeasureSpec, But in the cascade flow of ListView requires again after a MeasureSpec makeMeasureSpec process to calculate each column widthMeasureSpec, incoming parameters is we just save the global variable mColumnWidth. After this modification, the child View width obtained by calling child.getMeasuredWidth() is the column width, not the ListView width.
Next, on line 48, determine needToMeasure. NeedToMeasure is true if it’s a normal fill or ListView scroll, but if it’s clicking on the ListView that triggers the onItemClick event, NeedToMeasure will be false. The logic to handle these two scenarios is also different, so let’s first look at the case where needToMeasure is true.
At line 49, the getColumnToAppend() method is called to get the column to which the new child View is to be added if it is swiping down, and the upper left and lower right positions of the child View are calculated. Finally, the child.layout() method is called to complete the layout. In the case of scrolling up, the getColumnToPrepend() method is called to get the column to which the new child View is to be added to, the upper left and lower right positions of the child View are also calculated, and the child.Layout () method is called to complete the layout. In addition, after setting up the child View layout, we did a few additional things. Child.settag () labels the current child View and records which column the child View belongs to, so we can call getTag() when trackMotionScroll() to get that value. The values in mColumnViews and mPosIndexMap are also populated here.
If needToMeasure is false, call mPosIndexMap’s get() method on line 72 to get which column the View belongs to, and then determine whether to slide down or up. If needToMeasure is false, Add the View to the end of the column in mColumnViews or, if you swipe up, to the top of the column in mColumnViews. The reason for doing this is that when needToMeasure is false, the position of all the ListView neutrons will not change, so you don’t need to call child.layout(), However, the ListView will still go through the layoutChildren process, which is a complete layout process. All cached values should be cleared here, so we need to re-assign the mColumnViews.
So all cached values should be cleared during layoutChildren. Obviously we haven’t done this yet, so now modify the code in the layoutChildren() method as follows:
protected void layoutChildren() { if (! blockLayoutRequests) { mBlockLayoutRequests = false;Copy the code
This is easy, since we already provided the helper methods, just call the clearColumnViews() method before starting the layoutChildren procedure.
MColumnViews is an ArrayList array with a length of mColumnCount, but each element in the array is empty. So we also need to initialize each element in the array before the ListView starts working. So 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<View>();
Copy the code
That’s basically all the work done. Now re-run the UIListViewTest project as shown below:
Well, the effect is quite good, indicating that we have successfully implemented the ListView function extension. It’s worth noting that this extension is completely opaque to the caller, which means that when using the waterfall ListView, the caller is still using the standard ListView usage, but automatically changes to the waterfall display mode without any special code adaptation. This design experience is very friendly to the caller.
In addition, the waterfall ListView does not support only two columns, but can easily specify any number of columns such as mColumnCount (3) to display three columns. But the three columns are a bit crowded, so I’ll set the screen to landscape to see what it looks like:
The test results are satisfactory.
Finally also need to remind everybody, this article study the examples are for reference only, is used to help people understand the source and promote level, cut to the code in this article used directly in the formal project, no matter in terms of functionality and stability, in the example code is still not up to the standard of the commercial products. If you really need a waterfall layout in your project, you can use code from PinterestLikeAdapterView, or you can use RecyclerView, a new Android control. RecyclerView StaggeredGridLayoutManager can also be easily in the waterfall flow distribution effect.
Ok, so today is here, ListView series content also ends here, I believe you through the study of these three articles, ListView must have a deeper understanding of the use of ListView encountered what problems can also be more from the source code and working principle of the level to consider how to solve. Thank you for seeing this to the end.
Pay attention to my technical public account “Guo Lin”, high-quality technical articles push.