私はあなたのために実用的な解決策を持っていると思います。
ここで言及されている主なファイルは、http://pastebin.com/u/morganbelfordのPasteBinにもあります。
私は基本的に、優れたLoopJのセットを使用して、前述のgithubプロジェクトに相当する簡略化されたhttps://github.com/maurycyw/StaggeredGridViewSmartImageViews
を実装しました。
私のソリューションは、ほど一般的で柔軟性がありませんが、StaggeredGridView
うまく機能しているようです。機能上の大きな違いの1つは、画像を常に左から右にレイアウトしてから、再び左から右にレイアウトすることです。次の画像を最短の列に配置しようとはしません。これにより、ビューの下部が少し不均一になりますが、Webからの初期ロード中に発生するシフトは少なくなります。
3つの主要なクラスがあります。カスタムは、データオブジェクトのセットを管理する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
使用するURL、各画像間のマージン、および列の数を示します。モジュール性を高めるために、これらのパラメーターをStagScrollViewに渡すこともできます(StagLayoutが含まれていますが、スクロールビューはとにかくレイアウトに渡す必要があります)。StagActivity
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;
}
そして、今、私たちがどのように本物を作成し、再利用し、処分するかを見てみましょ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();
}