👁️‍🗨️

React Native で「ユーザが画面を見る」イベントを発火する

2021/12/10に公開

はじめに

タイトル見て「ユーザが画面を見るイベントってなんぞや」ってなると思います。すみません、語彙力がなくてこれ以上説明できる文章が思いつかなかったです^_^…

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>
  );
}

datastringなので key はitem+indexにしました。keyにindexを使うことはよくないですけど、今は表示テストだけだしまあいいかなと^_^。

keyにindex使うことはよくない理由が気になる方はこの記事参考にしてください。
https://zenn.dev/luvmini511/articles/f7b22d93e9c182

このコードをシミュレーターで確認してみたらこんな感じになります。

FlatList はスクロールが出来ること以外にも Lazy Loading ができる、便利な props がたくさんあるというメリットがあります。本題である「ユーザが画面を見る」イベントも、FlatList のonViewableItemsChangedviewabilityConfigという props を使って実装します。

2. onViewableItemsChanged

onViewableItemsChangedは on 付いてるところからわかるようにイベント系のやつで、名前通り見える状態の要素が変わったとき発火するイベントです。型を見てみましょう。

onViewableItemsChanged?: ((info: { 
viewableItems: Array<ViewToken>; 
changed: Array<ViewToken> 
}) => void) | null;

引数はviewableItemschangedというプロパティを持ってるオブジェクトです。両方とも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.logviewableItemschangedを出力するイベントハンドラーを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

viewableItemschanged

  • viewableItems: 画面に表示されてる要素たち
  • changed: isViewableの状態が更新された要素

なるほどなるほど。わかってきた気がします。

ちなみにログをよく見ると最初に「Viewable状態の要素(viewableItems)」と「状態が変わった要素(changed)」に全く同じ配列が入ってます。多分初期発火では viewableItems が無から3つの要素が入ったのでviewableItemschanged両方にカウントされたのではないかと私は推測してます。

3. viewabilityConfig

viewabilityConfigonViewableItemsChangedについて色々設定できるオブジェクトです。これも型を見てみましょう。

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つのプロパティは全部任意ですが、itemVisiblePercentThresholdviewAreaCoveragePercentThresholdのどちらかの一つは必ず含まれないとonViewableItemsChangedが正常に発火しません。

3-5. カスタム

これで基本は理解できましたので色々カスタムできます。

例え「リストの各要素が画面に1秒以上、全体のサイズの30%以上は表示されたときユーザーが見たことにカウントしたい」ならこうなるのでしょう

const viewabilityConfig {
    minimumViewTime: 1000,
    itemVisiblePercentThreshold: 30,
}

色々設定できるから面白いイベント作れそうですねヽ(・∀・)ノ

GitHubで編集を提案

Discussion