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