8

我想以“拼贴”方式创建一个包含以下布局的大约 200 个 ImageViews(随机高度)的列表:

在此处输入图像描述

通常我会在 ListView 中执行此操作以获得使用适配器获得的性能,但由于我希望图像按列显示,并且根据图片具有不同的高度(参见图片示例),我不能为此目的使用单个列表视图.

我尝试使用以下方法实现此布局:

  • 三个具有同步滚动的 ListView = 慢
  • 每行包含三个图像的单个 ListView = 不允许不同的高度
  • GridView = 不允许不同的高度
  • GridLayout = 难以以编程方式实现不同的高度。由于没有适配器,OutOfMemoryErrors 很常见
  • FlowLayout = 因为没有适配器,OutOfMemoryErrors 很常见
  • 具有三个 Vertical LinearLayouts 的 ScrollView = 迄今为止最好的解决方案,但 OutOfMemoryErrors 很常见

我最终在 ScrollView 中使用了三个 LinearLayout,但这远非最佳。我宁愿使用带有适配器的东西。

编辑 我一直在查看 StaggeredGridView,如下面的回复中所示,但我发现它非常有问题。有没有更稳定的实现?

4

6 回答 6

1

我想我有一个适合你的解决方案。

这里提到的主要文件也位于http://pastebin.com/u/morganbelford的 PasteBin 上

我基本上使用一组出色的 LoopJ实现了与提到的 github 项目https://github.com/maurycyw/StaggeredGridViewSmartImageViews的简化等效项。

我的解决方案不像StaggeredGridView. 功能上的一大区别是我们总是从左到右排列图像,然后再从左到右排列。我们不会尝试将下一张图片放在最短的列中。这使得视图的底部更加不均匀,但在从 web 初始加载期间产生的移动更少。

主要有三个类,一个 custom StagScrollView,其中包含一个 custom StagLayout(subclassed FrameLayout),它管理一组ImageInfo数据对象。

这是我们的布局,stag_layout.xml(1000dp 初始高度无关紧要,因为它将根据图像大小在代码中重新计算):

// 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>

这是我们的主要活动 onCreate,它使用布局。StagActivity只是基本上告诉使用什么StagLayout网址,每个图像之间的边距应该是多少,以及有多少列。为了更加模块化,我们可以将这些参数传递给 StagScrollView(其中包含 StagLayout,但滚动视图无论如何都必须将它们传递给布局):

// 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

在我们了解解决方案的实质之前,这是我们的简单StagScrollView子类。他唯一的特殊行为是告诉他的主要孩子(我们的StagLayout)当前可见区域是哪个,以便他可以有效地使用尽可能少的已实现子视图。

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

接下来就是最重要的课了StagLayout

首先,setUrls设置我们的数据结构。

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

}

我们的主要数据结构ImageInfo. 它是一种轻量级的占位符,允许我们跟踪每个图像将在什么时候显示在哪里。当我们布局子视图时,我们将使用 ImageInfo 中的信息来确定放置实际视图的位置。将 ImageInfo 视为“虚拟图像视图”的一个好方法。

有关详细信息,请参阅内联评论。

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;
}

}

其余的魔法发生在StagLayout's onMeasure和中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);    
    }

}

好的,现在让我们看看我们实际上是如何安排所有 ImageInfo 的。

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;
}

而且,现在让我们看看我们如何SmartImageViewsonLayout.

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;

}

请记住,每次用户滚动时都会设置 _viewportTop 和 _viewportBottom。

// 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();
}
于 2013-01-08T19:47:24.980 回答
1

你可以看看https://github.com/maurycyw/StaggeredGridView

我没有亲自使用它,但你至少可以窃取一些概念。

于 2012-12-29T18:38:25.283 回答
0

你知道为什么 3 List View 解决方案很慢吗?

每列有多少种不同的尺寸?我认为,为了有效地回收视图,您需要为每种尺寸的图像创建一个视图类型,然后确保使用getItemViewType,以确保您正在回收正确类型的视图。否则,您将不会从回收中获得太多好处。您可能希望能够仅重置图像视图的源。

于 2013-01-08T16:02:05.913 回答
0

您不能使用包含在自定义列表中的当前解决方案吗?

在每一行的 getView 方法中,膨胀您现有的解决方案(当然检查 converview),即具有三个 Vertical LinearLayouts 的 ScrollView。

于 2013-01-08T10:37:22.030 回答
0

我想它可以用三个独立的列表视图来实现,只有你必须这样做才能为 imageview 膨胀布局并将其添加到列表视图中。

在膨胀期间使用以下作为布局参数。

布局宽度:match_parent 布局高度:wrap_content 您可以为所有三个列表视图分配布局权重为 0.3,其中 layout_width 为 0dp,高度为 fill_parent。

希望这可以帮助。

于 2013-01-08T05:47:44.350 回答
0
  1. 在布局中创建列表视图。
  2. 创建另一个与列表视图背景布局具有相同背景的布局,其中包含三个图像视图(彼此相邻,即彼此右侧),它们的属性水平设置为 Wrap_Content,图像视图所在的整个视图属性设置为 Wrap_Content .
  3. 在 listview 适配器的 getview() 方法中膨胀布局。在此您需要在膨胀布局的图像视图中设置 3 组图像。

希望这可以帮助!

于 2013-01-05T08:07:35.123 回答