RecyclerView技術棧
來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0307/4031.html
原文:小?子的簡書
概述
隨著2014年Google IO的召開,Android L Preview版隨之發布,對于開發著來說,帶來了性能上的改善,而對于消費者來說,得到了體驗上的提升。我想,無論是開發者還是使用者,一定都非常喜歡這次的版本跟新。
同時,這次也帶來了兩個全新的View控件:RecyclerView和CardView。這篇文章將重點介紹RecyclerView,它有許多內部類和接口。接下來,我將介紹它們的功能,已經如何使用。
當然,在這之前,我要聲明的是:RecyclerView 是Support Library的一部分。所以只需要在app/build.gradle
中添加以下依賴,便能立即使用:
dependencies { compile 'com.android.support:recyclerview-v7:23.2.0' }
然后點擊“Sync Project with Gradle files”,讓IDE去下載適當的資源文件。
為什么命名為RecyclerView?
先讓我們來看看Google在L Preview中是如何定義RecyclerView的:
A flexible view for providing a limited window into a large data set.
(能夠在有限的窗口中展示大數據集合的靈活視圖。)
所以我們能夠理解為,RecyclerView一個恰當的使用場景是:由于尺寸限制,用戶的設備不能一次性展現所有條目,用戶需要上下滾動以查看更多條目。滾出可見區域的條目將被回收,并在下一個條目可見的時候被復用。
我們可以從下圖中得到更直觀的解釋:
左邊的圖是數據初始化后的示例,當向上滾動視圖的時候,當條目不可見之后將被回收。右圖中紅色區域內的兩條不可見條目,將被放到緩存隊列中以便新的條目可見時進行復用。
對于減少內存開銷和CPU的計算,緩存條目是一個非常有用的方法,因為這意味著我們不必每次都創建新的條目,從而減小內存開銷和CPU的計算,而且還能夠有效降低屏幕的卡頓,保證滑動的順滑和16ms準則。
看到這里,你可能不禁會問:并沒有什么新東西啊,這和ListView有什么區別呀?我們已經使用ListView很長一段時間了呀,它一樣可以做到呀。不過,視圖回收本身并不是什么新鮮事。但是回想之前我們寫的ListView,無論從它的的性能表現著手,還是語法的書寫,甚至數據的綁定都未免略顯臃腫。那么現在,我們將再也不會出現上述癥狀,因為Google提供了一個更好,更靈活的控件――RecyclerView。
OK,從現在開始,讓我們一步一步,開始了解它。
結構
如果你想使用RecyclerView,需要做以下操作:
-
RecyclerView.Adapter
- 處理數據集合并負責綁定視圖 -
ViewHolder
- 持有所有的用于綁定數據或者需要操作的View -
LayoutManager
- 負責擺放視圖等相關操作 -
ItemDecoration
- 負責繪制Item附近的分割線 -
ItemAnimator
- 為Item的一般操作添加動畫效果,如,增刪條目等
我們可以從下圖更直觀的了解到RecyclerView的基本結構:
由此可見,想要在ListView中實現條目的增刪動畫是一件非常困難的事情,但是RecyclerView為我們提供了很好的便利。而且RecyclerView增強了ViewHolder設計模式,這在當前所使用的ListView中是不曾有的。
與傳統ListView比較
RecyclerView與老前輩ListView的不同點,主要在于以下幾個特性:
-
Adapter中的ViewHolder模式 - 對于ListView來說,通過創建
ViewHolder
來提升性能并不是必須的。因為ListView并沒有嚴格的ViewHolder設計模式。但是在使用RecyclerView的時候,Adapter
必須實現至少一個ViewHolder,必須遵循ViewHolder設計模式。 -
定制Item條目 - ListView只能實現垂直線性排列的列表視圖,與之不同的是,RecyclerView可以通過設置RecyclerView.LayoutManager來定制不同風格的視圖,比如水平滾動列表或者不規則的瀑布流列表。
-
Item動畫 - 在ListView中沒有提供任何方法或者接口,方便開發者實現Item的增刪動畫。相反地,可以通過設置RecyclerView的
RecyclerView.ItemAnimator
來為條目增加動畫效果。 -
設置數據源 - 在LisView中針對不同數據封裝了各種類型的
Adapter
,比如用來處理數組的ArrayAdapter
和用來展示Database結果的CursorAdapter
。相反地,在RecyclerView中必須自定義實現RecyclerView.Adapter
并為其提供數據集合。 -
設置條目分割線 - 在ListView中可以通過設置
android:divider
屬性來為兩個Item間設置分割線。如果想為RecyclerView添加此效果,則必須使用RecyclerView.ItemDecoration
,這種實現方式不僅更靈活,而且樣式也更加豐富。 -
設置點擊事件 - 在ListView中存在
AdapterView.OnItemClickListener
接口,用來綁定條目的點擊事件。但是,很遺憾的是在RecyclerView中,并沒有提供這樣的接口,不過,提供了另外一個接口RcyclerView.OnItemTouchListener
,用來響應條目的觸摸事件。
RecyclerView組件
RecyclerView.Adapter
確切的說,Adapter扮演著兩個角色。一是,根據不同ViewType創建與之相應的的Item-Layout,二是,訪問數據集合并將數據綁定到正確的View上。這就需要我們重寫以下兩個函數:
-
public VH onCreateViewHolder(ViewGroup parent, int viewType)
創建Item視圖,并返回相應的ViewHolder -
public void onBindViewHolder(VH holder, int position)
綁定數據到正確的Item視圖上。
另外我們還需要重寫另一個方法,像ListView-Adapter那樣,同樣地告訴RecyclerView-Adapter列表Items的總數:
-
public int getItemCount()
返回該Adapter所持有的Itme數量
RecyclerView.ViewHolder
ViewHolder的基本用法是用來存放View對象。Android團隊很早之前就推薦使用“ViewHolder設計模式”,但實際上他們并沒有把這種概念強加給開發者,而且也沒有要求開發者在Adapter中必須使用ViewHolder pattern。那么現在對于這種新型的RecyclerView.Adapter,我們必須實現并使用它。
另外值得一提的是,可以通過打印ViewHolder.toString
來獲取更多有效信息:
@Override public String toString() { final StringBuilder sb = new StringBuilder("ViewHolder{" + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); if (isScrap()) sb.append(" scrap"); if (isInvalid()) sb.append(" invalid"); if (!isBound()) sb.append(" unbound"); if (needsUpdate()) sb.append(" update"); if (isRemoved()) sb.append(" removed"); if (shouldIgnore()) sb.append(" ignored"); if (isChanged()) sb.append(" changed"); if (isTmpDetached()) sb.append(" tmpDetached"); if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")"); if (isAdapterPositionUnknown()) sb.append("undefined adapter position"); if (itemView.getParent() == null) sb.append(" no parent"); sb.append("}"); return sb.toString(); }
因此,一個基本的RecyclerView.Adapter
如下:
public class SimplerItemAdapter extends RecyclerView.Adapter<SimplerItemAdapter.SimpleItemViewHolder> { private List<String> items; public SimplerItemAdapter(@NonNull List<String> dateItems) { this.items = (dateItems != null ? dateItems : new ArrayList<String>()); } @Override public SimpleItemViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false); return new SimpleItemViewHolder(itemView); } @Override public void onBindViewHolder(SimpleItemViewHolder viewHolder, int position) { viewHolder.textView.setText(items.get(position)); } @Override public int getItemCount() { return (this.items != null) ? this.items.size() : 0; } protected final static class SimpleItemViewHolder extends RecyclerView.ViewHolder { protected TextView textView; public SimpleItemViewHolder(View itemView) { super(itemView); this.textView = (TextView) itemView.findViewById(R.id.text); } } }
RecyclerView.LayoutManager
LayoutManager的職責是擺放Item的位置,并且負責決定何時回收和重用Item。
必須為RecyclerView指定LayoutManager,否則會出現以下異常:
AndroidRuntime java.lang.NullPointerException: Attempt to invoke virtual method ‘void android.support.v7.widget.RecyclerView$LayoutManager.onMeasure(android.support.v7.widget.RecyclerView$Recycler, android.support.v7.widget.RecyclerView$State, int, int)’ on a null object reference
-
LinearLayoutManager
水平或者垂直的Item視圖。 -
GridLayoutManager
網格Item視圖。 -
StaggeredGridLayoutManager
交錯的網格Item視圖。
當然還有一些很實用的API:
-
findFirstVisibleItemPosition()
返回當前第一個可見Item的position -
findFirstCompletelyVisibleItemPosition()
返回當前第一個完全可見Item的position -
findLastVisibleItemPosition()
返回當前最后一個可見Item的position -
findLastCompletelyVisibleItemPosition()
返回當前最后一個完全可見Item的position
LayoutManager當前有且僅有一個抽象函數:
public LayoutParams generateDefaultLayoutParams()
另外值得注意的是,自定義LayoutManager還應該實現以下方法:
/** * Scroll to the specified adapter position. * * Actual position of the item on the screen depends on the LayoutManager implementation. * @param position Scroll to this adapter position. */ public void scrollToPosition(int position) { if (DEBUG) { Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract"); } }
RecyclerView.ItemDecoration
通過設置recyclerView.addItemDecoration(new DividerDecoration(this));
來改變Item之間的偏移量或者對Item進行裝飾。
當然,你也可以對RecyclerView設置多個ItemDecoration
,列表展示的時候會遍歷所有的ItemDecoration
并調用里面的繪制方法,對Item進行裝飾。
RecyclerView.ItemDecoration
是一個抽象類,可以通過重寫以下三個方法,來實現Item之間的偏移量或者裝飾效果:
-
public void onDraw(Canvas c, RecyclerView parent)
裝飾的繪制在Item條目繪制之前調用,所以這有可能被Item的內容所遮擋 -
public void onDrawOver(Canvas c, RecyclerView parent)
裝飾的繪制在Item條目繪制之后調用,因此裝飾將浮于Item之上 -
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
與padding或margin類似,LayoutManager在測量階段會調用該方法,計算出每一個Item的正確尺寸并設置偏移量。
RecyclerView.ItemAnimator
ItemAnimator能夠幫助Item實現獨立的動畫。
ItemAnimator作觸發于以下三種事件:
-
某條數據被插入到數據集合中
-
從數據集合中移除某條數據
-
更改數據集合中的某條數據
幸運的是,在Android中默認實現了一個DefaultItemAnimator
,我們可以通過以下代碼為Item增加動畫效果:
recyclerView.setItemAnimator(new DefaultItemAnimator());
在之前的版本中,當時據集合發生改變時,我們通過調用.notifyDataSetChanged()
,來刷新列表,因為這樣做會觸發列表的重繪,所以并不會出現任何動畫效果,因此需要調用一些以notifyItem*()
作為前綴的特殊方法,比如:
-
public final void notifyItemInserted(int position)
向指定位置插入Item -
public final void notifyItemRemoved(int position)
移除指定位置Item -
public final void notifyItemChanged(int position)
更新指定位置Item
Listeners
很遺憾,RecyclerView
并沒有像ListView
那樣提供以下兩個Item的點擊監聽事件
-
public void setOnItemClickListener(@Nullable OnItemClickListener listener)
Item點擊事件監聽 -
public void setOnItemLongClickListener(OnItemLongClickListener listener)
Item長按事件監聽
但是存在這樣一個觸摸事件的監聽RecyclerView.OnItemTouchListener
雖然變得更靈活,但是對應的代碼量和書寫難度卻有了一定的增長,至少對我是這樣的。
至此,所有與本文章相關的代碼都可以從Github上獲取到,另外這個倉庫中還有一份本人精心制作的PPT,可供參考。
參考資料:
Codepath - Codepath Website
A First Glance at Android’s RecyclerView - WolframRittmeyer