实现具有独立行高的多列ListView

时间:2012-12-29 18:07:46

标签: android layout android-image

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

enter image description here

通常情况下,我会在ListView中使用适配器获得的性能,但由于我希望图像以列显示,并且具有不同的高度(参见图片Example),具体取决于图片,我不能为此目的使用单个列表视图。

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

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

我最终在ScrollView中使用了三个LinearLayouts,但这远非最佳。我宁愿使用适配器。

修改 我一直在看StaggeredGridView,如下面的响应中所示,但我发现它非常多。有没有更稳定的实现?

6 个答案:

答案 0 :(得分:1)

您可以查看https://github.com/maurycyw/StaggeredGridView

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

答案 1 :(得分:1)

我想我有一个可行的解决方案。

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

我基本上使用一组优秀的LoopJ SmartImageViews实现了所提到的github项目的简化等效项https://github.com/maurycyw/StaggeredGridView

我的解决方案并不像StaggeredGridView那样通用和灵活,但似乎运行良好,而且很快。功能上一个很大的区别是我们总是从左到右布局图像,然后再从左到右布局图像。我们不会尝试将下一个图像放在最短的列中。这使得视图的底部稍微不均匀,但在从网络初始加载期间产生的移动较少。

有三个主要类,一个自定义StagScrollView,其中包含一个自定义StagLayout(子类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 onMeasureonLayout

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

}

好的,现在让我们看看我们实际安排所有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;
}

现在让我们看看我们如何在SmartImageViews期间创建,重新使用和处置真实的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;

}

请记住,每次用户滚动时都会设置_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();
}

答案 2 :(得分:0)

  1. 在布局中创建列表视图。
  2. 创建另一个与列表视图背景布局相同背景的布局,其中三个图像视图(彼此相邻,即彼此相邻),其属性设置为水平Wrap_Content,以及整个视图属性,其中图像视图为放到Wrap_Content。
  3. 在listview适配器的getview()方法中扩充布局。在此,您需要在充气布局的图像视图中设置3组图像。
  4. 希望这有帮助!

答案 3 :(得分:0)

我想它可以用三个独立的列表视图来实现,只需要用它来扩展imageview的布局并将其添加到listview。

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

布局宽度:match_parent 布局高度:wrap_content 你可以为所有三个列表视图指定布局权重为.3,layout_width为0dp,高度为fill_parent。

希望这会有所帮助。

答案 4 :(得分:0)

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

每个行的getView方法中的

会膨胀你现有的解决方案(检查converview ofcourse) 即带有三个垂直LinearLayouts的ScrollView。

答案 5 :(得分:0)

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

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