【騰訊Bugly干貨分享】RecyclerView 必知必會


本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:http://mp.weixin.qq.com/s/CzrKotyupXbYY6EY2HP_dA

導語

RecyclerView是Android 5.0提出的新UI控件,可以用來代替傳統的ListView。

Bugly之前也發過一篇相關文章,講解了 RecyclerView 與 ListView 在緩存機制上的一些區別:

Android ListView 與 RecyclerView 對比淺析--緩存機制

今天精神哥來給大家詳細介紹關於 RecyclerView,你需要了解的方方面面。

本文來自騰訊 天天P圖團隊——damonxia(夏正冬),Android工程師

前言

下文中Demo的源代碼地址:RecyclerViewDemo

  • 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 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!


免責聲明!

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



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