概述 RecyclerView是什麼 從Android 5.0開始,谷歌公司推出了一個用於大量數據展示的新控制項RecylerView,可以用來代替傳統的ListView,更加強大和靈活。RecyclerView的官方定義如下: A flexible view for providing a limi ...
概述
RecyclerView是什麼
從Android 5.0開始,谷歌公司推出了一個用於大量數據展示的新控制項RecylerView,可以用來代替傳統的ListView,更加強大和靈活。RecyclerView的官方定義如下:
A flexible view for providing a limited window into a large data set.
從定義可以看出,flexible(可擴展性)是RecyclerView的特點。
RecyclerView是support-v7包中的新組件,是一個強大的滑動組件,與經典的ListView相比,同樣擁有item回收復用的功能,這一點從它的名字Recyclerview即回收view也可以看出。
RecyclerView的優點
RecyclerView並不會完全替代ListView(這點從ListView沒有被標記為@Deprecated可以看出),兩者的使用場景不一樣。但是RecyclerView的出現會讓很多開源項目被廢棄,例如橫向滾動的ListView, 橫向滾動的GridView, 瀑布流控制項,因為RecyclerView能夠實現所有這些功能。
比如:有一個需求是屏幕豎著的時候的顯示形式是ListView,屏幕橫著的時候的顯示形式是2列的GridView,此時如果用RecyclerView,則通過設置LayoutManager一行代碼實現替換。
RecylerView相對於ListView的優點羅列如下:
- RecyclerView封裝了viewholder的回收復用,也就是說RecyclerView標準化了ViewHolder,編寫Adapter面向的是ViewHolder而不再是View了,復用的邏輯被封裝了,寫起來更加簡單。
直接省去了listview中convertView.setTag(holder)和convertView.getTag()這些繁瑣的步驟。 - 提供了一種插拔式的體驗,高度的解耦,異常的靈活,針對一個Item的顯示RecyclerView專門抽取出了相應的類,來控制Item的顯示,使其的擴展性非常強。
- 設置佈局管理器以控制Item的佈局方式,橫向、豎向以及瀑布流方式
例如:你想控制橫向或者縱向滑動列表效果可以通過LinearLayoutManager這個類來進行控制(與GridView效果對應的是GridLayoutManager,與瀑布流對應的還StaggeredGridLayoutManager等)。也就是說RecyclerView不再拘泥於ListView的線性展示方式,它也可以實現GridView的效果等多種效果。 - 可設置Item的間隔樣式(可繪製)
通過繼承RecyclerView的ItemDecoration這個類,然後針對自己的業務需求去書寫代碼。 - 可以控制Item增刪的動畫,可以通過ItemAnimator這個類進行控制,當然針對增刪的動畫,RecyclerView有其自己預設的實現。
但是關於Item的點擊和長按事件,需要用戶自己去實現。
基本使用
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this );
//設置佈局管理器
recyclerView.setLayoutManager(layoutManager);
//設置為垂直佈局,這也是預設的
layoutManager.setOrientation(OrientationHelper. VERTICAL);
//設置Adapter
recyclerView.setAdapter(recycleAdapter);
//設置分隔線
recyclerView.addItemDecoration( new DividerGridItemDecoration(this ));
//設置增加或刪除條目的動畫
recyclerView.setItemAnimator( new DefaultItemAnimator());
在使用RecyclerView時候,必須指定一個適配器Adapter和一個佈局管理器LayoutManager。適配器繼承RecyclerView.Adapter
類,具體實現類似ListView的適配器,取決於數據信息以及展示的UI。佈局管理器用於確定RecyclerView中Item的展示方式以及決定何時復用已經不可見的Item,避免重覆創建以及執行高成本的findViewById()
方法。
可以看見RecyclerView相比ListView會多出許多操作,這也是RecyclerView靈活的地方,它將許多動能暴露出來,用戶可以選擇性的自定義屬性以滿足需求。
基本使用
引用
在build.gradle文件中引入該類。
compile 'com.android.support:recyclerview-v7:23.4.0'
佈局
Activity佈局文件activity_rv.xml
...
Item的佈局文件item_1.xml
...
創建適配器
標準實現步驟如下:
① 創建Adapter:創建一個繼承RecyclerView.Adapter<VH>
的Adapter類(VH是ViewHolder的類名)
② 創建ViewHolder:在Adapter中創建一個繼承RecyclerView.ViewHolder
的靜態內部類,記為VH。ViewHolder的實現和ListView的ViewHolder實現幾乎一樣。
③ 在Adapter中實現3個方法:
-
onCreateViewHolder()
這個方法主要生成為每個Item inflater出一個View,但是該方法返回的是一個ViewHolder。該方法把View直接封裝在ViewHolder中,然後我們面向的是ViewHolder這個實例,當然這個ViewHolder需要我們自己去編寫。
需要註意的是在onCreateViewHolder()
中,映射Layout必須為
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
而不能是:
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
- onBindViewHolder()
這個方法主要用於適配渲染數據到View中。方法提供給你了一viewHolder而不是原來的convertView。 -
getItemCount()
這個方法就類似於BaseAdapter的getCount方法了,即總共有多少個條目。
可以看出,RecyclerView將ListView中getView()
的功能拆分成了onCreateViewHolder()
和onBindViewHolder()
。
基本的Adapter實現如下:
// ① 創建Adapter
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.VH>{
//② 創建ViewHolder
public static class VH extends RecyclerView.ViewHolder{
public final TextView title;
public VH(View v) {
super(v);
title = (TextView) v.findViewById(R.id.title);
}
}
private List<String> mDatas;
public NormalAdapter(List<String> data) {
this.mDatas = data;
}
//③ 在Adapter中實現3個方法
@Override
public void onBindViewHolder(VH holder, int position) {
holder.title.setText(mDatas.get(position));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//item 點擊事件
}
});
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
//LayoutInflater.from指定寫法
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
return new VH(v);
}
}
設置RecyclerView
創建完Adapter,接著對RecyclerView進行設置,一般來說,需要為RecyclerView進行四大設置,也就是後文說的四大組成:
- Layout Manager(必選)
- Adapter(必選)
- Item Decoration(可選,預設為空)
- Item Animator(可選,預設為DefaultItemAnimator)
如果要實現ListView的效果,只需要設置Adapter和Layout Manager,如下:
List<String> data = initData();
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new NormalAdapter(data));
四大組成
RecyclerView的四大組成是:
- Layout Manager:Item的佈局。
- Adapter:為Item提供數據。
- Item Decoration:Item之間的Divider。
- Item Animator:添加、刪除Item動畫。
Layout Manager佈局管理器
在最開始就提到,RecyclerView 能夠支持各種各樣的佈局效果,這是 ListView 所不具有的功能,那麼這個功能如何實現的呢?其核心關鍵在於 RecyclerView.LayoutManager 類中。從前面的基礎使用可以看到,RecyclerView 在使用過程中要比 ListView 多一個 setLayoutManager 步驟,這個 LayoutManager 就是用於控制我們 RecyclerView 最終的展示效果的。
LayoutManager負責RecyclerView的佈局,其中包含了Item View的獲取與回收。
RecyclerView提供了三種佈局管理器:
- LinerLayoutManager 以垂直或者水平列表方式展示Item
- GridLayoutManager 以網格方式展示Item
- StaggeredGridLayoutManager 以瀑布流方式展示Item
如果你想用 RecyclerView 來實現自己自定義效果,則應該去繼承實現自己的 LayoutManager,並重寫相應的方法,而不應該想著去改寫 RecyclerView。
LayoutManager 常見 API
關於 LayoutManager 的使用有下麵一些常見的 API(有些在 LayoutManager 實現的子類中)
canScrollHorizontally();//能否橫向滾動
canScrollVertically();//能否縱向滾動
scrollToPosition(int position);//滾動到指定位置
setOrientation(int orientation);//設置滾動的方向
getOrientation();//獲取滾動方向
findViewByPosition(int position);//獲取指定位置的Item View
findFirstCompletelyVisibleItemPosition();//獲取第一個完全可見的Item位置
findFirstVisibleItemPosition();//獲取第一個可見Item的位置
findLastCompletelyVisibleItemPosition();//獲取最後一個完全可見的Item位置
findLastVisibleItemPosition();//獲取最後一個可見Item的位置
上面僅僅是列出一些常用的 API 而已,更多的 API 可以查看官方文檔,通常你想用 RecyclerView 實現某種效果,例如指定滾動到某個 Item 位置,但是你在 RecyclerView 中又找不到可以調用的 API 時,就可以跑到 LayoutManager 的文檔去看看,基本都在那裡。
另外還有一點關於瀑布流佈局效果 StaggeredGridLayoutManager 想說的,看到網上有些文章寫的示例代碼,在設置了 StaggeredGridLayoutManager 後仍要去 Adapter 中動態設置 View 的高度,才能實現瀑布流,這種做法是完全錯誤的,之所以 StaggeredGridLayoutManager 的瀑布流效果出不來,基本是 item 佈局的 xml 問題以及數據問題導致。如果要在 Adapter 中設置 View 的高度,則完全違背了 LayoutManager 的設計理念了。
LinearLayoutManager源碼分析
這裡我們簡單分析LinearLayoutManager的實現。
對於LinearLayoutManager來說,比較重要的幾個方法有:
-
onLayoutChildren()
: 對RecyclerView進行佈局的入口方法。 -
fill()
: 負責填充RecyclerView。 -
scrollVerticallyBy()
:根據手指的移動滑動一定距離,並調用fill()
填充。 -
canScrollVertically()
或canScrollHorizontally()
: 判斷是否支持縱向滑動或橫向滑動。
onLayoutChildren()
的核心實現如下:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler); //將原來所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool
fill(recycler, mLayoutState, state, false); //填充現在所有的Item View
}
RecyclerView的回收機制有個重要的概念,即將回收站分為Scrap Heap和Recycle Pool,其中Scrap Heap的元素可以被直接復用,而不需要調用onBindViewHolder()
。detachAndScrapAttachedViews()
會根據情況,將原來的Item View放入Scrap Heap或Recycle Pool,從而在復用時提升效率。
fill()
是對剩餘空間不斷地調用layoutChunk()
,直到填充完為止。layoutChunk()
的核心實現如下:
public void layoutChunk() {
View view = layoutState.next(recycler); //調用了getViewForPosition()
addView(view); //加入View
measureChildWithMargins(view, 0, 0); //計算View的大小
layoutDecoratedWithMargins(view, left, top, right, bottom); //佈局View
}
其中next()
調用了getViewForPosition(currentPosition)
,該方法是從RecyclerView的回收機制實現類Recycler中獲取合適的View,在後文的回收機制中會介紹該方法的具體實現。
如果要自定義LayoutManager,可以參考:
- 創建一個 RecyclerView LayoutManager – Part 1
- 創建一個 RecyclerView LayoutManager – Part 2
- 創建一個 RecyclerView LayoutManager – Part 3
Adapter適配器
Adapter的使用方式前面已經介紹了,功能就是為RecyclerView提供數據,這裡主要介紹萬能適配器的實現。其實萬能適配器的概念在ListView就已經存在了,即base-adapter-helper。
這裡我們只針對RecyclerView,聊聊萬能適配器出現的原因。為了創建一個RecyclerView的Adapter,每次我們都需要去做重覆勞動,包括重寫onCreateViewHolder()
,getItemCount()
、創建ViewHolder,並且實現過程大同小異,因此萬能適配器出現了。
萬能適配器
這裡講解下萬能適配器的實現思路。
我們通過public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>
定義萬能適配器QuickAdapter類,T是列表數據中每個元素的類型,QuickAdapter.VH是QuickAdapter的ViewHolder實現類,稱為萬能ViewHolder。
首先介紹QuickAdapter.VH的實現:
static class VH extends RecyclerView.ViewHolder{
private SparseArray<View> mViews;
private View mConvertView;
private VH(View v){
super(v);
mConvertView = v;
mViews = new SparseArray<>();
}
public static VH get(ViewGroup parent, int layoutId){
View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
return new VH(convertView);
}
public <T extends View> T getView(int id){
View v = mViews.get(id);
if(v == null){
v = mConvertView.findViewById(id);
mViews.put(id, v);
}
return (T)v;
}
public void setText(int id, String value){
TextView view = getView(id);
view.setText(value);
}
}
其中的關鍵點在於通過SparseArray<View>
存儲item view的控制項,getView(int id)
的功能就是通過id獲得對應的View(首先在mViews中查詢是否存在,如果沒有,那麼findViewById()
並放入mViews中,避免下次再執行findViewById()
)。
QuickAdapter的實現如下:
public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>{
private List<T> mDatas;
public QuickAdapter(List<T> datas){
this.mDatas = datas;
}
public abstract int getLayoutId(int viewType);
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
return VH.get(parent,getLayoutId(viewType));
}
@Override
public void onBindViewHolder(VH holder, int position) {
convert(holder, mDatas.get(position), position);
}
@Override
public int getItemCount() {
return mDatas.size();
}
public abstract void convert(VH holder, T data, int position);
static class VH extends RecyclerView.ViewHolder{
private SparseArray<View> mViews;
private View mConvertView;
private VH(View v){
super(v);
mConvertView = v;
mViews = new SparseArray<>();
}
public static VH get(ViewGroup parent, int layoutId){
View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
return new VH(convertView);
}
public <T extends View> T getView(int id){
View v = mViews.get(id);
if(v == null){
v = mConvertView.findViewById(id);
mViews.put(id, v);
}
return (T)v;
}
public void setText(int id, String value){
TextView view = getView(id);
view.setText(value);
}
}
}
其中:
getLayoutId(int viewType)
是根據viewType返回佈局ID。convert()
做具體的bind操作。
就這樣,萬能適配器實現完成了。
通過萬能適配器能通過以下方式快捷地創建一個Adapter:
mAdapter = new QuickAdapter<String>(data) {
@Override
public int getLayoutId(int viewType) {
return R.layout.item;
}
@Override
public void convert(VH holder, String data, int position) {
holder.setText(R.id.text, data);
//holder.itemView.setOnClickListener(); 此處還可以添加點擊事件
}
};
是不是很方便。當然複雜情況也可以輕鬆解決。
mAdapter = new QuickAdapter<Model>(data) {
@Override
public int getLayoutId(int viewType) {
switch(viewType){
case TYPE_1:
return R.layout.item_1;
case TYPE_2:
return R.layout.item_2;
}
}
@Override
public int getItemViewType(int position) {
if(position % 2 == 0){
return TYPE_1;
} else{
return TYPE_2;
}
}
@Override
public void convert(VH holder, Model data, int position) {
int type = getItemViewType(position);
switch(type){
case TYPE_1:
holder.setText(R.id.text, data.text);
break;
case TYPE_2:
holder.setImage(R.id.image, data.image);
break;
}
}
};
Item Decoration間隔樣式
RecyclerView通過addItemDecoration()
方法添加item之間的分割線。Android並沒有提供實現好的Divider,因此任何分割線樣式都需要自己實現。
自定義間隔樣式需要繼承RecyclerView.ItemDecoration
類,該類是個抽象類,官方目前並沒有提供預設的實現類,主要有三個方法。
- onDraw(Canvas c, RecyclerView parent, State state),在Item繪製之前被調用,該方法主要用於繪製間隔樣式。
- onDrawOver(Canvas c, RecyclerView parent, State state),在Item繪製之前被調用,該方法主要用於繪製間隔樣式。
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),設置item的偏移量,偏移的部分用於填充間隔樣式,即設置分割線的寬、高;在RecyclerView的
onMesure()
中會調用該方法。
onDraw()
和onDrawOver()
這兩個方法都是用於繪製間隔樣式,我們只需要覆寫其中一個方法即可。
Google在sample中給了一個參考的實現類:DividerItemDecoration,這裡我們通過分析這個例子來看如何自定義Item Decoration。
自定義的間隔樣式的實現步驟
- ① 通過讀取系統主題中的 Android.R.attr.listDivider作為Item間的分割線,並且支持橫向和縱向。
該分割線是系統預設的,你可以在theme.xml中找到該屬性(android:listDivider)的使用情況。
如果要設置,則需要在value/styles.xml中設置:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:listDivider">@drawable/item_divider</item>
</style>
- ② 獲取到listDivider以後,該屬性的值是個Drawable,在getItemOffsets中,outRect去設置了繪製的範圍。
- ③ onDraw中實現了真正的繪製。
① 獲取listDivider
首先看構造函數,構造函數中獲得系統屬性android:listDivider
,該屬性是一個Drawable對象。
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Drawable mDivider;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
② getItemOffsets
接著來看getItemOffsets()
的實現:
public void getItemOffsets(Rect outRect, int position, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
這裡只看mOrientation == VERTICAL_LIST
的情況,outRect是當前item四周的間距,類似margin屬性,現在設置了該item下間距為mDivider.getIntrinsicHeight()
。
那麼getItemOffsets()
是怎麼被調用的呢?
RecyclerView繼承了ViewGroup,並重寫了measureChild()
,該方法在onMeasure()
中被調用,用來計算每個child的大小,計算每個child大小的時候就需要加上getItemOffsets()
設置的外間距:
public void measureChild(View child, int widthUsed, int heightUsed){
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//調用getItemOffsets()獲得Rect對象
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//...
}
③ onDraw
這裡我們只考慮mOrientation == VERTICAL_LIST
的情況,DividerItemDecoration的onDraw()
實際上調用了drawVertical()
:
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
// 畫每個item的分割線
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);/*規定好左上角和右下角*/
mDivider.draw(c);
}
}
那麼onDraw()
是怎麼被調用的呢?還有ItemDecoration還有一個方法onDrawOver()
,該方法也可以被重寫,那麼onDraw()
和onDrawOver()
之間有什麼關係呢?
我們來看下麵的代碼:
class RecyclerView extends ViewGroup{
public void draw(Canvas c) {
super.draw(c); //調用View的draw(),該方法會先調用onDraw(),再調用dispatchDraw()繪製children
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
}
根據View的繪製流程,首先調用RecyclerView重寫的draw()
方法,隨後super.draw()
即調用View的draw()
,該方法會先調用onDraw()
(這個方法在RecyclerView重寫了),再調用dispatchDraw()
繪製children。因此:ItemDecoration的onDraw()
在繪製Item之前調用,ItemDecoration的onDrawOver()
在繪製Item之後調用。
當然,如果只需要實現Item之間相隔一定距離,那麼只需要為Item的佈局設置margin即可,沒必要自己實現ItemDecoration這麼麻煩。
Item Animator動畫
RecyclerView能夠通過mRecyclerView.setItemAnimator(ItemAnimator animator)
設置添加、刪除、移動、改變的動畫效果。
RecyclerView提供了預設的ItemAnimator實現類:DefaultItemAnimator。如果沒有特殊的需求,預設使用這個動畫即可。
// 設置Item添加和移除的動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
下麵就添加一下刪除和添加Item的動作。在Adapter裡面添加方法。
public void addNewItem() {
if(mData == null) {
mData = new ArrayList<>();
}
mData.add(0, "new Item");
////更新數據集不是用adapter.notifyDataSetChanged()而是notifyItemInserted(position)與notifyItemRemoved(position) 否則沒有動畫效果。
notifyItemInserted(0);
}
public void deleteItem() {
if(mData == null || mData.isEmpty()) {
return;
}
mData.remove(0);
notifyItemRemoved(0);
}
添加事件的處理。
public void onClick(View v) {
int id = v.getId();
if(id == R.id.rv_add_item_btn) {
mAdapter.addNewItem();
// 由於Adapter內部是直接在首個Item位置做增加操作,增加完畢後列表移動到首個Item位置
mLayoutManager.scrollToPosition(0);
} else if(id == R.id.rv_del_item_btn){
mAdapter.deleteItem();
// 由於Adapter內部是直接在首個Item位置做刪除操作,刪除完畢後列表移動到首個Item位置
mLayoutManager.scrollToPosition(0);
}
}
準備工作完畢後,來看一下運行的效果。
DefaultItemAnimator源碼分析
這裡我們通過分析DefaultItemAnimator的源碼來介紹如何自定義Item Animator。
DefaultItemAnimator繼承自SimpleItemAnimator,SimpleItemAnimator繼承自ItemAnimator。
首先我們介紹ItemAnimator類的幾個重要方法:
- animateAppearance(): 當ViewHolder出現在屏幕上時被調用(可能是add或move)。
- animateDisappearance(): 當ViewHolder消失在屏幕上時被調用(可能是remove或move)。
- animatePersistence(): 在沒調用
notifyItemChanged()
和notifyDataSetChanged()
的情況下佈局發生改變時被調用。 - animateChange(): 在顯式調用
notifyItemChanged()
或notifyDataSetChanged()
時被調用。 - runPendingAnimations(): RecyclerView動畫的執行方式並不是立即執行,而是每幀執行一次,比如兩幀之間添加了多個Item,則會將這些將要執行的動畫Pending住,保存在成員變數中,等到下一幀一起執行。該方法執行的前提是前面
animateXxx()
返回true。 - isRunning(): 是否有動畫要執行或正在執行。
- dispatchAnimationsFinished(): 當全部動畫執行完畢時被調用。
上面的方法比較難懂,不過沒關係,因為Android提供了SimpleItemAnimator類(繼承自ItemAnimator),該類提供了一系列更易懂的API,在自定義Item Animator時只需要繼承SimpleItemAnimator即可:
- animateAdd(ViewHolder holder): 當Item添加時被調用。
- animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 當Item移動時被調用。
- animateRemove(ViewHolder holder): 當Item刪除時被調用。
- animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 當顯式調用
notifyItemChanged()
或notifyDataSetChanged()
時被調用。
對於以上四個方法,註意兩點:
- 當Xxx動畫開始執行前(在
runPendingAnimations()
中)需要調用dispatchXxxStarting(holder)
,執行完後需要調用dispatchXxxFinished(holder)
。 - 這些方法的內部實際上並不是書寫執行動畫的代碼,而是將需要執行動畫的Item全部存入成員變數中,並且返回值為true,然後在
runPendingAnimations()
中一併執行。
DefaultItemAnimator類是RecyclerView提供的預設動畫類。我們通過閱讀該類源碼學習如何自定義Item Animator。我們先看DefaultItemAnimator的成員變數:
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();//存放下一幀要執行的一系列add動畫
ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();//存放正在執行的一批add動畫
ArrayList<ViewHolder> mAddAnimations = new ArrayList<>(); //存放當前正在執行的add動畫
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
DefaultItemAnimator實現了SimpleItemAnimator的animateAdd()
方法,該方法只是將該item添加到mPendingAdditions中,等到runPendingAnimations()
中執行。
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder); //重置清空所有動畫
ViewCompat.setAlpha(holder.itemView, 0); //將要做動畫的View先變成透明
mPendingAdditions.add(holder);
return true;
}
接著看runPendingAnimations()
的實現,該方法是執行remove,move,change,add動畫,執行順序為:remove動畫最先執行,隨後move和change並行執行,最後是add動畫。為了簡化,我們將remove,move,change動畫執行過程省略,只看執行add動畫的過程,如下:
public void runPendingAnimations() {
//1、判斷是否有動畫要執行,即各個動畫的成員變數里是否有值。
//2、執行remove動畫
//3、執行move動畫
//4、執行change動畫,與move動畫並行執行
//5、執行add動畫
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
@Override
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder); //***** 執行動畫的方法 *****
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); //等remove,move,change動畫全部做完後,開始執行add動畫
}
}
}
為了防止在執行add動畫時外面有新的add動畫添加到mPendingAdditions中,從而導致執行add動畫錯亂,這裡將mPendingAdditions的內容移動到局部變數additions中,然後遍歷additions執行動畫。
在runPendingAnimations()
中,animateAddImpl()
是執行add動畫的具體方法,其實就是將itemView的透明度從0變到1(在animateAdd()
中已經將view的透明度變為0),實現如下:
void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder); //在開始add動畫前調用
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder); //在結束add動畫後調用
mAddAnimations.remove(holder);
if (!isRunning()) {
dispatchAnimationsFinished(); //結束所有動畫後調用
}
}
}).start();
}
開源動畫recyclerview-animators
從DefaultItemAnimator類的實現來看,發現自定義Item Animator好麻煩,需要繼承SimpleItemAnimator類,然後實現一堆方法。
別急,recyclerview-animators解救你,原因如下:
- 首先,recyclerview-animators提供了一系列的Animator,比如FadeInAnimator,ScaleInAnimator。
- 其次,如果該庫中沒有你滿意的動畫,該庫提供了BaseItemAnimator類,該類繼承自SimpleItemAnimator,進一步封裝了自定義Item Animator的代碼,使得自定義Item Animator更方便,你只需要關註動畫本身。如果要實現DefaultItemAnimator的代碼,只需要以下實現:
public class DefaultItemAnimator extends BaseItemAnimator {
public DefaultItemAnimator() {
}
public DefaultItemAnimator(Interpolator interpolator) {
mInterpolator = interpolator;
}
@Override protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.alpha(0)
.setDuration(getRemoveDuration())
.setListener(new DefaultRemoveVpaListener(holder))
.setStartDelay(getRemoveDelay(holder))
.start();
}
@Override protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
ViewCompat.setAlpha(holder.itemView, 0); //透明度先變為0
}
@Override protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.alpha(1)
.setDuration(getAddDuration())
.setListener(new DefaultAddVpaListener(holder))
.setStartDelay(getAddDelay(holder))
.start();
}
}
是不是比繼承SimpleItemAnimator方便多了。
局部刷新閃屏問題解決
對於RecyclerView的Item Animator,有一個常見的坑就是“閃屏問題”。
這個問題的描述是:當Item視圖中有圖片和文字,當更新文字並調用notifyItemChanged()
時,文字改變的同時圖片會閃一下。這個問題的原因是當調用notifyItemChanged()
時,會調用DefaultItemAnimator的animateChangeImpl()
執行change動畫,該動畫會使得Item的透明度從0變為1,從而造成閃屏。
解決辦法很簡單,在rv.setAdapter()
之前調用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)
禁用change動畫。
點擊事件
RecyclerView並沒有像ListView一樣暴露出Item點擊事件或者長按事件處理的api,也就是說使用RecyclerView時候,需要我們自己來實現Item的點擊和長按等事件的處理。
實現方法有很多:
- 可以監聽RecyclerView的Touch事件然後判斷手勢做相應的處理,
- 也可以通過在綁定ViewHolder的時候設置監聽,然後通過Apater回調出去
我們選擇第二種方法,更加直觀和簡單。
看一下Adapter的完整代碼。
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
// 展示數據
private ArrayList<String> mData;
// 事件回調監聽
private MyAdapter.OnItemClickListener onItemClickListener;
public MyAdapter(ArrayList<String> data) {
this.mData = data;
}
public void updateData(ArrayList<String> data) {
this.mData = data;
notifyDataSetChanged();
}
// 添加新的Item
public void addNewItem() {
if(mData == null) {
mData = new ArrayList<>();
}
mData.add(0, "new Item");
notifyItemInserted(0);
}
// 刪除Item
public void deleteItem() {
if(mData == null || mData.isEmpty()) {
return;
}
mData.remove(0);
notifyItemRemoved(0);
}
// ① 定義點擊回調介面
public interface OnItemClickListener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
// ② 定義一個設置點擊監聽器的方法
public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
this.onItemClickListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 實例化展示的view
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
<