React Native で「ユーザが画面を見る」イベントを発火する
はじめに
タイトル見て「ユーザが画面を見るイベントってなんぞや」ってなると思います。すみません、語彙力がなくてこれ以上説明できる文章が思いつかなかったです^_^…
1つ例をあげてみましょう。皆さんスマホでYoutube見てるとき動画リストをスクロールしてたら動画が勝手に再生されたこと、ありませんか?

動画の再生ボタン押してないのに不思議だなーと思いましたね。多分ユーザが動画リストをスクロールしてるとき「このセクションを見た」と判断したら再生させる処理をしてるのではないかなと推測しています。
今回話す「ユーザーが画面を見る」イベントもこれと同じです。もうちょっと正確な定義をすると「リストでレンダリングしてる各要素が画面に表示されると発火するイベント」でしょうか。
1. FlatList
本題に入る前に React Native のリストレンダリングについて軽く説明したいと思います。
ウェブの React プロジェクトではリストをレンダリングするときよくmapを使うと思いますが、React Native では FlatList というコンポーネントを使うことがメイジャーです。
mapでリストレンダリングすることも可能ではあります。しかし、React Native はコンテンツが溢れても自動的にスクロールを生成してくれないので ScrollView というコンポーネントで囲まないとスクロールできません。
FlatListを使うとそんなことしなくてもスクロールできます。超簡単なサンプルコードを見てみましょう。
const data = ['ITEM1','ITEM2','ITEM3','ITEM4', 'ITEM5', 'ITEM6', 'ITEM7','ITEM8','ITEM9','ITEM10'];
export function List() {
return (
<SafeAreaView>
<FlatList
// リストでレンダリングする情報の配列(array)
data={data}
// 各要素に key を付与する関数(function)
keyExtractor={((item, index) => `${item}${index}`)}
// dataで受け取った要素をレンダリングするコンポーネント(function)
renderItem={({item}) => (
<Text style={styles.item}>{item}</Text>
})
/>
</SafeAreaView>
);
}
dataがstringなので key はitem+indexにしました。keyにindexを使うことはよくないですけど、今は表示テストだけだしまあいいかなと^_^。
keyにindex使うことはよくない理由が気になる方はこの記事参考にしてください。
このコードをシミュレーターで確認してみたらこんな感じになります。

FlatList はスクロールが出来ること以外にも Lazy Loading ができる、便利な props がたくさんあるというメリットがあります。本題である「ユーザが画面を見る」イベントも、FlatList のonViewableItemsChanged、viewabilityConfigという props を使って実装します。
2. onViewableItemsChanged
onViewableItemsChangedは on 付いてるところからわかるようにイベント系のやつで、名前通り見える状態の要素が変わったとき発火するイベントです。型を見てみましょう。
onViewableItemsChanged?: ((info: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>
}) => void) | null;
引数はviewableItems、changedというプロパティを持ってるオブジェクトです。両方ともViewTokenというものの配列ですね。
interface ViewToken {
item: any;
key: string;
index: number | null;
isViewable: boolean;
section?: any;
}
ViewTokenもオブジェクトでした。
でも正直これだけ見たらどんな感じかあまりわからないんですよね〜そういうときは実戦です。実際に動かしてみましょう。
const data = ['ITEM1','ITEM2','ITEM3','ITEM4', 'ITEM5', 'ITEM6', 'ITEM7','ITEM8','ITEM9','ITEM10'];
export function List() {
return (
<SafeAreaView>
<FlatList
data={data}
renderItem={({item}) => (
<Text style={styles.item}>{item}</Text>
})
keyExtractor={((item, index) => `${item}${index}`)}
viewabilityConfig={viewabilityConfig}
// イベントハンドラー追加
onViewableItemsChanged={ ({viewableItems, changed}) => {
console.log("Viewable状態の要素", viewableItems);
console.log("状態が変わった要素", changed);
}}
/>
</SafeAreaView>
);
}
console.logでviewableItemsとchangedを出力するイベントハンドラーをonViewableItemsChangedで発火してみます。

console窓になんかいっぱい出力されました。
Viewable状態の要素 Array [
Object {
"index": 0,
"isViewable": true,
"item": "ITEM1",
"key": "ITEM10",
},
Object {
"index": 1,
"isViewable": true,
"item": "ITEM2",
"key": "ITEM21",
},
Object {
"index": 2,
"isViewable": true,
"item": "ITEM3",
"key": "ITEM32",
},
]
状態が変わった要素 Array [
Object {
"index": 0,
"isViewable": true,
"item": "ITEM1",
"key": "ITEM10",
},
Object {
"index": 1,
"isViewable": true,
"item": "ITEM2",
"key": "ITEM21",
},
Object {
"index": 2,
"isViewable": true,
"item": "ITEM3",
"key": "ITEM32",
},
]
Viewable状態の要素 Array [
Object {
"index": 0,
"isViewable": true,
"item": "ITEM1",
"key": "ITEM10",
},
Object {
"index": 1,
"isViewable": true,
"item": "ITEM2",
"key": "ITEM21",
},
Object {
"index": 2,
"isViewable": true,
"item": "ITEM3",
"key": "ITEM32",
},
Object {
"index": 3,
"isViewable": true,
"item": "ITEM4",
"key": "ITEM43",
},
]
状態が変わった要素 Array [
Object {
"index": 3,
"isViewable": true,
"item": "ITEM4",
"key": "ITEM43",
},
]
実際に動かしてみたらわかりやすかったです。
ViewToken
-
index:dataで渡した配列のindex -
isViewable: 該当要素が画面に表示されてるかどうか -
item:dataで渡した配列の要素 -
key:keyExtractorで生成した該当要素のkey
viewableItems、changed
-
viewableItems: 画面に表示されてる要素たち -
changed:isViewableの状態が更新された要素
なるほどなるほど。わかってきた気がします。
ちなみにログをよく見ると最初に「Viewable状態の要素(viewableItems)」と「状態が変わった要素(changed)」に全く同じ配列が入ってます。多分初期発火では viewableItems が無から3つの要素が入ったのでviewableItemsとchanged両方にカウントされたのではないかと私は推測してます。
3. viewabilityConfig
viewabilityConfigはonViewableItemsChangedについて色々設定できるオブジェクトです。これも型を見てみましょう。
interface ViewabilityConfig {
waitForInteraction?: boolean;
minimumViewTime?: number;
itemVisiblePercentThreshold?: number;
viewAreaCoveragePercentThreshold?: number;
}
4つのプロパティを持つオブジェクトですね。各プロパティが何を意味するかは今から説明していきます。
3-1. waitForInteraction
リストをレンダリング後、ユーザーがスクロールなどのインタラクションをするまでonViewableItemsChangedを発火せず待つかどうかの設定です。
waitForInteraction: falseの場合
ユーザーのインタラクションを待たないことになるので、初期レンダリングの瞬間からonViewableItemsChangedを発火します。

waitForInteraction: trueの場合
ユーザーのインタラクションがあるまでonViewableItemsChangedは発火されません。

3-2. minimumViewTime
各要素が画面にどれほど表示されるたらonViewableItemsChangedを発火するか時間(milliseconds)を指定します。
minimumViewTime: 0の場合
要素が1ミリセカンド(0.001秒)だけ画面に表示されてもonViewableItemsChangedを発火します。

ちょっとPCが重くなってて画像がガクガクしてますが、ぱっとスクロールを早く移動しても全要素についてonViewableItemsChangedが発火されてます。
minimumViewTime: 3000の場合
要素が3000ミリセカンド(3秒)表示されてからonViewableItemsChangedを発火します。

0のときとは違ってぱっとスクロール動かしても3秒以上表示された最後らへんの要素だけ発火されてますね。
3-3. itemVisiblePercentThreshold
各要素の何パーセントが画面に表示されたらonViewableItemsChangedを発火するかを指定します。
itemVisiblePercentThreshold: 0の場合
1pxでも画面に現れたらonViewableItemsChangedを発火します。

ITEM4が本当に少しだけ画面に現れたのにconsoleに出力されました。
itemVisiblePercentThreshold: 50の場合
各要素の50%以上が画面に現れたらonViewableItemsChangedを発火します。

3-4. viewAreaCoveragePercentThreshold
itemVisiblePercentThresholdと似てますが、基準が要素ではなくて viewport (画面全体)です。viewportの何パーセント占めたらonViewableItemsChangedを発火するかを指定します。
viewAreaCoveragePercentThreshold: 0の場合
1pxでも画面に現れたらonViewableItemsChangedを発火します。

viewAreaCoveragePercentThreshold: 10の場合
要素が画面の10%を占めたときonViewableItemsChangedを発火します。

viewAreaCoveragePercentThreshold: 100の場合
100にしたら画面全体を占めないとonViewableItemsChangedを発火しないと思うかもしれませんが、そうでもないです。
もちろん画面全体を占めたら発火します。でもそれ以外に全部表示されてる(Fully visible)要素も visible とみなされ、onViewableItemsChangedを発火します。

つまり100で設定すると画面全体を占める要素と、見切れず全部画面に出てる要素に対して発火することになります。
+)ちなみに4つのプロパティは全部任意ですが、itemVisiblePercentThresholdとviewAreaCoveragePercentThresholdのどちらかの一つは必ず含まれないとonViewableItemsChangedが正常に発火しません。
3-5. カスタム
これで基本は理解できましたので色々カスタムできます。
例え「リストの各要素が画面に1秒以上、全体のサイズの30%以上は表示されたときユーザーが見たことにカウントしたい」ならこうなるのでしょう
const viewabilityConfig {
minimumViewTime: 1000,
itemVisiblePercentThreshold: 30,
}
色々設定できるから面白いイベント作れそうですねヽ(・∀・)ノ
Discussion