Implementing a multi-column ListView with independent row heights

I would like to create a list of approximately 200 ImageViews (random heights) with the following location in the collage:

enter image description here

I usually did this in a ListView for performance obtained using adapters, but since I want the images to be displayed in columns and with different heights (see image Example ) depending on the images, I cannot use one list for this purpose.

I tried to implement this layout with:

  • Three lists with synchronized scrolling = slow
  • Single ListView with each row containing three images = Do not allow different heights
  • GridView = Avoid Different Heights
  • GridLayout = It is difficult to implement various heights programmatically. Due to the lack of an adapter, OutOfMemoryErrors are common.
  • FlowLayout = Due to the lack of an adapter, OutOfMemoryErrors are common.
  • ScrollView with three vertical LinearLayouts = The best solution so far, but OutOfMemoryErrors are common.

In the end, I used three LinearLayouts in ScrollView, but this is far from optimal. I would rather use something with an adapter.

EDIT I looked at the StaggeredGridView, as in the answer below, but I find it pretty buggy. Are there any implementations of this that are more stable?

+8
android layout android-image
source share
6 answers

I think I have a working solution for you.

The core files mentioned here are also on PasteBin at http://pastebin.com/u/morganbelford

I basically implemented the simplified equivalent of the aforementioned github project https://github.com/maurycyw/StaggeredGridView using a set of great LoopJ SmartImageViews .

My solution is not as versatile and flexible as StaggeredGridView , but it seems to work well and quickly. One big difference functionally is that we place the images always from left to right, and then from left to right. We are not trying to put the next image in the shortest column. This makes the bottom of the view a little more uneven, but generates less bias during boot from the Internet.

There are three main classes: the custom StagScrollView , which contains the custom StagLayout (a subclass of FrameLayout ) that controls the set of ImageInfo data objects.

Here is our layout , stag_layout.xml (the initial height of 1000dp does not matter, since it will be recalculated in the code based on the image size):

 // stag_layout.xml <?xml version="1.0" encoding="utf-8"?> <com.morganbelford.stackoverflowtest.pinterest.StagScrollView xmlns:a="http://schemas.android.com/apk/res/android" a:id="@+id/scroller" a:layout_width="match_parent" a:layout_height="match_parent" > <com.morganbelford.stackoverflowtest.pinterest.StagLayout a:id="@+id/frame" a:layout_width="match_parent" a:layout_height="1000dp" a:background="@drawable/pinterest_bg" > </com.morganbelford.stackoverflowtest.pinterest.StagLayout> </com.morganbelford.stackoverflowtest.pinterest.StagScrollView> 

Here is our main Activity onCreate that uses the layout. StagActivity simply basically tells StagLayout which URLs to use, what should be the gap between each image and the number of columns. For more modularity, we could pass these parameters to a StagScrollView (which contains a StagLayout, but the scroll view should just pass them by layout):

 // StagActivity.onCreate setContentView(R.layout.stag_layout); StagLayout container = (StagLayout) findViewById(R.id.frame); DisplayMetrics metrics = new DisplayMetrics(); ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics); float fScale = metrics.density; String[] testUrls = new String[] { "http://www.westlord.com/wp-content/uploads/2010/10/French-Bulldog-Puppy-242x300.jpg", "http://upload.wikimedia.org/wikipedia/en/b/b0/Cream_french_bulldog.jpg", "http://bulldogbreeds.com/breeders/pics/french_bulldog_64368.jpg", "http://www.drsfostersmith.com/images/articles/a-french-bulldog.jpg", "http://2.bp.blogspot.com/-ui2p5Z_DJIs/Tgdo09JKDbI/AAAAAAAAAQ8/aoTdw2m_bSc/s1600/Lilly+%25281%2529.jpg", "http://www.dogbreedinfo.com/images14/FrenchBulldog7.jpg", "http://dogsbreed.net/wp-content/uploads/2011/03/french-bulldog.jpg", "http://www.theflowerexpert.com/media/images/giftflowers/flowersandoccassions/valentinesdayflowers/sea-of-flowers.jpg.pagespeed.ce.BN9Gn4lM_r.jpg", "http://img4-2.sunset.timeinc.net/i/2008/12/image-adds-1217/alcatraz-flowers-galliardia-m.jpg?300:300", "http://images6.fanpop.com/image/photos/32600000/bt-jpgcarnation-jpgFlower-jpgred-rose-flow-flowers-32600653-1536-1020.jpg", "http://the-bistro.dk/wp-content/uploads/2011/07/Bird-of-Paradise.jpg", "http://2.bp.blogspot.com/_SG-mtHOcpiQ/TNwNO1DBCcI/AAAAAAAAALw/7Hrg5FogwfU/s1600/birds-of-paradise.jpg", "http://wac.450f.edgecastcdn.net/80450F/screencrush.com/files/2013/01/get-back-to-portlandia-tout.jpg", "http://3.bp.blogspot.com/-bVeFyAAgBVQ/T80r3BSAVZI/AAAAAAAABmc/JYy8Hxgl8_Q/s1600/portlandia.jpg", "http://media.oregonlive.com/ent_impact_tvfilm/photo/portlandia-season2jpg-7d0c21a9cb904f54.jpg", "https://twimg0-a.akamaihd.net/profile_images/1776615163/PortlandiaTV_04.jpg", "http://getvideoartwork.com/gallery/main.php?g2_view=core.DownloadItem&g2_itemId=85796&g2_serialNumber=1", "http://static.tvtome.com/images/genie_images/story/2011_usa/p/portlandia_foodcarts.jpg", "http://imgc.classistatic.com/cps/poc/130104/376r1/8728dl1_27.jpeg", }; container.setUrls(testUrls, fScale * 10, 3); // pass in pixels for margin, rather than dips 

Before moving on to the decision ball, here is our simple StagScrollView subclass. His only special behavior is to tell his main child (our StagLayout ), which is the currently visible area, so that he can effectively use the smallest possible number of realized subviews.

 // StagScrollView StagLayout _frame; @Override protected void onFinishInflate() { super.onFinishInflate(); _frame = (StagLayout) findViewById(R.id.frame); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (oldh == 0) _frame.setVisibleArea(0, h); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); _frame.setVisibleArea(t, t + getHeight()); } 

Then the most important StagLayout class.

setUrls first sets our data structures.

 public void setUrls(String[] urls, float pxMargin, int cCols) { _pxMargin = pxMargin; _cCols = cCols; _cMaxCachedViews = 2 * cCols; _infos = new ArrayList<ImageInfo>(urls.length); // should be urls.length for (int i = 0; i < 200; i++) // should be urls.length IRL, but this is a quick way to get more images, by using repeats { final String sUrl = urls[i % urls.length]; // could just be urls[i] IRL _infos.add(new ImageInfo(sUrl, new OnClickListener() { @Override public void onClick(View v) { Log.d("StagLayout", String.format("Image clicked: url == %s", sUrl)); } })); } _activeInfos = new HashSet<ImageInfo>(_infos.size()); _cachedViews = new ArrayList<SmartImageView>(_cMaxCachedViews); requestLayout(); // perform initial layout } 

Our core structure ImageInfo This is a kind of lightweight placeholder that allows us to keep track of where each image will be displayed, when necessary. When we post our child views, we will use the information in ImageInfo to figure out where to place the actual view. A good way to think about ImageInfo is to "view a virtual image."

See comments for details.

 public class ImageInfo { private String _sUrl; // these rects are in float dips private RectF _rLoaded; // real size of the corresponding loaded SmartImageView private RectF _rDefault; // lame default rect in case we don't have anything better to go on private RectF _rLayout; // rect that our parent tells us to use -- this corresponds to a real View layout rect as specified when parent ViewGroup calls child.layout(l,t,r,b) private SmartImageView _vw; private View.OnClickListener _clickListener; public ImageInfo(String sUrl, View.OnClickListener clickListener) { _rDefault = new RectF(0, 0, 100, 100); _sUrl = sUrl; _rLayout = new RectF(); _clickListener = clickListener; } // Bounds will be called by the StagLayout when it is laying out views. // We want to return the most accurate bounds we can. public RectF bounds() { // if there is not yet a 'real' bounds (from a loaded SmartImageView), try to get one if (_rLoaded == null && _vw != null) { int h = _vw.getMeasuredHeight(); int w = _vw.getMeasuredWidth(); // if the SmartImageView thinks it knows how big it wants to be, then ok if (h > 0 && w > 0) { _rLoaded = new RectF(0, 0, w, h); } } if (_rLoaded != null) return _rLoaded; // if we have not yet gotten a real bounds from the SmartImageView, just use this lame rect return _rDefault; } // Reuse our layout rect -- this gets called a lot public void setLayoutBounds(float left, float top, float right, float bottom) { _rLayout.top = top; _rLayout.left = left; _rLayout.right = right; _rLayout.bottom = bottom; } public RectF layoutBounds() { return _rLayout; } public SmartImageView view() { return _vw; } // This is called during layout to attach or detach a real view public void setView(SmartImageView vw) { if (vw == null && _vw != null) { // if detaching, tell view it has no url, or handlers -- this prepares it for reuse or disposal _vw.setImage(null, (SmartImageTask.OnCompleteListener)null); _vw.setOnClickListener(null); } _vw = vw; if (_vw != null) { // We are attaching a view (new or re-used), so tell it its url and attach handlers. // We need to set this OnCompleteListener so we know when to ask the SmartImageView how big it really is _vw.setImageUrl(_sUrl, R.drawable.default_image, new SmartImageTask.OnCompleteListener() { final private View vw = _vw; @Override public void onComplete() { vw.measure(MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED)); int h = vw.getMeasuredHeight(); int w = vw.getMeasuredWidth(); _rLoaded = new RectF(0, 0, w, h); Log.d("ImageInfo", String.format("Settings loaded size onComplete %dx %d for %s", w, h, _sUrl)); } }); _vw.setOnClickListener(_clickListener); } } // Simple way to answer the question, "based on where I have laid you out, are you visible" public boolean overlaps(float top, float bottom) { if (_rLayout.bottom < top) return false; if (_rLayout.top > bottom) return false; return true; } } 

The rest of the magic happens in StagLayout's onMeasure and onLayout .

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); // Measure each real view that is currently realized. Initially there are none of these for (ImageInfo info : _activeInfos) { View v = info.view(); v.measure(MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED)); } // This arranges all of the imageinfos every time, and sets _maxBottom // computeImageInfo(width); setMeasuredDimension(width, (int)_maxBottom); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // This figures out what real SmartImageViews we need, creates new ones, re-uses old ones, etc. // After this call _activeInfos is correct -- the list of ImageInfos that are currently attached to real SmartImageViews setupSubviews(); for (ImageInfo info : _activeInfos) { // Note: The layoutBounds of each info is actually computed in onMeasure RectF rBounds = info.layoutBounds(); // Tell the real view where it should be info.view().layout((int)rBounds.left, (int)rBounds.top, (int)rBounds.right, (int)rBounds.bottom); } } 

Ok, now let's see how we actually organize all ImageInfos.

 private void computeImageInfo(float width) { float dxMargin = _pxMargin; float dyMargin = _pxMargin; float left = 0; float tops[] = new float[_cCols]; // start at 0 float widthCol = (int)((width - (_cCols + 1) * dxMargin) / _cCols); _maxBottom = 0; // layout the images -- set their layoutrect based on our current location and their bounds for (int i = 0; i < _infos.size(); i++) { int iCol = i % _cCols; // new row if (iCol == 0) { left = dxMargin; for (int j = 0; j < _cCols; j++) tops[j] += dyMargin; } ImageInfo info = _infos.get(i); RectF bounds = info.bounds(); float scale = widthCol / bounds.width(); // up or down, for now, it does not matter float layoutHeight = bounds.height() * scale; float top = tops[iCol]; float bottom = top + layoutHeight; info.setLayoutBounds(left, top, left + widthCol, bottom); if (bottom > _maxBottom) _maxBottom = bottom; left += widthCol + dxMargin; tops[iCol] += layoutHeight; } // TODO Optimization: build indexes of tops and bottoms // Exercise for reader _maxBottom += dyMargin; } 

And now let's see how we create, restore and delete real SmartImageViews during onLayout .

 private void setupSubviews() { // We need to compute new set of active views // TODO Optimize enumeration using indexes of tops and bottoms // NeededInfos will be set of currently visible ImageInfos HashSet<ImageInfo> neededInfos = new HashSet<ImageInfo>(_infos.size()); // NewInfos will be subset that are not currently assigned real views HashSet<ImageInfo> newInfos = new HashSet<ImageInfo>(_infos.size()); for (ImageInfo info : _infos) { if (info.overlaps(_viewportTop, _viewportBottom)) { neededInfos.add(info); if (info.view() == null) newInfos.add(info); } } // So now we have the active ones. Lets get any we need to deactivate. // Start with a copy of the _activeInfos from last time HashSet<ImageInfo> unneededInfos = new HashSet<ImageInfo>(_activeInfos); // And remove all the ones we need now, leaving ones we don't need any more unneededInfos.removeAll(neededInfos); // Detach all the views from these guys, and possibly reuse them ArrayList<SmartImageView> unneededViews = new ArrayList<SmartImageView>(unneededInfos.size()); for (ImageInfo info : unneededInfos) { SmartImageView vw = info.view(); unneededViews.add(vw); info.setView(null); // at this point view is still a child of parent } // So now we try to reuse the views, and create new ones if needed for (ImageInfo info : newInfos) { SmartImageView vw = null; if (unneededViews.size() > 0) { vw = unneededViews.remove(0); // grab one of these -- these are still children and so dont need to be added to parent } else if (_cachedViews.size() > 0) { vw = _cachedViews.remove(0); // else grab a cached one and re-add to parent addViewInLayout(vw, -1, new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } else { vw = new SmartImageView(getContext()); // create a whole new one FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); addViewInLayout(vw, -1, lp); // and add to parent } info.setView(vw); // info should also set its data } // At this point, detach any unneeded views and add to our cache, up to limit for (SmartImageView vw : unneededViews) { // tell view to cancel removeViewInLayout(vw); // always remove from parent if (_cachedViews.size() < _cMaxCachedViews) _cachedViews.add(vw); } // Record the active ones for next time around _activeInfos = neededInfos; } 

Remember that _viewportTop and _viewportBottom are set each time the user scrolls.

 // called on every scroll by parent StagScrollView public void setVisibleArea(int top, int bottom) { _viewportTop = top; _viewportBottom = bottom; //fixup views if (getWidth() == 0) // if we have never been measured, dont do this - it will happen in first layout shortly return; requestLayout(); } 
+1
source share

You can watch https://github.com/maurycyw/StaggeredGridView

I did not work with him personally, but you could at least steal some concepts.

+1
source share
  • Create a list view in the layout.
  • Create another layout with the same background as the background image of the list with three images (next to each other, that is, to the right of each other), with their horizontal properties set to Wrap_Content and all Views properties in which you can see images put in Wrap_Content.
  • Fill the layout in the getview () method of the listview adapter. In this case, you need to install 3 sets of images in the image views of an overpriced layout.

Hope this helps!

0
source share

I assume that it can be implemented with three independent list views, the only thing you need to do is to inflate the layout for the image and add it to the list.

use layout options during inflation.

Layout width: match_parent layout Height: wrap_content you can set the layout weight as .3 for all three list views with layout_width as 0dp and height as fill_parent.

hope this helps.

0
source share

Can't you use the current solution enclosed in a custom list?

in the getView method, inflate an existing solution for each row (by checking conversions in the talk mode), i.e. ScrollView with three vertical linears.

0
source share

Do you know why 3 List View was slow?

How many different sizes in each column? I think that in order to reprocess the views efficiently, you would like to create a view type for each image size, and then be sure to use getItemViewType to make sure you are processing the correct view. Otherwise, you will not get big benefits from processing. You would like to be able to just reset the image source.

0
source share

All Articles