Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>I think I have a working solution for you.</p> <p><em>The main files mentioned here are also on PasteBin at <a href="http://pastebin.com/u/morganbelford" rel="nofollow">http://pastebin.com/u/morganbelford</a></em></p> <p>I basically implemented a simplified equivalent of the github project mentioned, <a href="https://github.com/maurycyw/StaggeredGridView" rel="nofollow">https://github.com/maurycyw/StaggeredGridView</a>, using a set of excellent LoopJ <code>SmartImageViews</code>.</p> <p>My solution is not nearly as generic and flexible as the <code>StaggeredGridView</code>, 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.</p> <p>There are three main classes, a custom <code>StagScrollView</code>, which contains a custom <code>StagLayout</code> (subclassed <code>FrameLayout</code>), which manages a set of <code>ImageInfo</code> data objects.</p> <p>Here is our <strong>layout</strong>, stag_layout.xml (<em>the 1000dp initial height is irrelevant, since it will get recomputed in code based on the image sizes</em>):</p> <pre><code>// stag_layout.xml &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;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" &gt; &lt;com.morganbelford.stackoverflowtest.pinterest.StagLayout a:id="@+id/frame" a:layout_width="match_parent" a:layout_height="1000dp" a:background="@drawable/pinterest_bg" &gt; &lt;/com.morganbelford.stackoverflowtest.pinterest.StagLayout&gt; &lt;/com.morganbelford.stackoverflowtest.pinterest.StagScrollView&gt; </code></pre> <p>Here is our main <strong>Activity's</strong> <code>onCreate</code>, which uses the layout. The <code>StagActivity</code> just basically tells the <code>StagLayout</code> 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):</p> <pre><code>// 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&amp;g2_itemId=85796&amp;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 </code></pre> <p>Before we get to the meat of the solution, here is our simple <strong><code>StagScrollView</code></strong> subclass. His only special behavior is to tell his main child (our <code>StagLayout</code>) which the currently visible area is, so that he can efficiently use the smallest possible number of realized subviews.</p> <pre><code>// 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()); } </code></pre> <p>Here then is the most important class <strong><code>StagLayout</code></strong>.</p> <p>First, <code>setUrls</code> sets up our data structures.</p> <pre><code>public void setUrls(String[] urls, float pxMargin, int cCols) { _pxMargin = pxMargin; _cCols = cCols; _cMaxCachedViews = 2 * cCols; _infos = new ArrayList&lt;ImageInfo&gt;(urls.length); // should be urls.length for (int i = 0; i &lt; 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&lt;ImageInfo&gt;(_infos.size()); _cachedViews = new ArrayList&lt;SmartImageView&gt;(_cMaxCachedViews); requestLayout(); // perform initial layout } </code></pre> <p>Our <strong>main data structure</strong> is <code>ImageInfo</code>. 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".</p> <p><em>See comments inline for details.</em></p> <pre><code>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 &amp;&amp; _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 &gt; 0 &amp;&amp; w &gt; 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 &amp;&amp; _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 &lt; top) return false; if (_rLayout.top &gt; bottom) return false; return true; } } </code></pre> <p>The rest of the magic happens in <code>StagLayout's</code> <code>onMeasure</code> and <code>onLayout</code>.</p> <pre><code>@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); } } </code></pre> <p>Ok, now let's see how we <strong>actually arrange</strong> all the ImageInfos.</p> <pre><code>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 &lt; _infos.size(); i++) { int iCol = i % _cCols; // new row if (iCol == 0) { left = dxMargin; for (int j = 0; j &lt; _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 &gt; _maxBottom) _maxBottom = bottom; left += widthCol + dxMargin; tops[iCol] += layoutHeight; } // TODO Optimization: build indexes of tops and bottoms // Exercise for reader _maxBottom += dyMargin; } </code></pre> <p>And, now let's see how we create, resuse and dispose of real <code>SmartImageViews</code> during <code>onLayout</code>.</p> <pre><code>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&lt;ImageInfo&gt; neededInfos = new HashSet&lt;ImageInfo&gt;(_infos.size()); // NewInfos will be subset that are not currently assigned real views HashSet&lt;ImageInfo&gt; newInfos = new HashSet&lt;ImageInfo&gt;(_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&lt;ImageInfo&gt; unneededInfos = new HashSet&lt;ImageInfo&gt;(_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&lt;SmartImageView&gt; unneededViews = new ArrayList&lt;SmartImageView&gt;(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() &gt; 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() &gt; 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() &lt; _cMaxCachedViews) _cachedViews.add(vw); } // Record the active ones for next time around _activeInfos = neededInfos; } </code></pre> <p>Remember that _viewportTop and _viewportBottom are set every time the user scrolls.</p> <pre><code>// 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(); } </code></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload