如何创建一个闭合(环形)的ListView?

58
我想创建一个自定义的ListView(或类似的控件),使其具有循环功能,即:
  1. 向下滚动 - 当滚动到最后一个项目时,第一个项目开始显示(..,n-1,n,1,2,..)
  2. 向上滚动 - 当滚动到第一个项目时,最后一个项目开始显示(..,2,1,n,n-1,..)
这在概念上听起来很简单,但显然没有直接的方法可以实现。有人能指导我正确的解决方案吗? 谢谢!
我已经收到了一个答案(来自Android开发Google群组的Streets Of Boston),但它听起来有些丑陋 :)
  

我通过创建自己的列表适配器(从BaseAdapter子类化)来实现了这一点。

     

我以这样的方式编写了自己的列表适配器,使得它的getCount()方法返回一个非常大的数字。      

如果选择项目“x”,则此项目对应于adapter position ='adapter.getCount() / 2 + x'

     

对于我的适配器方法getItem(int position),我查看支持适配器的数组并提取索引处的项目:      (position-getCount()/ 2)%myDataItems.length

     

您需要执行一些更多的“特殊”操作才能使其正常工作,但是您已经有了思路。

     

原则上,仍然可以到达列表的末尾或开头,但是如果将getCount()设置为大约一百万左右,这很难做到:-)


如果您坚持使用AdapterView层次结构(例如ListView),那么波士顿先生的解决方案是唯一可用的选项。 - CommonsWare
我并不坚持任何类别。我提到ListView只是为了给出控件的行为和外观的想法,如果你愿意,也可以称其为“表格”。非常定制化的东西可能是一组形成列表的单元格,当其中一个单元格超出可见区域时 - 引擎将更新(我认为是从不同的线程)其位置(坐标)和内容(文本+图像)。但这个更新过程可能会影响滚动的平稳性。 - user281076
我应该在哪里写position = adapter.getcount() / 2 + x? - Diffy
5个回答

84

我和我的同事Joe认为我们已经找到了一种更简单的解决问题的方法。在我们的解决方案中,我们不是扩展BaseAdapter而是扩展ArrayAdapter。

代码如下:

public class CircularArrayAdapter< T > extends ArrayAdapter< T >
{   

        public static final int HALF_MAX_VALUE = Integer.MAX_VALUE/2;
        public final int MIDDLE;
        private T[] objects;

        public CircularArrayAdapter(Context context, int textViewResourceId, T[] objects)
        {
            super(context, textViewResourceId, objects);
            this.objects = objects;
            MIDDLE = HALF_MAX_VALUE - HALF_MAX_VALUE % objects.length;
        }

        @Override
        public int getCount()
        {
            return Integer.MAX_VALUE;
        }

        @Override
        public T getItem(int position) 
        {
            return objects[position % objects.length];
        }
 }

所以这里创建了一个名为CircularArrayAdapter的类,它接受一个对象类型T(可以是任何东西)并使用它创建一个数组列表。 T通常是字符串,但也可能是其他任何类型。
构造函数与ArrayAdapter相同,但初始化了一个称为middle的常量。这是列表的中间位置。无论数组的长度如何,MIDDLE都可以用来将ListView居中于列表的中央。
getCount()被重写以返回一个巨大的值,就像上面创建一个很大的列表一样。
getItem()被重写以返回数组上的虚假位置。因此,在填充列表时,列表将以循环方式填充对象。
此时,CircularArrayAdapter仅替换文件中的ArrayAdapter,从而创建ListView。
要使ListView居中,必须在初始化ListView对象后,在创建ListView的文件中插入以下行:
listViewObject.setSelectionFromTop(nameOfAdapterObject.MIDDLE, 0);

使用之前初始化的MIDDLE常量,将视图居中,并使列表的顶部项目位于屏幕顶部。

: ) ~ 祝好,希望这个解决方案有用。


这应该是解决方案。这非常简单,而且完美无缺地运作。谢谢。 - Tom
@Dawson:当滚动到顶部时,项目是否会更新(即从底部向上滚动)。再次到达第一个项目后,最后的项目会显示在顶部吗? - Surej
我刚刚发现了这个解决方案,坦白说,代码是多么简单,真是太棒了!赞! - Sreedevi J
@prodaea 他们自己管理。如果我的数组中有19个项目,并滚动到列表底部,因此我的索引为0,一切都很好。当我向下滚动一个额外的项目时,因此我的索引为-1,-1%19在Java中评估为18,这是我的列表中的最后一个元素。在循环列表中,最后一个元素在逻辑上位于第一个元素之前,因此我们没问题。考虑到这一点,MIDDLE应该设置为0,使其成为Integer.MIN_VALUE和Integer.MAX_VALUE之间的中点。不过,我很高兴你提出了这个问题,因为我之前没有考虑到这种情况。 - Dawson
我认为这不是一个好主意。使用适配器的好处在于只有在需要时才会在内存中创建元素,而在这里,您正在使用整个元素列表初始化适配器。那么这就不是一个适配器了吧?它只是一个管理器,在需要时传递数据。 - Viswanath Lekshmanan
显示剩余3条评论

13

你提到的解决方案是我过去告诉其他开发者使用的方法。在getCount()中,只需返回Integer.MAX_VALUE,它将给您约20亿个项目,这应该足够了。


感谢您的回答。 巨大数字解决方案 :) - user281076
@Romain Guy:使用 Hugh Number 解决方案会在尝试处理大量数据时出现性能问题。 - Surej
1
@Surej,我相信你是在暗示返回“巨大的数字”会影响性能。实际上不会;对源代码的简要审查表明,ListViews并不会仅基于这个数字分配任何对象。唯一我不确定的例外是使用布局模式LAYOUT_FORCE_BOTTOM,因为它从底部向上填充,但我没有详细研究它。 - Paul Lammertsma
3
从底部进行布局不会影响性能。 - Romain Guy
@RomainGuy 奇怪的 bug - Integer.MAX_VALUE 不起作用(getView 没有被调用),但是 Integer.MAX_VALUE - 2 如预期般工作。你有什么想法吗? - shem

9

根据上面的答案,我认为我已经做得很好了。

希望这能对你有所帮助。

private static class RecipeListAdapter extends BaseAdapter {
    private static LayoutInflater   mInflater;
    private Integer[]               mCouponImages;
    private static ImageView        viewHolder;
    public RecipeListAdapter(Context c, Integer[] coupomImages) {
        RecipeListAdapter.mInflater = LayoutInflater.from(c);
        this.mCouponImages = coupomImages;
    }
    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

    @Override
    public Object getItem(int position) {
       // you can do your own tricks here. to let it display the right item in your array.
        return position % mCouponImages.length;
    }

    @Override
    public long getItemId(int position) {
        return position;
        // return position % mCouponImages.length;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.coupon_list_item, null);
            viewHolder = (ImageView) convertView.findViewById(R.id.item_coupon);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ImageView) convertView.getTag();
        }

        viewHolder.setImageResource(this.mCouponImages[position %     mCouponImages.length]);
        return convertView;
    }

}

如果您想向下滚动列表,可以这样做。

通常我们只需向上或向下滚动列表。

//查看我们要滚动的项目数。在这种情况下,为Integer.MAX_VALUE。

int listViewLength = adapter.getCount();

// see how many items a screen can dispaly, I use variable "span"
    final int span = recipeListView.getLastVisiblePosition() - recipeListView.getFirstVisiblePosition();

// 查看我们有多少页

int howManySpans = listViewLength / span;

//查看您希望在何处开始列表视图。您不必执行“-3”操作。 这是为了使我的应用程序正常工作。

recipeListView.setSelection((span * (howManySpans / 2)) - 3);

1
如果使用LoadersCallbacks,我创建了MyCircularCursor类,它将典型的游标包装成这样:
@Override
public void onLoadFinished(Loader<Cursor> pCursorLoader, Cursor pCursor) {
        mItemListAdapter.swapCursor(new MyCircularCursor(pCursor));
}

装饰器类的代码如下:

class Decorator:

public class MyCircularCursor implements Cursor {

private Cursor mCursor;

public MyCircularCursor(Cursor pCursor) {
    mCursor = pCursor;
}

@Override
public int getCount() {
    return mCursor.getCount() == 0 ? 0 : Integer.MAX_VALUE;
}

@Override
public int getPosition() {
    return mCursor.getPosition();
}

@Override
public boolean move(int pOffset) {
    return mCursor.move(pOffset);
}

@Override
public boolean moveToPosition(int pPosition) {
    int position = MathUtils.mod(pPosition, mCursor.getCount());
    return mCursor.moveToPosition(position);
}

@Override
public boolean moveToFirst() {
    return mCursor.moveToFirst();
}

@Override
public boolean moveToLast() {
    return mCursor.moveToLast();
}

@Override
public boolean moveToNext() {
    if (mCursor.isLast()) {
        mCursor.moveToFirst();
        return true;
    } else {
        return mCursor.moveToNext();
    }
}

@Override
public boolean moveToPrevious() {
    if (mCursor.isFirst()) {
        mCursor.moveToLast();
        return true;
    } else {
        return mCursor.moveToPrevious();
    }
}

@Override
public boolean isFirst() {
    return false;
}

@Override
public boolean isLast() {
    return false;
}

@Override
public boolean isBeforeFirst() {
    return false;
}

@Override
public boolean isAfterLast() {
    return false;
}

@Override
public int getColumnIndex(String pColumnName) {
    return mCursor.getColumnIndex(pColumnName);
}

@Override
public int getColumnIndexOrThrow(String pColumnName) throws IllegalArgumentException {
    return mCursor.getColumnIndexOrThrow(pColumnName);
}

@Override
public String getColumnName(int pColumnIndex) {
    return mCursor.getColumnName(pColumnIndex);
}

@Override
public String[] getColumnNames() {
    return mCursor.getColumnNames();
}

@Override
public int getColumnCount() {
    return mCursor.getColumnCount();
}

@Override
public byte[] getBlob(int pColumnIndex) {
    return mCursor.getBlob(pColumnIndex);
}

@Override
public String getString(int pColumnIndex) {
    return mCursor.getString(pColumnIndex);
}

@Override
public short getShort(int pColumnIndex) {
    return mCursor.getShort(pColumnIndex);
}

@Override
public int getInt(int pColumnIndex) {
    return mCursor.getInt(pColumnIndex);
}

@Override
public long getLong(int pColumnIndex) {
    return mCursor.getLong(pColumnIndex);
}

@Override
public float getFloat(int pColumnIndex) {
    return mCursor.getFloat(pColumnIndex);
}

@Override
public double getDouble(int pColumnIndex) {
    return mCursor.getDouble(pColumnIndex);
}

@Override
public int getType(int pColumnIndex) {
    return 0;
}

@Override
public boolean isNull(int pColumnIndex) {
    return mCursor.isNull(pColumnIndex);
}

@Override
public void deactivate() {
    mCursor.deactivate();
}

@Override
@Deprecated
public boolean requery() {
    return mCursor.requery();
}

@Override
public void close() {
    mCursor.close();
}

@Override
public boolean isClosed() {
    return mCursor.isClosed();
}

@Override
public void registerContentObserver(ContentObserver pObserver) {
    mCursor.registerContentObserver(pObserver);
}

@Override
public void unregisterContentObserver(ContentObserver pObserver) {
    mCursor.unregisterContentObserver(pObserver);
}

@Override
public void registerDataSetObserver(DataSetObserver pObserver) {
    mCursor.registerDataSetObserver(pObserver);
}

@Override
public void unregisterDataSetObserver(DataSetObserver pObserver) {
    mCursor.unregisterDataSetObserver(pObserver);
}

@Override
public void setNotificationUri(ContentResolver pCr, Uri pUri) {
    mCursor.setNotificationUri(pCr, pUri);
}

@Override
public boolean getWantsAllOnMoveCalls() {
    return mCursor.getWantsAllOnMoveCalls();
}

@Override
public Bundle getExtras() {
    return mCursor.getExtras();
}

@Override
public Bundle respond(Bundle pExtras) {
    return mCursor.respond(pExtras);
}

@Override
public void copyStringToBuffer(int pColumnIndex, CharArrayBuffer pBuffer) {
    mCursor.copyStringToBuffer(pColumnIndex, pBuffer);
}
}

1

我看到了一些很好的答案,我的朋友尝试通过一个简单的解决方案来实现这个目标。请查看GitHub项目。


我们如何创建一个双向循环列表视图,在向上滚动时它应该显示最后一项。你能给我一些提示吗? - Dory

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接