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

通常,我会在ListView中执行此操作以获取使用适配器获得的性能,但是由于我希望图像以列显示,并且根据图片的高度不同(请参见picture Example),因此我不能为此使用单个listview 。

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

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

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

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

    最佳答案

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

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

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

    我的解决方案不像StaggeredGridView那样通用和灵活,但是似乎运行良好且快速。功能上的一大不同是,我们总是始终从左到右布置图像,然后再从左到右布置图像。我们不会尝试将下一张图片放在最短的列中。这使 View 的底部更加不均匀,但在从腹板初始加载期间产生的偏移较小。

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

    这是我们的布局,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>
    

    这是我们主要 Activity 的 StagActivity,它使用布局。 StagLayout基本上只是告诉StagScrollView使用哪些网址,每个图像之间的边距应有多少以及有多少列。为了获得更多的模块化,我们可以将这些参数传递给StagScrollView(包含StagLayout,但是滚动 View 无论如何都必须将它们传递给布局):
    // 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
    

    在开始解决方案之前,这里是我们简单的 StagLayout 子类。他唯一的特殊行为是告诉他的主要 child (我们的StagLayout)当前可见的区域是哪个,以便他可以有效地使用最少数量的已实现 subview 。
    // 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());
    }
    

    然后是最重要的类 setUrls

    首先,ImageInfo设置我们的数据结构。
    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
    
    }
    

    我们的主要数据结构StagLayout's。它是一种轻量级的占位符,可让我们跟踪何时需要显示每个图像。当我们布置 subview 时,我们将使用ImageInfo中的信息来找出放置实际 View 的位置。考虑ImageInfo的一个好方法是“虚拟 ImageView ”。

    有关详细信息,请参见内联注释。
    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;
    }
    
    }
    

    其余的魔术发生在onMeasure onLayoutSmartImageViews中。
    @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;
    }
    

    并且,现在让我们看看在onLayout期间如何创建,重新使用和处置实际的ojit_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<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();
    }
    

    09-15 14:37