【騰訊Bugly乾貨分享】RecyclerView 必知必會
導語
RecyclerView是Android 5.0提出的新UI控制元件,可以用來代替傳統的ListView。
Bugly之前也發過一篇相關文章,講解了 RecyclerView 與 ListView 在快取機制上的一些區別:
今天精神哥來給大家詳細介紹關於 RecyclerView,你需要了解的方方面面。
本文來自騰訊 天天P圖團隊——damonxia(夏正冬),Android工程師
前言
- Demo1: RecyclerView新增HeaderView和FooterView,ItemDecoration範例。
- Demo2: ListView實現區域性重新整理。
- Demo3: RecyclerView實現拖拽、側滑刪除。
- Demo4: RecyclerView閃屏問題。
- Demo5: RecyclerView實現
setEmptyView()
。 - Demo6: RecyclerView實現萬能介面卡,瀑布流佈局,巢狀滑動機制。
基本概念
RecyclerView是Android 5.0提出的新UI控制元件,位於support-v7包中,可以通過在build.gradle中新增compile 'com.android.support:recyclerview-v7:24.2.1'
匯入。
RecyclerView的官方定義如下:
A flexible view for providing a limited window into a large data set.
從定義可以看出,flexible(可擴充套件性)是RecyclerView的特點。不過我們發現和ListView有點像,本文後面會介紹RecyclerView和ListView的區別。
為什麼會出現RecyclerView?
RecyclerView並不會完全替代ListView(這點從ListView沒有被標記為@Deprecated可以看出),兩者的使用場景不一樣。但是RecyclerView的出現會讓很多開源專案被廢棄,例如橫向滾動的ListView, 橫向滾動的GridView, 瀑布流控制元件,因為RecyclerView能夠實現所有這些功能。
比如有一個需求是螢幕豎著的時候的顯示形式是ListView,螢幕橫著的時候的顯示形式是2列的GridView,此時如果用RecyclerView,則通過設定LayoutManager一行程式碼實現替換。
ListView vs RecyclerView
ListView相比RecyclerView,有一些優點:
addHeaderView()
,addFooterView()
新增頭檢視和尾檢視。- 通過”android:divider”設定自定義分割線。
setOnItemClickListener()
和setOnItemLongClickListener()
設定點選事件和長按事件。
這些功能在RecyclerView中都沒有直接的介面,要自己實現(雖然實現起來很簡單),因此如果只是實現簡單的顯示功能,ListView無疑更簡單。
RecyclerView相比ListView,有一些明顯的優點:
- 預設已經實現了View的複用,不需要類似
if(convertView == null)
的實現,而且回收機制更加完善。 - 預設支援區域性重新整理。
- 容易實現新增item、刪除item的動畫效果。
- 容易實現拖拽、側滑刪除等功能。
RecyclerView是一個外掛式的實現,對各個功能進行解耦,從而擴充套件性比較好。
ListView實現區域性重新整理
我們都知道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的標準實現步驟如下:
- 建立Adapter:建立一個繼承
RecyclerView.Adapter<VH>
的Adapter類(VH是ViewHolder的類名),記為NormalAdapter。 - 建立ViewHolder:在NormalAdapter中建立一個繼承
RecyclerView.ViewHolder
的靜態內部類,記為VH。ViewHolder的實現和ListView的ViewHolder實現幾乎一樣。 - 在NormalAdapter中實現:
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:為Item提供資料。
- Layout Manager:Item的佈局。
- Item Animator:新增、刪除Item動畫。
- Item Decoration:Item之間的Divider。
Adapter
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操作。
就這樣,萬能介面卡實現完成了。
Item Decoration
RecyclerView通過addItemDecoration()
方法新增item之間的分割線。Android並沒有提供實現好的Divider,因此任何分割線樣式都需要自己實現。
方法是:建立一個類並繼承RecyclerView.ItemDecoration,重寫以下兩個方法:
- onDraw(): 繪製分割線。
- getItemOffsets(): 設定分割線的寬、高。
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這麼麻煩。
Layout Manager
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,可以參考:
Item Animator
RecyclerView能夠通過mRecyclerView.setItemAnimator(ItemAnimator animator)
設定新增、刪除、移動、改變的動畫效果。RecyclerView提供了預設的ItemAnimator實現類: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();
}
從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
新增setOnItemClickListener介面
RecyclerView預設沒有像ListView一樣提供setOnItemClickListener()
介面,而RecyclerView無法新增onItemClickListener最佳的高效解決方案這篇文章給出了通過recyclerView.addOnItemTouchListener(...)
新增點選事件的方法,但我認為根本沒有必要費這麼大勁對外暴露這個介面,因為我們完全可以把點選事件的實現寫在Adapter的onBindViewHolder()
中,不暴露出來。具體方法就是通過:
public void onBindViewHolder(VH holder, int position) {
holder.itemView.setOnClickListener(...);
}
新增HeaderView和FooterView
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;
}
}
新增setEmptyView
ListView提供了setEmptyView()
設定Adapter資料為空時的View檢視。RecyclerView雖然沒提供直接的API,但是也可以很簡單地實現。
- 建立一個繼承RecyclerView的類,記為EmptyRecyclerView。
- 通過
getRootView().addView(emptyView)
將空資料時顯示的View新增到當前View的層次結構中。 - 通過AdapterDataObserver監聽RecyclerView的資料變化,如果adapter為空,那麼隱藏RecyclerView,顯示EmptyView。
具體實現如下:
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回收機制
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回收機制
RecyclerView和ListView的回收機制非常相似,但是ListView是以View作為單位進行回收,RecyclerView是以ViewHolder作為單位進行回收。Recycler是RecyclerView回收機制的實現類,他實現了四級快取:
- mAttachedScrap: 快取在螢幕上的ViewHolder。
- mCachedViews: 快取螢幕外的ViewHolder,預設為2個。ListView對於螢幕外的快取都會呼叫
getView()
。 - mViewCacheExtensions: 需要使用者定製,預設不實現。
- mRecyclerPool: 快取池,多個RecyclerView共用。
在上文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的效果。
為了實現上圖的效果,需要用到的元件有:
- CoordinatorLayout: 佈局根元素。
- AppBarLayout: 包裹的內容作為應用的Bar。
- CollapsingToolbarLayout: 實現可摺疊的ToolBar。
- ToolBar: 代替ActionBar。
實現中需要注意的點有:
- 我們為ToolBar的
app:layout_collapseMode
設定為pin,表示摺疊之後固定在頂端,而為ImageView的app:layout_collapseMode
設定為parallax,表示視差模式,即漸變的效果。 - 為了讓RecyclerView支援巢狀滑動,還需要為它設定
app:layout_behavior="@string/appbar_scrolling_view_behavior"
。 - 為CollapsingToolbarLayout設定
app:layout_scrollFlags="scroll|exitUntilCollapsed"
,其中scroll表示滾動出螢幕,exitUntilCollapsed表示退出後摺疊。
具體實現參見Demo6。
回顧
回顧整篇文章,發現我們已經實現了RecyclerView的很多擴充套件功能,包括:打造萬能介面卡、新增Item事件、新增頭檢視和尾檢視、設定空佈局、側滑拖拽。BaseRecyclerViewAdapterHelper是一個比較火的RecyclerView擴充套件庫,仔細一看發現,這裡面80%的功能在我們這篇文章中都實現了。
擴充套件閱讀
更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!