Nobody knows the power of RecyclerView, it encapsulates ViewHolder, easy for us to recycle; Work with LayoutManager, ItemDecoration and ItemAnimator to make lists easier. Of course, there may be some “leftover beads” that you don’t know much about. Here are some of them.

1.SortedList

Sorted lists, as the name suggests, are suitable for scenarios where lists are ordered and not repeated. And SortedList will help you compare data differences and refresh data in a targeted way. Rather than simply notifyDataSetChanged().

I thought of a scene where on the page of city selection, we all had to sort by alphabetic initials. Let’s do that using SortedList.

City objects:

public class City { private int id; private String cityName; private String firstLetter; public City(int id, String cityName, String firstLetter) { this.id = id; this.cityName = cityName; this.firstLetter = firstLetter; }}Copy the code

Create SortedListAdapterCallback implementation class SortedListCallback, SortedListCallback defines how to sort and how to determine the item.

public class SortedListCallback extends SortedListAdapterCallback<City> { public SortedListCallback(RecyclerView.Adapter  adapter) { super(adapter); } @override public int compare(City o1, City o1); City o2) { return o1.getFirstLetter().compareTo(o2.getFirstLetter()); } /** * is used to determine whether two objects are the same Item. */ @Override public boolean areItemsTheSame(City item1, City item2) { return item1.getId() == item2.getId(); } /** * is used to determine whether two objects are items of content. */ @Override public boolean areContentsTheSame(City oldItem, City newItem) { if (oldItem.getId() ! = newItem.getId()) { return false; } return oldItem.getCityName().equals(newItem.getCityName()); }}Copy the code

Adapter part

Public class SortedAdapter extends RecyclerView.Adapter< sortedAdapter. ViewHolder> {// Data sources use SortedList private SortedList<City> mSortedList; private LayoutInflater mInflater; public SortedAdapter(Context mContext) { mInflater = LayoutInflater.from(mContext); } public void setSortedList(SortedList<City> mSortedList) { this.mSortedList = mSortedList; * * *} / batch updates, for example: * < pre > * mSortedList beginBatchedUpdates (); * try { * mSortedList.add(item1) * mSortedList.add(item2) * mSortedList.remove(item3) * ... * } finally { * mSortedList.endBatchedUpdates(); * } * </pre> * */ public void setData(List<City> mData){ mSortedList.beginBatchedUpdates(); mSortedList.addAll(mData); mSortedList.endBatchedUpdates(); } public void removeData(int index){ mSortedList.removeItemAt(index); } public void clear(){ mSortedList.clear(); } @Override @NonNull public SortedAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false)); } @Override public void onBindViewHolder(@NonNull SortedAdapter.ViewHolder holder, final int position) { ... } @Override public int getItemCount() { return mSortedList.size(); }... }Copy the code

Use part:

public class SortedListActivity extends AppCompatActivity { private SortedAdapter mSortedAdapter; private int count = 10; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sorted_list); RecyclerView mRecyclerView = findViewById(R.id.rv); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mSortedAdapter = new SortedAdapter(this); SortedList initializes SortedListCallback mSortedListCallback = new SortedListCallback(mSortedAdapter); SortedList mSortedList = new SortedList<>(City.class, mSortedListCallback); mSortedAdapter.setSortedList(mSortedList); mRecyclerView.setAdapter(mSortedAdapter); updateData(); } private void addData() {msortedData.setData (new City(count, "City" + count, "c")); count ++; } private List<City> mList = new ArrayList(); private void updateData() { mList.clear(); Mlist. add(new City(0, "Beijing ", "b")); mlist. add(new City(0," Beijing ", "b")); Mlist. add(new City(1, "Shanghai ", "s")); Mlist. add(new City(2, "gZ ", "g")); Mlist.add (new City(3, "shenzhen ", "s")); Mlist. add(new City(4, "hangzhou ", "h")); Mlist. add(new City(5, "xian ", "x")); mlist. add(new City(5," Xian ", "x")); Mlist.add (new City(6, "chengdu ", "c")); mlist.add (new City(6," Chengdu ", "c")); Mlist. add(new City(7, "wuhan ", "w")); Mlist.add (new City(8, "nanjing ", "n")); mlist.add (new City(8," nanjing ", "n")); Mlist.add (new City(9, "chongqing ", "c")); mlist.add (new City(9," Chongqing ", "c")); mSortedAdapter.setData(mList); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true; } private Random mRandom = new Random(); @Override public boolean onOptionsItemSelected(MenuItem item) { int i = item.getItemId(); if (i == R.id.menu_add) { addData(); } else if (I == r.i.menu_update) {// Delete updateData(); } else if (I = = R.i d.m enu_delete) {/ / deleted an if (mSortedAdapter. GetItemCount () > 0) { mSortedAdapter.removeData(mRandom.nextInt(mSortedAdapter.getItemCount())); } }else if (i == R.id.menu_clear){ mSortedAdapter.clear(); } return true; }}Copy the code

It is still very simple to use, take a look at the effect picture:

As you can see, every time I add a piece of c data, it will automatically sort it for me and refresh the list. The system automatically deduplicates data when modifying it. It’s so much more elegant than violent.

2. AsyncListUtil

AsyncListUtil exists at support-V7:23. It is a tool for asynchronously loading data, it is generally used to load database data, we do not need to query the cursor on the UI thread, and it can keep the UI and cache in sync, and only hold a limited amount of data in memory at all times. Use it for a better user experience.

Note that this class uses a single thread to load data, so it is suitable for loading data from disk, database, and not from the network.

AsyncListUtil is implemented as follows:

Public class MyAsyncListUtil extends AsyncListUtil<TestBean> {/** * */ private static final int TILE_SIZE = 20; public MyAsyncListUtil(RecyclerView mRecyclerView) { super(TestBean.class, TILE_SIZE, new MyDataCallback(), new MyViewCallback(mRecyclerView)); mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // Update current visible range data onRangeChanged(); }}); Public static class MyDataCallback extends DataCallback<TestBean>{/** * total number of data */ @override public int  refreshData() { return 200; } /** * populates data (background thread), */ @override public void fillData(@nonnull TestBean[] data, int startPosition, int itemCount) { for (int i = 0; i < itemCount; i++) { TestBean item = data[i]; If (item == null) {item = new TestBean(startPosition, "item:" + (startPosition + I)); data[i] = item; }} thread.sleep (500); } catch (InterruptedException e) { e.printStackTrace(); }} /** * Public static class MyViewCallback extends ViewCallback {private RecyclerView mRecyclerView; public MyViewCallback(RecyclerView mRecyclerView) { this.mRecyclerView = mRecyclerView; } / display data range * / * * * @ Override public void getItemRangeInto (@ NonNull int [] outRange) {RecyclerView. LayoutManager manager = mRecyclerView.getLayoutManager(); LinearLayoutManager mgr = (LinearLayoutManager) manager; outRange[0] = mgr.findFirstVisibleItemPosition(); outRange[1] = mgr.findLastVisibleItemPosition(); } /** * Override public void onDataRefresh() {mrecyclerView.getAdapter ().notifydatasetChanged (); } /** * Override public void onItemLoaded(int position) { mRecyclerView.getAdapter().notifyItemChanged(position); }}}Copy the code

Adapter:

public class AsyncListUtilAdapter extends RecyclerView.Adapter<AsyncListUtilAdapter.ViewHolder> { private MyAsyncListUtil mMyAsyncListUtil; public AsyncListUtilAdapter(Context mContext, MyAsyncListUtil mMyAsyncListUtil) { this.mMyAsyncListUtil = mMyAsyncListUtil; } @Override public int getItemCount() { return mMyAsyncListUtil.getItemCount(); } @Override public void onBindViewHolder(@NonNull AsyncListUtilAdapter.ViewHolder holder, final int position) { TestBean bean = mMyAsyncListUtil.getItem(position); // It is possible to get empty, which can be displayed while loading, waiting for synchronized data. If (bean == null){holder.mtvname.settext (" Loading..." ); }else { holder.mTvName.setText(bean.getName()); }}... }Copy the code

Note is still very clear, directly on the effect:

3.AsyncListDiffer

Although SortedList and AsyncListUtil are very convenient, most lists do not require sorting and loading local data, and most of them get network data display. At this point DiffUtil can be used. DiffUtil, a new utility class in support-V7:24.2.0, is used to compare old and new data sets, find minimal changes, and flush lists in a targeted way. Introduction of DiffUtil a long time ago in Zhang Xutong’s [Android] RecyclerView’s good companion: a detailed explanation of DiffUtil blog has been introduced in detail, I will not repeat here.

CalculateDiff (mDiffCallback) is a time-consuming operation, which needs to be processed by the sub-thread and finally refreshed in the main thread. To facilitate this, a DiffUtil wrapper class was added in support-V7:27.1.0, called AsyncListDiffer.

First of all, the effect map, a simple list display, at the same time add, delete, change operations.

I use AsyncListDiffer to achieve this effect. ItemCallback implements DiffUtil.itemCallback, similar to SortedList, and rules how to differentiate data. The use here is almost the same as DiffUtil, except that it implements DiffUtil.Callback.

Public class myUtilitemCallback extends DiffUtil.ItemCallback<TestBean> {/** * whether the same object */ @override public Boolean  areItemsTheSame(@NonNull TestBean oldItem, @NonNull TestBean newItem) { return oldItem.getId() == newItem.getId(); } @override public Boolean areContentsTheSame(@nonnull TestBean oldItem, @NonNull TestBean newItem) { return oldItem.getName().equals(newItem.getName()); } /** * areItemsTheSame() returns true and areContentsTheSame() returns false, that is, the two objects represent the same data, but the content is updated. This method is used for directional flushing and is optional. */ @Nullable @Override public Object getChangePayload(@NonNull TestBean oldItem, @NonNull TestBean newItem) { Bundle payload = new Bundle(); if (! oldItem.getName().equals(newItem.getName())) { payload.putString("KEY_NAME", newItem.getName()); } if (payload-size () == 0){// Return null if there is no change; } return payload; }}Copy the code

Adapter part has two ways to achieve, one is to achieve RecyclerView.Adapter,

public class AsyncListDifferAdapter extends RecyclerView.Adapter<AsyncListDifferAdapter.ViewHolder> { private LayoutInflater mInflater; // Data operations are implemented by AsyncListDiffer private AsyncListDiffer<TestBean> mDiffer; Public AsyncListDifferAdapter(Context mContext) {// Initialize AsyncListDiffe mDiffer = new AsyncListDiffer<>(this, new MyDiffUtilItemCallback()); mInflater = LayoutInflater.from(mContext); } public void setData(TestBean mData){ List<TestBean> mList = new ArrayList<>(); mList.addAll(mDiffer.getCurrentList()); mList.add(mData); mDiffer.submitList(mList); } public void setData(List<TestBean> mData){// Since DiffUtil is used to compare old and new data, we need to create a new collection to store the new data. // In practice, new data is retrieved each time, so this step is not required. List<TestBean> mList = new ArrayList<>(); mList.addAll(mData); mDiffer.submitList(mList); } public void removeData(int index){ List<TestBean> mList = new ArrayList<>(); mList.addAll(mDiffer.getCurrentList()); mList.remove(index); mDiffer.submitList(mList); } public void clear(){ mDiffer.submitList(null); } @Override @NonNull public AsyncListDifferAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) { if (payloads.isEmpty()) { onBindViewHolder(holder, position); } else { Bundle bundle = (Bundle) payloads.get(0); holder.mTvName.setText(bundle.getString("KEY_NAME")); } } @Override public void onBindViewHolder(@NonNull AsyncListDifferAdapter.ViewHolder holder, final int position) { TestBean bean = mDiffer.getCurrentList().get(position); holder.mTvName.setText(bean.getName()); } @Override public int getItemCount() { return mDiffer.getCurrentList().size(); } static class ViewHolder extends RecyclerView.ViewHolder { ...... }}Copy the code

Another way to write Adapter is to implement ListAdapter, which internally implements the initialization of getItemCount(), getItem(), and AsyncListDiffer.

The source code is as follows, very simple:

public abstract class ListAdapter<T, VH extends ViewHolder> extends Adapter<VH> { private final AsyncListDiffer<T> mHelper; protected ListAdapter(@NonNull ItemCallback<T> diffCallback) { this.mHelper = new AsyncListDiffer(new AdapterListUpdateCallback(this), (new Builder(diffCallback)).build()); } protected ListAdapter(@NonNull AsyncDifferConfig<T> config) { this.mHelper = new AsyncListDiffer(new AdapterListUpdateCallback(this), config); } public void submitList(@Nullable List<T> list) { this.mHelper.submitList(list); } protected T getItem(int position) { return this.mHelper.getCurrentList().get(position); } public int getItemCount() { return this.mHelper.getCurrentList().size(); }}Copy the code

One drawback, however, is that the getCurrentList() method is not provided to get the current collection directly. So you need to maintain a collection yourself. I hope I can add it later. So I don’t recommend it at this point… But we can do the encapsulation.

public class MyListAdapter extends ListAdapter<TestBean, MyListAdapter.ViewHolder> { private LayoutInflater mInflater; Private List<TestBean> mData = new ArrayList<>(); public MyListAdapter(Context mContext) { super(new MyDiffUtilItemCallback()); mInflater = LayoutInflater.from(mContext); } public void setData(TestBean testBean){ mData.add(testBean); List<TestBean> mList = new ArrayList<>(); mList.addAll(mData); // Submit a new dataset submitList(mList); } public void setData(List<TestBean> list){ mData.clear(); mData.addAll(list); List<TestBean> mList = new ArrayList<>(); mList.addAll(mData); submitList(mList); } public void removeData(int index){ mData.remove(index); List<TestBean> mList = new ArrayList<>(); mList.addAll(mData); submitList(mList); } public void clear(){ mData.clear(); submitList(null); } @Override @NonNull public MyListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(R.layout.item_test, parent, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) { if (payloads.isEmpty()) { onBindViewHolder(holder, position); } else { Bundle bundle = (Bundle) payloads.get(0); holder.mTvName.setText(bundle.getString("KEY_NAME")); } } @Override public void onBindViewHolder(@NonNull MyListAdapter.ViewHolder holder, final int position) { TestBean bean = getItem(position); holder.mTvName.setText(bean.getName()); } static class ViewHolder extends RecyclerView.ViewHolder { ...... }}Copy the code

Finally, the Activity:

public class AsyncListDifferActivity extends AppCompatActivity {

    private AsyncListDifferAdapter mAsyncListDifferAdapter;
    private int count = 10;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sorted_list);
        RecyclerView mRecyclerView = findViewById(R.id.rv);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mAsyncListDifferAdapter = new AsyncListDifferAdapter(this);
        mRecyclerView.setAdapter(mAsyncListDifferAdapter);
        initData();
    }

    private void addData() {
        mAsyncListDifferAdapter.setData(new TestBean(count, "Item " + count));
        count ++;
    }

    private List<TestBean> mList = new ArrayList();

    private void initData() {
        mList.clear();
        for (int i = 0; i < 10; i++){
            mList.add(new TestBean(i, "Item " + i));
        }
        mAsyncListDifferAdapter.setData(mList);
    }

    private void updateData() {
        mList.clear();
        for (int i = 9; i >= 0; i--){
            mList.add(new TestBean(i, "Item " + i));
        }
        mAsyncListDifferAdapter.setData(mList);
    }
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }
    
    private Random mRandom = new Random();

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int i = item.getItemId();
        if (i == R.id.menu_add) {
            addData();
        } else if (i == R.id.menu_update) {
            updateData();
        } else if (i == R.id.menu_delete) {
            if (mAsyncListDifferAdapter.getItemCount() > 0){
                mAsyncListDifferAdapter.removeData(mRandom.nextInt(mAsyncListDifferAdapter.getItemCount()));
            }
        }else if (i == R.id.menu_clear){
            mAsyncListDifferAdapter.clear();
        }
        return true;
    }
}
Copy the code

AsyncListDiffer submitList AsyncListDiffer submitList

public void submitList(@Nullable final List<T> newList) { final int runGeneration = ++this.mMaxScheduledGeneration; if (newList ! This.mlist) {if (newList == null) {// Empty the list when new data is null int countRemoved = this.mlist.size (); this.mList = null; this.mReadOnlyList = Collections.emptyList(); this.mUpdateCallback.onRemoved(0, countRemoved); } else if (this.mList == null) {this.mList = newList; this.mReadOnlyList = Collections.unmodifiableList(newList); this.mUpdateCallback.onInserted(0, newList.size()); } else { final List<T> oldList = this.mList; / / calculation data difference in a child thread enclosing mConfig. GetBackgroundThreadExecutor (). The execute (new Runnable () {public void the run () {final DiffResult result = DiffUtil.calculateDiff(new Callback() { ... }); / / main thread refresh list AsyncListDiffer. This. MMainThreadExecutor. Execute (new Runnable () {public void the run () {if (AsyncListDiffer.this.mMaxScheduledGeneration == runGeneration) { AsyncListDiffer.this.latchList(newList, result); }}}); }}); } } } void latchList(@NonNull List<T> newList, @NonNull DiffResult diffResult) { this.mList = newList; this.mReadOnlyList = Collections.unmodifiableList(newList); / / method of familiar dispatchUpdatesTo diffResult. DispatchUpdatesTo (enclosing mUpdateCallback); }Copy the code

This is where AsyncListDiffer does thread handling for us. Convenient for us to use the correct specification.

4.SnapHelper

SnapHelper is support-V7 :24.2.0, used to control the alignment of RecyclerView items after the slide stops. The default provides two alignments: PagerSnapHelper and LinearSnapHelper. PagerSnapHelper works like ViewPage, sliding one page at a time. LinearSnapHelper so this is the Item centered. It’s very simple to use:

 PagerSnapHelper mPagerSnapHelper = new PagerSnapHelper();
 mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
Copy the code

The effect is as follows:

Of course, we can customize SnapHelper to achieve the alignment we want, so let’s implement left alignment.

Public class MySnapHelper extends LinearSnapHelper{/** * horizontal and vertical metrics */ @nullable private OrientationHelper mVerticalHelper; @Nullable private OrientationHelper mHorizontalHelper; @NonNull private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { if (mVerticalHelper == null) { mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); } return mVerticalHelper; } @NonNull private OrientationHelper getHorizontalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { if (mHorizontalHelper == null) { mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); } return mHorizontalHelper; } /** * calculates the offset on the x and y axes needed to align the View to the specified position. */ @Nullable @Override public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { int[] out = new int[2]; // Calculate x direction when sliding horizontally, Otherwise offset 0 if (layoutManager canScrollHorizontally ()) {out [0] = distanceToStart (layoutManager targetView, getHorizontalHelper(layoutManager)); } else { out[0] = 0; } // Compute the y direction when sliding vertically, Otherwise offset 0 if (layoutManager canScrollVertically ()) {out [1] = distanceToStart (layoutManager targetView, getVerticalHelper(layoutManager)); } else { out[1] = 0; } return out; } private int distanceToStart(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {// RecyclerView final int start; if (layoutManager.getClipToPadding()) { start = helper.getStartAfterPadding(); } else { start = 0; } return helper.getDecoratedStart(targetView) - start; } / find the need to align the View * / * * * @ Nullable @ Override public View findSnapView (RecyclerView. LayoutManager LayoutManager) {if (layoutManager.canScrollVertically()) { return findStartView(layoutManager, getVerticalHelper(layoutManager)); } else { return findStartView(layoutManager, getHorizontalHelper(layoutManager)); } } private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; final int start; if (layoutManager.getClipToPadding()) { start = helper.getStartAfterPadding(); } else { start = 0; } int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); // ItemView left coordinate int childStart = helper.getdecoratedStart (child); // Calculate the distance between this ItemView and RecyclerView left int absDistance = math.abs (childStart - start); // find the one closest to the left ItemView then return if (absDistance < l/c) {l/c = absDistance; closestChild = child; } } return closestChild; } /** * Find the position of the View to be aligned, Is mainly used to fling operation * / @ Override public int findTargetSnapPosition (RecyclerView. LayoutManager LayoutManager, int velocityX, Int velocityY) {/ / left aligned and centered alignment, don't need to return the custom processing. Super findTargetSnapPosition (layoutManager, velocityX velocityY); }}Copy the code

The above notes have indicated the key points, I will not show the effect. You can download the code to try it out. All of this code has been uploaded to Github. Hope to support!!

Reference 5.

  • 【Android】 A good companion of RecyclerView: explain DiffUtil

  • Let you clearly use RecyclerView – SnapHelper details