RecyclerView で固定ヘッダーを作成するにはどうすればいいですか? (外部ライブラリなし) 質問する

RecyclerView で固定ヘッダーを作成するにはどうすればいいですか? (外部ライブラリなし) 質問する

外部ライブラリを使用せずに、下の画像のように画面上部のヘッダービューを修正したいと考えています。

ここに画像の説明を入力してください

私の場合、アルファベット順にしたくありません。2 種類のビュー (ヘッダーと通常) があります。一番上の最後のヘッダーのみに固定したいのです。

ベストアンサー1

ここでは、外部ライブラリを使用せずにこれを行う方法について説明します。非常に長い投稿になるので、覚悟してください。

まず最初に、ティム・パエツ彼の投稿に触発されて、私は s を使用して独自のスティッキー ヘッダーを実装する旅に出ましたItemDecoration。実装では彼のコードの一部を借用しました。

すでに経験されているかもしれませんが、自分でやろうとすると、良い説明を見つけるのは非常に困難です。どうやって実際にその技術を使ってやることですItemDecoration。つまり手順は何ですか? その背後にあるロジックは何ですか? ヘッダーをリストの上部に固定するにはどうすればよいですか?これらの質問に対する答えがわからないため、他の人は外部ライブラリを使用することになりますが、 を使用すると自分で行うのはItemDecoration非常に簡単です。

初期条件

  1. データセットは、list異なるタイプのアイテムで構成されている必要があります (「Java タイプ」の意味ではなく、「ヘッダー/アイテム」タイプの意味です)。
  2. リストはすでにソートされているはずです。
  3. リスト内のすべての項目は特定のタイプである必要があり、それに関連するヘッダー項目が存在する必要があります。
  4. 最初の項目はlistヘッダー項目である必要があります。

RecyclerView.ItemDecorationここでは、と呼ばれる完全なコードを提供しますHeaderItemDecoration。次に、実行した手順を詳しく説明します。

public class HeaderItemDecoration extends RecyclerView.ItemDecoration {

 private StickyHeaderInterface mListener;
 private int mStickyHeaderHeight;

 public HeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface listener) {
  mListener = listener;

  // On Sticky Header Click
  recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
   public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
    if (motionEvent.getY() <= mStickyHeaderHeight) {
     // Handle the clicks on the header here ...
     return true;
    }
    return false;
   }

   public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {

   }

   public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

   }
  });
 }

 @Override
 public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
  super.onDrawOver(c, parent, state);

  View topChild = parent.getChildAt(0);
  if (Util.isNull(topChild)) {
   return;
  }

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

  View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  fixLayoutSize(parent, currentHeader);
  int contactPoint = currentHeader.getBottom();
  View childInContact = getChildInContact(parent, contactPoint);
  if (Util.isNull(childInContact)) {
   return;
  }

  if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
   moveHeader(c, currentHeader, childInContact);
   return;
  }

  drawHeader(c, currentHeader);
 }

 private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
  int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
  int layoutResId = mListener.getHeaderLayout(headerPosition);
  View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
  mListener.bindHeaderData(header, headerPosition);
  return header;
 }

 private void drawHeader(Canvas c, View header) {
  c.save();
  c.translate(0, 0);
  header.draw(c);
  c.restore();
 }

 private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
  c.save();
  c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
  currentHeader.draw(c);
  c.restore();
 }

 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) {
     // This child overlaps the contactPoint
     childInContact = child;
     break;
    }
   }
  }
  return childInContact;
 }

 /**
  * Properly measures and layouts the top sticky header.
  * @param parent ViewGroup: RecyclerView in this case.
  */
 private void fixLayoutSize(ViewGroup parent, View view) {

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

  // Specs for children (headers)
  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(), mStickyHeaderHeight = view.getMeasuredHeight());
 }

 public interface StickyHeaderInterface {

  /**
   * This method gets called by {@link HeaderItemDecoration} to fetch the position of the header item in the adapter
   * that is used for (represents) item at specified position.
   * @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
   * @return int. Position of the header item in the adapter.
   */
  int getHeaderPositionForItem(int itemPosition);

  /**
   * This method gets called by {@link HeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
   * @param headerPosition int. Position of the header item in the adapter.
   * @return int. Layout resource id.
   */
  int getHeaderLayout(int headerPosition);

  /**
   * This method gets called by {@link HeaderItemDecoration} to setup the header View.
   * @param header View. Header to set the data on.
   * @param headerPosition int. Position of the header item in the adapter.
   */
  void bindHeaderData(View header, int headerPosition);

  /**
   * This method gets called by {@link HeaderItemDecoration} to verify whether the item represents a header.
   * @param itemPosition int.
   * @return true, if item at the specified adapter's position represents a header.
   */
  boolean isHeader(int itemPosition);
 }
}

ビジネスの論理

それで、どうすればそれを維持できるのでしょうか?

いいえ。RecyclerViewカスタムレイアウトの達人で、12,000行以上のコードを暗記していない限り、選択したアイテムをただ止めて上に貼り付けることはできませんRecyclerView。UIデザインではいつもそうであるように、何かを作ることができない場合は、偽物を作りましょう。すべての上にヘッダーを描くだけですを使用しますCanvas。また、ユーザーが現在表示できるアイテムも知っておく必要があります。 は、表示可能なアイテムに関する と のItemDecoration両方の情報を提供しますCanvas。これを使用して、基本的な手順を次に示します。

  1. ユーザーに表示される最初の(一番上の)項目を取得するonDrawOver方法です。RecyclerView.ItemDecoration

        View topChild = parent.getChildAt(0);
    
  2. どのヘッダーがそれを表すかを決定します。

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
    
  3. メソッドを使用して、RecyclerView の上に適切なヘッダーを描画しますdrawHeader()

また、新しい次のヘッダーが最上位のヘッダーと出会ったときの動作も実装したいと思います。つまり、次のヘッダーが最上位の現在のヘッダーをビューからゆっくりと押し出し、最終的にその位置を占めるように見える必要があります。

ここでも、「すべての上に描画する」という同じ手法が適用されます。

  1. 一番上の「スタック」ヘッダーが新しい次のヘッダーと出会うタイミングを決定します。

            View childInContact = getChildInContact(parent, contactPoint);
    
  2. この接触ポイントを取得します (これは、描画した固定ヘッダーの下部と、次のヘッダーの上部です)。

            int contactPoint = currentHeader.getBottom();
    
  3. リスト内の項目がこの「接触点」を侵害している場合は、スティッキー ヘッダーを再描画して、その下部が侵害している項目の上部にくるようにします。これは、translate()の方法で実現しますCanvas。その結果、上部のヘッダーの開始点が可視領域外になり、「次のヘッダーによって押し出されている」ように見えます。完全になくなったら、新しいヘッダーを上に描画します。

            if (childInContact != null) {
            if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
                moveHeader(c, currentHeader, childInContact);
            } else {
                drawHeader(c, currentHeader);
            }
        }
    

残りは、私が提供したコード内のコメントと詳細な注釈によって説明されています。

使い方は簡単です:

mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));

mAdapter動作させるには実装する必要があります。StickyHeaderInterface実装は、所有するデータによって異なります。

最後に、ここでは半透明のヘッダーが付いた GIF を提供します。これにより、アイデアを把握し、実際に内部で何が起こっているかを確認できます。

これは、「すべての上に描画する」というコンセプトの図です。「ヘッダー 1」という 2 つの項目があることがわかります。1 つは描画して固定された位置で一番上に留まり、もう 1 つはデータセットから取得され、他のすべての項目とともに移動します。半透明のヘッダーがないため、ユーザーには内部の動作は見えません。

「あらゆるものの上に描くだけ」というコンセプト

「押し出し」フェーズでは次のことが起こります:

「押し出す」段階

お役に立てれば幸いです。

編集

getHeaderPositionForItem()以下は、RecyclerView のアダプターでのメソッドの実際の実装です。

@Override
public int getHeaderPositionForItem(int itemPosition) {
    int headerPosition = 0;
    do {
        if (this.isHeader(itemPosition)) {
            headerPosition = itemPosition;
            break;
        }
        itemPosition -= 1;
    } while (itemPosition >= 0);
    return headerPosition;
}

Kotlinでの実装が若干異なる

おすすめ記事