react-router-nativeでタブナビゲーター+スワイプ対応をする


react-routerのネイティブ用実装であるreact-router-nativeでスワイプ対応のボトムタブナビゲーションを実装してみました.

navigationライブラリの選定

そもそもReactNativeのnavigation用のライブラリのいくつかはスワイプアニメーションに既に対応しています.
それらのライブラリを使わず, react-router-nativeを使った理由について, まずは説明します.

自分がナビゲーターに求める機能とその理由は以下の5点です.

  • タブ, スタックに対応できる
  • スワイプによる切り替えができる
    • 別のアクションを入れる場合もあるが, UX的にスワイプ対応は必須だと思っているから
  • スクリーンのマウントタイミングが切り替え時であること
    • componentDidMount時にリスト要素の取得をする場合, 余計なViewの読み込みをされると困る
  • UIのカスタマイズ性
    • 特定のUIしか提供できない場合, そのライブラリを使い続けるのが厳しくなる局面が想定される為
  • webにも対応している
    • ReactNativeを触ったモチベーションの1つにweb, nativeのコードシェアリングがあるので, 共通化できると好印象
    • ナビゲーション周りはwebと挙動が変わる可能性は高いので必須ではない

react-navigation

ReactNativeは公式でreact-navigationを推奨しています.
実際, 簡単にスタックやタブナビゲーターを簡単に作成でき, 派生ライブラリでスワイプアニメーションにも対応できます.
ただ, タブのカスタマイズ性はイマイチだなと感じました.

デフォルトの状態でLandscapeにするとこんな感じの見た目になります.

もう少し高さが欲しくて, 文字はアイコンの下が良いと思った場合に, 恐らくtabBarComponentを指定することで調整は可能だと思われますが, propsの仕様はコードを読むしかなく (かつ追いづらい), この時点で候補から外しました.

また, webに対応してるようなことが書かれていますが, 対応していません.
対応するPRが一部出ていますが, これだけでは足りなさそうなので, webで使うのは現時点では避けたほうが無難です.

react-navigation-fluid-transitions, react-native-router-flux

FluidTransitionsreact-native-router-fluxはreact-navigationを依存関係に持つアニメーションライブラリと拡張ライブラリです.

react-navigationを候補から外したと同時に, これらも候補から外しました.

react-native-swiper

react-native-swiperは簡単にスワイプ対応ができる便利なライブラリです.
使い勝手はとても良いのですが, マウントタイミングの制御に難がありました. 同時保持スクリーン数は制御できるものの, スワイプする前に読み込ませることができず (スワイプ中はインジケータが表示される), 候補から外しました.

また, ナビゲーションライブラリではないので, ルーティングは自分で実装する必要があります.

react-router

react-routerはURLパスからのルーティングに対応している主にweb側で使われている (と思われる) ルーティングライブラリです.
ナビゲーションを提供するというよりは, リンクを貼る為のコンポーネントや遷移した際の画面切り替え用のコンポーネントが用意されている為, これを使って自分でナビゲーションを作る形になります.

手間は多いですが, ルーティングに重きを置いた設計思想は好印象です. また, web, nativeの両方への対応が同じレポジトリ内 (但しパッケージは別) で行われている為, 一緒にメンテされそうです.

UIは自分で作るので, 当然スワイプの対応もありませんが, そのぐらいやったらーということで, 実装するに至りました.

他のライブラリ

他にもいくつかあることは知っていますが, 調査しませんでした.
この時点で大分時間を持っていかれていたので疲れたというのと, react-routerをweb側に使うことはURLパスルーティング(?)の関係で, ほぼほぼ確定しており, react-router-nativeで可能なら実装したかったという思いもあります.

react-router-nativeでのスワイプ実装

実行フロー

さて本題です. 設計自体は割とシンプルに以下の手順です.
これに加えてスワイプによる遷移のトリガーを追加すれば大丈夫そうです.

  1. flex:1でViewを置く
  2. 1のViewのonLayoutで横幅, 縦幅のサイズを取得する
  3. 2で取得した横幅x3のViewを用意し, 表示位置を右/左に移動することでアニメーションを行う

実装

ここに周辺の実装は公開しているので, 必要があればこちらも参照してください.

実装の要点

最上位のViewは必須

onLayout用に1階層余分なViewがいますが, このViewは現状 (0.56) 必須です.
ReactNativeのバグな気がしますが, AndroidでAnimated.Viewflexを指定して同様の実装をした場合にtransform周りがおかしな挙動になります.
これを回避する為に1階層余分にViewを配置しています.

表示要素のkey指定はComponentの再生成を防ぐ為

prevLocation.pathnameのようにパスをkeyに指定することで, 前回と今回のrender結果に同じパスが含まれる場合に, Componentの再生成ではなく, propsの更新になるようにしています.

translateXでのアニメーション

多少なりともパフォーマンス良くスワイプする為に, useNativeDriverを指定しています.
その関係でtranslateXで位置を指定しています. leftはNativeDriverに対応していません.
また, webで使う場合はuseNativeDriverfalseに指定する必要があるはずです. (他にも動作しない箇所がある可能性あり)

パス判定は完全一致でしか判定していないので, 要調整

react-routerにはパスマッチ条件の調整ができる機能があります. これに対応していないので, 実際に使う場合は調整が必要です.

完成図


(マウントタイミングがわかるようにログを表示していますが, 2つの出力が1回分です)

それっぽい感じになった気がします. やったー.


投稿を作成しました 11

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

関連する投稿

検索語を上に入力し、 Enter キーを押して検索します。キャンセルするには ESC を押してください。