前言
RecyclerView是Android 5.0之後推出的列表類控件,具有高度的解耦性和靈活性。通過使用合適的LayoutManager,可以實現ListView、橫向ListView、GridView和瀑布流列表的效果。本文將對RecyclerView的相關知識點進行詳細講解。
基本用法
使用步驟
RecyclerView是支持庫中的控件,因此在使用前需要先在build.gradle
文件中添加依賴,如下:
implementation 'com.android.support:recyclerview-v7:26.0.0-beta1'
注意: AndroidStudio在升級到3.0
版本後,不再使用compile
關鍵字引入依賴庫,而改用implementation
關鍵字。
配置好依賴後,就可以正式開始使用RecyclerView了。首先,提供列表項(Item)的佈局文件,本例中命名爲recycler_view_item.xml
,代碼如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f36c60">
<TextView
android:id="@+id/text_view_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textSize="16sp"
android:textColor="#fff"
android:gravity="center"/>
</LinearLayout>
RecyclerView和ListView類似,都是藉助Adapter訪問數據源,因此還需要實現自己的適配器,示例代碼如下:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>{
private List<String> dataList;//數據源
private LayoutInflater inflater;//佈局解析器
public RecyclerViewAdapter(List<String> dataList){
this.dataList = dataList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
if(inflater==null){//避免多次初始化
inflater=LayoutInflater.from(parent.getContext());
}
View itemView=inflater.inflate(R.layout.recycler_view_item,parent,false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position){
final String itemContent=dataList.get(position);
holder.textView.setText(itemContent);
}
@Override
public int getItemCount() {
return dataList.size();
}
//自定義ViewHolder
static class ViewHolder extends RecyclerView.ViewHolder{
private TextView textView;
public ViewHolder(View itemView) {
super(itemView);
textView=itemView.findViewById(R.id.text_view_recycler);
}
}
}
可以看到,RecyclerViewAdapter繼承自RecyclerView.Adapter
,並通過繼承RecyclerView.ViewHolder
實現了靜態類ViewHolder,這是爲了充分利用RecyclerView的View複用機制。
主要重寫的方法有onCreateViewHolder
、onBindViewHolder
和getItemCount
,分別用於創建ViewHolder、綁定數據和返回數據總數量。
在爲RecyclerView設置Adapter之前,我們先爲RecyclerView設置合適的LayoutManager。LayoutManager用於管理列表項的排列方式,通過使用不同的LayoutManager,可以在不改變適配器的情況下隨意改變列表排列方式,這也是RecyclerView得以解耦合的原因。示例代碼如下:
LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);//設置爲縱向排列
recyclerView.setLayoutManager(linearLayoutManager);//設置佈局管理器
在本例中使用LinearLayoutManager
。這是一個線性的佈局管理器,可以設置爲橫向或縱向排列,選擇爲縱向排列其實就實現了ListView的效果。
最後,再爲RecyclerView設置好適配器就行了,示例代碼如下:
//生成隨機數據
private List<String> createDataList(){
List<String> list=new ArrayList<>();
String[] rootArray={"Java","Android","Swift","Python","Ruby"};
for(int i=0;i<60;i++){
list.add(rootArray[i%rootArray.length]+i);
}
return list;
}
List<String> dataList=createDataList();//數據源
RecyclerViewAdapter recyclerViewAdapter=new RecyclerViewAdapter(dataList);
recyclerView.setAdapter(recyclerViewAdapter);//設置適配器
最後,總結一下RecyclerView的使用步驟:
- 準備列表項佈局文件
- 實現適配器
- 爲RecyclerView設置佈局管理器
- 爲RecyclerView設置適配器
效果截圖:
監聽列表項的點擊事件
和ListView不同,RecyclerView並沒有提供爲列表項設置點擊監聽器的方法,因此我們需要自己去實現這一需求。
首先,在Adapter類中定義一個內部接口,並將其作爲Adapter的成員變量,以及實現相應的setter方法,代碼如下:
...
private ItemClickListener itemClickListener;//列表項點擊監聽器
//爲RecyclerView設置點擊監聽器
public void setItemClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
//自定義的點擊監聽器接口
public interface ItemClickListener{
void onItemClick(String clickItem);//單擊事件
void onItemLongClick(String clickItem);//長按事件
}
...
之後,在onBindViewHolder
方法中爲列表項設置點擊監聽器,並調用ItemClickListener
中相應的方法,代碼如下:
@Override
public void onBindViewHolder(ViewHolder holder, int position){
....
//爲列表項設置點擊監聽
if(itemClickListener!=null){
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
itemClickListener.onItemClick(itemContent);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
itemClickListener.onItemLongClick(itemContent);
return true;
}
});
}
}
最後,只需要爲RecyclerView設置相應的接口,就輕鬆地實現了監聽列表項點擊事件的需求,代碼如下:
recyclerViewAdapter.setItemClickListener(new RecyclerViewAdapter.ItemClickListener() {
@Override
public void onItemClick(String clickItem) {
Toast.makeText(RecyclerViewActivity.this,"點擊:"+clickItem,
Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(String clickItem) {
Toast.makeText(RecyclerViewActivity.this,"長按:"+clickItem,
Toast.LENGTH_SHORT).show();
}
});
使用不同的LayoutManager
在上面的例子中,我們使用LinearLayoutManager實現了類似ListView的效果。實際上,RecyclerView一共提供了三種LayoutManger,用於實現多種佈局效果。下面簡單介紹一下這幾種佈局管理器:
- LinearLayoutManager:線性佈局管理器,有橫向和縱向兩種佈局方向,可以通過
setOrientation
方法設置佈局方向。 - GridLayoutManager:網格佈局管理器,可以實現類似GridView的排列效果,屬於LinearLayoutManager的子類。
- StaggeredGridLayoutManager:可以實現瀑布流的佈局管理器。
注意:如果要實現瀑布流式佈局,要求Item的高度不同(縱向排列時),否則StaggeredGridLayoutManager的顯示效果和GridLayoutManager相同。
GridLayoutManager使用示例:
GridLayoutManager gridLayoutManager=new GridLayoutManager(RecyclerViewActivity.this,3);//3列
recyclerView.setLayoutManager(gridLayoutManager);
效果截圖:
StaggeredGridLayoutManager使用示例:
//垂直排列、4列
StaggeredGridLayoutManager staggeredGridLayoutManager=new StaggeredGridLayoutManager(4,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
效果截圖:
相關方法
RecyclerView
添加Item裝飾器:
public void addItemDecoration(ItemDecoration decor);
//index:指定位置
public void addItemDecoration(ItemDecoration decor, int index);
判斷RecyclerView是否在執行動畫:
public boolean isAnimating();
獲取指定位置的ViewHolder:
public RecyclerView.ViewHolder findViewHolderForAdapterPosition(int position);
public RecyclerView.ViewHolder findViewHolderForLayoutPosition(int position);
這兩個方法都是返回指定位置的ViewHolder,如果指定位置的View還不存在,則會返回null
。這兩者的區別在於,findViewHolderForAdapterPosition
以Adapter中的最新數據爲基準,而findViewHolderForLayoutPosition
以已佈局的舊數據爲基準。在數據源發生改變而這一改變還沒有更新到RecyclerView中的這一小段時間裏(16ms),兩者的返回結果將不同。
LinearLayoutManager
構造方法:
//默認縱向排列
public LinearLayoutManager(Context context);
//orientation:佈局方向(橫向或縱向)
//reverseLayout:是否逆序排列
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout);
如果reverseLayout
爲true,那麼列表將對數據源進行逆序排列。以縱向排列爲例,列表將從底部開始依次加載數據,並且將首先顯示列表末尾的內容而不是頭部內容(感覺就像列表自動滑到了列表末尾)。
設置是否對數據逆序排列:
public void setReverseLayout(boolean reverseLayout);
設置佈局方向:
//orientation:佈局方向 可選值:[LinearLayoutManager.HORIZONTAL|LinearLayoutManager.VERTICAL]
public void setOrientation(int orientation);
設置是否優先展示列表尾部內容:
public void setStackFromEnd(boolean stackFromEnd);
以縱向排列爲例,如果stackFromEnd設置爲true,那麼打開RecyclerView首先看到的就是最底部的內容,看起來就像是RecyclerView已經滾動到了最後一行;如果設置爲false,就和默認狀態一樣,首先看到第一行的內容。
跳轉到指定位置:
public void scrollToPosition(int position);
//offset:偏移量
public void scrollToPositionWithOffset(int position, int offset);
注意:這兩個方法都只保證指定位置的列表項可見,並不保證該列表項處於第一個可見位置。實際上,這兩個方法都會盡量只滑動最小的距離。
平滑移動到指定位置:
//recyclerView:目標recyclerView
//state:可以傳入null
//position:指定位置
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,int position)
和scrollToPosition
方法不同,這個方法可以實現平滑移動,因此移動過程不會顯得那麼突兀。
獲取可見的列表項:
public int findFirstVisibleItemPosition();//獲取第一個可見的列表項位置
public int findFirstCompletelyVisibleItemPosition();//獲取第一個完整可見的列表項位置
public int findLastVisibleItemPosition();//獲取最後一個可見的列表項位置
public int findLastCompletelyVisibleItemPosition();//獲取最後一個完整可見的列表項位置
GridLayoutManger
構造方法:
//默認縱向排列
//spanCount:列數
public GridLayoutManager(Context context, int spanCount);
//orientation:排列方向(橫向或縱向)
//spanCount:行數或列數(取決於排列方向)
//reverseLayout:是否倒序排列
public GridLayoutManager(Context context, int spanCount, int orientation,boolean reverseLayout);
注意:如果orientation爲縱向,spanCount就代表列數;如果orientation爲橫向,spanCount就代表行數。
設置行數和列數:
public void setSpanCount(int spanCount);
GridLayoutManger
是LinearLayoutManager
的子類,因此繼承了LinearLayoutManager的所有方法,這裏不再贅述。不過要注意,GridLayoutManger並不支持setStackFromEnd
方法。
StaggeredGridLayoutManager
構造方法:
//orientation:排列方向(橫向或縱向)
//spanCount:行數或列數(取決於排列方向)
public StaggeredGridLayoutManager(int spanCount, int orientation);
注意:如果orientation爲縱向,spanCount就代表列數;如果orientation爲橫向,spanCount就代表行數。
其他方法:
public void setOrientation(int orientation);//設置佈局方向
public void setSpanCount(int spanCount);//設置行數或列數
public void setReverseLayout(boolean reverseLayout);//設置是否對數據逆序排列
public void scrollToPosition(int position);//跳轉到指定位置
public void scrollToPositionWithOffset(int position, int offset);//帶偏移量跳轉到指定位置
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,int position)//平滑移動到指定位置
public int findFirstVisibleItemPosition();//獲取第一個可見的列表項位置
public int findFirstCompletelyVisibleItemPosition();//獲取第一個完整可見的列表項位置
public int findLastVisibleItemPosition();//獲取最後一個可見的列表項位置
public int findLastCompletelyVisibleItemPosition();//獲取最後一個完整可見的列表項位置
實現多佈局列表(包括列表頭和列表尾)
在實際開發中,列表項可能並不是只有一種佈局方式。通過重寫Adapter的getItemViewType
方法,可以在不同的情形下構建合適的佈局。此外,通過這種方式還可以爲RecyclerView設置列表頭和列表尾,這時只需要將列表頭和列表尾視爲兩種獨立的佈局方式即可。在這裏,將介紹如何實現一個簡單的多佈局列表,最終的效果如下:
準備佈局文件
在本例中,主要有兩種列表項,即標題項和內容項。因此,準備兩個對應的佈局文件,分別命名爲recycler_view_multi_title.xml
和recycler_view_multi_item.xml
,代碼如下:
recycler_view_multi_title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp" />
</LinearLayout>
recycler_view_multi_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_image"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:textAllCaps="false"
android:textSize="16sp"
android:textColor="#000000"/>
</LinearLayout>
此外,也爲列表頭和列表尾準備兩個佈局文件,本例中命名爲recycler_view_header.xml
和recycler_view_footer.xml
,代碼如下:
recycler_view_header.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/recycler_view_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginBottom="8dp"
android:textSize="20sp"
android:text="HeaderView"/>
</LinearLayout>
recycler_view_footer.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/recycler_view_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:textSize="20sp"
android:text="FooterView"/>
</LinearLayout>
準備實體類
對於不同的佈局而言,應該使用不同的實體類。在本例中,有兩種列表項,因此需要兩個實體類。首先可以建立一個基類,本例中命名爲BaseMultiBean
,代碼如下:
public abstract class BaseMultiBean {
public static final int TYPE_TITLE=0;//標題項
public static final int TYPE_ITEM=1;//內容項
protected int type;//類型
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
可以看到,基類中主要是封裝了實體的類型屬性,這一屬性將用於確定要使用的列表項佈局。然後,再建立兩個繼承自基類的實體類,分別對應標題項和內容項,本例中命名爲TitleBean
和ItemBean
,代碼如下:
TitleBean
public class TitleBean extends BaseMultiBean{
private String title;
public TitleBean(String title) {
this.title = title;
this.type=TYPE_TITLE;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
ItemBean
public class ItemBean extends BaseMultiBean{
private int imageRes;//圖片資源
private String content;//內容
public ItemBean(int imageRes, String content) {
this.imageRes = imageRes;
this.content = content;
this.type=TYPE_ITEM;
}
public int getImageRes() {
return imageRes;
}
public void setImageRes(int imageRes) {
this.imageRes = imageRes;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
創建適配器
有了佈局和實體類,就可以開始着手創建適配器了,本例中命名爲StyleRecyclerViewAdapter
,代碼如下:
public class StyleRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
public static final int TYPE_TITLE=0;//標題形式的列表項
public static final int TYPE_CONTENT=1;//內容形式的列表項
public static final int TYPE_HEADER=2;//列表頭
public static final int TYPE_FOOTER=3;//列表尾
private View headerView;//頭部View
private View footerView;//尾部View
private int headerCount;//頭部View數量(0或1)
private List<BaseMultiBean> dataList;//數據源
private LayoutInflater inflater;//佈局解析器
public StyleRecyclerViewAdapter(List<BaseMultiBean> dataList) {
this.dataList = dataList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(inflater==null){//只初始化一次
inflater=LayoutInflater.from(parent.getContext());
}
switch (viewType){//根據佈局類型創建合適的ViewHolder
case TYPE_HEADER:
return new HeaderFooterViewHolder(headerView);
case TYPE_FOOTER:
return new HeaderFooterViewHolder(footerView);
case TYPE_TITLE:
View titleView=inflater.inflate(R.layout.recycler_view_multi_title,parent,false);
return new TitleViewHolder(titleView);
case TYPE_CONTENT:
View contentView=inflater.inflate(R.layout.recycler_view_multi_item,parent,false);
return new ContentViewHolder(contentView);
default:break;
}
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType=getItemViewType(position);
if(viewType==TYPE_TITLE){//爲標題形式的列表項綁定數據
TitleBean titleBean= (TitleBean) getItem(position);
TitleViewHolder titleViewHolder= (TitleViewHolder) holder;
titleViewHolder.titleView.setText(titleBean.getTitle());
}
if(viewType==TYPE_CONTENT){//爲內容形式的列表項綁定數據
ItemBean itemBean= (ItemBean) getItem(position);
ContentViewHolder contentViewHolder= (ContentViewHolder) holder;
contentViewHolder.itemImageView.setImageResource(itemBean.getImageRes());
contentViewHolder.itemContentView.setText(itemBean.getContent());
}
}
@Override
public int getItemCount() {//計算列表項的真正數量
int count=dataList.size();
if(headerView!=null){
count++;
}
if(footerView!=null){
count++;
}
return count;//返回列表頭、列表尾和列表項的總數量
}
@Override
public int getItemViewType(int position) {
if(headerView!=null&&position==0){
return TYPE_HEADER;
}
if(footerView!=null&&position==headerCount+dataList.size()){
return TYPE_FOOTER;
}
BaseMultiBean baseMultiBean=dataList.get(position-headerCount);
return baseMultiBean.getType();
}
//設置列表頭
public void setHeaderView(View headerView){
this.headerView=headerView;
headerCount=1;
}
//移除列表頭
public void removeHeaderView(){
headerView=null;
headerCount=0;
}
//設置列表尾
public void setFooterView(View footerView){
this.footerView=footerView;
}
//移除列表尾
public void removeFooterView(){
footerView=null;
}
//獲取數據源中的真實數據(避免HeaderView的影響)
private BaseMultiBean getItem(int position){
return dataList.get(position-headerCount);
}
//內容Item的ViewHolder
static class ContentViewHolder extends RecyclerView.ViewHolder{
private TextView itemContentView;
private ImageView itemImageView;
public ContentViewHolder(View itemView) {
super(itemView);
itemContentView=itemView.findViewById(R.id.item_content);
itemImageView=itemView.findViewById(R.id.item_image);
}
}
//標題Item的ViewHolder
static class TitleViewHolder extends RecyclerView.ViewHolder{
private TextView titleView;
public TitleViewHolder(View itemView) {
super(itemView);
titleView=itemView.findViewById(R.id.item_title);
}
}
//頭部和尾部佈局的ViewHolder
static class HeaderFooterViewHolder extends RecyclerView.ViewHolder{
public HeaderFooterViewHolder(View itemView) {
super(itemView);
}
}
}
可以看到,我們爲標題形式的列表項、內容形式的列表項、列表頭/尾分別定義了ViewHolder類,並在onCreateViewHolder
方法中根據viewType
返回對應的ViewHolder對象。而在onBindViewHolder
方法中,則根據viewType
的值進行數據綁定。
注意:在獲取列表項對象時,要排除HeaderView對position的影響,即當HeaderView存在時讓position減去1。
爲RecyclerView設置適配器
完成前面的準備工作後,就可以着手爲RecyclerView設置適配器了,代碼如下:
//初始化列表頭和列表尾
headerView=LayoutInflater.from(this).inflate(R.layout.recycler_view_header,null);
footerView=LayoutInflater.from(this).inflate(R.layout.recycler_view_footer,null);
//初始化多佈局的RecyclerView
List<BaseMultiBean> multiDataList=new ArrayList<>();
multiDataList.add(new TitleBean("第一個區域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《小王子》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《獅子王》"));
multiDataList.add(new TitleBean("第二個區域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《資本論》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《三體》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《孤獨的進化者》"));
styleRecyclerViewAdapter=new StyleRecyclerViewAdapter(multiDataList);
//設置列表頭和列表尾
styleRecyclerViewAdapter.setHeaderView(headerView);
styleRecyclerViewAdapter.setFooterView(footerView);
//設置佈局管理器和適配器
LinearLayoutManager styleLayoutManager=new LinearLayoutManager(this);
styleRecyclerView.setLayoutManager(styleLayoutManager);
styleRecyclerView.setAdapter(styleRecyclerViewAdapter);
完善列表頭和列表尾
上文介紹了添加列表頭和列表尾的方法,但針對的只是垂直排列的LinearLayoutManager
。如果使用GridLayoutManager或StaggeredGridLayoutManager,列表頭/尾就會顯示異常。因此針對這兩種管理器,還需要使用額外的佈局措施。
GridLayoutManager
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager();
//針對網格型的佈局管理器進行額外處理,避免頭/尾佈局顯示異常
if(layoutManager instanceof GridLayoutManager){
final GridLayoutManager gridLayoutManager= (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup=gridLayoutManager
.getSpanSizeLookup();//保存舊的佈局方式
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int viewType=getItemViewType(position);
if(viewType==TYPE_HEADER||viewType==TYPE_FOOTER){
return gridLayoutManager.getSpanCount();//返回當前網格的列數(即讓列表頭/尾佔據一行)
}
return spanSizeLookup.getSpanSize(position);
}
});
}
}
針對GridLayoutManager,需要重寫RecyclerView.Adapter
的onAttachedToRecyclerView
方法,並在顯示列表頭/尾的時候讓其佔據整行,就可以保證列表頭/爲尾正常顯示。
StaggeredGridLayoutManager
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
int viewType=holder.getItemViewType();
if(viewType==TYPE_HEADER||viewType==TYPE_FOOTER){
ViewGroup.LayoutParams layoutParams=holder.itemView.getLayoutParams();
//針對瀑布流式的佈局管理器進行額外處理,避免頭/尾佈局顯示異常
if(layoutParams instanceof StaggeredGridLayoutManager.LayoutParams){
StaggeredGridLayoutManager.LayoutParams staggerLayoutParams=
(StaggeredGridLayoutManager.LayoutParams) layoutParams;
staggerLayoutParams.setFullSpan(true);//列表頭/尾佔據一行
}
}
}
針對StaggeredGridLayoutManager,需要重寫RecyclerView.Adapter
的onViewAttachedToWindow
方法,並在顯示列表頭/尾的時候讓其佔據整行,就可以保證列表頭/爲尾正常顯示。
常用技巧
實現局部刷新
除了使用notifyDatasetChanged
方法通知整個列表刷新外,RecyclerView.Adapter
還提供了多個局部刷新的方法,說明如下:
通知指定位置的Item已經改變:
public final void notifyItemChanged(int position);
public final void notifyItemChanged(int position, Object payload);
這裏需要重點說明payload
參數的作用,簡單來說就是實現列表項的局部更新。在很多情況下,一個列表項中可能存在多個View,典型的例子如朋友圈中的一條動態,就有圖片、頭像、點贊、評論等多個組成部分。如果只是點贊數發生了變化,就沒有必要更新整個列表項,而只需更新點贊區域即可。此時,只需要爲payload
傳入一個不爲null的參數,就可以做到局部更新。
以上文介紹的多佈局RecyclerView爲例,我們來實現局部更新內容列表項的文字部分。首先,重寫ViewHolder中的onBindViewHolder(RecyclerView.ViewHolder holder,int position,List<Object> payloads)
方法,這個方法會在onBindViewHolder(RecyclerView.ViewHolder holder, int position)
方法之前調用。示例代碼如下:
//在這個方法中實現Item的局部更新(比如只更新ViewHolder中的一個View)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> payloads) {
if(payloads.isEmpty()){//如果payloads爲空,就調用默認實現
super.onBindViewHolder(holder,position,payloads);
}
else{//在payloads不爲空的時候實現ViewHolder中的部分更新
if("TYPE_CONTENT".equals(payloads.get(0))){
ContentViewHolder contentViewHolder= (ContentViewHolder) holder;
ItemBean itemBean= (ItemBean) getItem(position);
contentViewHolder.itemContentView.setText(itemBean.getContent());
}
}
}
這個方法中的payloads參數是一個不爲null的List,裏面就包含在notifyItemChanged
方法中傳入的payload參數。通過判斷payloads是否爲空,就知道是否需要進行局部更新了。
隨後,在代碼中調用相應的notifyItemChanged方法,並傳入payload參數,示例代碼如下:
ItemBean itemBean= (ItemBean) multiDataList.get(2);
itemBean.setContent("《通過局部更新獲得的內容》");
multiDataList.set(2,itemBean);
//這裏的payload用於標識要更新的列表項類型
styleRecyclerViewAdapter.notifyItemChanged(3,"TYPE_CONTENT");
注意:如果不使用局部更新的方式,即使列表項中的圖片並未發生改變,在刷新過程中圖片區域依舊會出現短暫的閃爍現象,使用局部更新就可以解決這一問題。
普通刷新效果截圖:
局部更新效果截圖:
通知指定範圍內的Item已經改變:
//itemCount:改變的Item數量
public final void notifyItemRangeChanged(int positionStart, int itemCount);
public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload);
payload
參數的作用上面已經說明了,這裏不再贅述。
通知有新的數據插入:
public final void notifyItemInserted(int position);
public final void notifyItemRangeInserted(int positionStart, int itemCount);
效果截圖:
通知有數據被移除:
public final void notifyItemRangeRemoved(int positionStart, int itemCount);
public final void notifyItemRemoved(int position);
效果截圖:
通知有Item發生了移動:
public final void notifyItemMoved(int fromPosition, int toPosition);
以上這些方法都只會對RecyclerView進行局部刷新,優化了運行效率,同時也會觸發動畫效果,大幅度改善了用戶體驗。
注意:以上這些局部刷新方法中的position
位置參數應該傳入正確的值,否則可能導致RecyclerView顯示異常。
爲列表項設置添加和刪除動畫
調用RecyclerView的setItemAnimator
方法就可以設置動畫效果,這個方法原型如下:
public void setItemAnimator(ItemAnimator animator);
參數的類型是RecyclerView.ItemAnimator
,系統已經提供了一個默認實現類DefaultItemAnimator
,使用方式如下:
recyclerView.setItemAnimator(new DefaultItemAnimator());//設置默認的動畫效果
除此之外,還可以通過繼承RecyclerView.ItemAnimator
實現自定義動畫效果,這裏推薦使用開源的動畫庫:
爲列表項設置分割線
RecyclerView中的列表項默認是沒有分割線的,如果想要實現這一需求,就要通過繼承RecyclerView.ItemDecoration
這個抽象類實現我們自己的列表項裝飾器。這個類需要實現的主要方法如下:
public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
onDraw
方法會在繪製列表項之前調用,因此繪製的內容會在列表項之下;而onDrawOver
會在繪製列表項之後調用,因此繪製的內容會在列表項之上(只可以用於實現角標等需求);getItemOffsets
方法可以通過outRect.set()
的方式爲列表項設置偏移量。
這裏推薦一個第三方的開源庫:
列表項裝飾器:RecyclerItemDecoration
小提示:如果僅僅想要在列表項之間增加一些間隔,也可以簡單地在Item的佈局文件中設置margin
屬性,在一些簡單的場景下這樣做代價更小。
添加頭部和尾部
請參考上文:
[實現多佈局列表(包括列表頭和列表尾)]
設置EmptyView
個人並不推薦通過重寫RecyclerView的方式實現EmptyView,因此後續會寫一篇博客介紹如何通過自定義View的方式實現一個通用的多狀態佈局(加載中、無數據、加載錯誤等)。
《Android 通過自定義View實現通用的多狀態佈局》(待填坑)
這裏先推薦兩個簡單的多佈局開源庫:
監聽滾動狀態
監聽滾動狀態需要使用RecyclerView的addOnScrollListener
方法,示例代碼如下:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//滑動狀態發生改變
//newState的可能值:[SCROLL_STATE_IDLE|SCROLL_STATE_DRAGGING|SCROLL_STATE_SETTLING]
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//滑動過程中將反覆觸發
//dx:水平方向的滑動距離
//dy:垂直方向的滑動距離
}
});
onScrollStateChanged方法會在滑動狀態發生改變時回調,newState有三種三種取值,含義如下:
- SCROLL_STATE_IDLE:靜止狀態
- SCROLL_STATE_DRAGGING:滑動狀態(用戶此時觸碰着屏幕且在滑動)
- SCROLL_STATE_SETTLING:慣性滑動狀態(用戶此時未觸碰屏幕,RecyclerView藉助上一次滑動的慣性滑動)
onScrolled方法會在滑動過程中將反覆觸發,dx和dy的含義如下:
- dx:水平方向的滑動距離。如果dx大於0,代表手指向左滑動;如果dx小於0,代表手指向右滑動。如果RecyclerView是垂直佈局(只能上下滑動),則dx始終爲0。
- dy:垂直方向的滑動距離。如果dy大於0,代表手指向上滑動;如果dy小於0,代表手指向下滑動。如果RecyclerView是水平佈局(只能左右滑動),則dy始終爲0。
注意:如果可見列表項發生了變化,onScrolled方法也會回調,此時dx和dy都爲0。
判斷RecyclerView是否已經滾動到底部或頂部
需要使用的關鍵方法是canScrollVertically
,該方法的原型如下:
//direction:傳入正數代表是否還能向下滾動;傳入負數代表是否還能向上滾動
public boolean canScrollVertically(int direction);
比如調用recyclerView.canScrollVertically(1),返回false就代表RecyclerView已經滾動到底部;調用recyclerView.canScrollVertically(-1),返回false就表示RecyclerView已經滾動到頂部。
同理,canScrollHorizontally
用於判斷RecyclerView是否已經滾動到最左端或最右端。
//direction:傳入正數代表是否還能向右滾動;傳入負數代表是否還能向左滾動
public boolean canScrollHorizontally(int direction);
比如調用recyclerView.canScrollHorizontally(1),返回false就代表RecyclerView已經滾動到最右端;調用recyclerView.canScrollHorizontally(-1),返回false就表示RecyclerView已經滾動到最左端。
更多博客
《Android UI ListView講解》:詳細講解ListView的使用和常用技巧。
《 Android UI GridView講解》:詳細講解GridView的使用方法和常用技巧。
《 Android UI 常用控件講解》:包括CheckBox、RadioButton、ToggleButton、Switch、ProgressBar、SeekBar、RatingBar、Spinner、ImageButton。
demo下載地址
相關的開源庫
動畫效果庫:recyclerview-animators
列表項裝飾器:RecyclerItemDecoration
參考資料
https://blog.csdn.net/qq_26585943/article/details/73739427
https://blog.csdn.net/lmj623565791/article/details/45059587
https://stackoverflow.com/questions/33176336/need-an-example-about-recyclerview-adapter-notifyitemchangedint-position-objec
https://www.jianshu.com/p/ce347cf991db