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 が表示される
- Directional Light
- 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 を使わない場合は書き換えないといけない?)
- TrackableFoundTest
- SDK
- NRCameraRig
- 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 を意味する。
TrackableImageEmulator (Scene)
- Directional Light : 部屋に上から差し込むライト
- [C] Light
- NRCameraRig (Prefab)
- NRInput (Prefab)
- NRTrackableImageTarget (Prefab)
- TrackableFoundTest
- EmulatorRoom (Model) : 仮想の部屋
- CubeCenter : Track対象ImageのLost/Foundで消滅/出現する
- Cube
- [C] Mesh Renderer
- [C] Box Collider
- Cube
- Directional Light : 部屋に上から差し込むライト
-
- [C] NRSessionBehavior
- [C] NRHMDPoseTracker
- [C] AppManager
- [C] NRMultiDisplayManager
- LeftCamera
- [C] Camera
- CenterCamera
- [C] Camera
- [C] Audio Listener
- RightCamera
- [C] Camera
-
- [C] NRInput
- [C] ControllerAnchorsHelper
- Gaze
- [C] GazeTracker
- GazeRaycaster
- [C] NRPointerRaycaster
- Reticle
- [C] NRLaserReticle
- Canvas
- [C] Canvas
- [C] CanvasScaler
- default
- [C] CanvasRenderer
- [C] Image
- hover
- [C] CanvasRenderer
- [C] Image
- Right
- ControllerTracker
- [C] ControllerTracker
- LaserRaycaster
- [C] NRPointerRaycaster
- Laser
- [C] NRLaserVisual
- [C] LineRenderer
- Reticle : GazeのReticleと同じ
- ModelAnchor
- ControllerTracker
- Left : Rightと同じ
NRTrackableImageTarget (Prefab) : 追跡対象のImage
- [C] Mech Renderer
- [C] NRTrackableImageBehavior
- [C] TrackableObserver (Target Type = Trackable Image)
-
- [C] Canvas
- [C] Canvas Scaler
- [C] NREmulatorController
- DefaultControllerPanel : コントローラーの表示 (子のいずれかが有効になる)
- Default
- [C] Image
- App
- [C] Image
- Confirm
- [C] Image
- Home
- [C] Image
- Left
- [C] Image
- Right
- [C] Image
- Up
- [C] Image
- Down
- [C] Image
- Default
Scripts
TrackableFoundTest の Scripts
- TrackableFoundTest (in Emulator/Scripts)
- Observer(=NRTrackableImageTarget) に Lost/Found の Event を登録する
- Lost/Found によって Obj(=CubeCenter) を消滅/出現させる
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 を制御するMonoBehavior
- NSSessionManager (in Scripts/Managers)
- NRHMDPoseTracker (Reference) (in Scripts)
- GetHeadPose で Pose (position と rotation の組) を取得できる
- NRMultiDisplayManager (in Scripts/Managers)
- NRVirtualDisplay をインスタンス化
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
- Inspector での設定項目
- ControllerAnchorsHelper (in Scripts/Input/Controller)
- 以下のAnchor を保持する
- GazePoseTrackerAnchor
- RightPoseTrackerAnchor
- LeftPoseTrackerAnchor
- RightModelAnchor
- LeftModelAnchor
- RightLaserAnchor
- LeftLaserAnchor
- 以下のAnchor を保持する
- GazeTracker (in Scripts/Input/Controller)
- Gaze には注視、凝視という意味がある
- Inspector での設定項目
- raycaster に NRPointerRaycaster を設定する
- GazeRaycaster (GameObject) の NRPointerRaycaster (Component) が設定されている
- raycaster に NRPointerRaycaster を設定する
- 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 に近づける
- Inspector での設定項目
- NRPointerRaycaster (in Scripts/Input/EventSystem/Raycasters)
- Inspector での設定項目
- 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 の位置
- Inspector での設定項目
NREmulatorManager の Scripts
- NREmulatorManager (in Emulator/Scripts)
- NativeEmulatorApi の CreateSIMTracking を呼び出す
- NativeEmulatorApi の CreateSIMController を呼び出す
NREmulatorHeadPose の Scripts
- NREmulatorHeadPose (in Emulator/Scripts)
- キーボード、マウス操作を HMD の動きとして Emulate する
NREmulatorController の Scripts
参考文献
NrealLight
(3ヶ月前にメモ書きしていた記事です。)
概要
中国のスタートアップNreal(エンリアル)社が開発したMRグラス。
メガネ型、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
- Testing Tool
- 次バージョンでサポート
- Unity Editor の play mode で動作するエミュレーター
- Observer View
サードパーティSDK拡張 (3rd Party SDK Extension)
- センサー(例:RGBカメラ)からデータにアクセスする、など
参考文献
Chromeで範囲選択したテキストをDeepL翻訳で翻訳する
概要
Google翻訳を超える翻訳の精度と噂されるDeepL翻訳を使う際、わざわざDeepL翻訳のページを開いてテキストを入力するのは手間がかかります。 Google翻訳には選択範囲をGoogle翻訳で翻訳するChrome拡張機能がありますが、DeepL翻訳はまだ無いようです。 しかし、Chrome拡張機能「Selection Search」を使えば範囲選択した範囲を翻訳させることができます。 この記事では、「Selection Search」を使用してDeepL翻訳を使用する方法を説明します。
目次
確認環境
- Google Chrome 80.x
- Selection Search 0.8.55
解説
目指している状態は次の画像のとおりです。テキストを範囲選択し、メニューで en -> ja (英語から日本語に翻訳) 、ja -> en (日本語から英語に翻訳)を選択するとDeepLのページが開き翻訳が行われます。
まず、Chrome 拡張機能 Selection Search を追加します。
次に、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
参考情報
- logging - How can we print line numbers to the log in java - Stack Overflow
- Javaで実行中のクラス名・メソッド名を取得する方法 - Qiita
- 実行中のメソッドのクラス名とメソッド名を取得する方法
解説
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 (お好みで)
- Trigger
- 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 (お好みで)
- Trigger
- 未割り当て
- 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
参考情報
- Android Kotlin Fundamentals Course
- 06.1 の内容から抜粋
解説
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 つ
- Delete アノテーションは 1 レコード削除の場合に使う
- 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 } } } }
- version はスキーマが変更された場合に上げる
- スキーマを変更する場合にはマイグレーションが必要 (Understanding migrations with Room - Android Developers - Medium)
- fallbackToDestructiveMigration メソッドを使うことで、マイグレーションなしでデータベースを破棄、再構築できる
- exportSchema はスキーマ定義をファイルに残す場合は true
- 但し、アノテーションプロセッサの引数 (
room.schemaLocation
) にファイルを保存するディレクトリを指定する必要がある - バージョン管理システムでスキーマのバージョン管理をする用途で使い、配布するアプリをビルドする際には false にする
- 但し、アノテーションプロセッサの引数 (
- Volatile はマルチスレッドの場合に最新の値を全スレッドで常に読み込める様にする (Javaの理論と実践: volatile を扱う)
- INSTANCE プロパティはマルチスレッドを考慮して読み書き時に synchronized でロックする
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
参考情報
- Android Kotlin Fundamentals Course
- 03.1, 03.2, 03.3 の内容から抜粋
解説
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.xml
... <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 のファクトリメソッドに引数が追加される
navdrawer_menu.xml
... <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 の存在を検証し、存在しない場合はメニューを非表示にする