安卓開發筆記(十):升級ListView為RecylerView的使用


概述

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,可以參考:

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); // 實例化viewholder ViewHolder viewHolder = new ViewHolder(v); return viewHolder; } @Override public void onBindViewHolder(final ViewHolder holder, int position) { // 綁定數據 holder.mTv.setText(mData.get(position)); //③ 對RecyclerView的每一個itemView設置點擊事件 holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { if(onItemClickListener != null) { int pos = holder.getLayoutPosition(); onItemClickListener.onItemClick(holder.itemView, pos); } } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if(onItemClickListener != null) { int pos = holder.getLayoutPosition(); onItemClickListener.onItemLongClick(holder.itemView, pos); } //表示此事件已經消費,不會觸發單擊事件 return true; } }); } @Override public int getItemCount() { return mData == null ? 0 : mData.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { TextView mTv; public ViewHolder(View itemView) { super(itemView); mTv = (TextView) itemView.findViewById(R.id.item_tv); } } } 

設置Adapter的事件監聽。

mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() { @Override public void onItemClick(View view, int position) { Toast.makeText(MDRvActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show(); } @Override public void onItemLongClick(View view, int position) { Toast.makeText(MDRvActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show(); } }); 

最后的實現效果。

 

 





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM