1. 程式人生 > >Android開發之漫漫長途 XIV——RecyclerView

Android開發之漫漫長途 XIV——RecyclerView

計算 來看 就是 們的 anim nullable 源碼 添加 問題

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡量按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑒了其他的優質博客,在此向各位大神表示感謝,膜拜!!!


前言

上文我們很詳細的分析了ListView的使用、優化、及ListView的RecycleBin機制,讀者如果對ListView不太清楚,那麽請參看我的上篇博文。不過呢,Google Material Design提供的RecyclerView已經逐漸的取代ListView。RecyclerView提供了一種插拔式的體驗,高度的解耦,異常的靈活,通過設置它提供的不同LayoutManager,ItemDecoration , ItemAnimator實現令人瞠目的效果。

如果說上面的理由只是大而空泛的話,那我們來看以下場景

  1. 你想控制數據的顯示方式,列表顯示、網格顯示、瀑布流顯示等等,之前你需要ListView,GridView和自定義View,而現在你可以通過RecyclerView的布局管理器LayoutManager控制
  2. 你想要控制Item間的間隔(可繪制),想自定義更多樣式的分割線,之前你可以設置divider,那麽現在你可以使用RecyclerView的ItemDecoration,想怎麽畫怎麽畫。
  3. 你想要控制Item增刪的動畫,ListView呢我們只能自己通過屬性動畫來操作 Item 的視圖。RecyclerView可使用ItemAnimator
  4. 你想要局部刷新某個Item,對於ListView來說,我們知道notifyDataSetChanged 來通知視圖更新變化,但是該方法會重繪每個Item,而對於RecyclerView.Adapter 則提供了 notifyItemChanged 用於更新單個 Item View 的刷新,我們可以省去自己寫局部更新的工作。

除了上述場景外,RecyclerView強制使用了ViewHolder模式,我們知道ListView使用ViewHolder來進行性能優化,但是這不是必須得,但是在RecyclerView中是必須的,另外RecyclerView還有許多優勢,這裏就不一一列舉了,總體來說現在越來越多的項目使用RecyclerView,許多老舊項目也漸漸使用RecyclerView來替代ListView。

註:當我們想要一個列表顯示控件的時候,需要支持動畫,或者頻繁更新,局部刷新,建議使用RecyclerView,更加強大完善,易擴展;其他情況下ListView在使用上反而更加方便,快捷。

前言我們就講到這,那麽我們來進入正題。

RecyclerView的使用

作為一個“新”控件,RecyclerView的使用有許多需要註意的地方

RecyclerView的簡單使用

一樣的我們新建一個Demo來演示RecyclerView的使用

[RecyclerViewDemo1Activity.java]

public class RecyclerViewDemo1Activity extends AppCompatActivity {
    @BindView(R.id.recycler_view)
    RecyclerView mRecyclerView;

    private List<String> mData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_demo1_view);
        ButterKnife.bind(this);

        //LayoutManager必須指定,否則無法顯示數據,這裏指定為線性布局,
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        //虛擬數據
        mData = createDataList();
        //設置Adapter必須指定,否則數據怎麽顯示
        mRecyclerView.setAdapter(new RecyclerViewDemo1Adapter(mData));
    }

    protected List<String> createDataList() {
        mData = new ArrayList<>();
        for (int i=0;i<20;i++){
            mData.add("這是第"+i+"個View");
        }
        return mData;
    }

}

其對應的布局文件也很簡單activity_recycler_demo1_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </android.support.v7.widget.RecyclerView>


</LinearLayout>

那麽我們再來看RecyclerViewDemo1Adapter

/**
 * 與ListView的Adapter不同,RecyclerView的Adapter需要繼承RecyclerView.Adapter<VH>(VH是ViewHolder的類名)
 * 記為RecyclerViewDemo1Adapter。
 * 創建ViewHolder:在RecyclerViewDemo1Adapter中創建一個繼承RecyclerView.ViewHolder的靜態內部類,記為ViewHolder
 * (RecyclerView必須使用ViewHolder模式,這裏的ViewHolder實現幾乎與ListView優化時所使用的ViewHolder一致)
 * 在RecyclerViewDemo1Adapter中實現:
 *      ViewHolder onCreateViewHolder(ViewGroup parent, int viewType): 映射Item Layout Id,創建VH並返回。
 *      
 *      void onBindViewHolder(ViewHolder holder, int position): 為holder設置指定數據。
 *      
 *      int getItemCount(): 返回Item的個數。
 *      
 * 可以看出,RecyclerView將ListView中getView()的功能拆分成了onCreateViewHolder()和onBindViewHolder()。
 */
public class RecyclerViewDemo1Adapter extends RecyclerView.Adapter<RecyclerViewDemo1Adapter.ViewHolder> {

    private List<String> mData;

    public RecyclerViewDemo1Adapter(List<String> data) {
        this.mData = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater
                    .from(parent.getContext())
                    .inflate(R.layout.item_menu_main, parent, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.setData(this.mData.get(position));
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //item點擊事件
            }
        });
    }

    @Override
    public int getItemCount() {
        return this.mData != null ? this.mData.size() : 0;
    }

    static class ViewHolder extends RecyclerView.ViewHolder{
        private TextView mTextView;
        public ViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.tv_title);
        }

        public void setData(String title) {
            this.mTextView.setText(title);
        }
    }
}

需要註意的是RecyclerView沒有提供如ListView的setOnItemClickListener或者setOnItemLongClickListener之類的Item點擊事件,我們必須自己去實現該部分功能,實現的方法有很多種,也比較容易,本例中采用在Adapter中BindViewHolder綁定數據的時候為item設置了點擊事件。

小結

RecyclerView的四大組成分別是:

  • Adapter:為Item提供數據。必須提供,關於Adapter我們上面的代碼註釋已經說的很明白了
  • Layout Manager:Item的布局。必須提供,我們需要為RecyclerView指定一個布局管理器
  • Item Animator:添加、刪除Item動畫。可選提供,默認是DefaultItemAnimator
  • Item Decoration:Item之間的Divider。可選提供,默認是空

所以上面代碼的運行結果看起來像是是一個沒有分割線的ListView
技術分享圖片

RecyclerView的進階使用

上面的基本使用我們是會了,而且點擊Item也有反應了,不過巨醜無比啊有木有。起碼的分割線都沒有,真無語

為RecyclerView添加分割線

那麽如何創建分割線呢,
創建一個類並繼承RecyclerView.ItemDecoration,重寫以下兩個方法:

  • onDraw()或者onDrawOver: 繪制分割線。
  • getItemOffsets(): 設置分割線的寬、高。

然後使用RecyclerView通過addItemDecoration()方法添加item之間的分割線。
我們來看一下代碼

public class RecyclerViewDemo2Activity extends AppCompatActivity {
    @BindView(R.id.recycler_view)
    RecyclerView mRecyclerView;

    private List<String> mData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_demo1_view);
        ButterKnife.bind(this);

        //LayoutManager必須指定,否則無法顯示數據,這裏指定為線性布局,
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

        //虛擬數據
        mData = createDataList();

        //設置Adapter必須指定,否則數據怎麽顯示
        mRecyclerView.setAdapter(new RecyclerViewDemo2Adapter(mData));

        //設置分割線
        mRecyclerView.addItemDecoration(
            new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
    }

    protected List<String> createDataList() {
        mData = new ArrayList<>();
        for (int i=0;i<20;i++){
            mData.add("這是第"+i+"個View");
        }
        return mData;
    }

}

布局文件還跟上面的一致,代碼也大致相同,不過我們多了一行

//設置分割線
mRecyclerView.addItemDecoration(
    new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

這裏的DividerItemDecoration是Google給了一個參考的實現類,這裏我們通過分析這個例子來看如何自定義Item Decoration。

[DividerItemDecoration.java]

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
    public static final int VERTICAL = LinearLayout.VERTICAL;

    private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };

    private Drawable mDivider;

  
    private int mOrientation;

    private final Rect mBounds = new Rect();

    /**
     * 創建一個可使用於LinearLayoutManager的分割線
     *
     */
    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }



    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    @SuppressLint("NewApi")
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

首先看構造函數,構造函數中獲得系統屬性android:listDivider,該屬性是一個Drawable對象。接著設置mOrientation,我們這裏傳入的是DividerItemDecoration.VERTICAL。

上面我們就說了如何添加分割線,那麽作為實例,我們先看DividerItemDecoration的getItemOffsets方法

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
        RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

outRect是當前item四周的間距,類似margin屬性,現在設置了該item下間距為mDivider.getIntrinsicHeight()。

那麽getItemOffsets()是怎麽被調用的呢?

RecyclerView繼承了ViewGroup,並重寫了measureChild(),該方法在onMeasure()中被調用,用來計算每個child的大小,計算每個child大小的時候就需要加上getItemOffsets()設置的外間距:

public void measureChild(View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    ......
}

也就是說getItemOffsets()方法是確定分割線的大小的(這個大小指的是高度,寬度)

那麽接著onDraw()以及onDrawOver(),兩者的作用是什麽呢以及兩者之間有什麽關系呢?

public class RecyclerView extends ViewGroup {
    @Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ......
    }

    @Override
    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之後調用。

在RecyclerView的onDraw()方法中會得到分割線的數目,並循環調用其onDraw()方法,我們再來看分割線實例DividerItemDecoration的onDraw()方法

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getLayoutManager() == null) {
        return;
    }
    if (mOrientation == VERTICAL) {
        drawVertical(c, parent);
    } else {
        drawHorizontal(c, parent);
    }
}

這裏調用了drawVertical

@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
    canvas.save();
    final int left;
    final int right;
    if (parent.getClipToPadding()) {
        left = parent.getPaddingLeft();
        right = parent.getWidth() - parent.getPaddingRight();
        canvas.clipRect(left, parent.getPaddingTop(), right,
                parent.getHeight() - parent.getPaddingBottom());
    } else {
        left = 0;
        right = parent.getWidth();
    }

    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}

drawVertical的邏輯比較簡單,重要的代碼

 //為分割線設置bounds
 mDivider.setBounds(left, top, right, bottom);
 //畫出來
 mDivider.draw(canvas);

小結

在RecyclerView中添加分割線需要的操作已經在上文中比較詳細的說明了,這裏再總結一下。我們在為RecyclerView添加分割線的時候使用

//設置分割線
mRecyclerView.addItemDecoration(
    new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

其中addItemDecoration方法的參數即為分割線的實例,那麽如何創建分割線呢,
創建一個類並繼承RecyclerView.ItemDecoration,重寫以下兩個方法:

  • onDraw()或者onDrawOver: 繪制分割線。
  • getItemOffsets(): 設置分割線的寬、高。

為RecyclerView添加HeaderView以及FooterView

基本功能設計

RecyclerView沒有提供類似ListView的addHeaderView或者addFooterView方法,所以我們要自己實現。關於實現的方法也有很多種。目前網上能搜到的主流解決辦法是在Adapter中重寫getItemViewType方法為頭部或者底部布局生成特定的item。從而實現頭部布局以及底部布局。

本篇的解決辦法與上面的並無本質上的不同,只是我們在Adapter的外面再包上一層,以類似裝飾者設計模式的方式對Adapter進行無侵入式的包裝。

我們希望使用的方式比較簡單

//這個是真正的Adapter,在本例中不需要對其改變
mAdapter = new RecyclerViewDemo2Adapter(mData);
//包裝的wrapper,對Adapter進行包裝。實現添加Header以及Footer等的功能
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);

TextView t1 = new TextView(this);
t1.setText("Header 1");
TextView t2 = new TextView(this);
t2.setText("Header 2");
mHeaderAndFooterWrapper.addHeaderView(t1);
mHeaderAndFooterWrapper.addHeaderView(t2);

mRecyclerView.setAdapter(mHeaderAndFooterWrapper);
mHeaderAndFooterWrapper.notifyDataSetChanged();

我們下面先對HeaderAndFooterWrapper基本功能

public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
    //以較高的數值作為基數,每一個Header或者Footer對應不同的數值
    private static final int BASE_ITEM_TYPE_HEADER = 100000;
    private static final int BASE_ITEM_TYPE_FOOTER = 200000;
    
    //存儲Header和Footer的集合
    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
    
    //內部的真正的Adapter
    private RecyclerView.Adapter mInnerAdapter;

    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
    {
        mInnerAdapter = adapter;
    }

    private boolean isHeaderViewPos(int position)
    {
        return position < getHeadersCount();
    }

    private boolean isFooterViewPos(int position)
    {
        return position >= getHeadersCount() + getRealItemCount();
    }

    
    public void addHeaderView(View view)
    {
        mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
    }

    public void addFootView(View view)
    {
        mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
    }

    public int getHeadersCount()
    {
        return mHeaderViews.size();
    }

    public int getFootersCount()
    {
        return mFootViews.size();
    }
}

我們這裏使用SparseArrayCompat作為存儲Header和Footer的集合,SparseArrayCompat有什麽特點呢?它類似於Map,只不過在某些情況下比Map的性能要好,並且只能存儲key為int的情況。

我們這裏可以看到HeaderAndFooterWrapper是繼承於RecyclerView.Adapter

//真正進行數據處理以及展示的Adapter
mAdapter = new RecyclerViewDemo2Adapter(mData);
//添加Header以及Footer的wrapper
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
//設置空View的wrapper
mEmptyWrapperAdapter = new EmptyWrapper(mHeaderAndFooterWrapper);

mRecyclerView.setAdapter(mEmptyWrapperAdapter);

重寫相關方法

    public class HeaderAndFooterWrapper<T> extends 
        RecyclerView.Adapter<RecyclerView.ViewHolder>
{
    private static final int BASE_ITEM_TYPE_HEADER = 100000;
    private static final int BASE_ITEM_TYPE_FOOTER = 200000;
    //SparseArrayCompat類似於Map,其用法與map相似
    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();

    private RecyclerView.Adapter mInnerAdapter;

    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
    {
        mInnerAdapter = adapter;
    }

    /**
     * 重寫onCreateViewHolder,創建ViewHolder
     * @param parent 父容器,這裏指的是RecyclerView
     * @param viewType view的類型,用int表示,也是SparseArrayCompat的key
     * @return
     */
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        if (mHeaderViews.get(viewType) != null)
        {//如果以viewType為key獲取的View為null

            //創建ViewHolder並返回
            ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
            return holder;

        } else if (mFootViews.get(viewType) != null)
        {
            ViewHolder holder =  new ViewHolder(parent.getContext(), mFootViews.get(viewType));
            return holder;
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    /**
     * 獲得對應position的type
     * @param position
     * @return
     */
    @Override
    public int getItemViewType(int position)
    {
        if (isHeaderViewPos(position))
        {
            return mHeaderViews.keyAt(position);
        } else if (isFooterViewPos(position))
        {
            return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
        }
        return mInnerAdapter.getItemViewType(position - getHeadersCount());
    }

    private int getRealItemCount()
    {
        return mInnerAdapter.getItemCount();
    }

    /**
     * 綁定數據
     * @param holder
     * @param position
     */
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {
        if (isHeaderViewPos(position))
        {
            return;
        }
        if (isFooterViewPos(position))
        {
            return;
        }
        mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
    }

    /**
     * 得到item數量 (包括頭部布局數量和尾部布局數量)
     * @return
     */
    @Override
    public int getItemCount()
    {
        return getHeadersCount() + getFootersCount() + getRealItemCount();
    }


    private boolean isHeaderViewPos(int position)
    {
        return position < getHeadersCount();
    }

    private boolean isFooterViewPos(int position)
    {
        return position >= getHeadersCount() + getRealItemCount();
    }

    /**
    *以mHeaderViews.size() + BASE_ITEM_TYPE_HEADER為key,頭部布局View為Value
    *放入mHeaderViews
    */
    public void addHeaderView(View view)
    {
        mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
    }

    public void addFootView(View view)
    {
        mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
    }

    public int getHeadersCount()
    {
        return mHeaderViews.size();
    }

    public int getFootersCount()
    {
        return mFootViews.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        private View mConvertView;
        private Context mContext;

        public ViewHolder(Context context, View itemView) {
            super(itemView);
            mContext = context;
            mConvertView = itemView;
        }
    }
}

看上面的代碼,HeaderAndFooterWrapper繼承於RecyclerView.Adapter

/**
 * 重寫onCreateViewHolder,創建ViewHolder
 * @param parent 父容器,這裏指的是RecyclerView
 * @param viewType view的類型,用int表示,也是SparseArrayCompat的key
 * @return
 */
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
    if (mHeaderViews.get(viewType) != null)
    {//如果以viewType為key獲取的View為null

        //創建ViewHolder並返回
        ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
        return holder;

    } else if (mFootViews.get(viewType) != null)
    {
        ViewHolder holder =  new ViewHolder(parent.getContext(), mFootViews.get(viewType));
        return holder;
    }
    return mInnerAdapter.onCreateViewHolder(parent, viewType);
}

我們先看onCreateViewHolder方法,該方法返回ViewHolder,我們在其中為頭部以及底部布局單獨創建ViewHolder,對於普通的item,我們依然調用內部的mInnerAdapter的onCreateViewHolder方法

創建好ViewHolder後,便進行綁定的工作了

/**
 * 綁定數據
 * @param holder
 * @param position
 */
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    if (isHeaderViewPos(position))
    {
        return;
    }
    if (isFooterViewPos(position))
    {
        return;
    }
    mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}

這裏我們頭部以及底部布局不進行數據的綁定,其他普通的item依然調用內部真正的mInnerAdapter.onBindViewHolder

運行結果如下

技術分享圖片

適配GridLayoutManager

上面我們已經初步實現為RecyclerView添加Header以及Footer了,不過上面的我們的布局模式是LinearyLayoutManager,當我們使用GridLayoutManager時,效果就不是我們所想像的那樣了

//設置GridLayoutManager
mRecyclerView.setLayoutManager(new GridLayoutManager(this,3));

技術分享圖片

當我們設置GridLayoutManager時,可以看到頭部布局所展示的樣子,頭部布局還真的被當做一個普通的item布局了。那麽我們需要為這個布局做一些特殊處理。我們知道使用GridLayoutManager的SpanSizeLookup設置某個Item所占空間

在我們的HeaderAndFooterWrapper中重寫onAttachedToRecyclerView方法(該方法在Adapter與RecyclerView相關聯時回調),如下:

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
    mInnerAdapter.onAttachedToRecyclerView(recyclerView);

    RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager)
    {
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
        final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
        {
            @Override
            public int getSpanSize(int position)
            {
               int viewType = getItemViewType(position);
              if (mHeaderViews.get(viewType) != null)
              {
                  return layoutManager.getSpanCount();
              } else if (mFootViews.get(viewType) != null)
              {
                  return layoutManager.getSpanCount();
              }
              if (spanSizeLookup != null)
                  return spanSizeLookup.getSpanSize(position);
              return 1;
            }
        });
        gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
    }
}

當發現layoutManager為GridLayoutManager時,通過設置SpanSizeLookup,對其getSpanSize方法,返回值設置為layoutManager.getSpanCount();

適配StaggeredGridLayoutManager

mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, 
    OrientationHelper.VERTICAL));

當我們設置StaggeredGridLayoutManager時,可以看到如下效果
技術分享圖片

而針對於StaggeredGridLayoutManager,我們需要使用 StaggeredGridLayoutManager.LayoutParams

在我們的HeaderAndFooterWrapper中重寫onViewAttachedToWindow方法(該方法在Adapter與RecyclerView相關聯時回調),如下:

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
    mInnerAdapter.onViewAttachedToWindow(holder);
    int position = holder.getLayoutPosition();
    if (isHeaderViewPos(position) || isFooterViewPos(position))
    {
         ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

        if (lp != null
                && lp instanceof StaggeredGridLayoutManager.LayoutParams)
        {

            StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;

            p.setFullSpan(true);
        }
    }
}

為RecyclerView設置EmptyView

上面已經詳細給出了為RecyclerView添加Header以及Footer的例子,關於EmptyView的實現方法與上面基本類似,讀者可自行實現,當然在本篇末會給出完整的源碼地址。

RecyclerView的緩存機制

RecyclerView和ListView的回收機制非常相似,但是ListView是以View作為單位進行回收,RecyclerView是以ViewHolder作為單位進行回收。相比於ListView,RecyclerView的回收機制更為完善

Recycler是RecyclerView回收機制的實現類,他實現了四級緩存:

  • mAttachedScrap: 緩存在屏幕上的ViewHolder。
  • mCachedViews: 緩存屏幕外的ViewHolder,默認為2個。ListView對於屏幕外的緩存都會調用getView()。
  • mViewCacheExtensions: 需要用戶定制,默認不實現。
  • mRecyclerPool: 緩存池,多個RecyclerView共用。

要想理解RecyclerView的回收機制,我們就必須從其數據展示談起,我們都知道RecyclerView使用LayoutManager管理其數據布局的顯示。

註:以下源碼來自support-v7 25.4.0

RecyclerView$LayoutManager

LayoutManager是RecyclerView下的一個抽象類,Google提供了LinearLayoutManager,GridLayoutManager以及StaggeredGridLayoutManager基本上能滿足大部分開發者的需求。這三個類的代碼都非常長,這要分析下來可了不得。本篇文章只分析LinearLayoutManager的一部分內容

與分析ListView時類似,RecyclerView作為一個ViewGroup,肯定也跑不了那幾大過程,我們依然還是只分析其layout過程

[RecyclerView.java]

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        //1 沒有執行過布局流程的情況
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() 
        || mLayout.getWidth() != getWidth() ||
            mLayout.getHeight() != getHeight()) {
        //2 執行過布局流程,但是之後size又有變化的情況
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        //3 執行過布局流程,可以直接使用之前數據的情況
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

不過,無論什麽情況,最終都是完成dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3這三步,這樣的情況區分只是為了避免重復計算。

其中第二步的dispatchLayoutStep2是真正的布局!

private void dispatchLayoutStep2() {
    ...... // 設置狀態
    mState.mInPreLayout = false; // 更改此狀態,確保不是會執行上一布局操作
    // 真正布局就是這一句話,布局的具體策略交給了LayoutManager
    mLayout.onLayoutChildren(mRecycler, mState);
    ......// 設置和恢復狀態
}

由上面的代碼可以知道布局的具體操作都交給了具體的LayoutManager,那我們來分析其中的LinearLayoutManager

[LinearLayoutManager.java]

/**
*LinearLayoutManager的onLayoutChildren方法代碼也比較多,這裏也不進行逐行分析
*只來看關鍵的幾個點
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, 
                                RecyclerView.State state) {
    
    ......
    //狀態判斷以及一些準備操作
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    /**
    *1 感覺這個函數應該跟上一篇我們所分析的ListView的detachAllViewsFromParent();有點像
    */
    detachAndScrapAttachedViews(recycler);
    ......
    //2 感覺這個函數跟上一篇我們所分析的ListView的fillUp有點像
    fill(recycler, mLayoutState, state, false);

}

上面已經給出了真正布局的代碼。我們還是按照上一篇的思路來分析,兩次layout

第1次layout

第1個重要函數

[RecyclerView$LayoutManager]

    /**
     *暫時detach和scrap所有當前附加的子視圖。視圖將被丟棄到給定的回收器中(即參數recycler)。
    *回收器(即Recycler)可能更喜歡重用scrap的視圖。
     *
     * @param recycler 指定的回收器Recycler
     */
    public void detachAndScrapAttachedViews(Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }

第1次layout時,RecyclerView並沒有Child,所以跳過該函數,不過我們從上面的代碼註釋也知道了該函數跟緩存Recycler有關

第2個重要函數

[LinearLayoutManager.java]

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ......
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) 
            && layoutState.hasMore(state)) {//這裏循環判斷是否還有空間放置item
        ......
        //真正放置的代碼放到了這裏
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        ......
    }
    if (DEBUG) {
        validateChildOrder();
    }
    return start - layoutState.mAvailable;
}

跟進layoutChunk

[LinearLayoutManager.java]

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    /**
    *獲取一個View,這個函數應該是重點了,
    */
    View view = layoutState.next(recycler);
    ......
    //添加View
    addView(view);
    ......
    //計算View的大小
    measureChildWithMargins(view, 0, 0);
    ......
    //布局
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    ......
}

跟進next()

[LinearLayoutManager$LayoutState]

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

getViewForPosition方法可以說是RecyclerView中緩存策略最重要的方法,該方法是從RecyclerView的回收機制實現類Recycler中獲取合適的View,或者新創建一個View

View getViewForPosition(int position, boolean dryRun) {
    /**
    *從這個函數就能看出RecyclerView是以ViewHolder為緩存單位的些許端倪
    */
    return tryGetViewHolderForPositionByDeadline
    (position, dryRun, FOREVER_NS).itemView;
}

跟進tryGetViewHolderForPositionByDeadline

/**
 *試圖獲得給定位置的ViewHolder,無論是從 
 *mAttachedScrap、mCachedViews、mViewCacheExtensions、mRecyclerPool、還是直接創建。
 *
 * @return ViewHolder for requested position
 */
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ......
    // 1) 嘗試從mAttachedScrap獲取
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        ......
    }

    if (holder == null) {
        ......
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 嘗試從mCachedViews獲取
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        
        // 3) 嘗試從mViewCacheExtensions獲取
        if (holder == null && mViewCacheExtension != null) {
            ......
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ......
            }
        }

        // 4) 嘗試從mRecyclerPool獲取
        if (holder == null) { // fallback to pool
           
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
           // 5) 直接創建
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
           
        }
    }

    ......
    // 6) 判斷是否需要bindHolder
    if (!holder.isBound() 
        || holder.needsUpdate() 
        || holder.isInvalid()) {
            
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            bound = tryBindViewHolderByDeadline
                (holder, offsetPosition, position, deadlineNs);
        }
    ......

    return holder;
}

那麽在第1次layout時,,前4步都不能獲得ViewHolder,那麽進入第5, 直接創建

holder = mAdapter.createViewHolder(RecyclerView.this, type);

public final VH createViewHolder(ViewGroup parent, int viewType) {
    TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
    //這裏終於看到我們的親人onCreateViewHolder
    final VH holder = onCreateViewHolder(parent, viewType);
    holder.mItemViewType = viewType;
    TraceCompat.endSection();
    return holder;
}

這個onCreateViewHolder正是在RecyclerViewDemo1Adapter中我們重寫的

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    Log.d(TAG,"onCreateViewHolder->viewtype"+viewType);
    View view = LayoutInflater
                .from(parent.getContext())
                .inflate(R.layout.item_menu_main, parent, false);

    return new ViewHolder(view);
}

初次創建了ViewHolder之後,便進入6,導致我們重寫的onBindViewHolder回調,數據與View綁定了

第2次layout

從上一篇ListView中我們就知道了再簡單的View也至少需要兩次Layout,在ListView中通過把屏幕的子View detach並加入mActivieViews,以避免重復添加item並可通過attach提高性能,那麽在RecyclerView中,它的做法與ListView十分類似,RecyclerView也是通過detach子View,並把子View對應的ViewHolder加入其1級緩存mAttachedScrap。這部分我們就不詳細分析了,讀者可參照上一篇的步驟進行分析。

RecyclerView與ListView 緩存機制對比分析

ListView和RecyclerView最大的區別在於數據源改變時的緩存的處理邏輯,ListView是”一鍋端”,將所有的mActiveViews都移入了二級緩存mScrapViews,而RecyclerView則是更加靈活地對每個View修改標誌位,區分是否重新bindView。

小結

在一些場景下,如界面初始化,滑動等,ListView和RecyclerView都能很好地工作,兩者並沒有很大的差異,但是在需要支持動畫,或者頻繁更新,局部刷新,建議使用RecyclerView,更加強大完善,易擴展


本篇總結

本篇呢,我們分析了RecyclerView的使用方法以及RecyclerView部分源碼。目的是為了更好的掌握RecyclerView。

這裏呢再上圖總結一下RecyclerView的layout流程

技術分享圖片

下篇預告

下篇呢,也是一篇幹貨,上面兩篇文章,我們的數據都是虛擬的,靜態的,而實際開發中數據通常都是從服務器動態獲得的,這也產生了一系列問題,如列表的下拉刷新以及上拉加載、ListVIew異步獲取圖片顯示錯位等等問題


參考博文

http://blog.csdn.net/lmj623565791/article/details/51854533


源碼地址:源碼傳送門

此致,敬禮

Android開發之漫漫長途 XIV——RecyclerView