使用 RecyclerView

使用 RecyclerView

概述


RecyclerView 是一个 ViewGroup,它用于渲染任何基于适配器的 View。它被官方定义为 ListViewGridView 的取代者,是在 Support V7 包中引入的。使用该组件的一个理由是:它有一个更易于扩展的框架,尤其是它提供了横向和纵向两个方向滚动的能力。当数据集合根据用户的操作或网络状态的变化而变化时,你很需要这个控件。

要使用 RecyclerView,需要以下的几个元素:

  • RecyclerView.Adapter :用于处理数据集合,并把数据绑定到视图上

  • LayoutManager :用于定位列表项

  • ItemAnimator :用于让常见操作(例如添加或移除列表项)变得活泼

enter description here

工作流

此处,当 RecyclerView 添加或移除列表项时,它还提供了动画支持,动画操作在当前的实现下,是非常困难的事情。并且,ViewHolderRecyclerView 中被深度集成,不再只是一个推荐方式。

与 ListView 的对比


因为以下几个理由,RecyclerView 与它的前辈 ListView 是不相同的:

  • 适配器中需要 ViewHolderListView 中,要提升性能,你可以不实现 ViewHolder,可以尝试其它的选择;但是在 RecyclerView 的适配器中,ViewHolder 是必须要使用的。

  • 自定义列表项布局ListView 只能把列表项以线性垂直的方式进行安排,并且不能自定义;RecyclerViewRecyclerView.LayoutManager 类,可以让任何列表项在水平方向排列,或是以交错的网格模式排列。

  • 简单的列表项动画 :关于添加或移除列表项的操作,ListView 并没有添加任何的规定;对于 RecyclerView 来说,它有一个 RecyclerView.ItemAnimator 类,可以用来处理列表项的动画。

  • 手动的数据源 :对于不同类型的数据源来说,ListView 有着不同的适配器与之对应,例如 ArrayAdapterCursorAdapter。与此相反,RecyclerAdapter 需要开发者自己实现提供给适配器的数据。

  • 手动的列表项装饰ListViewandroid:divider 属性,用于设置列表项之间的分隔。与此相反,要给 RecyclerView 设置分隔线的装饰,需要手动使用 RecyclerView.ItemDecoration 对象。

  • 手动监测点击事件ListView 为列表上的每个列表项的点击事件都使用 AdapterView.OnItemClickListener 接口进行了绑定。与之不同的是,RecyclerView 只提供了 RecyclerView.OnItemTouchListener 接口,它可以管理单个的 touch 事件,而不再内嵌点击事件的处理。

RecyclerView 的组件

LayoutManager


LayoutManager 用于在 RecyclerView 中管理列表项的位置,对于不再对用户可见的视图来说,它还能决定什么时候重用这些视图。

RecyclerView 提供了以下几种内嵌的布局管理器:

  • LinearLayoutManager :在水平或垂直的滚动列表上显示列表项

  • GridLayoutManager :在网格中显示列表项

  • StaggeredGridLayoutManager :在交错的网格中显示列表项

继承 RecyclerView.LayoutManager 就可以创建自定义的布局管理器。

RecyclerView.Adapter


RecyclerView 包含一种新的适配器,它与你之前使用过的适配器很类似,只是包含了一些特殊之处,例如必须的 ViewHolder 等。要使用这些适配器,需要重写两个方法:1. 用于渲染视图和 ViewHolder 的方法;2. 用于把数据绑定到视图的方法。每当需要创建一个新的视图时,都会调用第一个方法,不再需要检测视图是否被回收。

ItemAnimator


RecyclerView.ItemAnimator 将会使要通知适配器的 ViewGroup 的改变(例如添加/删除/选择列表项)动起来。DefaultItemAnimator 可以用于基本的默认动画,并且表现不俗。

使用 RecyclerView


使用 RecyclerView 需要遵循下面的关键步骤:

  1. gradle 构建文件中添加 RecyclerView 的支持库

  2. 定义作为数据源使用的 model

  3. Activity 中添加 RecyclerView

  4. 创建用于展现列表项的自定义行布局文件

  5. 把数据源绑定到适配器中,用于填充 RecyclerView

安装


app/build.gradle 文件中申明:

  1. dependencies { 
  2. ... 
  3. compile 'com.android.support:support-v4:24.2.1' 
  4. compile 'com.android.support:recyclerview-v7:24.2.1' 

定义 Model


每个 RecyclerView 都有一个数据源支持。在本示例中,将定义一个 Contact 类,用于定义 RecyclerView 显示的数据模型:

  1. public class Contact
  2. private String mName; 
  3. private boolean mOnline; 
  4.  
  5. public Contact(String name, boolean online)
  6. mName = name; 
  7. mOnline = online; 

  8.  
  9. public String getName()
  10. return mName; 

  11.  
  12. public boolean isOnline()
  13. return mOnline; 

  14.  
  15. private static int lastContactId = 0
  16.  
  17. public static ArrayList<Contact> createContactsList(int numContacts)
  18. ArrayList<Contact> contacts = new ArrayList<Contact>(); 
  19.  
  20. for (int i = 1; i <= numContacts; i++) { 
  21. contacts.add(new Contact("Person " + ++lastContactId, i <= numContacts / 2)); 

  22.  
  23. return contacts; 


在布局文件中创建 RecycleView


res/layout/activity_user.xml 文件中,添加 RecyclerView

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  2. android:layout_width="match_parent" 
  3. android:layout_height="match_parent" > 
  4.  
  5. <android.support.v7.widget.RecyclerView 
  6. android:id="@+id/rvContacts" 
  7. android:layout_width="match_parent" 
  8. android:layout_height="match_parent" /> 
  9.  
  10. </RelativeLayout> 

创建自定义的行布局


在创建适配器之前,先定义列表用于每行显示数据的布局。当前的列表项是一个水平的线性布局,布局中有一个文本框、一个按钮:

enter description here

行布局

行布局文件保存在 res/layout/item_contact.xml 中,列表中每一行都会用到它。

注意:LinearLayout 的 layout_height 参数的值应该设置为 wrap_content,这是因为早于 23.2.1 版本的 RecyclerView 将会忽略布局参数。

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <LinearLayout 
  3. xmlns:android="http://schemas.android.com/apk/res/android" 
  4. android:orientation="horizontal" 
  5. android:layout_width="match_parent" 
  6. android:layout_height="wrap_content" 
  7. android:paddingTop="10dp" 
  8. android:paddingBottom="10dp" 
  9. > 
  10.  
  11. <TextView 
  12. android:id="@+id/contact_name" 
  13. android:layout_width="0dp" 
  14. android:layout_height="wrap_content" 
  15. android:layout_weight="1" 
  16. /> 
  17.  
  18. <Button 
  19. android:id="@+id/message_button" 
  20. android:layout_width="wrap_content" 
  21. android:layout_height="wrap_content" 
  22. android:paddingLeft="16dp" 
  23. android:paddingRight="16dp" 
  24. android:textSize="10sp" 
  25. /> 
  26. </LinearLayout> 

创建 RecyclerView.Adapter


接着,创建一个适配器,用于把数据填充到 RecyclerView 中。适配器的角色是:用于把某个位置上的对象转换为列表项,并显示。

当然,RecyclerView 的适配器需要 ViewHolder 对象,用于描述和访问每个列表项中的所有视图。在 ContactsAdapter.java 类中,先创建一个基本的、没有实际内容的适配器:

  1. // Create the basic adapter extending from RecyclerView.Adapter 
  2. // Note that we specify the custom ViewHolder which gives us access to our views 
  3. public class ContactsAdapter extends  
  4. RecyclerView.Adapter<ContactsAdapter.ViewHolder>
  5.  
  6. // Provide a direct reference to each of the views within a data item 
  7. // Used to cache the views within the item layout for fast access 
  8. public static class ViewHolder extends RecyclerView.ViewHolder
  9. // Your holder should contain a member variable 
  10. // for any view that will be set as you render a row 
  11. public TextView nameTextView; 
  12. public Button messageButton; 
  13.  
  14. // We also create a constructor that accepts the entire item row 
  15. // and does the view lookups to find each subview 
  16. public ViewHolder(View itemView)
  17. // Stores the itemView in a public final member variable that can be used 
  18. // to access the context from any ViewHolder instance. 
  19. super(itemView); 
  20.  
  21. nameTextView = (TextView) itemView.findViewById(R.id.contact_name); 
  22. messageButton = (Button) itemView.findViewById(R.id.message_button); 



适配器和 ViewHolder 已经定义好了,接下来该填充它们了。首先,定义一个成员变量,用于存储联系人数据,它在构造方法中被赋值:

  1. public class ContactsAdapter extends  
  2. RecyclerView.Adapter<ContactsAdapter.ViewHolder>
  3.  
  4. // ... view holder defined above... 
  5.  
  6. // Store a member variable for the contacts 
  7. private List<Contact> mContacts; 
  8. // Store the context for easy access 
  9. private Context mContext; 
  10.  
  11. // Pass in the contact array into the constructor 
  12. public ContactsAdapter(Context context, List<Contact> contacts)
  13. mContacts = contacts; 
  14. mContext = context; 

  15.  
  16. // Easy access to the context object in the recyclerview 
  17. private Context getContext()
  18. return mContext; 


每个适配器都有三个主要的方法:1. onCreateViewHolder :实例化列表项布局、创建 Holder;2. onBindViewHolder:基于数据设置视图属性;3. getItemCount:检测列表项的数量。为了完成适配器,需要实现它们:

  1. public class ContactsAdapter extends  
  2. RecyclerView.Adapter<ContactsAdapter.ViewHolder>
  3.  
  4. // ... constructor and member variables 
  5.  
  6. // Usually involves inflating a layout from XML and returning the holder 
  7. @Override 
  8. public ContactsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
  9. Context context = parent.getContext(); 
  10. LayoutInflater inflater = LayoutInflater.from(context); 
  11.  
  12. // Inflate the custom layout 
  13. View contactView = inflater.inflate(R.layout.item_contact, parent, false); 
  14.  
  15. // Return a new holder instance 
  16. ViewHolder viewHolder = new ViewHolder(contactView); 
  17. return viewHolder; 

  18.  
  19. // Involves populating data into the item through holder 
  20. @Override 
  21. public void onBindViewHolder(ContactsAdapter.ViewHolder viewHolder, int position)
  22. // Get the data model based on position 
  23. Contact contact = mContacts.get(position); 
  24.  
  25. // Set item views based on your views and data model 
  26. TextView textView = viewHolder.nameTextView; 
  27. textView.setText(contact.getName()); 
  28. Button button = viewHolder.messageButton; 
  29. button.setText("Message"); 

  30.  
  31. // Returns the total count of items in the list 
  32. @Override 
  33. public int getItemCount()
  34. return mContacts.size(); 


把适配器绑定到 RecyclerView


Activity 中,给 RecyclerView 填充用于测试的用户数据:

  1. public class UserListActivity extends AppCompatActivity
  2.  
  3. ArrayList<Contact> contacts; 
  4.  
  5. @Override 
  6. protected void onCreate(Bundle savedInstanceState)
  7. // ... 
  8. // Lookup the recyclerview in activity layout 
  9. RecyclerView rvContacts = (RecyclerView) findViewById(R.id.rvContacts); 
  10.  
  11. // Initialize contacts 
  12. contacts = Contact.createContactsList(20); 
  13. // Create adapter passing in the sample user data 
  14. ContactsAdapter adapter = new ContactsAdapter(this, contacts); 
  15. // Attach the adapter to the recyclerview to populate items 
  16. rvContacts.setAdapter(adapter); 
  17. // Set layout manager to position the items 
  18. rvContacts.setLayoutManager(new LinearLayoutManager(this)); 
  19. // That's all! 


编译运行应用,你将看到类似于下图的结果。如果创建了足够多的元素,列表就可以滚动了,你将会发现它的滚动远比 ListView 流畅的多。

enter description here

最终结果

通知适配器


ListView 不同的是,通过 RecyclerView 的适配器,没有办法直接添加或移除列表项。你需要做的是:直接操作数据源,然后通知适配器数据源发生了变化。同时,每当添加或移除元素时,总是应该对已经存在的列表进行操作。例如,下面这句的改变,并不会影响适配器,这是因为适配器保存了旧数据的引用,但是生成的新数据的引用发生了变化。

  1. // do not reinitialize an existing reference used by an adapter 
  2. contacts = Contact.createContactsList(5); 

相反,你需要在已经存在的引用上直接进行操作:

  1. // add to the existing list 
  2. contacts.addAll(Contact.createContactsList(5)); 

如果要给适配器通知不同类型的改变,可以使用下面的这些方法:

方法 描述
notifyItemChanged(int pos) 某个位置上元素改变的通知
notifyItemInserted(int pos) 某个位置上插入元素的通知
notifyItemRemoved(int pos) 某个位置上元素移除的通知
notifyDataSetChanged() 数据集改变的通知

ActivityFragment 中,应该这样使用:

  1. // Add a new contact 
  2. contacts.add(0, new Contact("Barney", true)); 
  3. // Notify the adapter that an item was inserted at position 0 
  4. adapter.notifyItemInserted(0); 

每当我们想给 RecyclerView 添加或移除数据时,我们需要显式地通知适配器正在发生的事件是什么。与 ListView 的适配器不同,RecyclerView 的适配器不应该依赖于 notifyDataSetChanged(),应该使用粒度更细的动作。

如果你打算更新一个已经存在的数据列表,请在改变之前,务必要获取当前元素的数量。例如,应该调用适配器的 getItemCount() 方法来记录改变前的索引:

  1. // record this value before making any changes to the existing list 
  2. int curSize = adapter.getItemCount(); 
  3.  
  4. // replace this line with wherever you get new records 
  5. ArrayList<Contact> newItems = Contact.createContactsList(20);  
  6.  
  7. // update the existing list 
  8. contacts.addAll(newItems); 
  9. // curSize should represent the first element that got added 
  10. // newItems.size() represents the itemCount 
  11. adapter.notifyItemRangeInserted(curSize, newItems.size()); 

定义大的改变


实际生产中,列表的改变往往更为复杂一些(例如:对已经存在的数据排序),改变一复杂,就不能精确对改变进行一个分类。这种情况下,通常,需要在整个适配器上调用 notifyDataSetChanged() 方法来更新整个屏幕,这样,使用动画来展示改变的能力,就会被减弱。

support v7 库的 v24.2.0 版本中,新增了 DiffUtil 类,用于计算新旧数据之间的不同。对于比较大的数据列表来说,推荐使用后台线程执行计算。

要使用 DiffUtil 类,需要实现该类并实现 DiffUtil.Callback 回调,该回调可以接收新、旧的数据列表:

  1. public class ContactDiffCallback extends DiffUtil.Callback
  2.  
  3. private List<Contact> mOldList; 
  4. private List<Contact> mNewList; 
  5.  
  6. public ContactDiffCallback(List<Contact> oldList, List<Contact> newList)
  7. this.mOldList = oldList; 
  8. this.mNewList = newList; 

  9. @Override 
  10. public int getOldListSize()
  11. return mOldList.size(); 

  12.  
  13. @Override 
  14. public int getNewListSize()
  15. return mNewList.size(); 

  16.  
  17. @Override 
  18. public boolean areItemsTheSame(int oldItemPosition, int newItemPosition)
  19. // add a unique ID property on Contact and expose a getId() method 
  20. return mOldList.get(oldItemPosition).getId() == mNewList.get(newItemPosition).getId(); 

  21.  
  22. @Override 
  23. public boolean areContentsTheSame(int oldItemPosition, int newItemPosition)
  24. Contact oldContact = mOldList.get(oldItemPosition); 
  25. Contact newContact = mNewList.get(newItemPosition); 
  26.  
  27. if (oldContact.getName() == newContact.getName() && oldContact.isOnline() == newContact.isOnline()) { 
  28. return true

  29. return false


接着,要在适配器中实现 swapItems() 方法用于执行差异化比较。比较过后,如果有数据被插入、移除、移动或删除时,调用 dispatchUpdates() 方法来通知适配器:

  1. public class ContactsAdapter extends 
  2. RecyclerView.Adapter<ContactsAdapter.ViewHolder>
  3.  
  4. public void swapItems(List<Contact> contacts)
  5. // compute diffs 
  6. final ContactDiffCallback diffCallback = new ContactDiffCallback(this.mContacts, contacts); 
  7. final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); 
  8.  
  9. // clear contacts and add  
  10. this.mContacts.clear(); 
  11. this.mContacts.addAll(contacts); 
  12.  
  13. diffResult.dispatchUpdatesTo(this); // calls adapter's notify methods after diff is computed 


完整的示例代码请参见 mrmike/DiffUtil-sample

滚动到新的列表项


把列表项插入到列表的前部分,但是当前列表视图处于列表的后部分,这时,如果你想把列表显示顶部的位置,可以通过下列代码实现:

  1. adapter.notifyItemInserted(0);  
  2. rvContacts.scrollToPosition(0); // index 0 position 

如果把列表项添加到列表的底部,我们可以这样做:

  1. adapter.notifyItemInserted(contacts.size() - 1); // contacts.size() - 1 is the last element position 
  2. rvContacts.scrollToPosition(mAdapter.getItemCount() - 1); // update based on adapter  

实现连续的滚动


当用户滚动到列表的底部时,想要实现加载数据,把新加载的数据添加到列表的尾部的功能,需要使用 RecyclerViewaddOnScrollListener() 方法,并添加 onLoadMore 方法。具体的实现参见另一篇教程:EndlessScrollViewScrollListener

配置 RecyclerView


RecyclerView 可以自定制,它非常的灵活,可以从以下几个方面得以体现:

性能


如果数据源是静态、不会发生变化的,那我们可以启用最优化,来显著的提升平滑滚动的效果:

  1. recyclerView.setHasFixedSize(true); 

布局


使用 layout manager 可以配置列表项的位置。我们可以在 LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManager 这几个布局管理器中进行选择。

列表项在水平或垂直方向进行线性排列的示例:

  1. // Setup layout manager for items with orientation 
  2. // Also supports `LinearLayoutManager.HORIZONTAL` 
  3. LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); 
  4. // Optionally customize the position you want to default scroll to 
  5. layoutManager.scrollToPosition(0); 
  6. // Attach layout manager to the RecyclerView 
  7. recyclerView.setLayoutManager(layoutManager); 

在网格或交错网格上显示列表项的示例:

  1. // First param is number of columns and second param is orientation i.e Vertical or Horizontal 
  2. StaggeredGridLayoutManager gridLayoutManager =  
  3. new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 
  4. // Attach the layout manager to the recycler view 
  5. recyclerView.setLayoutManager(gridLayoutManager); 

交错的网格布局可能如下所示:

enter description here

交错网格效果展示

要自定义 layout manager,可以参见 这篇文章

装饰


我们可以使用与 RecyclerView 相关的一些装饰来修饰列表项,例如使用 DividerItemDecoration

  1. RecyclerView.ItemDecoration itemDecoration = new  
  2. DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST); 
  3. recyclerView.addItemDecoration(itemDecoration); 

装饰的作用是在列表项之间显示分隔:

enter description here

列表项分隔

Grid 空间装饰


GridStaggeredGrid 中,修饰可以在显示的元素之间添加一致的空间。把 SpacesItemDecoration.java 这个修饰拷贝到你的项目中,看一下效果吧。要想知道更多的细节,请参考 这篇教程

动画


RecyclerView 的列表项进入、添加或删除时,使用 ImteAnimator 可以给列表项添加自定义的动画。DefaultItemAnimator 用于定义默认的动画,它源码(源码地址)中复杂的实现说明了要确保以某个序列执行的动画效果所需要的必须的逻辑。

目前,在 RecyclerView 上实现列表项动画的最快方式就是使用第三方库。wasabeef/recyclerview-animators 库中包含了大量可以使用的动画。引入方式如下:

  1. repositories { 
  2. jcenter() 

  3.  
  4. //If you are using a RecyclerView 23.1.0 or higher. 
  5. dependencies { 
  6. compile 'jp.wasabeef:recyclerview-animators:2.2.3' 

  7.  
  8. //If you are using a RecyclerView 23.0.1 or below. 
  9. dependencies { 
  10. compile 'jp.wasabeef:recyclerview-animators:1.3.0' 

代码中的使用:

  1. recyclerView.setItemAnimator(new SlideInUpAnimator()); 

下面是效果:

enter description here

列表项动画效果

新的动画接口


support v23.1.0 开始,RecyclerView.ItemAnimator 新增了一个接口。该库添加了 ItemHolderInfo 类,它的表现与 MoveInfo 类似,但是在动画过渡状态之间,它能更为优雅的传递状态信息。

不同布局的列表项


如果在单个的 RecyclerView 中,你想展示多种类型的列表项,那你应该参这篇文章

enter description here

不同类型的列表项

处理 Touch 事件


  1. recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { 
  2.  
  3. @Override 
  4. public void onTouchEvent(RecyclerView recycler, MotionEvent event)
  5. // Handle on touch events here 

  6.  
  7. @Override 
  8. public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event)
  9. return false

  10.  
  11. }); 

Snap to Center 效果


在某些情况下,我们可能需要一个横向滚动的 RecyclerView。当用户滚动时,如果某个列表项被暴露了,我们想让它 snap to center,如下图所示:

enter description here

snap to center

LinearSnapHelper


当用户滑动时,如要实现上图的效果,在 support v24.2.0 及以上版本中,可以使用内嵌的 LinearSnapHelper 类:

  1. SnapHelper snapHelper = new LinearSnapHelper(); 
  2. snapHelper.attachToRecyclerView(recyclerView); 
posted @ 2016-11-21 18:25  wchhuangya  阅读(1904)  评论(0编辑  收藏  举报