TrackableImageEmulator を読み解いてみる

概要

NRSDK (Nreal Light の Software Developer Kit) の Emulator を理解するために、 サンプルとして提供されている TrackableImageEmulator (Scene) のソースコードを解読してみます。 個人的な読解結果であるため誤りが含まれている可能性があることをご了承ください。

目次

確認環境

  • NRSDK 1.2.1

解説

まず、TrackableImageEmulator (Scene) を開き Hierarchy を確認しました。 Hierarchy に GameObject と GameObject にアタッチされている Component の一覧を網羅しています。 続いて、網羅した Component に含まれている Script を確認しました。 Scripts に Script の一覧を網羅しています。

網羅するだけでは分かりづらいので、サンプルコードを Application、Emulator、SDKの 3 つに分類します。

  • Application
    • Application に応じて変わるコードやオブジェクト
    • つまり、サンプルの部分になるコードやオブジェクト
  • Emulator
    • Emulator で動かす場合に必要となるコードやオブジェクト
    • Unity Editor の Play mode で動かす
  • SDK
    • Emulator を使わない場合でも必ず必要になるコードやオブジェクト
    • 大体の Application で使うのではと想像する

Game Object を上記の規則で分類すると以下の様になると思います。

  • Application
    • Directional Light
      • 無くても構わない
      • Emulator の Room、Image と Application の Cube を照らす
    • CubeCenter
      • Image Tracking の結果を示すためのオブジェクト
      • Image を認識したら Cube が表示される
  • Emulator
    • TrackableFoundTest
      • TrackableObserver と GameObject を関連づける
      • TrackableObserver の FoundEvent, LostEvent にリスナーを登録する
        • Found では GameObject の位置、方向を設定して有効化する
        • Lost では GameObject を無効化する
      • (Demo に合わせた機能になっている気がしないでもない)
      • (Emulator フォルダには Emulator の Demo も含まれていると考えた方がよいのかも)
    • EmulatorRoom
      • 部屋の Model
      • Emulator で動作させない時には不要になる
    • NRTrackableImageTarget
      • EmulatorRoom 同様、Emulator で動作させない時には不要なオブジェクト(のはず)
      • NRTrackableImageBehavior (Component) では、GameView に表示されている間は TrackingState を Tracking として Emulate する
      • TrackableObserver (Component) では、TrackingState に応じて FoundEvent/LostEvent を呼び出す
        • (Emulator を使わない場合でも必要な処理だと思うのだが、Emulator を使わない場合は書き換えないといけない?)
  • SDK
    • NRCameraRig
      • とにもかくにも必須になるカメラ
      • HMD の位置と方向を追跡する NRHMDPoseTracker (Component) を持つ
      • Emulator として動かすと以下をインスタンス化する
        • Prefabs/NREmulatorManager
        • Prefabs/NREmulatorHeadPose
    • NRInput
      • 入力を扱う為に必須になる
      • GazeTracker, ControllerTracker (Left/Right) により 3 つの Raycast を扱う
      • Laser の描画、Raycast が hit する点の描画を行う
      • デフォルトでは、ControllerTracker (Right) だけが有効
        • NRInput (Component) の DomainHand で Left/Right を指定
        • NRInput (Component) の Raycast Mode で Gaze/Laser を指定
      • Emulator として動かすと以下をインスタンス化する
        • Prefabs/NREmulatorManager
        • Prefabs/NREmulatorController

Hierarchy

※ [C] は Component を意味する。

Scripts

TrackableFoundTest の Scripts

  • TrackableFoundTest (in Emulator/Scripts)

NRTrackableImageTarget の Scripts

  • TrackableObserver (in Emulator/Scripts)
    • NRTrackableImageBehavior の DatabaseIndex を参照して、追跡対象であるかを検証している
    • NRFrame.GetTrackables(List<NRTrackableImage>, NRTrackableQueryFilter.All) を呼び出す
    • NRTrackableImage の TrackingState が Tracking ならば FoundEvent を呼び出し、Tracking 以外ならば LostEvent を呼び出す
    • FoundEvent の呼び出しでは、NRTrackableImage の CenterPose の position と rotation を渡す
      • CubeCenter の position と rotation に使われる
  • NRTrackableImageBehavior : NRTrackableBehaviour (in Emulator/Scripts)
    • NREmulatorManager.IsInGameView で transform (Tracking対象) が GameView に表示されているかを調べる
    • NREmulatorManager.NativeEmulatorApi.UpdateTrackableData() により TrackingState を Tracking/Stopped に切り替える
    • UpdateTrackableData を呼び出す際に、DatabaseIndex (NRTrackableBehaviour のメンバ) を渡している
    • つまり、GameView に表示されていれば画像の追跡は Tracking、表示されていなければ画像の追跡は Stopped として Emulate している

NRCameraRig の Scripts

  • AppManager (in Demos/HelloMR/Scripts)
    • OnEnable/OnDisable で NRInput に ClickListener を追加/削除している
    • ClickListener では HomeButton, AppButton の動作を設定している
    • (NRCameraRig (Prefab) から Demo の Script を参照してしまっていいんですかね?)
  • NRSessionBehavior (Reference) (in Scripts)
    • NSSessionManager を制御するMonoBehavior
      • Awake : CreateSession
      • Start : StartSession
      • OnApplicationPause(true) : DisableSession
      • OnApplicationPause(false) : ResumeSession
      • OnDisable : DisableSession
      • OnDestroy : DestroySession
    • NRSessionConfig で設定できる
      • Start のタイミングで NSSessionManager.SetConfiguration に渡される
  • NSSessionManager (in Scripts/Managers)
    • NRDevice の Init/Destroy
    • NativeTracking, NativeHeadTracking の開始/終了
    • InitEmulator
  • NRHMDPoseTracker (Reference) (in Scripts)
    • GetHeadPose で Pose (position と rotation の組) を取得できる
  • NRMultiDisplayManager (in Scripts/Managers)

NRInput の Scripts

  • NRInput (in Scripts/Input/Controller)
    • Inspector での設定項目
      • Emulate Virtual Display : false
      • Override Camera Center : None (Transform)
      • Anchor Helper
      • Raycast Mode : Laser
      • Click Interval : 0.3
      • Drag Threshold : 0.02
    • 各種入力の取得
    • 各種入力イベントのリスナー登録
    • DomainHand の設定により有効化するコントローラーを指定
    • InitEmulator
  • ControllerAnchorsHelper (in Scripts/Input/Controller)
    • 以下のAnchor を保持する
      • GazePoseTrackerAnchor
      • RightPoseTrackerAnchor
      • LeftPoseTrackerAnchor
      • RightModelAnchor
      • LeftModelAnchor
      • RightLaserAnchor
      • LeftLaserAnchor
  • GazeTracker (in Scripts/Input/Controller)
    • Gaze には注視、凝視という意味がある
    • Inspector での設定項目
      • raycaster に NRPointerRaycaster を設定する
        • GazeRaycaster (GameObject) の NRPointerRaycaster (Component) が設定されている
    • NRInput.OnControllerStatesUpdated に UpdateTracker を登録する
    • UpdateTracker では
      • NRInput.RaycastMode == RaycastModeEnum.Gaze ならば、GazeTracker を有効にする
      • GazeTracker が有効ならば
        • raycaster を有効にする
        • GazeTracker の position, rotation を NRInput.CameraCenter の position, rotation にする
  • ControllerTracker (in Scripts/Input/Controller)
    • Inspector での設定項目
      • defaultHandEnum に Right/Left を設定する
      • raycaster に NRPointerRaycaster を設定する
        • LaserRaycaster (GameObject) の NRPointerRaycaster (Component) が設定されている
      • modelAnchor に ModelAnchor (GameObject) が設定されている
    • NRInput.OnControllerRecentering に OnRecentering を登録する
    • NRInput.OnControllerStatesUpdated に UpdateTracker を登録する
    • OnRecentering では
      • VerifyYAngle の再計算
    • UpdateTracker では
      • NRInput.CheckControllerAvailable(defaultHandEnum) が真ならば、ControllerTracker を有効にする
      • ControllerTracker が有効ならば
        • NRInput.RaycastMode == RaycastModeEnum.Laser ならば、raycaster を有効にする
        • modelAnchor を有効にする
        • TrackPose を呼び出す
    • TrackPose では
      • NRInput.GetControllerAvailableFeature を呼び出して 6 DOF であるかを調べる
      • 6 DOF ならば
        • ControllerTracker の position を NRInput.GetPosition(defaultHandEnum) にする
        • raycaster, modelAnchor の localPosition を zero にする
        • ControllerTracker の localRotation を NRInput.GetRotation(defaultHandEnum) に VerifyYAngle の回転を加えた値にする
      • 6 DOF でなければ
        • ControllerTracker の position を SmoothTrackTargetPosition で計算して設定する
        • raycaster, modelAnchor の localPosition を起動時の初期値にする
        • ControllerTracker の localRotation を NRInput.GetRotation(defaultHandEnum) に VerifyYAngle の回転を加えた値にする
    • SmoothTrackTargetPosition では
      • TargetPos を NRInput.CameraCenter の位置と向きに基づいて計算する
      • ControllerTracker と TargetPos の距離が MaxDistanceFromTarget より大きくなれば、IsMovingToTarget を真にする
      • ControllerTracker と TargetPos の距離が 0.02f より小さくなれば、IsMovingToTarget を偽にする
      • IsMovintToTarget が真であれば、ControllerTracker を TargetPos に近づける
  • NRPointerRaycaster (in Scripts/Input/EventSystem/Raycasters)
    • Inspector での設定項目
      • Near Distance : 0
      • Far Distance : 15
      • Mask Type : Exclusive (/Inclusive)
      • Mask : Nothing (LayerMask value)
      • Show Debug Ray : true
      • Enable Physics Raycast : true
      • Enable Graphic Raycast : true
    • Raycast()
      • Near Distance と Far Distance の間で Ray をとばす
      • sortedRaycastResults に Raycast の結果が保存される
      • breakPoints (要素数 2 の List<Vector3>) に Ray の始点と終点を保存する
        • hit した場合は、終点は hit した位置
        • hit しなかった場合は、Far Distance の位置
    • Raycast(Ray ray, float distance, List raycastResults)
      • Enable Physics Raycast が有効ならば、PhysicsRaycast を呼び出す
      • Enable Graphic Raycast が有効ならば、CanvasTargetCollector.GetCanvases() で得られた全ての ICanvasRaycastTarget に対して GraphicRaycast を呼び出す
    • PhysicsRaycast(Ray ray, float distance, List<RaycastResult> raycastResults)
      • Physics.RaycastNonAlloc に Mask を渡す
      • Mask は Mask Type によって Exclusive/Inclusive を切り替える
    • GraphicRaycast(Canvas canvas, bool ignoreReversedGraphics, Ray ray, float distance, NRPointerRaycaster raycaster, List<RaycastResult> raycastResults)
  • NRLaserReticle (in Scripts/Input/EventSystem)
    • 焦点板、十字線、焦点面につける十字線のことをレチクルと呼ぶらしい
    • Inspector での設定項目
      • Raycaster : NRPointerRaycaster
      • Default Visual : ReticleState.Normal のときに有効にされる GameObject
      • Hover Visual : ReticleState.Hover のときに有効にされる GameObject
      • Default Distance : Raycaster の Near Distance と Far Distance の間に収まる値
        • ReticleState.Normal のときの表示に使われる
      • Reticle Size Ratio
        • localScale の計算に使われる
    • NRInput.ReticleVisualActive が false の場合は、ReticleState.Hide になる
    • Raycaster で hit があれば、ReticleState.Hover になる
      • HitTarget に hit した GameObject が設定される
    • Raycaster で hit がなければ、ReticleState.Normal になる
      • HitTarget は null に設定される
  • NRLaserVisual (in Scripts/Input/EventSystem)
    • Inspector での設定項目
      • Raycaster : NRPointerRaycaster
      • Line Renderer
      • Show On Hit Only
      • Default Distance : Raycaster の Near Distance と Far Distance の間に収まる値
    • NRInput.LaserVisualActive が false の場合は、Line Renderer が無効になる
    • Show On Hit Only が true、かつ、Raycaster で hit がなければ、Line Renderer が無効になる
    • Show On Hit Only が flase、または、Raycaster で hit があれば、Line Renderer で線を描画する
      • 始点は Near Distance の位置
      • 終点は hit した位置、または、Default Distance の位置

NREmulatorManager の Scripts

  • NREmulatorManager (in Emulator/Scripts)
    • NativeEmulatorApi の CreateSIMTracking を呼び出す
    • NativeEmulatorApi の CreateSIMController を呼び出す

NREmulatorHeadPose の Scripts

  • NREmulatorHeadPose (in Emulator/Scripts)
    • キーボード、マウス操作を HMD の動きとして Emulate する

NREmulatorController の Scripts

  • NREmulatorController (in Emulator/Scripts)
    • キーボード、マウス操作を Controller の動きとして Emulate する

参考文献

NrealLight

(3ヶ月前にメモ書きしていた記事です。)

概要

中国のスタートアップNreal(エンリアル)社が開発したMRグラス。

www.nreal.ai

メガネ型、88g、4基のカメラで空間認識、視野角52度(Oculus Quest*1は100度)、解像度1920×1080(非公表数値; Oculus Questは1600x1440)。 SLAM(Simultaneous Localization and Mapping; 自己位置推定と環境地図作成)、平面認識、画像認識を搭載。 現実世界のオブジェクトを解析してCGを現実世界に反映させる高度なMR機能は対応していないらしい。

Snapdragon 855を搭載したAndroidスマートフォンとUSB Type-Cで有線接続して使用。以下は、Snapdragon 855搭載Androidスマートフォン:

アプリケーションは以下が動作可能になる:

主な出来事

時期 出来事
2019年5月 KDDIとの提携を発表
2019年12月9日 KDDIとNreal社はMRグラス「NrealLight」を用いた開発プログラム「EVE2020」を開始
2019年9月 開発者版提供開始(1199ドル)
2020年初旬 一般発売予定(499ドル)

NRSDK 1.0 Beta

Unityのみ対応(いずれは Unreal Engine, Android Native にも対応)

空間コンピューティング(Spatial Computing)

  • 6DoFのトラッキング(6DoF Tracking)
  • 平面認識(Plane Detection)
    • 1.0 Beta では水平のみ
  • 画像認識(Image Tracking)
  • 環境のマッピング
  • 視界内の対象物を分析、認識、理解

レンダリングの最適化(Rendering Optimization)

Multi-modal Interactions

  • Nreal Light Controller (3DoF)
  • Nreal Phone Controller (3DoF)
    • 未サポート

Developer Tools

サードパーティSDK拡張 (3rd Party SDK Extension)

  • センサー(例:RGBカメラ)からデータにアクセスする、など

参考文献

*1:Oculus QuestはVRバイスであり、MRデバイスであるNrealLightとの比較は不適切かもしれませんが、比較的手頃に入手できるデバイスとの比較ということで。私が所持しているというということが一番の理由ですが。

Chromeで範囲選択したテキストをDeepL翻訳で翻訳する

概要

Google翻訳を超える翻訳の精度と噂されるDeepL翻訳を使う際、わざわざDeepL翻訳のページを開いてテキストを入力するのは手間がかかります。 Google翻訳には選択範囲をGoogle翻訳で翻訳するChrome拡張機能がありますが、DeepL翻訳はまだ無いようです。 しかし、Chrome拡張機能「Selection Search」を使えば範囲選択した範囲を翻訳させることができます。 この記事では、「Selection Search」を使用してDeepL翻訳を使用する方法を説明します。

目次

確認環境

解説

目指している状態は次の画像のとおりです。テキストを範囲選択し、メニューで en -> ja (英語から日本語に翻訳) 、ja -> en (日本語から英語に翻訳)を選択するとDeepLのページが開き翻訳が行われます。

まず、Chrome 拡張機能 Selection Search を追加します。

chrome.google.com

次に、Selection Search のオプションを変更します。オプションは次の画像のメニューから開けます。アドレスバーに chrome-extension://gipnlpdeieaidmmeaichnddnmjmcakoe/options/options.html を貼り付けてもオプションが開けます。

オプション項目の Search engines を編集します。#en/ja/#ja/en/ で翻訳元言語と翻訳先言語を指定しています。

ここまででも構いませんが、お好みに合わせて以下の設定を変えるとよいでしょう。

Popup Menu を Auto にするとテキストを範囲選択するだけでメニューが開きます。範囲選択するだけでメニューが開くと邪魔な場合は、Mouse Click などを選択すると良いでしょう。

Other Options には Open search in new tab (新しいタブで検索結果を開く)設定があります。他にも設定があるので、色々と試しつつお好みの設定を探してみると良いと思います。

Android Studio のログにソースコードへのリンクを表示する

概要

Android Studio のログ出力にソースコードへのリンクを表示する方法です。

目次

確認環境

  • AndroidStudio 3.5.2
  • Kotlin 1.3.50

参考情報

解説

Android Studio のログ出力では、クラス名.メソッド名(ファイル名:行数) の形式で表示したときにソースコードへのリンクが有効になります。 Logcat, Instrumentation Test の実行ログでリンクが有効になることを確認しています。

fun printLinkToCode(tag: String) {
    val clazz = object {}::class.java
    val enclosingClassName = requireNotNull(clazz.enclosingClass?.name)
    val enclosingMethodName = requireNotNull(clazz.enclosingMethod?.name)

    val stackTrace = Thread.currentThread().stackTrace
    val indexOfThisMethod = stackTrace.indexOfFirst {
        it.className == enclosingClassName && it.methodName == enclosingMethodName
    }

    val caller = stackTrace[indexOfThisMethod + 1]
    Log.i(tag, "${caller.className}.${caller.methodName}(${caller.fileName}:${caller.lineNumber})")
}

Zero Caliber VR を Oculus Quest でプレイする

概要

SteamVR で配信されている Zero Caliber VR を Qculus Quest でプレイする方法です。Virtual Desktop を使用しています。

目次

確認環境

  • Virtual Desktop 1.6.2 (SIdeloading)
  • Zero Caliber VR (2019/10/14 時点のバージョン)

解説

SteamVR のアプリを Oculus Quest で実行する方法については、他の記事を参照してください。Zero Caliber VR を Oculus Quest で起動できた前提で話しを進めます。

2019/10/14 現在、Zero Caliber VR を Oculus Quest で実行すると Oculus Touch の操作を何も受け付けてくれません。この問題は SteamVR の Controller Settings から設定を行うことで解決できます。

Current Binding の Edit を選択すると Zero Caliber VR の Action を設定できます。少々面倒ですが、各ボタンに Action を割り当てます。私は下記の割り当てでチュートリアルまでクリアできました。

  • Left Oculus Touch
    • Trigger
      • Click : TriggerLeft
      • Pull : TriggerAxisLeft
    • Joystick
      • Click : TurnLeft (座ってプレイする場合)
      • Position : MotionControllerThumbLeft
    • Grip
      • Click : GripLeft
    • X Button
      • Click : Open Console (お好みで)
    • Y Button
      • Click : MenuLeft (お好みで)
  • Right Oculus Touch
    • Trigger
      • Click : TriggerRight
      • Pull : TriggerAxisRight
    • Joystick
      • Click : TurnRight (座ってプレイする場合)
      • Touch : Run (お好みで)
      • Position : MotionControllerThumbRight
    • Grip
      • Click : GripRight
    • A Button
      • Click : Skill_ReleaseManazine (お好みで)
    • B Button
      • Click : Skill_FireModeChange (お好みで)
  • 未割り当て
    • Skill_RelaseSlider
    • ControllerMovementLeft
    • ControllerMovementRight

立ってプレイする場合には、自分が振り向けば良いので TurnLeft/Right は不要です。立ってプレイしないとしゃがめないので被弾してしまう場面があり、立ってプレイすることは必須なのですが...。

設定は保存できるので保存を推奨します。次回起動時、設定がクリアされてしまいます。設定を保存しておかないと、設定をやり直すことになります。

Room の使い方

概要

Android Kotlin Fundamentals 06.1 (Room) の備忘録です。ポイントとなるコードのみを抜粋しています。

目次

確認環境

  • AndroidStudio 3.5
    • compileSdkVersion 28
    • minSdkVersion 19
  • Gradle 5.4.1
  • Kotlin 1.3.11

参考情報

解説

build.gradle

buildscript {

    ext {
        kotlin_version = '1.3.11'
        archLifecycleVersion = '1.1.1'
        room_version = '2.0.0'
        coroutine_version = '1.0.0'
        gradleVersion = '3.3.0'
        navigationVersion = '1.0.0-alpha08'
        dataBindingCompilerVersion = gradleVersion // Always need to be the same.
    }

    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath "com.android.tools.build:gradle:$gradleVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}

...

app/build.gradle

...

dependencies {
    ...
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

SleepNight.kt

...

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
        @PrimaryKey(autoGenerate = true)
        var nightId: Long = 0L,

        @ColumnInfo(name = "start_time_milli")
        val startTimeMilli: Long = System.currentTimeMillis(),

        @ColumnInfo(name = "end_time_milli")
        var endTimeMilli: Long = startTimeMilli,

        @ColumnInfo(name = "quality_rating")
        var sleepQuality: Int = -1
)

SleepDatabaseDao.kt

...

@Dao
interface SleepDatabaseDao {
    @Insert
    fun insert(night: SleepNight)

    @Update
    fun update(night: SleepNight)

    @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
    fun get(key: Long): SleepNight?

    @Query("DELETE FROM daily_sleep_quality_table")
    fun clear()

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
    fun getTonight(): SleepNight?

    @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
    fun getAllNights(): LiveData<List<SleepNight>>
}
  • アノテーションは Insert, Update, Delete, Query の 4 つ
  • LiveData を戻り値にするとデータベースのデータが変更された場合に変更を通知する
    • (変更の検知はデータベース単位?テーブル単位?便利だけどパフォーマンスは問題にならない?)

SleepDatabase.kt

...

@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {

    abstract val sleepDatabaseDao: SleepDatabaseDao

    companion object {
        @Volatile
        private var INSTANCE: SleepDatabase? = null

        fun getInstance(context: Context): SleepDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(context.applicationContext, SleepDatabase::class.java, "sleep_history_database")
                            .fallbackToDestructiveMigration()
                            .build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

SleepDatabaseTest.kt

...

@RunWith(AndroidJUnit4::class)
class SleepDatabaseTest {

    private lateinit var sleepDao: SleepDatabaseDao
    private lateinit var db: SleepDatabase

    @Before
    fun createDb() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        db = Room.inMemoryDatabaseBuilder(context, SleepDatabase::class.java)
                .allowMainThreadQueries()
                .build()
        sleepDao = db.sleepDatabaseDao
    }

    @After
    @Throws(IOException::class)
    fun closeDb() {
        db.close()
    }

    @Test
    @Throws(Exception::class)
    fun insertAndGetNight() {
        val night = SleepNight()
        sleepDao.insert(night)
        val tonight = sleepDao.getTonight()
        assertEquals(tonight?.sleepQuality, -1)
    }
}
  • inMemoryDatabaseBuilder を使うことでテスト毎にデータベースをクリアする
  • allowMainThreadQueries を実行することでデータベースへの問い合わせを MainThread で実行できる様にする
    • 通常は MainThread でデータベースに問い合わせるとエラーになる

Navigation component の使い方

概要

Android Kotlin Fundamentals 03.x (Fragment と Navigation) の備忘録です。ポイントとなるコードのみを抜粋しています。

目次

確認環境

  • AndroidStudio 3.5
    • compileSdkVersion 28
    • minSdkVersion 19
  • Gradle 5.4.1
  • Kotlin 1.3.11

参考情報

解説

build.gradle

buildscript {
    ext {
        kotlin_version = '1.3.11'
        archLifecycleVersion = '1.1.1'
        gradleVersion = '3.5.0'
        supportlibVersion = '1.0.0-rc03'
        navigationVersion = '1.0.0-rc02'
        dataBindingCompilerVersion = gradleVersion // Always need to be the same.
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradleVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}
  • Safe Args Gradle plugin を classpath に追加

app/build.gradle

...
apply plugin: 'androidx.navigation.safeargs'
...
dependencies {
    ...
    implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
    implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"
    ...
}
  • Safe Args Gradle plugin を有効化
  • Navigation components 関連のライブラリを追加

activity_main.xml

...
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayout"
        ...>

        <!-- サンプルでは LinearLayout の子になっている -->
        <fragment
            android:id="@+id/myNavHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/navigation"
            app:defaultNavHost="true"
            ... />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navView"
            android:layout_gravity="start"
            app:headerLayout="@layout/nav_header"
            app:menu="@menu/navdrawer_menu"
            ... />

    </androidx.drawerlayout.widget.DrawerLayout>
</layout>
  • Drawer を使用しない場合は DrawerLayout は不要
  • Navigation components を使って Fragment を切り替えるため NavHostFragment を使用
  • defaultNavHost を true に設定した Fragment が Back ボタンをインターセプト
  • NavigationView は Drawer に表示するナビゲーション
...
<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@id/titleFragment">

    <fragment
        android:id="@+id/titleFragment"
        android:name="com.example.android.navigation.TitleFragment"
        android:label="fragment_title"
        tools:layout="@layout/fragment_title" >
        <action
            android:id="@+id/action_titleFragment_to_gameFragment"
            app:destination="@id/gameFragment" />
    </fragment>

    <fragment
        android:id="@+id/gameFragment"
        android:name="com.example.android.navigation.GameFragment"
        android:label="fragment_game"
        tools:layout="@layout/fragment_game" >
        <action
            android:id="@+id/action_gameFragment_to_gameOverFragment"
            app:destination="@id/gameOverFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_gameFragment_to_gameWonFragment"
            app:destination="@id/gameWonFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/gameOverFragment"
        android:name="com.example.android.navigation.GameOverFragment"
        android:label="fragment_game_over"
        tools:layout="@layout/fragment_game_over" >
        <action
            android:id="@+id/action_gameOverFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
    </fragment>

    <fragment
        android:id="@+id/gameWonFragment"
        android:name="com.example.android.navigation.GameWonFragment"
        android:label="fragment_game_won"
        tools:layout="@layout/fragment_game_won" >
        <action
            android:id="@+id/action_gameWonFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
        <argument
            android:name="numQuestions"
            app:argType="integer" />
        <argument
            android:name="numCorrect"
            app:argType="integer" />
    </fragment>

    <fragment
        android:id="@+id/aboutFragment"
        android:name="com.example.android.navigation.AboutFragment"
        android:label="fragment_about"
        tools:layout="@layout/fragment_about" />
    <fragment
        android:id="@+id/rulesFragment"
        android:name="com.example.android.navigation.RulesFragment"
        android:label="fragment_rules"
        tools:layout="@layout/fragment_rules" />
</navigation>
  • tools:layout は Design editor でレイアウトを表示するために指定
  • app:popUpTo は Back 時の遷移先
  • app:popUpToInclusive が false の場合は popUoTo の Fragment に遷移する
  • app:popUpToInclusive が true の場合は popUoTo の Fragment もバックスタックから除かれ、もう一つ前の Fragment に遷移する
    • titleFragment → gameFragment → gameOverFragment と順にスタックに積まれているので、popUpTo=gameFragment , inclusive=true だと titleFragment に遷移する
  • Safe Args が有効の場合は XXXFragmentDirections クラス (NavDirections を実装) が生成される
    • argument を指定すると XXXFragmentArgs クラスが生成され、遷移元の NavDirections のファクトリメソッドに引数が追加される
...
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/rulesFragment"
        android:title="@string/rules" />
    <item
        android:id="@+id/aboutFragment"
        android:title="@string/about" />
</menu>
  • item の id と navigation.xml で指定した fragment の id を合わせるとライブラリが良きに計らってくれる

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var drawerLayout: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        drawerLayout = binding.drawerLayout
        val navController = findNavController(R.id.myNavHostFragment)
        NavigationUI.setupWithNavController(binding.navView, navController)
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
    }

    override fun onSupportNavigateUp(): Boolean {
        return NavigationUI.navigateUp(findNavController(R.id.myNavHostFragment), drawerLayout)
    }
}
  • setupWithNavController はスライド操作で Drawer を表示するために必要
  • setupActionBarWithNavController はアクションバーで Drawer の表示を行うために必要
  • onSupportNavigateUp メソッドはアクションバーで戻る操作を行うために必要

TitleFragment.kt

class TitleFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater, R.layout.fragment_title, container, false)
        binding.playButton.setOnClickListener { view ->
            view.findNavController().navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
        }
        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.options_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController())
                || super.onOptionsItemSelected(item)
    }
}
  • navigate メソッドの引数は navigation.xml における action の id でもよい
  • Safe Args を使用している場合は navigate メソッドの引数に NavDirections を使用できる
  • onNavDestinationSelected は menu の item id から navigation の fragment id を特定して遷移する

GameFragment.kt

class GameFragment : Fragment() {
    ...

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        ...
                        view.findNavController().navigate(
                                GameFragmentDirections.actionGameFragmentToGameWonFragment(
                                        numQuestions,
                                        questionIndex
                                ))
        ...
    }

    ...
}
  • Safe Args を使用しており、navigation の fragment に argument が指定されている場合には NavDirections のファクトリメソッドに引数が追加される

GameWonFragment.kt

class GameWonFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        ...
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
        setHasOptionsMenu(true)
        return binding.root
    }

    private fun getShareIntent() : Intent {
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        return Intent(Intent.ACTION_SEND)
                .setType("text/plain")
                .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
    }

    private fun shareSuccess() {
        startActivity(getShareIntent())
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.winner_menu, menu)
        if (getShareIntent().resolveActivity(activity!!.packageManager) == null) {
            menu?.findItem(R.id.share)?.isVisible = false
        }
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
    }
  • XXXFragmentArgs.fromBundle を使えば型安全を得た状態で値を受け取れる
  • onCreateOptionsMenu では ACTION_SEND に対応する Activity の存在を検証し、存在しない場合はメニューを非表示にする