Androidでスティッキーヘッダーを自作したので実装方法を載せておきます。
サンプルはこちら
https://github.com/teradonburi/stickyheader
ライブラリをいくつか見てみたのですが、今一つ導入しにくかったのと
そもそもの中身がわからないとカスタマイズできない気がしたので自作しました。
スティッキーヘッダーとはスクロール時に下のような動きをするヘッダーです。
下の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
}