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);
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;
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() {
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(); }