ListView의 addHeaderView(Footer)를 RecyclerView 에서 구현하기

안드로이드 앱 개발시 가장 많이 사용하던 위젯은 ListView이다. 그리고 RecyclerView는 Android 5 Lollipop 버전이 업데이트 되었을 때, Support Library V7에 추가되었고 이를 사용하기 위해서는 build.gradle에 다음과 같이 추가하면 사용할 수 있다.

compile 'com.android.support:recyclerview-v7:${supportLibraryVersion}'

기존에 ListView에서 사용하던 상, 하단에 뷰 추가를 지원하던 addHeaderViewaddFooterView가 RecyclerView에서는 더이상 지원하지 않는다. 그래서 대부분의 안드로이드 개발자들은 StackOverFlow에 ‘How to add header view to recyclerview’ 다음과 같이 검색했을 것이다. 이미 RecyclerView에 HeaderView나 FooterView를 추가해주는 오픈 소스는 많이 공개되어 있다. 필요하다면 찾아서 사용하면 쉽게 뷰를 상하단에 추가할 수 있다. 결과적으로 기존 ListView에서는 addHeaderView가 어떻게 동작 했으며 RecyclerView에서는 그 기능을 동작하도록 할 수 있는지 한번 알아보려고 한다.

HeaderViewListAdapter (android.widget)
public void addHeaderView(View v, Object data, boolean isSelectable) {
    ...
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
    …
    }
}

ListView의 addHeaderView의 내부를 들여다 보면 기존의 ListAdapter 대신에 HeaderViewListAdapter 어댑터 클래스를 사용하며 이 위젯은 래핑에 필요한 WrapperListAdapter 인터페이스를 상속하고 있으며 Adapter를 가지고 있는 껍데기가 된다. 기존의 Adapter를 HeaderViewListAdapter로 재생성(Wrapping)하면서 mAdapter를 파라미터로 넘겨주는 이유가 바로 이 때문이다. 따라서 HeaderViewListAdapter에서는 기존의 Adapter를 그대로 가지고 있으면서 화면에 상하단에 추가된 뷰를 노출시켜주는 함수를 더 가지고 있다. 다음으로 getView함수를 살펴보자.

public View getView(int position, View convertView, ViewGroup parent) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
    int numHeaders = getHeadersCount();
    if (position < numHeaders) {
        return mHeaderViewInfos.get(position).view;
    }

    // Adapter
    final int adjPosition = position - numHeaders;
    int adapterCount = 0;
    if (mAdapter != null) {
        adapterCount = mAdapter.getCount();
        if (adjPosition < adapterCount) {
            return mAdapter.getView(adjPosition, convertView, parent);
        }
    }

    // Footer (off-limits positions will throw an IndexOutOfBoundsException)
    return mFooterViewInfos.get(adjPosition - adapterCount).view;
}

Header로 추가된 상단뷰의 수보다 position이 작은 경우에는 HeaderView를 리턴해주고 ListView에서 Row Item의 position은 Header 수 만큼 빼서 그려준다. 상하단에 추가된 뷰가 있을 경우, 그것을 보여주고 그게 아니라면 Adapter의 기본 로직으로 동작하게 된다.

HeaderRecyclerViewAdapter 구현하기
public abstract class HeaderRecyclerViewAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {

개발 중인 서비스에서 다양한 데이터 타입이나 뷰를 사용 한다면 일일이 대응되는 클래스(RecyclerView.Adapter 상속 받는)를 생성하기 보다는 HeaderRecyclerView를 사용하거나 구현 방법을 한번 보는 것을 추천한다. 목록을 보여주는 ListView와 RecyclerView는 기본 개념이 같이 때문에 내부 동작 또한 크게 차이는 없을 것이다.

달라진 것이 있다면 ListView에서 종종 findViewById의 비용을 줄이기 위해 사용하던 ViewHolder가 RecyclerView.ViewHolder로 추가되어 예전에는 Adapter에서 getView를 통해 View를 생성했다면 이제는 ViewHolder를 생성하고(onCreateViewHolder) 데이터 바인딩할 때(onBindViewHolder), 파라미터로 다시 받아 사용할 수 있다.

public class HeaderRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

  private int TYPE_HEADER = -1;
  private int TYPE_ROW_ITEM = -2;

…
  public void addHeaderView(View header) {
    mHeaderViews.add(header);
  }

  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (isHeaderPosition(position)) {
      return new ViewHolder(mHeaderViews.get(position));
    } else {
      return mInnerAdapter.onCreateViewHolder(parent, position);
    }
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderPosition(position)) {
      //  bind HeaderViewHolder
    } else {
      //  bind ItemViewHolder
    }
  }

  @Override
  public int getItemViewType(int position) {
    // return 3 item type by position
  }
…

ListView의 addHeaderView 함수를 HeaderRecyclerViewAdapter에 추가한다. 그리고 RecyclerView.Adapter가 position과 ViewHolder의 타입에 따라 동작해야 하므로 위의 3가지의 함수가 동작에 있어서 가장 중요하다. 마찬가지로 만약에 FooterView를 RecyclerView에 추가하고 싶을 경우, 뷰의 식별자를 선언하고 ViewHolder를 생성하는 부분과 데이터를 바인딩하는 부분에 추가한다. 그리고 getItemViewType에서 화면상에 FooterView가 노출되는 position일 때 리턴해주면 된다. 마지막으로 앞으로 자주 사용할 RecyclerView와 많이 사용했던 ListView를 자유롭게 다뤄보자.