RecyclerView添加Header的正確方式

看了一下博客目錄,已經有好幾篇博客是關於RecyclerView的,不過對於這麼一款強大的控件,我還是要再寫一篇博客來學習一下,這篇博客的主題是《爲RecyclerView添加header》,當然在看完這篇博客後,相信添加Footer你也應該能夠學會。話說在這麼多新控件中爲何RecyclerView備受開發者的喜愛?這還是因爲在Android發展到今天基本上還沒有像RecyclerView這麼靈活的一個玩意,鑑於他的靈活以及強大,很多人(包括我)已經開始拋棄ListViewGridView轉爲RecyclerView了,再使用過RecyclerView和被善變的需求折磨後,我相信會有越來越多的人轉到RecyclerView的使用上。

問題

好了,廢話不多說了,這篇博客我們要解決的問題有:

  1. 如何爲RecyclerView添加Header
  2. 如何讓Header適配各種LayoutManager
  3. 在有Header的情況下,我們的分割線該怎麼畫
  4. 作爲一個懶惰的程序員,如何將這些做到最簡便

如果爲RecyclerView添加Header

大家在使用ListView的時候可以很輕鬆的添加headers, 但是不知道大家發現沒有,RecyclerView和各種LayoutManager都沒有哪個方法是爲添加header而設立的,這個時候我們就開始思考如何爲RecyclerView添加header了。 這裏我們的解決方案和網上你能搜到的大多數方案一樣,是通過控制AdapteritemType來設置的,思路就是根據不同的itemType去加載不同的佈局

/**
 * Created by qibin on 2015/11/5.
 */
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public static final int TYPE_HEADER = 0;
    public static final int TYPE_NORMAL = 1;

    private ArrayList<String> mDatas = new ArrayList<>();

    private View mHeaderView;

    private OnItemClickListener mListener;

    public void setOnItemClickListener(OnItemClickListener li) {
        mListener = li;
    }

    public void setHeaderView(View headerView) {
        mHeaderView = headerView;
        notifyItemInserted(0);
    }

    public View getHeaderView() {
        return mHeaderView;
    }

    public void addDatas(ArrayList<String> datas) {
        mDatas.addAll(datas);
        notifyDataSetChanged();
    }

    @Override
    public int getItemViewType(int position) {
        if(mHeaderView == null) return TYPE_NORMAL;
        if(position == 0) return TYPE_HEADER;
        return TYPE_NORMAL;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(mHeaderView != null && viewType == TYPE_HEADER) return new Holder(mHeaderView);
        View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        return new Holder(layout);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        if(getItemViewType(position) == TYPE_HEADER) return;

        final int pos = getRealPosition(viewHolder);
        final String data = mDatas.get(pos);
        if(viewHolder instanceof Holder) {
            ((Holder) viewHolder).text.setText(data);
            if(mListener == null) return;
            viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onItemClick(pos, data);
                }
            });
        }
    }

    public int getRealPosition(RecyclerView.ViewHolder holder) {
        int position = holder.getLayoutPosition();
        return mHeaderView == null ? position : position - 1;
    }

    @Override
    public int getItemCount() {
        return mHeaderView == null ? mDatas.size() : mDatas.size() + 1;
    }

    class Holder extends RecyclerView.ViewHolder {

        TextView text;

        public Holder(View itemView) {
            super(itemView);
            if(itemView == mHeaderView) return;
            text = (TextView) itemView.findViewById(R.id.text);
        }
    }

    interface OnItemClickListener {
        void onItemClick(int position, String data);
    }
}

這裏我們重寫了getItemViewType方法,並根據位置來返回不同的type,這個type是我們預先商定好的常量,接在onCreateViewHolder方法中來判斷itemType,如果是header,則返回我們設置的headerView,否則正常加載item佈局,相信大家對於上面的代碼不會有任何疑問,接下來我們就在Activity中用一下試試看,

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mRecyclerView = (RecyclerView) findViewById(R.id.list);
    mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
    mRecyclerView.setLayoutManager(mLayoutManager);
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());

    mAdapter = new MyAdapter();
    mRecyclerView.setAdapter(mAdapter);
    mAdapter.addDatas(generateData());
    setHeader(mRecyclerView);
    mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(int position, String data) {
            Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
        }
    });
}

private void setHeader(RecyclerView view) {
    View header = LayoutInflater.from(this).inflate(R.layout.header, view, false);
    mAdapter.setHeaderView(header);
}

這裏LayoutManager我們使用了LinearLayoutManager,並且給Adapter設置了一個header,運行一下
看看效果:

恩,還不錯,item的點擊事件也很完美,那接下來,我們將LayoutManager換成GridLayoutManager看看咋樣。

爲GridLayoutManager添加header

//  mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mLayoutManager = new GridLayoutManager(this, 2);

哎喲,我的小心臟啊,快受不了了,這是什麼玩意,我們的header竟然作爲一個cell出現在了界面上,這完全不是我們想要的效果啊! 冷靜下來想想,肯定會有解決方法的吧。這時候我們就該引入一個不太常用的方法了:

gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        return getItemViewType(position) == TYPE_HEADER
                ? gridManager.getSpanCount() : 1;
    }
});

我們解釋一下這段代碼,首先我們設置了一個SpanSizeLookup,這個類是一個抽象類,而且僅有一個抽象方法getSpanSize,這個方法的返回值決定了我們每個position上的item佔據的單元格個數,而我們這段代碼綜合上面爲GridLayoutManager設置的每行的個數來解釋的話,
就是當前位置是header的位置,那麼該item佔據2個單元格,正常情況下佔據1個單元格。那這段代碼放哪呢? 爲了以後的封裝,我們還是在Adapter中找方法放吧。
我們在Adapter中再重寫一個方法onAttachedToRecyclerView

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
    RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
    if(manager instanceof GridLayoutManager) {
        final GridLayoutManager gridManager = ((GridLayoutManager) manager);
        gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return getItemViewType(position) == TYPE_HEADER
                        ? gridManager.getSpanCount() : 1;
            }
        });
    }
}

這個時候我們再來看一下效果,

恩,這次達到我們的要求了,不過對於StaggeredGridLayoutManager我們還沒做處理,而且我們還發現StaggeredGridLayoutManager中並沒有像GridLayoutManager中這樣的方法,我們還需要單獨爲StaggeredGridLayoutManager單獨處理一下。

爲StaggeredGridLayoutManager添加header

我們繼續重寫Adapter中另外一個方法。

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    if(lp != null
            && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
            StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
            p.setFullSpan(holder.getLayoutPosition() == 0);
    }
}

這裏的處理方式是用通過LayoutParams,而且這裏更簡單,StaggeredGridLayoutManager.LayoutParams爲我們提供了一個setFullSpan方法來設置佔領全部空間,好開心,看一下StaggeredGridLayoutManager的效果,

啊, 怎麼和上面的效果一樣? 很簡單嘛,我們的item都是等高的。

處理分隔符

這是我們開開心心的繼續寫代碼,並且爲我們的item添加了分隔符,分隔符我還是用的翔哥寫的那個,畢竟翔哥寫的太好了,而且我們沒有必要重複造輪子,不過這時候問題出現了,相信你也肯定能猜到應該會出現問題了,因爲不管我們怎麼處理,header對於RecyclerView來說還是一個普普通通的item,這時候我們添加分割線,肯定也會對header產生影響,那下面,我們再來對翔哥的分割線改造一下吧。

public class GridItemDecoration extends RecyclerView.ItemDecoration {
    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
    private Drawable mDivider;
    private boolean hasHeader;

    public GridItemDecoration(Context context) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
    }

    public GridItemDecoration(Context context, boolean header) {
        this(context);
        hasHeader = header;
    }

    ...

    @Override
    public void getItemOffsets(Rect outRect, View view,
                               RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view);
        int spanCount = getSpanCount(parent);
        int childCount = parent.getAdapter().getItemCount();
        int pos = position;

        if(hasHeader) {
            if(position == 0) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
                return;
            } else {
                pos = position - 1;
            }
        }

        if (isLastColum(parent, pos, spanCount, childCount)) {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(),
                    mDivider.getIntrinsicHeight());
        }
    }
}

改造的地方是獲取偏移量的方法我們換了一個,因爲原來的那個已經過時了,而且,這裏我們還加了一個boolean類型的hasHeader變量來表示是不是有header,如果hasHeader並且position爲0,那麼我們僅僅繪製底部的分割線,其他的地方不繪製,在有header的情況下,我們還需要將position減1,因爲我們認爲的第1個item其實是第2個。這個時候我們再來看看有分割線的效果。

看來我們的想法是對的,header部分除了底部有一個分割線外,並沒有其他的分割線,這也完全符合我們的需求。

封裝

這下好了,基本上完美的處理好了,可是難道我們對於不同的Adapter都需要寫那麼多代碼嗎? 對於一個懶程序員來說,這肯定是一個可怕的事情,所以,我們還需要對我們的Adapter進行封裝,目的就是可以輕輕鬆鬆的寫代碼,

/**
 * Created by qibin on 2015/11/5.
 */
public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public static final int TYPE_HEADER = 0;
    public static final int TYPE_NORMAL = 1;

    private ArrayList<T> mDatas = new ArrayList<>();

    private View mHeaderView;

    private OnItemClickListener mListener;

    public void setOnItemClickListener(OnItemClickListener li) {
        mListener = li;
    }

    public void setHeaderView(View headerView) {
        mHeaderView = headerView;
        notifyItemInserted(0);
    }

    public View getHeaderView() {
        return mHeaderView;
    }

    public void addDatas(ArrayList<T> datas) {
        mDatas.addAll(datas);
        notifyDataSetChanged();
    }

    @Override
    public int getItemViewType(int position) {
        if(mHeaderView == null) return TYPE_NORMAL;
        if(position == 0) return TYPE_HEADER;
        return TYPE_NORMAL;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
        if(mHeaderView != null && viewType == TYPE_HEADER) return new Holder(mHeaderView);
        return onCreate(parent, viewType);
    }


    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        if(getItemViewType(position) == TYPE_HEADER) return;

        final int pos = getRealPosition(viewHolder);
        final T data = mDatas.get(pos);
        onBind(viewHolder, pos, data);

        if(mListener != null) {
            viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onItemClick(pos, data);
                }
            });
        }
    }

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

        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if(manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    return getItemViewType(position) == TYPE_HEADER
                            ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        if(lp != null
                && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                        StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
            p.setFullSpan(holder.getLayoutPosition() == 0);
        }
    }

    public int getRealPosition(RecyclerView.ViewHolder holder) {
        int position = holder.getLayoutPosition();
        return mHeaderView == null ? position : position - 1;
    }

    @Override
    public int getItemCount() {
        return mHeaderView == null ? mDatas.size() : mDatas.size() + 1;
    }

    public abstract RecyclerView.ViewHolder onCreate(ViewGroup parent, final int viewType);
    public abstract void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, T data);

    public class Holder extends RecyclerView.ViewHolder {
        public Holder(View itemView) {
            super(itemView);
        }
    }

    public interface OnItemClickListener<T> {
        void onItemClick(int position, T data);
    }
}

我們將BaseRecyclerAdapter抽象起來,並且提供兩個抽象方法onCreateonBind用來創建holder和綁定數據,而對於header做的一系列工作,我們都放到了BaseRecyclerAdapter中,而繼承BaseRecyclerAdapter後,我們僅僅關心我們的holder怎麼創建和數據怎麼綁定就ok。例如下面代碼:

/**
 * Created by qibin on 2015/11/7.
 */
public class MyAdapter extends BaseRecyclerAdapter<String> {

    @Override
    public RecyclerView.ViewHolder onCreate(ViewGroup parent, int viewType) {
        View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        return new MyHolder(layout);
    }

    @Override
    public void onBind(RecyclerView.ViewHolder viewHolder, int RealPosition, String data) {
        if(viewHolder instanceof MyHolder) {
            ((MyHolder) viewHolder).text.setText(data);
        }
    }

    class MyHolder extends BaseRecyclerAdapter.Holder {
        TextView text;
        public MyHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.text);
        }
    }
}

這樣我們再用起來就簡單多了,對於這樣的封裝,我們還算滿意,再做完添加header後,相信大家對於footer也有想法了,有想法就實現它吧,擴展一下BaseRecyclerAdapter就ok啦。

好了,這篇博客就到這裏吧,最後是本文代碼的下載。

代碼下載,戳這裏

發佈了82 篇原創文章 · 獲贊 782 · 訪問量 85萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章