8

I would like to create a list of about 200 ImageViews (random heights) with the following layout in a 'collage' fashion:

enter image description here

Normally I would do this in a ListView for the peformance gained by using Adapters but since i want the images to be displayed in columns, and with different height (See picture Example ) depending on the pictures, I cannot use a single listview for this purpose.

I have tried implementing this layout with:

  • Three ListViews with synchronized scrolling = Slow
  • Single ListView with each row containing three images = Not allowing different heights
  • GridView = Not allowing different heights
  • GridLayout = Difficult to implement different heights programmatically. Because of no adapter, OutOfMemoryErrors are common
  • FlowLayout = Because of no adapter, OutOfMemoryErrors are common
  • ScrollView with three Vertical LinearLayouts = Best solution so far, but OutOfMemoryErrors are common

I have ended up using three LinearLayouts in a ScrollView, but this is far from optimal. I would rather use something with an Adapter.

EDIT I have been looking at the StaggeredGridView, as in a response below, but I find it quite buggy. Are there any implementations of this that are more stable?

Cœur
  • 37,241
  • 25
  • 195
  • 267
DagW
  • 955
  • 1
  • 15
  • 28
  • 1
    You're looking something like the Pinterest app. If you search you could find some questions related to this on stackoverflow. Also, I hoped you used some sort of image loader for the three `ListView` case. – user Dec 29 '12 at 18:16
  • Why not to use a "container" as `LinearLayout` with horizontal orientation and 3 `LinearLayout`s with vertical orientation and equal weight inside? Then populate the inner layouts with images. – Stan Dec 29 '12 at 18:53
  • Thats the solution i am using right now, it is not optimized like a listview. – DagW Dec 29 '12 at 18:54
  • How are you getting images into the image views? Are they resources, assets, or are you pulling them from the network? – Morgan Jan 04 '13 at 03:31
  • Hi! From the network. So I do now know the heights at creation. For this I am using LoopJ SmartImageView. – DagW Jan 04 '13 at 08:09
  • A big challenge I see with that is what if the user scrolls down quickly? To layout any given "viewport", you will need the heights of all the images in that viewport and above that viewport. And since you are using SmartImageView to load those on the fly, you will need to keep all the SmartImageViews in or above the current viewport "active". So if the user scrolls to the bottom, effectively all the image views will be in memory. Boom. – Morgan Jan 04 '13 at 22:15
  • 1
    Obviously, if there were a way for you to fetch all the sizes ahead of time, that would help a lot. If you can't do that (e.g., because all you are working with is a generic set of image urls that could be pointing anywhere), I would recommend thinking about download them yourself, measuring them, then storing the bits on disk. Then you could do your layout one time (determining the position of each image). If you can do that, I think I have a solution where you dynamically add and remove ImageViews as the user scrolls to keep memory pressure low(er). – Morgan Jan 04 '13 at 22:18
  • If you are still interested in this question, I think I have a solution for you. – Morgan Jan 07 '13 at 09:01
  • Hi Morgan! I would be intrested to see your solotion to the problem! – DagW Jan 07 '13 at 10:23
  • Id love to try the app when you have it done – Morgan Jan 09 '13 at 07:45

6 Answers6

1

You can have a look at https://github.com/maurycyw/StaggeredGridView

I have not worked with it personally, but you could atleast steal some concepts.

Abhinav Manchanda
  • 6,546
  • 3
  • 39
  • 46
1

I think I have a working solution for you.

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

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

My solution is not nearly as generic and flexible as the StaggeredGridView, but seems to work well, and quickly. One big difference functionally is that we layout the images always just left to right, then left to right again. We don't try to put the next image in the shortest column. This makes the bottom of the view a little more uneven, but generates less shifting around during initial load from the web.

There are three main classes, a custom StagScrollView, which contains a custom StagLayout (subclassed FrameLayout), which manages a set of ImageInfo data objects.

Here is our layout, stag_layout.xml (the 1000dp initial height is irrelevant, since it will get recomputed in code based on the image sizes):

// 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's onCreate, which uses the layout. The StagActivity just basically tells the StagLayout what urls to use, what the margin should be between each image, and how many columns there are. For more modularity, we could have passed these params to the StagScrollView (which contains the StagLayout, but the the scroll view would have just had to pass them down the layout anyway):

// 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 we get to the meat of the solution, here is our simple StagScrollView subclass. His only special behavior is to tell his main child (our StagLayout) which the currently visible area is, so that he can efficiently 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());
}

Here then is the most important class StagLayout.

First, setUrls sets up 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 main data structure is ImageInfo. It is a kind of lightweight placeholder that allows us to keep track of where each image is going to be displayed, when it needs to be. When we layout our child views, we will use the information in the ImageInfo to figure out where to put the actual view. A good way to think about ImageInfo is as a "virtual image view".

See comments inline 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's 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 %d x %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 arrange all the 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, resuse and dispose of 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 every 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();
}
Morgan
  • 814
  • 9
  • 10
  • Thanks! Ill try this out and try to get back to you before the bounty ends! – DagW Jan 08 '13 at 20:18
  • Could you post the entire class file on say PasteBin? – DagW Jan 08 '13 at 20:18
  • Just made all pastebin entries public. Sorry for delay. – Morgan Jan 08 '13 at 21:48
  • The whole project is not in github yet, but I can zip it up for you if you can't hook it all together from the PasteBin items. – Morgan Jan 08 '13 at 21:52
  • Thank you! That will not be a problem. – DagW Jan 09 '13 at 07:35
  • This solution is the best one so far, Items heights remain intact. I do have problems with OutOfMemory but Im finding this to be understandable enough to work with! Therefore i award you the bounty! Thanks! – DagW Jan 09 '13 at 10:12
  • Do upload it to GitHub and send me a message, maybe I can help improving it later on! – DagW Jan 09 '13 at 10:12
0
  1. Create a list view in a layout.
  2. Create another layout with same background as that of list view background layout with three Image Views (next to each other ie to the right of each other) with their properties set to Wrap_Content horizontally and the whole Views properties in which image views are put to Wrap_Content.
  3. Inflate the layout in the getview() method of listview adapter. In this you need to set 3 set of images in Image Views of the inflated Layout.

Hope this helps!

Guri Jagat
  • 51
  • 1
  • 8
  • Hi! I dont think this is a good solution, because the images would not be "staggered", each row would be as high as the largest image. – DagW Jan 07 '13 at 10:25
0

I guess it can be implemented with three independent list view, only thing which you have to do it to inflate layout for imageview and add it to listview.

use following as layout parameters during inflation.

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

hope this helps.

Techfist
  • 4,314
  • 6
  • 22
  • 32
0

Can't you use your current solution wrapped in a custom list ?

in getView method for each row inflate your existing solution (checking converview ofcourse) i.e. ScrollView with three Vertical LinearLayouts.

android2013
  • 415
  • 2
  • 5
0

Do you know why the 3 List View solution was slow?

How many different sizes are in each column? I think that for the recycling of views to be efficient, you would want to create a view type for each size of image, and then make sure that you use getItemViewType, to be sure that you're recycling the correct type of view. Otherwise, you will not get much benefit from the recycling. You would want to be able to just reset the source for the image view.

ndw
  • 513
  • 6
  • 14