🍣

【RN】ScrollViewでSticky Header(固定ヘッダ)を実装

2021/05/21に公開

はじめに

今回はReactNativeScrollViewFlatListを使って、動的なデータから固定ヘッダを作る方法をご紹介します。
何らかの規則に従って渡されるデータをリスト表示するアプリだと頻出のUIです。

今回作るもの

以下が動作イメージです。

sample

AからEまでのヘッダ要素があり、それぞれの配下に任意の数のデータがあります。
それらをスクロール可能なリスト表示していますが、各ヘッダは次のヘッダがくるまで上部に固定されています。
一般的にSticky Header(スティッキーヘッダー)と呼ばれるようなUIです。

扱うデータ

扱うデータは以下の通りです。

const data = [
  {
    label: 'ヘッダ要素A',
    list: ['データA1', 'データA2', 'データA3', 'データA4', 'データA5'],
  },
  {
    label: 'ヘッダ要素B',
    list: ['データB1', 'データB2'],
  },
  {
    label: 'ヘッダ要素C',
    list: ['データC1', 'データC2', 'データC3'],
  },
  {
    label: 'ヘッダ要素D',
    list: ['データD1', 'データD2', 'データD3', 'データD4'],
  },
  {
    label: 'ヘッダ要素E',
    list: ['データE1', 'データE2', 'データE3', 'データE4', 'データE5'],
  },
];

labelとして表示させたいヘッダ文字列と、子要素に該当する文字列の配列であるlistから成るオブジェクトの配列です。

ScrollViewもしくはFlatListstickyheaderindicesを使う

ReactNativeのリスト表示における代表的なコンポーネントといえばScrollViewもしくはFlatListですが、
実はこれらにはstickyheaderindicesというプロパティが用意されています。

https://reactnative.dev/docs/scrollview#stickyheaderindices

stickyheaderindices数値型の配列を受け取り、その配列内に一致するindexのコンポーネントをSticky Headerとして扱います。

例えば[0,3]という値をstickyheaderindicesに渡すと、0番目と3番目のコンポーネントがSticky Headerとして振る舞うようになります。
今回はこの性質を利用していきます。

reduceを使ってヘッダ要素のindexを抽出する

冒頭で提示したサンプルにおけるScrollView内のコンポーネントは以下のような並びになっています。
※各カッコ内の数字はindexです

[0] ヘッダ要素A
[1] データA1
[2] データA2
[3] データA3
[4] データA4
[5] データA5
[6] ヘッダ要素B
[7] データB1
[8] データB2
[9] ヘッダ要素C
...
[23] データE5

ここではヘッダ要素のindexを抽出したい(上記の例でいうと06など)のでreduceを使います。
reduceの挙動については下記記事で扱っています。

https://zenn.dev/nekoniki/articles/07c09eb6811c85a753de

実際にstickyHeaderIndicesを得るには下記のようにします。

const stickyHeaderIndices: number[] = data.reduce(
  (acc, cur, index, _list) => {
    // 最後の要素は使用しない
    if (_list.length - 1 !== index) {
      // 直前のSticky要素の位置を取得
      const lastStickyIndex: number = acc[acc.length - 1];
      // ヘッダとしての1要素 + 内包しているlist要素数を足して新しいStickyのindexとして設定
      acc.push(lastStickyIndex + cur.list.length + 1);
    }
    return acc;
  },
  [0],
);

console.log(stickyHeaderIndices);
// --> [0, 6, 9, 13, 18]

実際の実装

ここまでの内容を踏まえたソース一式が以下になります。

import React from 'react';
import {
  SafeAreaView,
  ScrollView,
  View,
} from 'react-native';

const App = () => {
  // 表示データ
  const data = [
    {
      label: 'ヘッダ要素A',
      list: ['データA1', 'データA2', 'データA3', 'データA4', 'データA5'],
    },
    {
      label: 'ヘッダ要素B',
      list: ['データB1', 'データB2'],
    },
    {
      label: 'ヘッダ要素C',
      list: ['データC1', 'データC2', 'データC3'],
    },
    {
      label: 'ヘッダ要素D',
      list: ['データD1', 'データD2', 'データD3', 'データD4'],
    },
    {
      label: 'ヘッダ要素E',
      list: ['データE1', 'データE2', 'データE3', 'データE4', 'データE5'],
    },
  ];

  // 表示データからstickyHeaderIndicesを取得
  const stickyHeaderIndices: number[] = data.reduce(
    (acc, cur, index, _list) => {
      // 最後の要素は使用しない
      if (_list.length - 1 !== index) {
        // 直前のSticky要素の位置を取得
        const lastStickyIndex: number = acc[acc.length - 1];
        // ヘッダとしての1要素 + 内包しているlist要素数を足して新しいStickyのindexとして設定
        acc.push(lastStickyIndex + cur.list.length + 1);
      }
      return acc;
    },
    [0]
  );

  return(
    <SafeAreaView style={{flex: 1}}>
      <ScrollView stickyHeaderIndices={stickyHeaderIndices}>
        {data.map((d, index) => {
          return [
            <View
              key={`header_${d.label}`}
              style={{padding: 10, backgroundColor: '#FFAAFF'}}>
              <Text>{d.label}</Text>
            </View>,
            ...d.list.map(record => {
              return (
                <View key={`${d.list}_${record}`} style={{padding: 10}}>
                  <Text>{record}</Text>
                </View>
              );
            }),
          ];
        })}
      </ScrollView>
    </SafeAreaView>
  )
}

これで冒頭のサンプルのような挙動になりました。

まとめ

今回はScrollViewFlatListを使って動的なデータからSticky Headerを実装する方法をご紹介しました。
リスト表示は凝り出すと複雑な挙動になりがちですが、ReactNative側でこういったプロパティが用意されていると非常に助かるなという印象です。

Discussion