1. 程式人生 > >android 打造真正的下拉重新整理上拉載入recyclerview(二):新增刪除頭尾部

android 打造真正的下拉重新整理上拉載入recyclerview(二):新增刪除頭尾部

前言

在上一篇文章中,我們介紹了下拉重新整理上拉載入RecyclerView的使用,從這篇開始,我將對這個專案的具體實現詳細介紹,這篇首先介紹新增刪除頭尾部的實現。

大家都知道recyclerview並不能像listview一樣,可以直接使用addHeaderView和addFooterView新增頭部和尾部,而在實際的專案中,我們常常需要實現這些功能。既然recyclerview不提供,那我們就自己寫一個唄。

但是啊,新增有了,刪除頭尾部的介紹網上比較少。看過鴻洋大神這篇文章的同學想必都知道,給recyclerview新增頭尾部,其實就是 在adapter中使用鍵值陣列對儲存頭部和尾部

,addHeaderView其實就是往頭部的鍵值對陣列中新增view。那麼removeHeaderView呢?當然就是從頭部的鍵值對陣列中刪除view啦!!沒錯,就是這麼簡單。

這時候,有同學把網上的程式碼拷下來,開始在adapter中新增removeHeaderView方法,恩,完美。新增View0,新增View1,刪除View1,哦!刪除成功,新增View2,哎?怎麼新增的是View1??我的View2呢?

不要問我怎麼知道的,因為我就這樣幹過。

新增刪除頭尾部

那麼我們還是先一步步來介紹下頭尾部的實現吧,recyclerview的adapter需要實現以下幾個方法:

  • 建立ViewHolder:onCreateViewHolder(ViewGroup parent, int viewType);
  • 繫結資料:onBindViewHolder(RecyclerView.ViewHolder holder, int position);
  • 獲得item的總數:getItemCount();

恩,跟ListView還是有點不一樣的。在ListView的時候,當我們需要展示幾種型別的item時,我們會使用到一個方法:

  • 獲得item的型別:getItemViewType(int position);

幸運的是,recyclerview中也提供這個方法,有這個方法就好辦了,我們把頭尾部當成是一種item不就好了嘛,用鍵值對陣列儲存頭尾部,型別type作為key,view作為value。

因此思路就是:

  1. 使用鍵值對陣列(ArrayList、HashMap、SparseArrayCompat等)儲存頭尾部
  2. 在getItemViewType方法中,判斷是否頭尾部,是的話返回對應的key,也就是type
  3. 在onCreateViewHolder中,判斷是否是頭尾部,是的話返回對應的ViewHolder
  4. 在onBindViewHolder中,判斷是否是頭尾部,是的話不處理

大概就是這樣,下面我們看看這部分程式碼,一個一個來:

首先,鍵值對儲存頭尾部:

protected SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
protected SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();
  • 1
  • 2

然後,getItemCount方法,返回item數量+頭尾部總數:

@Override
public int getItemCount() {
    return getRealItemCount()+getHeadersCount()+getFootersCount();
}
  • 1
  • 2
  • 3
  • 4

再然後,getItemViewType方法:

@Override
public int getItemViewType(int position) {
    //如果是頭部,返回對應的type
    if (isHeaderPosition(position)) {
        return mHeaderViews.keyAt(position);
    }
    //如果是尾部,返回對應的type
    if (isFooterPosition(position)) {
        return mFooterViews.keyAt(position - getHeadersCount() - getRealItemCount());
    }
    //如果是item,返回真正的adapter的getItemViewType方法
   return mRealAdapter.getItemViewType(position - getHeadersCount());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

再再然後,onCreateViewHolder方法:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//        如果是頭部
    if (isHeaderType(viewType)) {
        int headerPosition = mHeaderViews.indexOfKey(viewType);
        View headerView = mHeaderViews.valueAt(headerPosition);
        return createHeaderAndFooterViewHolder(headerView);
    }
//        如果是尾部
    if (isFooterType(viewType)) {
        int footerPosition = mFooterViews.indexOfKey(viewType);
        View footerView = mFooterViews.valueAt(footerPosition);
        return createHeaderAndFooterViewHolder(footerView);
    }
    return mRealAdapter.onCreateViewHolder(parent, viewType);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

再再再然後,onBindViewHolder方法:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderPosition(position) || isFooterPosition(position)) {

    } else {
        mRealAdapter.onBindViewHolder(holder, realPosition);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

再再再再然後,新增addHeaderView和addFooterView方法:

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

public void addFooterView(View view) {
    mFooterViews.put(BASE_ITEM_TYPE_FOOTER ++,view);
    notifyDataSetChanged();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意:這裡如果你使用了網上的程式碼,比如鴻洋大神的:

private static final int BASE_ITEM_TYPE_HEADER = 100000;

public void addHeaderView(View view)
{
    mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果你是這樣寫的,那就會造成前言部分說了View2沒有新增成功的問題了。

那麼是為什麼呢?我們來推演一番:

  1. 新增View0,type是100000;
  2. 新增View1,type是100001;
  3. 刪除View1;
  4. 新增View2,type是100001;

啊哦,getItemViewType拿到的type還是原來view1的type,那自然View2會變成View1了。那怎麼辦呢?我們用自增不就好了嘛。

恩,接著上面,繼續加上刪除頭尾部的方法:

public void removeHeaderView(View view) {
    int index = mHeaderViews.indexOfValue(view);
    if (index < 0) return;
    mHeaderViews.removeAt(index);
    notifyDataSetChanged();
}

public void removeFooterView(View view) {
    int index = mFooterViews.indexOfValue(view);
    if (index < 0) return;
    mFooterViews.removeAt(index);
    notifyDataSetChanged();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

恩,這下應該沒問題了。

等等!LinearLayoutManager是沒問題了,GridLayoutManager和StaggeredGridLayoutManager呢?其實想想就知道,肯定會有問題的嘛~~問題就是頭部和尾部的寬度會和item一樣長,而不是我們希望的填滿recyclerview的寬度,每個頭尾部單獨佔一行。

當然解決辦法鴻洋大大也說過了,網上也有一大堆:

/**解決GridLayoutManager問題*/
    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mRealAdapter.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    if (isHeaderPosition(position) || isFooterPosition(position))
                        return gridLayoutManager.getSpanCount();
                    return 1;
                }
            });
        }

    }

    /**解決瀑布流佈局問題*/
    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mRealAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderPosition(position) || isFooterPosition(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams) lp;
                layoutParams.setFullSpan(true);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

搞定~

好了,再看下效果圖吧:

頭尾部

Grid頭尾部

Staggred頭尾部

當然,我們要做的是封裝一個可以新增刪除頭尾部的RecyclerView,這算哪門子的封裝啊?我們當然要把這些個方法封裝好一點,使用起來也爽嘛。

一提起封裝,我腦子裡就冒出個東西:泛型

泛型真是個好東西啊,那個靈活,那個爽,嘖嘖嘖,嘿嘿……好了,關於泛型這裡就不詳細介紹了,我們還是來說下封裝吧。(一本正經臉)

在開始封裝之前,先問問自己:“如果是你使用,你想怎麼用?”

“那還用說?當然是還和原來一樣setAdapter,像listview一樣直接用addHeaderView啊!”

“恩,沒問題,開始吧”

  1. 繼承RecyclerView,重寫setAdapter方法,新增addHeaderView等方法;
  2. 使用者設定的adapter,作為真正的adapter(mRealAdapter)傳入頭尾部Adapter;

先看看第一步:

protected HeaderAndFooterAdapter mAdapter;
protected Adapter mRealAdapter;

@Override
public void setAdapter(Adapter adapter) {
    mRealAdapter = adapter;
    if (adapter instanceof HeaderAndFooterAdapter) {
        mAdapter = (HeaderAndFooterAdapter) adapter;
    }
    else {
        mAdapter = new HeaderAndFooterAdapter(getContext(),adapter);
    }
    super.setAdapter(mAdapter);
}

public void addHeaderView(View view) {
    if (null == view) {
        throw new IllegalArgumentException("the view to add must not be null !");
    } else if (mAdapter == null) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.addHeaderView(view);
    }
}

public void addFooterView(View view) {
    if (null == view) {
        throw new IllegalArgumentException("the view to add must not be null !");
    } else if (mAdapter == null) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.addFooterView(view);
    }
}

public void removeHeaderView(View view) {
    if (null == view) {
        throw new IllegalArgumentException("the view to remove must not be null !");
    } else if (mAdapter == null) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.removeHeaderView(view);
    }
}

public void removeFooterView(View view) {
    if (null == view) {
        throw new IllegalArgumentException("the view to remove must not be null !");
    } else if (mAdapter == null) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.removeFooterView(view);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

完美,再看看第二步,使用泛型就好啦~

public class HeaderAndFooterAdapter<T extends RecyclerView.Adapter> extends RecyclerView.Adapter {
    protected T mRealAdapter;
    protected Context mContext;

    public HeaderAndFooterAdapter(Context mContext, T mRealAdapter) {
        super();
        this.mContext = mContext;
        this.mRealAdapter = mRealAdapter;
    }

    public T getRealAdapter() {
        return mRealAdapter;
    }

    //其他方法上面已介紹,這裡就省略了。

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

好了,這樣就可以了。使用的時候,你還是按照你原來的方式setAdapter,不用管其他,想addHeaderView就直接調HeaderAndFooterRecyclerView的addHeaderView方法就好啦,和ListView是一樣樣的。

新增點選事件

本來新增點選事件我不想寫了,因為網上太多太多例子了,但想到都說了這麼多了,再說一下也沒什麼啦。

其實新增點選事件就是給每個item新增一個點選監聽嘛,然後傳入position就夠了,注意:因為我們添加了頭部和尾部,position需要處理一下,不然到時候對使用者來說,你的position就是不準的。

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
   if (isHeaderPosition(position) || isFooterPosition(position)) {

    } else {
        final int realPosition = position - getHeadersCount();
        mRealAdapter.onBindViewHolder(holder, realPosition);
        if (mOnItemClickListener != null) {
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mOnItemClickListener.OnItemClick(realPosition);
                }
            });
        }
        if (mOnItemLongClickListener != null) {
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    return mOnItemLongClickListener.onItemLongClick(realPosition);
                }
            });
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

這樣就可以了,點選和長按就加上去了,接下來跟addHeaderView一樣,加幾個方法用來呼叫就可以了。

在adapter中加入:

protected OnItemClickListener mOnItemClickListener;
protected OnItemLongClickListener mOnItemLongClickListener;

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    this.mOnItemClickListener = onItemClickListener;
}

public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
    this.mOnItemLongClickListener = onItemLongClickListener;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在HeaderAndFooterRecyclerView中加入:

 public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    if (null == mAdapter) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.setOnItemClickListener(onItemClickListener);
    }
}

public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
    if (null == mAdapter) {
        throw new IllegalStateException("u must set a adapter first !");
    } else {
        mAdapter.setOnItemLongClickListener(onItemLongClickListener);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

搞定,至此,PTLRecyclerView的頭尾部相關就介紹完了,其實PTLRecyclerView的HeaderAndFooterRecyclerView中還封裝了EmptyView的實現,這裡就不介紹了,有興趣的同學可以去看看原始碼。

有意見或建議或疑問等等,歡迎提出~~