本文來自於騰訊Bugly公衆號(weixinBugly),未經做者贊成,請勿轉載,原文地址:http://mp.weixin.qq.com/s/CzrKotyupXbYY6EY2HP_dAhtml
RecyclerView是Android 5.0提出的新UI控件,能夠用來代替傳統的ListView。java
Bugly以前也發過一篇相關文章,講解了 RecyclerView 與 ListView 在緩存機制上的一些區別:android
今天精神哥來給你們詳細介紹關於 RecyclerView,你須要瞭解的方方面面。github
本文來自騰訊 每天P圖團隊——damonxia(夏正冬),Android工程師設計模式
下文中Demo的源代碼地址:RecyclerViewDemo。緩存
setEmptyView()
。RecyclerView是Android 5.0提出的新UI控件,位於support-v7包中,能夠經過在build.gradle中添加compile 'com.android.support:recyclerview-v7:24.2.1'
導入。微信
RecyclerView的官方定義以下:app
A flexible view for providing a limited window into a large data set.ide
從定義能夠看出,flexible(可擴展性)是RecyclerView的特色。不過咱們發現和ListView有點像,本文後面會介紹RecyclerView和ListView的區別。
RecyclerView並不會徹底替代ListView(這點從ListView沒有被標記爲@Deprecated能夠看出),二者的使用場景不同。可是RecyclerView的出現會讓不少開源項目被廢棄,例如橫向滾動的ListView, 橫向滾動的GridView, 瀑布流控件,由於RecyclerView可以實現全部這些功能。
好比有一個需求是屏幕豎着的時候的顯示形式是ListView,屏幕橫着的時候的顯示形式是2列的GridView,此時若是用RecyclerView,則經過設置LayoutManager一行代碼實現替換。
ListView相比RecyclerView,有一些優勢:
addHeaderView()
, addFooterView()
添加頭視圖和尾視圖。setOnItemClickListener()
和setOnItemLongClickListener()
設置點擊事件和長按事件。這些功能在RecyclerView中都沒有直接的接口,要本身實現(雖然實現起來很簡單),所以若是隻是實現簡單的顯示功能,ListView無疑更簡單。
RecyclerView相比ListView,有一些明顯的優勢:
if(convertView == null)
的實現,並且回收機制更加完善。RecyclerView是一個插件式的實現,對各個功能進行解耦,從而擴展性比較好。
咱們都知道ListView經過adapter.notifyDataSetChanged()
實現ListView的更新,這種更新方法的缺點是全局更新,即對每一個Item View都進行重繪。但事實上不少時候,咱們只是更新了其中一個Item的數據,其餘Item其實能夠不須要重繪。
這裏給出ListView實現局部更新的方法:
public void updateItemView(ListView listview, int position, Data data){ int firstPos = listview.getFirstVisiblePosition(); int lastPos = listview.getLastVisiblePosition(); if(position >= firstPos && position <= lastPos){ //可見才更新,不可見則在getView()時更新 //listview.getChildAt(i)得到的是當前可見的第i個item的view View view = listview.getChildAt(position - firstPos); VH vh = (VH)view.getTag(); vh.text.setText(data.text); } }
能夠看出,咱們經過ListView的getChildAt()
來得到須要更新的View,而後經過getTag()
得到ViewHolder,從而實現更新。
RecyclerView的標準實現步驟以下:
RecyclerView.Adapter<VH>
的Adapter類(VH是ViewHolder的類名),記爲NormalAdapter。RecyclerView.ViewHolder
的靜態內部類,記爲VH。ViewHolder的實現和ListView的ViewHolder實現幾乎同樣。VH onCreateViewHolder(ViewGroup parent, int viewType)
: 映射Item Layout Id,建立VH並返回。void onBindViewHolder(VH holder, int position)
: 爲holder設置指定數據。int getItemCount()
: 返回Item的個數。能夠看出,RecyclerView將ListView中getView()
的功能拆分紅了onCreateViewHolder()
和onBindViewHolder()
。
基本的Adapter實現以下:
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.VH>{ private List<String> mDatas; public NormalAdapter(List<String> data) { this.mDatas = data; } @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) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false); return new VH(v); } public static class VH extends RecyclerView.ViewHolder{ public final TextView title; public VH(View v) { super(v); title = (TextView) v.findViewById(R.id.title); } } }
建立完Adapter,接着對RecyclerView進行設置,通常來講,須要爲RecyclerView進行四大設置,也就是後文說的四大組成:Adapter(必選),Layout Manager(必選),Item Decoration(可選,默認爲空), Item Animator(可選,默認爲DefaultItemAnimator)。
須要注意的是在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);
若是要實現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));
ListView只提供了notifyDataSetChanged()
更新整個視圖,這是很不合理的。RecyclerView提供了notifyItemInserted()
,notifyItemRemoved()
,notifyItemChanged()
等API更新單個或某個範圍的Item視圖。
RecyclerView的四大組成是:
Adapter的使用方式前面已經介紹了,功能就是爲RecyclerView提供數據,這裏主要介紹萬能適配器的實現。其實萬能適配器的概念在ListView就已經存在了,即base-adapter-helper。
這裏咱們只針對RecyclerView,聊聊萬能適配器出現的緣由。爲了建立一個RecyclerView的Adapter,每次咱們都須要去作重複勞動,包括重寫onCreateViewHolder()
,getItemCount()
、建立ViewHolder,而且實現過程大同小異,所以萬能適配器出現了,他能經過如下方式快捷地建立一個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; } } 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; } } };
這裏講解下萬能適配器的實現思路。
咱們經過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{...} }
其中:
getLayoutId(int viewType)
是根據viewType返回佈局ID。convert()
作具體的bind操做。就這樣,萬能適配器實現完成了。
RecyclerView經過addItemDecoration()
方法添加item之間的分割線。Android並無提供實現好的Divider,所以任何分割線樣式都須要本身實現。
方法是:建立一個類並繼承RecyclerView.ItemDecoration,重寫如下兩個方法:
Google在sample中給了一個參考的實現類:DividerItemDecoration,這裏咱們經過分析這個例子來看如何自定義Item Decoration。
首先看構造函數,構造函數中得到系統屬性android:listDivider
,該屬性是一個Drawable對象。
所以若是要設置,則須要在value/styles.xml中設置:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:listDivider">@drawable/item_divider</item> </style>
接着來看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; //... }
這裏咱們只考慮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這麼麻煩。
LayoutManager負責RecyclerView的佈局,其中包含了Item View的獲取與回收。這裏咱們簡單分析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可以經過mRecyclerView.setItemAnimator(ItemAnimator animator)
設置添加、刪除、移動、改變的動畫效果。RecyclerView提供了默認的ItemAnimator實現類:DefaultItemAnimator。這裏咱們經過分析DefaultItemAnimator的源碼來介紹如何自定義Item Animator。
DefaultItemAnimator繼承自SimpleItemAnimator,SimpleItemAnimator繼承自ItemAnimator。
首先咱們介紹ItemAnimator類的幾個重要方法:
notifyItemChanged()
和notifyDataSetChanged()
的狀況下佈局發生改變時被調用。notifyItemChanged()
或notifyDataSetChanged()
時被調用。animateXxx()
返回true。上面用斜體字標識的方法比較難懂,不過不要緊,由於Android提供了SimpleItemAnimator類(繼承自ItemAnimator),該類提供了一系列更易懂的API,在自定義Item Animator時只須要繼承SimpleItemAnimator便可:
notifyItemChanged()
或notifyDataSetChanged()
時被調用。對於以上四個方法,注意兩點:
runPendingAnimations()
中)須要調用dispatchXxxStarting(holder)
,執行完後須要調用dispatchXxxFinished(holder)
。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() { //一、判斷是否有動畫要執行,即各個動畫的成員變量裏是否有值。 //二、執行remove動畫 //三、執行move動畫 //四、執行change動畫,與move動畫並行執行 //五、執行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(); }
從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同樣提供setOnItemClickListener()
接口,而RecyclerView沒法添加onItemClickListener最佳的高效解決方案這篇文章給出了經過recyclerView.addOnItemTouchListener(...)
添加點擊事件的方法,但我認爲根本沒有必要費這麼大勁對外暴露這個接口,由於咱們徹底能夠把點擊事件的實現寫在Adapter的onBindViewHolder()
中,不暴露出來。具體方法就是經過:
public void onBindViewHolder(VH holder, int position) { holder.itemView.setOnClickListener(...); }
RecyclerView默認沒有提供相似addHeaderView()
和addFooterView()
的API,所以這裏介紹如何優雅地實現這兩個接口。
若是你已經實現了一個Adapter,如今想爲這個Adapter添加addHeaderView()
和addFooterView()
接口,則須要在Adapter中添加幾個Item Type,而後修改getItemViewType()
,onCreateViewHolder()
,onBindViewHolder()
,getItemCount()
等方法,並添加switch語句進行判斷。那麼如何在不破壞原有Adapter實現的狀況下完成呢?
這裏引入裝飾器(Decorator)設計模式,該設計模式經過組合的方式,在不破話原有類代碼的狀況下,對原有類的功能進行擴展。
這偏偏知足了咱們的需求。咱們只須要經過如下方式爲原有的Adapter(這裏命名爲NormalAdapter)添加addHeaderView()
和addFooterView()
接口:
NormalAdapter adapter = new NormalAdapter(data); NormalAdapterWrapper newAdapter = new NormalAdapterWrapper(adapter); View headerView = LayoutInflater.from(this).inflate(R.layout.item_header, mRecyclerView, false); View footerView = LayoutInflater.from(this).inflate(R.layout.item_footer, mRecyclerView, false); newAdapter.addFooterView(footerView); newAdapter.addHeaderView(headerView); mRecyclerView.setAdapter(newAdapter);
是否是看起來特別優雅。具體實現思路其實很簡單,建立一個繼承RecyclerView.Adapter<RecyclerView.ViewHolder>
的類,並重寫常見的方法,而後經過引入ITEM TYPE的方式實現:
public class NormalAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder>{ enum ITEM_TYPE{ HEADER, FOOTER, NORMAL } private NormalAdapter mAdapter; private View mHeaderView; private View mFooterView; public NormalAdapterWrapper(NormalAdapter adapter){ mAdapter = adapter; } @Override public int getItemViewType(int position) { if(position == 0){ return ITEM_TYPE.HEADER.ordinal(); } else if(position == mAdapter.getItemCount() + 1){ return ITEM_TYPE.FOOTER.ordinal(); } else{ return ITEM_TYPE.NORMAL.ordinal(); } } @Override public int getItemCount() { return mAdapter.getItemCount() + 2; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(position == 0){ return; } else if(position == mAdapter.getItemCount() + 1){ return; } else{ mAdapter.onBindViewHolder(((NormalAdapter.VH)holder), position - 1); } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(viewType == ITEM_TYPE.HEADER.ordinal()){ return new RecyclerView.ViewHolder(mHeaderView) {}; } else if(viewType == ITEM_TYPE.FOOTER.ordinal()){ return new RecyclerView.ViewHolder(mFooterView) {}; } else{ return mAdapter.onCreateViewHolder(parent,viewType); } } public void addHeaderView(View view){ this.mHeaderView = view; } public void addFooterView(View view){ this.mFooterView = view; } }
ListView提供了setEmptyView()
設置Adapter數據爲空時的View視圖。RecyclerView雖然沒提供直接的API,可是也能夠很簡單地實現。
getRootView().addView(emptyView)
將空數據時顯示的View添加到當前View的層次結構中。具體實現以下:
public class EmptyRecyclerView extends RecyclerView{ private View mEmptyView; private AdapterDataObserver mObserver = new AdapterDataObserver() { @Override public void onChanged() { Adapter adapter = getAdapter(); if(adapter.getItemCount() == 0){ mEmptyView.setVisibility(VISIBLE); EmptyRecyclerView.this.setVisibility(GONE); } else{ mEmptyView.setVisibility(GONE); EmptyRecyclerView.this.setVisibility(VISIBLE); } } public void onItemRangeChanged(int positionStart, int itemCount) {onChanged();} public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {onChanged();} public void onItemRangeRemoved(int positionStart, int itemCount) {onChanged();} public void onItemRangeInserted(int positionStart, int itemCount) {onChanged();} public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {onChanged();} }; public EmptyRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setEmptyView(View view){ this.mEmptyView = view; ((ViewGroup)this.getRootView()).addView(mEmptyView); //加入主界面佈局 } public void setAdapter(RecyclerView.Adapter adapter){ super.setAdapter(adapter); adapter.registerAdapterDataObserver(mObserver); mObserver.onChanged(); } }
Android提供了ItemTouchHelper類,使得RecyclerView可以輕易地實現滑動和拖拽,此處咱們要實現上下拖拽和側滑刪除。首先建立一個繼承自ItemTouchHelper.Callback
的類,並重寫如下方法:
getMovementFlags()
: 設置支持的拖拽和滑動的方向,此處咱們支持的拖拽方向爲上下,滑動方向爲從左到右和從右到左,內部經過makeMovementFlags()
設置。onMove()
: 拖拽時回調。onSwiped()
: 滑動時回調。onSelectedChanged()
: 狀態變化時回調,一共有三個狀態,分別是ACTION_STATE_IDLE(空閒狀態),ACTION_STATE_SWIPE(滑動狀態),ACTION_STATE_DRAG(拖拽狀態)。此方法中能夠作一些狀態變化時的處理,好比拖拽的時候修改背景色。clearView()
: 用戶交互結束時回調。此方法能夠作一些狀態的清空,好比拖拽結束後還原背景色。isLongPressDragEnabled()
: 是否支持長按拖拽,默認爲true。若是不想支持長按拖拽,則重寫並返回false。具體實現以下:
public class SimpleItemTouchCallback extends ItemTouchHelper.Callback { private NormalAdapter mAdapter; private List<ObjectModel> mData; public SimpleItemTouchCallback(NormalAdapter adapter, List<ObjectModel> data){ mAdapter = adapter; mData = data; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //s上下拖拽 int swipeFlag = ItemTouchHelper.START | ItemTouchHelper.END; //左->右和右->左滑動 return makeMovementFlags(dragFlag,swipeFlag); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); Collections.swap(mData, from, to); mAdapter.notifyItemMoved(from, to); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int pos = viewHolder.getAdapterPosition(); mData.remove(pos); mAdapter.notifyItemRemoved(pos); } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { super.onSelectedChanged(viewHolder, actionState); if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){ NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder; holder.itemView.setBackgroundColor(0xffbcbcbc); //設置拖拽和側滑時的背景色 } } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder; holder.itemView.setBackgroundColor(0xffeeeeee); //背景色還原 } }
而後經過如下代碼爲RecyclerView設置該滑動、拖拽功能:
ItemTouchHelper helper = new ItemTouchHelper(new SimpleItemTouchCallback(adapter, data)); helper.attachToRecyclerView(recyclerview);
前面拖拽的觸發方式只有長按,若是想支持觸摸Item中的某個View實現拖拽,則核心方法爲helper.startDrag(holder)
。首先定義接口:
interface OnStartDragListener{ void startDrag(RecyclerView.ViewHolder holder); }
而後讓Activity實現該接口:
public MainActivity extends Activity implements OnStartDragListener{ ... public void startDrag(RecyclerView.ViewHolder holder) { mHelper.startDrag(holder); } }
若是要對ViewHolder的text對象支持觸摸拖拽,則在Adapter中的onBindViewHolder()
中添加:
holder.text.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ mListener.startDrag(holder); } return false; } });
其中mListener是在建立Adapter時將實現OnStartDragListener接口的Activity對象做爲參數傳進來。
ListView爲了保證Item View的複用,實現了一套回收機制,該回收機制的實現類是RecycleBin,他實現了兩級緩存:
View[] mActiveViews
: 緩存屏幕上的View,在該緩存裏的View不須要調用getView()
。ArrayList<View>[] mScrapViews;
: 每一個Item Type對應一個列表做爲回收站,緩存因爲滾動而消失的View,此處的View若是被複用,會以參數的形式傳給getView()
。接下來咱們經過源碼分析ListView是如何與RecycleBin交互的。其實ListView和RecyclerView的layout過程大同小異,ListView的佈局函數是layoutChildren()
,實現以下:
void layoutChildren(){ //1. 若是數據被改變了,則將全部Item View回收至scrapView //(而RecyclerView會根據狀況放入Scrap Heap或RecyclePool);不然回收至mActiveViews if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } //2. 填充 switch(){ case LAYOUT_XXX: fillXxx(); break; case LAYOUT_XXX: fillXxx(); break; } //3. 回收多餘的activeView mRecycler.scrapActiveViews(); }
其中fillXxx()
實現了對Item View進行填充,該方法內部調用了makeAndAddView()
,實現以下:
View makeAndAddView(){ if (!mDataChanged) { child = mRecycler.getActiveView(position); if (child != null) { return child; } } child = obtainView(position, mIsScrap); return child; }
其中,getActiveView()
是從mActiveViews中獲取合適的View,若是獲取到了,則直接返回,而不調用obtainView()
,這也印證了若是從mActiveViews獲取到了可複用的View,則不須要調用getView()
。
obtainView()
是從mScrapViews中獲取合適的View,而後以參數形式傳給了getView()
,實現以下:
View obtainView(int position){ final View scrapView = mRecycler.getScrapView(position); //從RecycleBin中獲取複用的View final View child = mAdapter.getView(position, scrapView, this); }
接下去咱們介紹getScrapView(position)
的實現,該方法經過position獲得Item Type,而後根據Item Type從mScrapViews獲取可複用的View,若是獲取不到,則返回null,具體實現以下:
class RecycleBin{ private View[] mActiveViews; //存儲屏幕上的View private ArrayList<View>[] mScrapViews; //每一個item type對應一個ArrayList private int mViewTypeCount; //item type的個數 private ArrayList<View> mCurrentScrap; //mScrapViews[0] View getScrapView(int position) { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0) { return null; } if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } return null; } private View retrieveFromScrap(ArrayList<View> scrapViews, int position){ int size = scrapViews.size(); if(size > 0){ return scrapView.remove(scrapViews.size() - 1); //從回收列表中取出最後一個元素複用 } else{ return null; } } }
RecyclerView和ListView的回收機制很是類似,可是ListView是以View做爲單位進行回收,RecyclerView是以ViewHolder做爲單位進行回收。Recycler是RecyclerView回收機制的實現類,他實現了四級緩存:
getView()
。在上文Layout Manager中已經介紹了RecyclerView的layout過程,可是一筆帶過了getViewForPosition()
,所以此處介紹該方法的實現。
View getViewForPosition(int position, boolean dryRun){ if(holder == null){ //從mAttachedScrap,mCachedViews獲取ViewHolder holder = getScrapViewForPosition(position,INVALID,dryRun); //此處得到的View不須要bind } final int type = mAdapter.getItemViewType(offsetPosition); if (mAdapter.hasStableIds()) { //默認爲false holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } if(holder == null && mViewCacheExtension != null){ final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type); //從 if(view != null){ holder = getChildViewHolder(view); } } if(holder == null){ holder = getRecycledViewPool().getRecycledView(type); } if(holder == null){ //沒有緩存,則建立 holder = mAdapter.createViewHolder(RecyclerView.this, type); //調用onCreateViewHolder() } if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){ mAdapter.bindViewHolder(holder, offsetPosition); } return holder.itemView; }
從上述實現能夠看出,依次從mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool尋找可複用的ViewHolder,若是是從mAttachedScrap或mCachedViews中獲取的ViewHolder,則不會調用onBindViewHolder()
,mAttachedScrap和mCachedViews也就是咱們所說的Scrap Heap;而若是從mViewCacheExtension或mRecyclerPool中獲取的ViewHolder,則會調用onBindViewHolder()
。
RecyclerView局部刷新的實現原理也是基於RecyclerView的回收機制,即能直接複用的ViewHolder就不調用onBindViewHolder()
。
Android 5.0推出了嵌套滑動機制,在以前,一旦子View處理了觸摸事件,父View就沒有機會再處理此次的觸摸事件,而嵌套滑動機制解決了這個問題,可以實現以下效果:
爲了支持嵌套滑動,子View必須實現NestedScrollingChild接口,父View必須實現NestedScrollingParent接口,而RecyclerView實現了NestedScrollingChild接口,而CoordinatorLayout實現了NestedScrollingParent接口,上圖是實現CoordinatorLayout嵌套RecyclerView的效果。
爲了實現上圖的效果,須要用到的組件有:
實現中須要注意的點有:
app:layout_collapseMode
設置爲pin,表示摺疊以後固定在頂端,而爲ImageView的app:layout_collapseMode
設置爲parallax,表示視差模式,即漸變的效果。app:layout_behavior="@string/appbar_scrolling_view_behavior"
。app:layout_scrollFlags="scroll|exitUntilCollapsed"
,其中scroll表示滾動出屏幕,exitUntilCollapsed表示退出後摺疊。具體實現參見Demo6。
回顧整篇文章,發現咱們已經實現了RecyclerView的不少擴展功能,包括:打造萬能適配器、添加Item事件、添加頭視圖和尾視圖、設置空佈局、側滑拖拽。BaseRecyclerViewAdapterHelper是一個比較火的RecyclerView擴展庫,仔細一看發現,這裏面80%的功能在咱們這篇文章中都實現了。
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!