React Native開発者必見!FlatList#viewabilityConfigCallbackPairsでUXを向上させる方法
React NativeにおけるFlatList#viewabilityConfigCallbackPairs
について
FlatList
は、React Nativeで大量のデータを効率的にリスト表示するためのコンポーネントです。スクロールに応じて要素のレンダリングや破棄を行うことで、メモリ使用量を抑え、パフォーマンスを向上させます。
viewabilityConfigCallbackPairs
プロパティは、FlatList
の**「表示されている要素」に関する詳細な制御と、それらの要素の表示状態が変化した際のコールバック**を設定するための強力な機能です。
これは配列の配列として定義され、各内部配列は以下の2つの要素から構成されます。
-
viewabilityConfig
:object
表示状態を判定するための設定オブジェクトです。例えば、「要素がどれくらい画面に表示されたら『表示された』とみなすか」といった閾値を設定できます。主なプロパティには以下のようなものがあります。minimumViewTime
:number
(ミリ秒) 要素が「表示された」とみなされるまでに、最低限表示されていなければならない時間です。itemVisiblePercentThreshold
:number
(0-100) 要素の何パーセントが画面に表示されたら「表示された」とみなすか、という閾値です。例えば、50
と設定すると、要素の半分が画面に表示されればコールバックが発火します。waitForInteraction
:boolean
ユーザーがスクロールするなどの操作を行うまで、表示判定を待つかどうかを制御します。viewAreaCoveragePercentThreshold
:number
(0-100)itemVisiblePercentThreshold
と似ていますが、これは要素の表示領域全体に対する割合で判定します。
-
onViewableItemsChanged
:(info: {changed: Array<ViewToken>, viewableItems: Array<ViewToken>}) => void
viewabilityConfig
で設定された条件に基づいて、要素の表示状態が変化した際に呼び出されるコールバック関数です。このコールバックには、以下の情報を持つオブジェクトが引数として渡されます。changed
: 表示状態が変化したアイテムのリストです。各ViewToken
には、そのアイテムが新しく表示されたのか(isViewable: true
)、それとも表示されなくなったのか(isViewable: false
)の情報が含まれます。viewableItems
: 現在画面に表示されていると判定されたすべてのアイテムのリストです。
なぜviewabilityConfigCallbackPairs
を使うのか?
このプロパティは、以下のような高度なユースケースで特に役立ちます。
- パフォーマンス最適化: 不要なレンダリングや処理を、要素が実際に表示されるまで遅延させる。
- UI/UXの改善: 特定のセクションが画面に表示されたら、そのセクションに関連するUI要素をハイライト表示する。
- Lazy Loading: 特定のコンポーネントやデータが画面に表示されそうになったら、事前にデータを読み込む(プリフェッチ)。
- 広告のトラッキング: 広告が実際にユーザーの画面に表示されたことを追跡し、インプレッション数をカウントする。
- 動画の自動再生/停止: ユーザーが動画コンテンツまでスクロールしたら自動的に再生を開始し、画面外にスクロールしたら停止する。
import React, { useRef } from 'react';
import { FlatList, View, Text, Dimensions } from 'react-native';
const DATA = Array.from({ length: 50 }, (_, i) => ({ id: String(i), title: `Item ${i + 1}` }));
const { height } = Dimensions.get('window');
function MyFlatList() {
const onViewableItemsChanged = useRef(({ changed, viewableItems }) => {
console.log('--- 表示状態が変化したアイテム ---');
changed.forEach(item => {
console.log(`ID: ${item.item.id}, 表示状態: ${item.isViewable ? '表示された' : '表示されなくなった'}`);
});
console.log('--- 現在表示されているアイテム ---');
viewableItems.forEach(item => {
console.log(`ID: ${item.item.id}`);
});
}).current;
// 表示判定の設定
const viewabilityConfig = {
itemVisiblePercentThreshold: 50, // アイテムの50%が表示されたらコールバックを発火
minimumViewTime: 500, // 最低500ミリ秒表示されていなければならない
};
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig, onViewableItemsChanged }
]).current;
const renderItem = ({ item }) => (
<View style={{ height: height * 0.3, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: 'gray', marginVertical: 5 }}>
<Text style={{ fontSize: 24 }}>{item.title}</Text>
</View>
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
// onViewableItemsChanged={onViewableItemsChanged} // 古い形式ではこのように直接渡していました
// viewabilityConfig={viewabilityConfig} // これも古い形式
/>
);
}
export default MyFlatList;
useRef
を使ってonViewableItemsChanged
やviewabilityConfigCallbackPairs
をメモ化しているのは、FlatList
が頻繁に再レンダリングされる際に、これらのオブジェクトが不必要に再作成されるのを防ぐためです。これにより、パフォーマンスの向上と予期せぬコールバックの再発火を防ぐことができます。- 以前は
onViewableItemsChanged
とviewabilityConfig
という別々のプロパティがありましたが、FlatList
のバージョンアップに伴い、より柔軟な制御のためにviewabilityConfigCallbackPairs
が導入されました。これにより、複数の異なる表示判定ロジックとコールバックのペアを同時に設定することが可能になりました。
"Changing onViewableItemsChanged on the fly is not supported" エラー
これは最もよく遭遇するエラーの一つです。onViewableItemsChanged
コールバック関数やviewabilityConfig
オブジェクトが、コンポーネントの再レンダリング時に新しく作成されてしまうと発生します。React Nativeは、これらのプロパティがレンダリングサイクル中に変更されることを許可していません。
原因
viewabilityConfig
オブジェクトも同様に、再レンダリング時に新しく作成される。onViewableItemsChanged
関数が、コンポーネントの内部で直接定義されており、コンポーネントが再レンダリングされるたびに新しい関数インスタンスが作成される。
トラブルシューティング/解決策
-
useRefまたはuseCallbackで関数と設定をメモ化する
onViewableItemsChanged
コールバック関数とviewabilityConfig
オブジェクトを、useRef
またはuseCallback
(関数用)とuseMemo
(オブジェクト用)を使ってメモ化し、コンポーネリングが再レンダリングされても同じインスタンスが使用されるようにします。import React, { useRef, useCallback, useMemo } from 'react'; import { FlatList, View, Text } from 'react-native'; function MyFlatList() { // onViewableItemsChanged を useCallback でメモ化 const onViewableItemsChanged = useCallback(({ changed, viewableItems }) => { // ロジック console.log('表示状態が変化しました:', changed); }, []); // 依存配列を空にすることで、関数インスタンスが作成時に一度だけ生成される // viewabilityConfig を useMemo でメモ化 const viewabilityConfig = useMemo(() => ({ itemVisiblePercentThreshold: 50, minimumViewTime: 500, }), []); // 依存配列を空にすることで、オブジェクトインスタンスが作成時に一度だけ生成される // viewabilityConfigCallbackPairs も useRef でメモ化 const viewabilityConfigCallbackPairs = useRef([ { viewabilityConfig, onViewableItemsChanged } ]).current; // ... FlatList のレンダリング ... return ( <FlatList // ...他のProps... viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs} /> ); }
古い
onViewableItemsChanged
とviewabilityConfig
のプロパティを直接使う場合でも、同様にuseRef
やuseCallback
/useMemo
でメモ化する必要があります。
onViewableItemsChangedが期待通りに発火しない/遅延する
コールバックが発火しない、または発火が遅れることがあります。これは主にviewabilityConfig
の設定が厳しすぎる場合に起こります。
原因
- リストの高さやアイテムの高さが適切でない
FlatList
は仮想化を行うため、高さが不適切だと表示領域の計算が狂うことがあります。 - waitForInteractionがtrueになっている
ユーザーがスクロールなどの操作を行うまで、表示判定が行われません。デバッグ中など、自動で発火させたい場合には注意が必要です。 - itemVisiblePercentThresholdやviewAreaCoveragePercentThresholdが高すぎる
要素の大部分が画面に表示されないと判定されないため、少しでも隠れると発火しないことがあります。 - minimumViewTimeが長すぎる
要素が「表示された」と判定されるまでに、指定された時間(ミリ秒)だけ表示されている必要があります。この値が長すぎると、素早いスクロールではコールバックが発火しにくくなります。
トラブルシューティング/解決策
- FlatListやリストアイテムのスタイルを確認する
- アイテムに適切な高さが設定されているか確認します。特に
flex
レイアウトを使用している場合は、親コンポーネントや他の兄弟要素との兼ね合いも確認してください。 FlatList
自体にflex: 1
や適切な高さが設定されていることを確認します。
- アイテムに適切な高さが設定されているか確認します。特に
- viewabilityConfigの値を調整する
minimumViewTime
を短くする(例:0
または100
ミリ秒)。itemVisiblePercentThreshold
やviewAreaCoveragePercentThreshold
を小さくする(例:1
や10
)。waitForInteraction
をfalse
に設定する。
コールバック内のステートが古い(Stale Closure)
onViewableItemsChanged
コールバック内でコンポーネントのステートを参照している場合、そのステートの値がコールバックの作成時の値のままで、最新の値ではないことがあります。これはJavaScriptのクロージャとReactのレンダリングサイクルに起因する問題です。
原因
useRef
で参照している関数が、初期レンダリング時のステートをキャプチャしてしまっている場合。useCallback
を使用し、依存配列にステート変数を追加し忘れた場合。
トラブルシューティング/解決策
-
useRefで可変値を保持する
ステートの値ではなく、他の変更可能な値をコールバック内で参照したいが、コールバックの再作成は避けたい場合、useRef
を使って可変値を保持します。const myMutableValue = useRef(initialValue); // ...どこかで myMutableValue.current = newValue; で値を更新... const onViewableItemsChanged = useCallback(({ changed, viewableItems }) => { console.log('参照している値:', myMutableValue.current); }, []); // 依存配列は空
-
ステート更新関数を使用する (Function Update Form)
onViewableItemsChanged
内でステートを更新する場合、直接ステートの値を参照するのではなく、更新関数を使用します。これにより、コールバックが古いステートをキャプチャしていても、更新時には最新のステートに基づいて処理が行われます。const [viewableItemIds, setViewableItemIds] = useState([]); const onViewableItemsChanged = useCallback(({ viewableItems }) => { // `prevIds` は常に最新のステート値 setViewableItemIds(prevIds => { const newViewableIds = viewableItems.map(item => item.item.id); // ロジックに基づいて新しいIDリストを返す return newViewableIds; }); }, []); // 依存配列は空でOK、なぜなら更新関数は常に同じ参照を保つため
-
useCallbackの依存配列に全てのステート変数を含める
onViewableItemsChanged
関数が依存するステート変数やプロップスを、useCallback
の第二引数の配列(依存配列)に含めることで、それらの値が変更されたときに新しい関数インスタンスが生成され、最新のステートがキャプチャされます。const [count, setCount] = useState(0); const onViewableItemsChanged = useCallback(({ changed, viewableItems }) => { // count は最新の値になる console.log('現在のカウント:', count); // ... }, [count]); // count が変更されたら新しい関数が作成される
ただし、これにより
onViewableItemsChanged
関数が頻繁に再作成され、「Changing onViewableItemsChanged on the fly is not supported」エラーが再度発生する可能性があります。このジレンマを解決するためには、以下の方法を検討します。
FlatListのパフォーマンス問題と関連する誤解
viewabilityConfigCallbackPairs
は表示判定を行うため、パフォーマンスに影響を与える可能性があります。
よくある誤解と問題
- 不要な再レンダリング
onViewableItemsChanged
がステートを更新することで、関連するコンポーネントが不要に再レンダリングされ、パフォーマンスが低下することがあります。 - onViewableItemsChanged内で重い処理を行う
コールバックはスクロール中に頻繁に発火する可能性があるため、ここで時間のかかる処理(大量のデータ操作、複雑なコンポーネントのレンダリング、ネットワークリクエストなど)を行うと、UIのフリーズやラグが発生します。
トラブルシューティング/解決策
- デバッグモードでのパフォーマンス低下
React Nativeのデバッグモード(Chrome Debuggerなど)では、パフォーマンスが大幅に低下することがよくあります。実機でのリリースビルド(Release Mode)でパフォーマンスを評価することが重要です。 - initialNumToRender, maxToRenderPerBatch, windowSizeなどのプロパティを調整する
これらのFlatList
のパフォーマンス関連プロパティを調整することで、レンダリングされるアイテムの数を制御し、表示判定の負荷を軽減できる場合があります。 - React.memoやPureComponentでアイテムコンポーネントを最適化する
renderItem
で描画される個々のリストアイテムが、不要なプロップスの変更で再レンダリングされないように、React.memo
(関数コンポーネント)やPureComponent
(クラスコンポーネント)でラップすることを検討します。これにより、FlatList
の仮想化の恩恵を最大限に受けられます。 - コールバック内の処理を最小限にする
onViewableItemsChanged
では、表示状態の判定と、必要最低限のステート更新のみを行うようにします。重い処理は、そのステート変更をトリガーとして、useEffect
などで非同期に実行することを検討します。
直接viewabilityConfigCallbackPairs
のエラーではありませんが、FlatList
全般の共通エラーとして、keyExtractor
が適切に設定されていないと、リストアイテムの再レンダリングや表示状態の追跡に問題が生じることがあります。
原因
keyExtractor
がそもそも設定されていない(デフォルトではindex
が使われ、これは推奨されない)。keyExtractor
がユニークなキーを返さない。
トラブルシューティング/解決策
-
各リストアイテムにユニークなキーを設定する
data
配列の各要素には、id
のようなユニークなプロパティがあるはずです。それをkeyExtractor
で返すようにします。<FlatList data={DATA} renderItem={renderItem} keyExtractor={item => item.id} // item.id がユニークであることを確認 // ... />
FlatList#viewabilityConfigCallbackPairs
は、FlatList
内の要素の表示状態(ビューアビリティ)を詳細に監視し、その状態が変化した際に特定の処理を実行するための強力な機能です。ここでは、いくつかの一般的なユースケースに焦点を当ててコード例を見ていきます。
基本的な表示・非表示のログ出力
最も基本的な例として、アイテムが画面に表示されたり非表示になったりしたときにコンソールにログを出力する例です。
import React, { useRef, useCallback, useMemo } from 'react';
import { FlatList, View, Text, Dimensions, StyleSheet } from 'react-native';
const { height: screenHeight } = Dimensions.get('window');
// ダミーデータ
const DATA = Array.from({ length: 30 }, (_, i) => ({
id: String(i),
title: `アイテム ${i + 1}`,
description: `これはアイテム ${i + 1} の詳細です。`,
}));
function BasicViewabilityExample() {
// `onViewableItemsChanged` コールバック関数をメモ化
// 依存配列が空なので、コンポーネントのマウント時に一度だけ作成される
const onViewableItemsChanged = useCallback(({ changed, viewableItems }) => {
console.log('--- 表示状態が変化したアイテム (changed) ---');
changed.forEach(item => {
console.log(`ID: ${item.item.id}, Viewable: ${item.isViewable}`);
});
// 現在表示されているすべてのアイテムをログに出力
// console.log('--- 現在表示されているアイテム (viewableItems) ---');
// viewableItems.forEach(item => {
// console.log(`ID: ${item.item.id}`);
// });
}, []);
// `viewabilityConfig` オブジェクトをメモ化
// アイテムの50%が表示され、かつ最低100ms表示されたら発火
const viewabilityConfig = useMemo(() => ({
itemVisiblePercentThreshold: 50, // アイテムの50%が表示されたら
minimumViewTime: 100, // 最低100ミリ秒表示されたら
// waitForInteraction: false, // デフォルトはfalseなので不要だが明示的に書いても良い
}), []);
// `viewabilityConfigCallbackPairs` を useRef でメモ化
// これにより、FlatList に渡される配列インスタンスが常に同じになる
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig, onViewableItemsChanged }
]).current;
// 各リストアイテムのレンダリング
const renderItem = useCallback(({ item }) => (
<View style={styles.itemContainer}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemDescription}>{item.description}</Text>
</View>
), []); // item が変更されない限り再レンダリング不要なので useCallback でラップ
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
// FlatList 自体の高さ設定
style={styles.flatList}
contentContainerStyle={styles.contentContainer}
/>
);
}
const styles = StyleSheet.create({
flatList: {
flex: 1,
backgroundColor: '#f5f5f5',
},
contentContainer: {
paddingVertical: 10,
},
itemContainer: {
height: screenHeight * 0.25, // 画面の高さの25%を各アイテムの高さとする
backgroundColor: 'white',
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
justifyContent: 'center',
alignItems: 'center',
},
itemTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 5,
},
itemDescription: {
fontSize: 14,
color: '#666',
},
});
export default BasicViewabilityExample;
解説
onViewableItemsChanged
コールバックは、表示状態が変化したchanged
アイテムと、現在表示されているviewableItems
のリストを受け取ります。この例ではchanged
リストを使って、どのアイテムが表示されたり非表示になったかをログ出力しています。viewabilityConfig
では、アイテムが50%画面に表示され、かつ最低100ミリ秒その状態が継続した場合にコールバックが発火するよう設定しています。useRef
,useCallback
,useMemo
を使用して、onViewableItemsChanged
関数とviewabilityConfig
オブジェクトのインスタンスが再レンダリング時に変わらないようにしています。これにより、「Changing onViewableItemsChanged on the fly is not supported」というエラーを防ぎます。
表示された動画の自動再生/停止
ユーザーが動画アイテムまでスクロールしたら自動的に再生を開始し、画面外にスクロールしたら停止する例です。
import React, { useRef, useState, useCallback, useMemo } from 'react';
import { FlatList, View, Text, Dimensions, StyleSheet, Button } from 'react-native';
// 動画ライブラリのモック (実際には react-native-video などを使用)
const VideoPlayer = ({ videoId, isPlaying }) => {
console.log(`Video ${videoId}: ${isPlaying ? '再生中' : '停止中'}`);
return (
<View style={styles.videoPlayer}>
<Text style={styles.videoText}>{videoId}</Text>
<Text style={styles.videoStatus}>{isPlaying ? ' 再生中' : ' 停止中'}</Text>
</View>
);
};
const { height: screenHeight } = Dimensions.get('window');
const VIDEO_DATA = Array.from({ length: 10 }, (_, i) => ({
id: `video-${i + 1}`,
title: `動画 ${i + 1}`,
}));
function VideoAutoPlayExample() {
// 現在再生中の動画IDを保持するステート
// onMomentumScrollEnd などで初期化することも考慮に入れる
const [playingVideoId, setPlayingVideoId] = useState(null);
// onMomentumScrollEnd または onScrollEndDrag での利用を想定した useRef
const lastViewableItemsRef = useRef([]);
// `onViewableItemsChanged` コールバック
const onViewableItemsChanged = useCallback(({ changed, viewableItems }) => {
lastViewableItemsRef.current = viewableItems; // 最新の表示アイテムを保存
// 現在再生中の動画を特定し、状態を更新する
const currentViewableVideo = viewableItems.find(item => item.item.id.startsWith('video-'));
if (currentViewableVideo && currentViewableVideo.item.id !== playingVideoId) {
// 新しい動画が表示されたら再生
setPlayingVideoId(currentViewableVideo.item.id);
} else if (!currentViewableVideo && playingVideoId) {
// 表示中の動画がなくなり、かつ何か再生中だった場合は停止
setPlayingVideoId(null);
}
}, [playingVideoId]); // playingVideoId が変更されたら新しいコールバックインスタンスを生成
// ビューアビリティ設定
// 画面中央付近に動画が完全に表示されたら発火するよう厳しめに設定
const videoViewabilityConfig = useMemo(() => ({
itemVisiblePercentThreshold: 90, // アイテムの90%以上が表示されたら
minimumViewTime: 200, // 最低200ms表示
waitForInteraction: false, // ユーザー操作を待たない
}), []);
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig: videoViewabilityConfig, onViewableItemsChanged }
]).current;
const renderItem = useCallback(({ item }) => (
<View style={styles.videoItemContainer}>
<Text style={styles.videoTitle}>{item.title}</Text>
<VideoPlayer
videoId={item.id}
isPlaying={playingVideoId === item.id} // 現在のアイテムが再生中か判定
/>
</View>
), [playingVideoId]); // playingVideoId が変更されたら、該当アイテムが再レンダリングされる
return (
<FlatList
data={VIDEO_DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
style={styles.flatList}
contentContainerStyle={styles.contentContainer}
// スクロール停止時にのみ再生を切り替える場合は以下のプロパティも検討
// onMomentumScrollEnd={() => {
// const currentViewableVideo = lastViewableItemsRef.current.find(item => item.item.id.startsWith('video-'));
// if (currentViewableVideo && currentViewableVideo.item.id !== playingVideoId) {
// setPlayingVideoId(currentViewableVideo.item.id);
// } else if (!currentViewableVideo && playingVideoId) {
// setPlayingVideoId(null);
// }
// }}
/>
);
}
const styles = StyleSheet.create({
flatList: {
flex: 1,
backgroundColor: '#e0f7fa',
},
contentContainer: {
paddingVertical: 10,
},
videoItemContainer: {
height: screenHeight * 0.6, // 各動画アイテムは画面の60%の高さ
backgroundColor: '#fff',
marginVertical: 10,
marginHorizontal: 16,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 6,
elevation: 5,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden', // VideoPlayer の表示を確実に
},
videoTitle: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 10,
color: '#333',
},
videoPlayer: {
width: '90%',
height: '70%',
backgroundColor: '#263238',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
videoText: {
fontSize: 28,
fontWeight: 'bold',
color: '#eceff1',
},
videoStatus: {
fontSize: 16,
marginTop: 5,
color: '#a7ffeb',
}
});
export default VideoAutoPlayExample;
解説
viewabilityConfig
は、動画がほぼ完全に画面に表示された場合にのみ再生を開始するように、itemVisiblePercentThreshold: 90
と厳しめに設定しています。renderItem
はplayingVideoId
の変更に依存するため、useCallback
の依存配列にplayingVideoId
を含めています。これにより、再生状態が変化した動画アイテムのみが再レンダリングされ、効率的です。- 新しい動画が表示された場合はそのIDを
playingVideoId
に設定し、動画が画面から外れた場合はplayingVideoId
をnull
に設定します。 onViewableItemsChanged
コールバック内で、viewableItems
リストをフィルタリングし、現在画面に表示されている動画アイテムを特定します。playingVideoId
ステートで現在再生中の動画のIDを管理します。
複数の異なる表示判定ロジックとコールバック
viewabilityConfigCallbackPairs
は配列なので、複数の異なる表示判定ルールとそれに対応するコールバックを設定できます。例えば、ある条件で広告のインプレッションを計測し、別の条件でアイテムのデータをプリフェッチする、といった使い方が考えられます。
この例では、以下の2つのルールを設定します。
- 「短時間でも表示されたら」 ログ出力
- 「半分以上表示されたら」 特定のステートを更新
import React, { useRef, useState, useCallback, useMemo } from 'react';
import { FlatList, View, Text, Dimensions, StyleSheet } from 'react-native';
const { height: screenHeight } = Dimensions.get('window');
const MIXED_DATA = Array.from({ length: 40 }, (_, i) => ({
id: `item-${i + 1}`,
type: i % 5 === 0 ? 'AD' : 'NORMAL', // 5つに1つを広告としてマーク
content: `コンテンツ ${i + 1}`,
}));
function MultipleViewabilityExample() {
const [fullyViewedItems, setFullyViewedItems] = useState(new Set()); // 完全に表示されたアイテムのIDを追跡
// --- 1つ目のコールバック: 短時間表示されたアイテムのログ ---
const onAnyItemViewed = useCallback(({ changed }) => {
changed.forEach(item => {
if (item.isViewable) {
console.log(`[Any Viewable] ID: ${item.item.id} が表示されました。`);
} else {
console.log(`[Any Viewable] ID: ${item.item.id} が非表示になりました。`);
}
});
}, []);
// --- 2つ目のコールバック: 半分以上表示されたアイテムのステート更新 ---
const onHalfItemViewed = useCallback(({ changed }) => {
// 古いステートに依存する更新なので、関数形式のステート更新を使う
setFullyViewedItems(prevSet => {
const newSet = new Set(prevSet);
changed.forEach(item => {
if (item.isViewable) {
newSet.add(item.item.id);
} else {
newSet.delete(item.item.id);
}
});
return newSet;
});
}, []); // 依存配列は空でOK、setFullyViewedItems は stable な参照のため
// --- 1つ目の viewabilityConfig: 1%でも見えたら発火 ---
const configAnyViewable = useMemo(() => ({
itemVisiblePercentThreshold: 1, // アイテムの1%が表示されたら
minimumViewTime: 50, // 最低50ミリ秒
}), []);
// --- 2つ目の viewabilityConfig: 50%以上見えたら発火 ---
const configHalfViewable = useMemo(() => ({
itemVisiblePercentThreshold: 50, // アイテムの50%が表示されたら
minimumViewTime: 200, // 最低200ミリ秒
}), []);
// 2つのペアを設定
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig: configAnyViewable, onViewableItemsChanged: onAnyItemViewed },
{ viewabilityConfig: configHalfViewable, onViewableItemsChanged: onHalfItemViewed },
]).current;
const renderItem = useCallback(({ item }) => (
<View
style={[
styles.mixedItemContainer,
{ backgroundColor: item.type === 'AD' ? '#ffe0b2' : '#e3f2fd' }, // 広告と通常で色分け
fullyViewedItems.has(item.id) && styles.fullyViewedItem, // 完全に表示されたらスタイル変更
]}
>
<Text style={styles.mixedItemTitle}>{item.content}</Text>
{item.type === 'AD' && <Text style={styles.adLabel}>広告</Text>}
{fullyViewedItems.has(item.id) && (
<Text style={styles.statusText}>完全に表示中!</Text>
)}
</View>
), [fullyViewedItems]); // fullyViewedItems が変更されたら再レンダリング
return (
<FlatList
data={MIXED_DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
style={styles.flatList}
contentContainerStyle={styles.contentContainer}
/>
);
}
const styles = StyleSheet.create({
flatList: {
flex: 1,
backgroundColor: '#f9f9f9',
},
contentContainer: {
paddingVertical: 10,
},
mixedItemContainer: {
height: screenHeight * 0.2, // 各アイテムは画面の20%の高さ
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ddd',
},
mixedItemTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
adLabel: {
fontSize: 12,
fontWeight: 'bold',
color: 'red',
position: 'absolute',
top: 5,
right: 10,
},
fullyViewedItem: {
borderColor: '#4caf50', // 完全に表示されたら緑色のボーダー
borderWidth: 2,
},
statusText: {
marginTop: 5,
fontSize: 12,
color: '#4caf50',
fontWeight: 'bold',
}
});
export default MultipleViewabilityExample;
fullyViewedItems
のステート更新には、関数形式のsetFullyViewedItems(prevSet => ...)
を使用しています。これにより、onHalfItemViewed
コールバックが古いfullyViewedItems
の値をクロージャとして保持していても、常に最新のステートに基づいてSet
を更新できます。configHalfViewable
とonHalfItemViewed
のペアは、アイテムの半分以上(50%)が表示された場合に、fullyViewedItems
ステートを更新します。このステートは、アイテムのスタイル変更に利用されています。configAnyViewable
とonAnyItemViewed
のペアは、アイテムが少しでも(1%)表示されればログを出力します。これは例えば、ユーザーが特定のアイテムを「見た」という単純な計測に使えます。viewabilityConfigCallbackPairs
配列に2つの異なる{ viewabilityConfig, onViewableItemsChanged }
ペアを設定しています。
onScroll イベントと NativeEvent を利用する
最も基本的な代替手段であり、スクロールイベントを監視して手動で要素の位置と表示状態を計算する方法です。