AndroidのRecyclerViewでスティッキーヘッダーを自作する

Androidでスティッキーヘッダーを自作したので実装方法を載せておきます。

サンプルはこちら
https://github.com/teradonburi/stickyheader


ライブラリをいくつか見てみたのですが、今一つ導入しにくかったのと
そもそもの中身がわからないとカスタマイズできない気がしたので自作しました。

スティッキーヘッダーとはスクロール時に下のような動きをするヘッダーです。
<img src="https://github.com/teradonburi/stickyheader/raw/master/demo.gif" width="30%" />

下のstackoverflowの回答を参考にさせていただきました。
https://stackoverflow.com/a/44327350


# 解説
基本的にRecyclerView.ItemDecorationを継承して実装します。
スクロール時にスティッキーヘッダーと通常のヘッダーの判定と
スティッキーヘッダー部はCanvasで描画してます。

``` StickyHeaderItemDecoration.java

public class StickyHeaderItemDecoration extends RecyclerView.ItemDecoration {

    private StickyHeaderInterface mListener;
    private View currentHeader;

    public StickyHeaderItemDecoration(@NonNull StickyHeaderInterface listener) {
        mListener = listener;
    }



    // RecyclerViewのセルが表示されたときに呼ばれる
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        // 一番上のビュー
        View topChild = parent.getChildAt(0);
        if (topChild == null) {
            return; // RecyclerViewの中身がない
        }

        int topChildPosition = parent.getChildAdapterPosition(topChild);
        if (topChildPosition == RecyclerView.NO_POSITION) {
            return;
        }


        int prevHeaderPosition = mListener.getHeaderPositionForItem(topChildPosition);
        if(prevHeaderPosition == -1){
           return;
        }

        // ヘッダービューが表示された
        currentHeader = getHeaderViewForItem(topChildPosition, parent);
        fixLayoutSize(parent, currentHeader);
        int contactPoint = currentHeader.getBottom();
        // 次のセルを取得
        View childInContact = getChildInContact(parent, contactPoint);
        if (childInContact == null) {
            return; // 次のセルがない
        }

        // ヘッダーの判定
        if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
            // 既存のStickyヘッダーを押し上げる
            moveHeader(c, currentHeader, childInContact);
            return;
        }

        // Stickyヘッダーの描画
        drawHeader(c, currentHeader);

    }

    // dp <=> pixel変換
    public static float convertDp2Px(float dp, Context context){
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return dp * metrics.density;
    }


    // Stickyヘッダービューの取得
    private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
        int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
        int layoutResId = mListener.getHeaderLayout(headerPosition);
        // Stickyヘッダーレイアウトをinflateする
        View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
        //header.setElevation(header,convertDp2Px(R.dimen.shadow,header.getContext()));
        // Stickyレイアウトにデータバインドする
        mListener.bindHeaderData(header, headerPosition);
        return header;
    }

    // Stickyヘッダーを描画する
    private void drawHeader(Canvas c, View header) {
        c.save();
        c.translate(0, 0);
        header.draw(c);
        drawShadow(header,c);
        c.restore();
    }

    // Stickyヘッダーを動かす
    private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
        c.save();
        c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
        currentHeader.draw(c);
        c.restore();
    }

    private void drawShadow(View target,Canvas c){
        Paint paint = new Paint();
        paint.setShadowLayer(10.0f, 0.0f, 2.0f, 0xff000000);
        ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
        c.drawRect(0, 0, layoutParams.width, layoutParams.height, paint);
    }

    // 座標から次のRecyclerViewのセル位置を取得
    private View getChildInContact(RecyclerView parent, int contactPoint) {
        View childInContact = null;
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            if (child.getBottom() > contactPoint) {
                if (child.getTop() <= contactPoint) {
                    childInContact = child;
                    break;
                }
            }
        }
        return childInContact;
    }

    // Stickyヘッダーのレイアウトサイズを取得
    private void fixLayoutSize(ViewGroup parent, View view) {

        // RecyclerViewのSpec
        int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);

        // headersのSpec
        int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
        int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);

        view.measure(childWidthSpec, childHeightSpec);

        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
    }

    // Stickyヘッダーインタフェース
    public interface StickyHeaderInterface {

        int getHeaderPositionForItem(int itemPosition);

        int getHeaderLayout(int headerPosition);

        void bindHeaderData(View header, int headerPosition);

        boolean isHeader(int itemPosition);
    }
}

```

Adapter側でStickyHeaderInterfaceを実装します。
getHeaderPositionForItemで一つ前のスティッキヘッダー位置を取得します。
getHeaderLayoutでスティッキーヘッダーのレイアウトファイルを指定します。
isHeaderでスティッキーヘッダーかの判定を行います。
bindHeaderDataでスティッキヘッダーのデータをバインドします。

```RecyclerViewAdapter.java
public class RecyclerViewAdapter extends RecyclerView.Adapter
        implements StickyHeaderItemDecoration.StickyHeaderInterface
{
    private List<Item> items = new ArrayList<>();
    private String headerTitle;
![Payload Too Large]()

    public void setItems(List<Item> items) {
        this.items = items;
        notifyDataSetChanged();
    }

    // 数
    @Override
    public int getItemCount() {
        return items.size();
    }

    // データの種別
    @Override
    public int getItemViewType(int position) {
        return items.get(position).type.ordinal();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == HEADER.ordinal()){
            return HeaderViewHolder.create(parent);
        }
        else{
            return ItemViewHolder.create(parent);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if(holder instanceof HeaderViewHolder){
            ((HeaderViewHolder)holder).update(items.get(position));
        }else if(holder instanceof ItemViewHolder){
            ((ItemViewHolder)holder).update(items.get(position));
        }
    }

    private interface ViewHolderInterface{
        void update(Item item);
    }


    //region HeaderViewHolder

    private static class HeaderViewHolder extends RecyclerView.ViewHolder
        implements ViewHolderInterface
    {
        private ViewHeaderBinding binding;

        public HeaderViewHolder(ViewHeaderBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        public static HeaderViewHolder create(ViewGroup parent){
            ViewHeaderBinding binding =  DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),R.layout.view_header,parent,false);
            return new HeaderViewHolder(binding);
        }

        @Override
        public void update(Item item){
            binding.setItem(item);
            binding.executePendingBindings();
        }

    }

    //endregion


    //region ItemViewHolder

    private static class ItemViewHolder extends RecyclerView.ViewHolder
            implements ViewHolderInterface
    {
        private ViewItemBinding binding;

        public ItemViewHolder(ViewItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        public static ItemViewHolder create(ViewGroup parent){
            ViewItemBinding binding =  DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),R.layout.view_item,parent,false);
            return new ItemViewHolder(binding);
        }

        @Override
        public void update(Item item){
            binding.setItem(item);
            binding.executePendingBindings();
        }

    }

    //endregion


    //region StickyHeaderInterface

    // Stickyヘッダーの前の位置取得
    @Override
    public int getHeaderPositionForItem(int itemPosition) {
        int headerPosition = -1;
        do {
            if (this.isHeader(itemPosition)) {
                headerPosition = itemPosition;
                break;
            }
            itemPosition -= 1;
        } while (itemPosition >= 0);
        return headerPosition;
    }

    // Stickyヘッダーレイアウト取得
    @Override
    public int getHeaderLayout(int headerPosition) {
        return R.layout.view_sticky_header;
    }

    // Stickyヘッダーのデータバインド
    @Override
    public void bindHeaderData(View header, int headerPosition) {

        if(items.get(headerPosition).type == HEADER){
            TextView headerTextView = (TextView) header.findViewById(R.id.header);
            headerTextView.setText(items.get(headerPosition).text);
            if(TextUtils.isEmpty(headerTitle) || !TextUtils.equals(headerTitle,items.get(headerPosition).text)){
                headerTitle = items.get(headerPosition).text;
            }
        }

    }
    // Stickyヘッダーの判定
    @Override
    public boolean isHeader(int itemPosition) {

        if(items.get(itemPosition).type == HEADER){
            return true;
        }

        return false;
    }

    //endregion
}
```