什么是SortedList<T>在RecyclerView.Adapter中的作用?

72

Android Support Library 22.1已于昨日发布。v4和v7支持库中添加了许多新功能,其中android.support.v7.util.SortedList<T>引起了我的关注。

据说,SortedList是一种新的数据结构,可与RecyclerView.Adapter一起使用,保留了由RecyclerView提供的添加/删除/移动/更改动画。它听起来像是ListView中的List<T>,但似乎更先进和强大。

那么,SortedList<T>List<T>之间有什么区别?我应该如何有效地使用它?如果如此,SortedList<T>List<T>更强制执行吗?有人能贴一些示例吗?

欢迎任何提示或代码。谢谢。


2
它必须是一个基于泛型类型T的Comparable实现进行排序的列表 - 如果不是,那么谷歌在命名约定部门失去了一些分数。 - A Nice Guy
这是一个根据比较回调函数对元素进行排序的列表。因此,您可以添加元素,它们将出现在“正确”的位置(而不是在末尾)。请查看Javadoc。http://developer.android.com/reference/android/support/v7/util/SortedList.html - Thilo
1
最重要的是,如何与RecyclerView.Adapter一起使用?有人写过相关示例吗? - SilentKnight
4个回答

97

SortedList通过Callback处理与回收器适配器的通信。

SortedListList之间的一个区别可以在下面示例中的addAll帮助方法中看到。

public void addAll(List<Page> items) {
        mPages.beginBatchedUpdates();
        for (Page item : items) {
            mPages.add(item);
        }
        mPages.endBatchedUpdates();
    }
  1. 保留最新添加的项目

假设我有10个缓存项目需要在我的回收站列表填充时立即加载。同时,我查询网络以获取相同的10个项目,因为它们可能已经改变了。我可以调用相同的addAll方法,而SortedList会在幕后用获取的项目替换缓存项目(始终保留最新添加的项目)。

// After creating adapter
myAdapter.addAll(cachedItems)
// Network callback
myAdapter.addAll(fetchedItems)
在一个普通的 List 中,我将拥有所有项目的重复副本(列表大小为20)。而对于 SortedList,它使用回调函数的 areItemsTheSame 替换相同的项目。
  1. 它会智能地在适当的时候更新视图
当添加 fetchedItems 时,仅当一个或多个 Page 的标题更改时,onChange 才会被调用。您可以自定义 Callback 的 areContentsTheSame 以决定 SortedList 查找什么内容。
  1. 它是高效的

如果要向 SortedList 添加多个项,则 BatchedCallback 将把单个的 onInserted(index,1) 调用转换为一次 onInserted(index,N) 调用,如果项目添加到连续索引中。这种更改可以帮助 RecyclerView 更轻松地解析更改。

示例

您可以在适配器上设置一个 getter 用于您的 SortedList,但我决定向我的适配器添加 helper 方法。

适配器类:

  public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private SortedList<Page> mPages;

    public MyAdapter() {
        mPages = new SortedList<Page>(Page.class, new SortedList.Callback<Page>() {
            @Override
            public int compare(Page o1, Page o2) {
                return o1.getTitle().compareTo(o2.getTitle());
            }

            @Override
            public void onInserted(int position, int count) {
                notifyItemRangeInserted(position, count);
            }

            @Override
            public void onRemoved(int position, int count) {
                notifyItemRangeRemoved(position, count);
            }

            @Override
            public void onMoved(int fromPosition, int toPosition) {
                notifyItemMoved(fromPosition, toPosition);
            }

            @Override
            public void onChanged(int position, int count) {
                notifyItemRangeChanged(position, count);
            }

            @Override
            public boolean areContentsTheSame(Page oldItem, Page newItem) {
                // return whether the items' visual representations are the same or not.
                return oldItem.getTitle().equals(newItem.getTitle());
            }

            @Override
            public boolean areItemsTheSame(Page item1, Page item2) {
                return item1.getId() == item2.getId();
            }
        });

    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.viewholder_page, parent, false);
        return new PageViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        PageViewHolder pageViewHolder = (PageViewHolder) holder;
        Page page = mPages.get(position);
        pageViewHolder.textView.setText(page.getTitle());
    }

    @Override
    public int getItemCount() {
        return mPages.size();
    }

    // region PageList Helpers
    public Page get(int position) {
        return mPages.get(position);
    }

    public int add(Page item) {
        return mPages.add(item);
    }

    public int indexOf(Page item) {
        return mPages.indexOf(item);
    }

    public void updateItemAt(int index, Page item) {
        mPages.updateItemAt(index, item);
    }

    public void addAll(List<Page> items) {
        mPages.beginBatchedUpdates();
        for (Page item : items) {
            mPages.add(item);
        }
        mPages.endBatchedUpdates();
    }

    public void addAll(Page[] items) {
        addAll(Arrays.asList(items));
    }

    public boolean remove(Page item) {
        return mPages.remove(item);
    }

    public Page removeItemAt(int index) {
        return mPages.removeItemAt(index);
    }

    public void clear() {
       mPages.beginBatchedUpdates();
       //remove items at end, to avoid unnecessary array shifting
       while (mPages.size() > 0) {
          mPages.removeItemAt(mPages.size() - 1);
       }
       mPages.endBatchedUpdates();
    }
}

页面类:

public class Page {
    private String title;
    private long id;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}

视图持有者 XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/text_view"
        style="@style/TextStyle.Primary.SingleLine"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

视图持有者类:

public class PageViewHolder extends RecyclerView.ViewHolder {
    public TextView textView;


    public PageViewHolder(View itemView) {
        super(itemView);
        textView = (TextView)item.findViewById(R.id.text_view);
    }
}

1
RecyclerView.Adapter的字段中使用SortedList是最好的方式吗? - SilentKnight
19
请注意,有一个名为 SortedListAdapterCallback 的类,它以 RecyclerView.Adapter 作为构造函数参数,可以处理 onInserted() 等方法的实现细节。 - CommonsWare
3
实现是错误的,它违反了 equals() 和 compareTo() 之间的约定。在比较标题之前,您必须在 compareTo() 中检查元素是否相等并返回0。否则,将会出现重复的情况,因为 SortedList 依赖于该约定。 - Paul Woitaschek
如果用户想通过设置动态更改排序顺序,该怎么做?您如何在适配器中实现动态更改呢? - Raghunandan
3
如果在更新物品时,有新的、已更新的和已删除的物品,您该如何处理?.addAll无法帮助,因为它不会删除现在已删除的物品,而.clear()会“重置”列表(我需要确认这一点)。通过封装一个.remove()循环和一个.addAll().beginBatchUpdate是否有所帮助? - David Corsalini
显示剩余4条评论

14
SortedList是在v7 support library中的实现。

SortedList是一种实现可以保持项目顺序并通知列表更改以便可以将其绑定到RecyclerView.Adapter的方式。

它使用compare(Object, Object)方法来保持项目的顺序,并使用二进制搜索来检索项目。如果您的项目排序标准可能发生变化,请确保在编辑它们时调用适当的方法,以避免数据不一致性。

通过SortedList.Callback参数,您可以控制项目的顺序和更改通知。

下面是一个SortedList的使用示例,我想这就是你想要的,看看它并享受吧!

public class SortedListActivity extends ActionBarActivity {
    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLinearLayoutManager;
    private SortedListAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sorted_list_activity);
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mLinearLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLinearLayoutManager);
        mAdapter = new SortedListAdapter(getLayoutInflater(),
                new Item("buy milk"), new Item("wash the car"),
                new Item("wash the dishes"));
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setHasFixedSize(true);
        final EditText newItemTextView = (EditText) findViewById(R.id.new_item_text_view);
        newItemTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                if (id == EditorInfo.IME_ACTION_DONE &&
                        (keyEvent == null || keyEvent.getAction() == KeyEvent.ACTION_DOWN)) {
                    final String text = textView.getText().toString().trim();
                    if (text.length() > 0) {
                        mAdapter.addItem(new Item(text));
                    }
                    textView.setText("");
                    return true;
                }
                return false;
            }
        });
    }

    private static class SortedListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
        SortedList<Item> mData;
        final LayoutInflater mLayoutInflater;
        public SortedListAdapter(LayoutInflater layoutInflater, Item... items) {
            mLayoutInflater = layoutInflater;
            mData = new SortedList<Item>(Item.class, new SortedListAdapterCallback<Item>(this) {
                @Override
                public int compare(Item t0, Item t1) {
                    if (t0.mIsDone != t1.mIsDone) {
                        return t0.mIsDone ? 1 : -1;
                    }
                    int txtComp = t0.mText.compareTo(t1.mText);
                    if (txtComp != 0) {
                        return txtComp;
                    }
                    if (t0.id < t1.id) {
                        return -1;
                    } else if (t0.id > t1.id) {
                        return 1;
                    }
                    return 0;
                }

                @Override
                public boolean areContentsTheSame(Item oldItem,
                        Item newItem) {
                    return oldItem.mText.equals(newItem.mText);
                }

                @Override
                public boolean areItemsTheSame(Item item1, Item item2) {
                    return item1.id == item2.id;
                }
            });
            for (Item item : items) {
                mData.add(item);
            }
        }

        public void addItem(Item item) {
            mData.add(item);
        }

        @Override
        public TodoViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
            return new TodoViewHolder (
                    mLayoutInflater.inflate(R.layout.sorted_list_item_view, parent, false)) {
                @Override
                void onDoneChanged(boolean isDone) {
                    int adapterPosition = getAdapterPosition();
                    if (adapterPosition == RecyclerView.NO_POSITION) {
                        return;
                    }
                    mBoundItem.mIsDone = isDone;
                    mData.recalculatePositionOfItemAt(adapterPosition);
                }
            };
        }

        @Override
        public void onBindViewHolder(TodoViewHolder holder, int position) {
            holder.bindTo(mData.get(position));
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }
    }

    abstract private static class TodoViewHolder extends RecyclerView.ViewHolder {
        final CheckBox mCheckBox;
        Item mBoundItem;
        public TodoViewHolder(View itemView) {
            super(itemView);
            mCheckBox = (CheckBox) itemView;
            mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (mBoundItem != null && isChecked != mBoundItem.mIsDone) {
                        onDoneChanged(isChecked);
                    }
                }
            });
        }

        public void bindTo(Item item) {
            mBoundItem = item;
            mCheckBox.setText(item.mText);
            mCheckBox.setChecked(item.mIsDone);
        }

        abstract void onDoneChanged(boolean isChecked);
    }

    private static class Item {
        String mText;
        boolean mIsDone = false;
        final public int id;
        private static int idCounter = 0;

        public Item(String text) {
            id = idCounter ++;
            this.mText = text;
        }
    }
}

抱歉几个月后才评论,但是我是唯一一个在添加几个项目时遇到严重问题并导致回收的人吗?如果我添加了12个项目,它就会变得疯狂,并开始触发一些动作,此外,它还会删除所有先前输入的数据... - fiipi
抱歉,我指的是示例代码,让我稍微解释一下。也许最好单独提出一个问题。 - fiipi
简而言之:在构建你的SortedList<>时,请使用 SortedListAdapterCallback - lionello

5
在支持库源代码仓库中有一个示例SortedListActivity,演示如何在RecyclerView.Adapter内使用SortedList和SortedListAdapterCallback。从SDK的根目录开始,在安装了支持库的情况下,它应该在extras/android/support/samples/Support7Demos/src/com/example/android/supportv7/util/SortedListActivity.java(也可在github上找到)。
这些特定示例的存在只在Google的文档中提到一次,位于处理不同主题的页面底部,所以我不怪你没有找到它。

2
死链。你知道它现在在哪里吗? - MidasLefko
https://android.googlesource.com/platform/development/+/52e76ba/samples/Support7Demos/src/com/example/android/supportv7/util/SortedListActivity.java - Gabor

2
关于 SortedList 的实现,它是由一个默认最小容量为 10 个条目的 <T> 数组支持的。一旦数组已满,数组将被调整大小至 size() + 10
源代码可在此处找到:https://github.com/android/platform_frameworks_support/blob/master/v7/recyclerview/src/android/support/v7/util/SortedList.java文档 中了解:

排序列表实现,可以保持项目顺序并通知列表中的更改,以便将其绑定到 RecyclerView.Adapter。

它使用 compare(Object, Object) 方法保持项目有序,并使用二进制搜索检索项目。如果您的项目排序标准可能会更改,请确保在编辑它们时调用适当的方法,以避免数据不一致。

您可以通过 SortedList.Callback 参数控制项目的顺序和更改通知。

关于性能,他们还添加了 SortedList.BatchedCallback,以执行多个操作而不是一次执行一个操作。

可以批处理由 SortedList 发送的通知事件的回调实现。

如果您想在 SortedList 上执行多个操作,但不想逐个分发每个事件,则此类可能很有用,这可能导致性能问题。

例如,如果要将多个项目添加到 SortedList 中,BatchedCallback 调用会将单个 onInserted(index, 1) 调用转换为一个 onInserted(index, N),如果连续添加到索引中。此更改可以帮助 RecyclerView 更轻松地解决更改。

如果 SortedList 中的连续更改不适合批处理,则 BatchingCallback 将在检测到此类情况时立即分派它们。完成 SortedList 上的编辑后,必须始终调用 dispatchLastEvent() 来将所有更改刷新到 Callback。


讲解得很好。提供示例会更好。 - SilentKnight
目前我没有时间来准备一个样例,建议阅读文档并深入了解。 - Axxiss

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