【RN】ScrollViewでSticky Header(固定ヘッダ)を実装
はじめに
今回はReactNative
のScrollView
やFlatList
を使って、動的なデータから固定ヘッダを作る方法をご紹介します。
何らかの規則に従って渡されるデータをリスト表示するアプリだと頻出のUIです。
今回作るもの
以下が動作イメージです。
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
もしくはFlatList
のstickyheaderindices
を使う
ReactNative
のリスト表示における代表的なコンポーネントといえばScrollView
もしくはFlatList
ですが、
実はこれらには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を抽出したい(上記の例でいうと0
や6
など)のでreduce
を使います。
reduce
の挙動については下記記事で扱っています。
実際に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>
)
}
これで冒頭のサンプルのような挙動になりました。
まとめ
今回はScrollView
やFlatList
を使って動的なデータからSticky Header
を実装する方法をご紹介しました。
リスト表示は凝り出すと複雑な挙動になりがちですが、ReactNative
側でこういったプロパティが用意されていると非常に助かるなという印象です。
Discussion