实现具有独立行高的多列ListView [英] Implementing a multicolumn ListView with independent Row-heights

查看:84
本文介绍了实现具有独立行高的多列ListView的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想以拼贴"方式创建具有以下布局的大约200个ImageView(随机高度)的列表:

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

通常,我会在ListView中执行此操作,以获取使用适配器获得的性能,但由于我希望图像以不同的高度显示在列中(请参见图片

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:

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

我最终在ScrollView中使用了三个LinearLayouts,但这远非最佳选择.我宁愿将某些东西与适配器搭配使用.

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

编辑 我一直在看StaggeredGridView,如下面的响应所示,但是我发现它有很多问题.有没有更稳定的实现方式?

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?

推荐答案

我认为我为您提供了一个可行的解决方案.

I think I have a working solution for you.

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

我基本上实现了提到的github项目的简化等效项, https://github.com/maurycyw/StaggeredGridView ,使用一组出色的L​​oopJ SmartImageViews.

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

我的解决方案不像StaggeredGridView那样通用和灵活,但是似乎可以很好且快速地工作.功能上的一大不同是,我们总是始终从左到右布置图像,然后再从左到右布置图像.我们不会尝试将下一张图片放在最短的列中.这样会使视图的底部更加不均匀,但在从Web进行初始加载期间不会产生较大的移位.

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.

共有三个主要类,一个自定义StagScrollView,其中包含一个自定义StagLayout(子类为FrameLayout),该自定义StagLayout管理一组ImageInfo数据对象.

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

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

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>

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

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

在深入探讨解决方案之前,这里是我们简单的 StagScrollView 子类.他唯一的特殊行为是告诉主要孩子(我们的StagLayout)当前可见区域是哪个,以便他可以有效地使用最少数量的已实现子视图.

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

这是最重要的课程 StagLayout .

Here then is the most important class StagLayout.

首先,setUrls建立我们的数据结构.

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

}

我们的主要数据结构ImageInfo.它是一种轻量级的占位符,可让我们跟踪何时需要显示每个图像.当我们布置子视图时,我们将使用ImageInfo中的信息来找出放置实际视图的位置.考虑ImageInfo的一种好方法是将其视为虚拟图像视图".

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".

有关详细信息,请参阅嵌入式注释.

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中.

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

现在,让我们看看我们如何在onLayout期间创建,重新使用和处置实际的SmartImageViews.

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;

}

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

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

这篇关于实现具有独立行高的多列ListView的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆