@apexnative/navigation 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +23 -14
  4. package/android/CORE.md +32 -19
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +60 -13
  6. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  7. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  8. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  10. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
  11. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
  12. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  13. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  14. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -6
  15. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +19 -51
  17. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +30 -39
  19. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  21. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +12 -10
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +10 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +22 -21
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +12 -9
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -5
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +13 -5
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +2 -5
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +4 -9
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  53. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  56. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  57. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  63. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  64. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  65. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  66. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  67. package/cli/doctor/checks/splash.js +4 -22
  68. package/cli/splash/index.js +3 -1
  69. package/docs-md/architecture/transitions.md +4 -2
  70. package/docs-md/containers/modals.md +4 -2
  71. package/docs-md/containers/overlays.md +1 -1
  72. package/docs-md/containers/side-menu.md +1 -1
  73. package/docs-md/containers/tabs.md +2 -0
  74. package/docs-md/flows/bottom-sheets.md +1 -1
  75. package/docs-md/fundamentals/starting-the-app.md +1 -1
  76. package/docs-md/guides/cli.md +1 -2
  77. package/docs-md/help/glossary.md +2 -2
  78. package/docs-md/help/troubleshooting.md +23 -2
  79. package/docs-md/index.json +12 -0
  80. package/docs-md/migration/from-expo-router.md +1 -1
  81. package/docs-md/migration/from-react-native-navigation.md +1 -1
  82. package/docs-md/migration/from-react-navigation.md +2 -2
  83. package/docs-md/screen-ui/back-and-gestures.md +36 -0
  84. package/docs-md/screen-ui/bar-buttons.md +4 -4
  85. package/docs-md/screen-ui/search.md +1 -1
  86. package/docs-md/screen-ui/system-ui.md +16 -11
  87. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  88. package/docs-md/screen-ui/top-bar.md +8 -4
  89. package/docs-md/start/installation.md +1 -1
  90. package/ios/Animations/ANNFrameDriver.mm +2 -0
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  92. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  93. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  94. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  95. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  96. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  97. package/ios/CLAUDE.md +17 -15
  98. package/ios/CORE.md +30 -26
  99. package/ios/Contract/ANNCommandAnswer.h +9 -2
  100. package/ios/Contract/ANNCommandAnswer.mm +25 -12
  101. package/ios/Contract/ANNCommandRunner.h +8 -7
  102. package/ios/Contract/ANNCommandRunner.mm +21 -27
  103. package/ios/Contract/ANNLayoutNode.h +3 -3
  104. package/ios/Contract/ANNLayoutReader.h +3 -3
  105. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  106. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  107. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  108. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  109. package/ios/Core/ANNNavigationState.h +4 -6
  110. package/ios/Core/ANNNavigationState.mm +0 -4
  111. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  112. package/ios/Core/ApexNavigation+Internal.h +1 -1
  113. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  114. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  115. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  116. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  117. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  118. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  119. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  120. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  121. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  122. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  123. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  124. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  125. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  126. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  127. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  128. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  129. package/ios/Layouts/Stack/ANNPreviewPush.mm +142 -27
  130. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  131. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  132. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  133. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  134. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  135. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  136. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  137. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  138. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  139. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  140. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  141. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  142. package/ios/Options/ANNOptions.h +18 -0
  143. package/ios/Options/ANNOptions.mm +112 -1
  144. package/ios/Options/ANNOptionsReader.mm +110 -0
  145. package/ios/Options/Params/ANNParam.h +15 -0
  146. package/ios/Options/Params/ANNParam.mm +61 -0
  147. package/ios/Options/Params/Color.h +3 -0
  148. package/ios/Options/Params/Color.mm +21 -0
  149. package/ios/Options/Params/Image.h +4 -0
  150. package/ios/Options/Params/Image.mm +20 -0
  151. package/ios/React/ANNReactView.mm +6 -25
  152. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  153. package/ios/React/ANNSurfaceHostingView.h +6 -0
  154. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  155. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  156. package/ios/TopBar/ANNBarAppearance.h +2 -1
  157. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  158. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  159. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  160. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  161. package/ios/TopBar/ANNBarButtons.h +5 -3
  162. package/ios/TopBar/ANNBarButtons.mm +186 -17
  163. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  164. package/ios/TopBar/ANNMainList.h +3 -0
  165. package/ios/TopBar/ANNMainList.mm +4 -0
  166. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  167. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  168. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  169. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  170. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  171. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  172. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  173. package/lib/module/ARCHITECTURE.md +8 -4
  174. package/lib/module/api/commands/commands.js +54 -6
  175. package/lib/module/api/commands/commands.js.map +1 -1
  176. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  177. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  178. package/lib/module/api/options/OptionsChain.js +2 -2
  179. package/lib/module/engine/commands.js +21 -1
  180. package/lib/module/engine/commands.js.map +1 -1
  181. package/lib/module/engine/places.js +8 -0
  182. package/lib/module/engine/places.js.map +1 -1
  183. package/lib/module/engine/screens.js +29 -4
  184. package/lib/module/engine/screens.js.map +1 -1
  185. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  186. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  187. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  188. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  189. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  190. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  191. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/places.d.ts +2 -0
  194. package/lib/typescript/engine/places.d.ts.map +1 -1
  195. package/lib/typescript/engine/screens.d.ts +20 -6
  196. package/lib/typescript/engine/screens.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/Options.d.ts +1 -1
  198. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/animations.d.ts +30 -9
  200. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  202. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  204. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  205. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  206. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  207. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  208. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  209. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  210. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  211. package/package.json +1 -1
  212. package/src/ARCHITECTURE.md +8 -4
  213. package/src/api/app/createNavigation.ts +3 -3
  214. package/src/api/commands/commands.ts +48 -7
  215. package/src/api/commands/presentation.ts +4 -3
  216. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  217. package/src/api/options/OptionsChain.ts +2 -2
  218. package/src/engine/commands.ts +30 -6
  219. package/src/engine/places.ts +9 -0
  220. package/src/engine/screens.tsx +29 -8
  221. package/src/interfaces/Options.ts +1 -0
  222. package/src/interfaces/options/animations.ts +31 -9
  223. package/src/interfaces/options/bottomTabs.ts +18 -5
  224. package/src/interfaces/options/buttons.ts +8 -8
  225. package/src/interfaces/options/stack.ts +9 -0
  226. package/src/interfaces/options/system.ts +8 -5
  227. package/src/interfaces/options/toolbar.ts +2 -2
  228. package/src/interfaces/options/topBar.ts +32 -29
  229. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  230. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  231. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  232. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
  233. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  234. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  235. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
@@ -3,13 +3,14 @@ package dev.apexnative.navigation.react
3
3
  import android.util.Log
4
4
  import com.facebook.react.bridge.Promise
5
5
  import dev.apexnative.navigation.react.events.EventEmitter
6
- import dev.apexnative.navigation.react.events.NavigationStateReporter
6
+ import dev.apexnative.navigation.react.events.NavigationState
7
7
  import dev.apexnative.navigation.utils.NavigationLog
8
8
 
9
9
  /**
10
10
  * The one answer of a command with a promise: [resolve] or [reject], once; after that, and once the JavaScript it came
11
- * from went ([generation]), nothing. [CommandRunner] makes it on the main thread for each command; area code that takes
12
- * a [CommandListener] answers through [asListener]. Main thread only.
11
+ * from went ([generation]), nothing. It owns the command's one state step ([reportStateNow]). [CommandRunner] makes it
12
+ * on the main thread for each command; area code that takes a [CommandListener] answers through [asListener]. Main
13
+ * thread only.
13
14
  */
14
15
  internal class CommandAnswer(
15
16
  /** The method of the command: `push`, `setRoot`, … */
@@ -27,19 +28,36 @@ internal class CommandAnswer(
27
28
  var isAnswered = false
28
29
  private set
29
30
 
31
+ // The state step of this answer ran ([reportStateNow]).
32
+ private var stateReported = false
33
+
30
34
  /** Whether an answer now would reach the JavaScript the command came from. */
31
35
  val canAnswer: Boolean
32
36
  get() = !isAnswered && ReloadCoordinator.isCurrent(generation)
33
37
 
34
38
  /**
35
- * Resolves the promise with [value]. For the commands that change what shows, the navigation state goes first,
36
- * when it changed, so JavaScript has it when its `await` goes on; `ANN.CommandCompleted` follows the promise, for
37
- * the commands that show or dismiss. A failure of either is logged; the promise goes all the same.
39
+ * The navigation state now, sent when it changed, once per answer: the end of the command's transition asks for it
40
+ * after the DidAppear it sends and before the transition's own events (`AppearanceDispatcher.endTransition`);
41
+ * otherwise [resolve] asks for it before the promise. A later call sends nothing, and nothing goes for a JavaScript
42
+ * that went. A failure of the step is logged and costs only the state.
43
+ */
44
+ fun reportStateNow() {
45
+ if (stateReported) return
46
+ stateReported = true
47
+ if (!ReloadCoordinator.isCurrent(generation)) return
48
+ guarded("report the navigation state") { events.stateNow() }
49
+ }
50
+
51
+ /**
52
+ * Resolves the promise with [value]. For the commands that change what shows (`CommandContract.STATE`), the
53
+ * navigation state goes first, when it changed and the command's transition didn't send it already
54
+ * ([reportStateNow]), so JavaScript has it when its `await` goes on; `ANN.CommandCompleted` follows the promise,
55
+ * for the commands that show or dismiss. A failure of either is logged; the promise goes all the same.
38
56
  */
39
57
  fun resolve(value: Any?) {
40
58
  if (!canAnswer) return
41
59
  isAnswered = true
42
- if (commandName in CommandContract.STATE) guarded("report the navigation state") { events.beforeAnswer() }
60
+ if (commandName in CommandContract.STATE) reportStateNow()
43
61
  promise.resolve(value)
44
62
  val commandId = commandId
45
63
  if (commandName in CommandContract.COMPLETED && commandId != null) {
@@ -69,18 +87,15 @@ internal class CommandAnswer(
69
87
 
70
88
  /** What goes to JavaScript around the answer of a command. Main thread only. */
71
89
  internal interface AnswerEvents {
72
- /** Right before the promise resolves: the navigation state, when it changed. */
73
- fun beforeAnswer()
90
+ /** The answer's one state step ([CommandAnswer.reportStateNow]): the navigation state now, when it changed. */
91
+ fun stateNow()
74
92
 
75
93
  /** After the promise resolved: `ANN.CommandCompleted`. */
76
94
  fun commandCompleted(commandName: String, commandId: String, completionTime: Long)
77
95
 
78
96
  /** Through the sender of the JavaScript that runs now. */
79
97
  object Current : AnswerEvents {
80
- override fun beforeAnswer() {
81
- val emitter = EventEmitter.current()
82
- if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
83
- }
98
+ override fun stateNow() = NavigationState.updateNow()
84
99
 
85
100
  override fun commandCompleted(commandName: String, commandId: String, completionTime: Long) {
86
101
  EventEmitter.current().emitCommandCompleted(commandName, commandId, completionTime)
@@ -9,7 +9,6 @@ import android.os.Build
9
9
  import androidx.lifecycle.DefaultLifecycleObserver
10
10
  import androidx.lifecycle.Lifecycle
11
11
  import androidx.lifecycle.LifecycleOwner
12
- import com.facebook.react.ReactApplication
13
12
  import com.facebook.react.bridge.Arguments
14
13
  import com.facebook.react.bridge.WritableMap
15
14
  import dev.apexnative.navigation.react.events.EventContract
@@ -22,7 +21,8 @@ import java.lang.ref.WeakReference
22
21
  * sensor (Android 11+), in degrees to a tenth as on iOS. Android has no public state of a closed device: closed is a
23
22
  * window without a fold, or an angle under 90° ([valueOf]). No sensor and no fold: no hinge, `{}`. It runs while
24
23
  * JavaScript has a hook mounted (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value
25
- * is not sent twice. Main thread only, but [get].
24
+ * is not sent twice. The values go to the JavaScript that turned the updates on (`EventEmitter.current` then): after a
25
+ * reload, until the updates stop, they go nowhere. Main thread only, but [get].
26
26
  */
27
27
  internal object Hinge {
28
28
  /** A value of the hinge: its state, and its angle in degrees to a tenth; none while the state is unknown. */
@@ -33,6 +33,8 @@ internal object Hinge {
33
33
  private const val CLOSED_BELOW = 90f
34
34
 
35
35
  private var enabled = false
36
+ // The sender of the JavaScript that turned the updates on; none while they are off.
37
+ private var emitter: EventEmitter? = null
36
38
  private var activity: WeakReference<Activity>? = null
37
39
  private var folds: WindowFolds? = null
38
40
  private var sensorManager: SensorManager? = null
@@ -63,9 +65,11 @@ internal object Hinge {
63
65
  fun setEnabled(activity: Activity?, enabled: Boolean) {
64
66
  this.enabled = enabled
65
67
  if (!enabled) {
68
+ emitter = null
66
69
  detach()
67
70
  return
68
71
  }
72
+ emitter = EventEmitter.current()
69
73
  activity?.let(::attachTo)
70
74
  update()
71
75
  }
@@ -134,8 +138,7 @@ internal object Hinge {
134
138
  val value = valueOf(folds.folds.firstOrNull(), sensor != null, angle)
135
139
  if (value == last) return
136
140
  last = value
137
- val reactContext = (activity?.get()?.application as? ReactApplication)?.reactHost?.currentReactContext ?: return
138
- EventEmitter(reactContext).emitHingeChanged(toMap(value))
141
+ emitter?.emitHingeChanged(toMap(value))
139
142
  }
140
143
 
141
144
  /**
@@ -236,11 +236,11 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
236
236
  check = {},
237
237
  ) { _, answer -> navigator().stackCommands.setColumnShown(componentId, column, shown, answer) }
238
238
 
239
- override fun getArrangementState(componentId: String): WritableMap = ArrangementStates.get(componentId)
239
+ override fun getArrangementState(componentId: String): WritableMap = ScreenEnvironment.getArrangementState(componentId)
240
240
 
241
- override fun getScreenInsets(componentId: String): WritableMap = ScreenInsets.get(componentId)
241
+ override fun getScreenInsets(componentId: String): WritableMap = ScreenEnvironment.getScreenInsets(componentId)
242
242
 
243
- override fun getScreenRegions(componentId: String): WritableMap = ScreenRegions.get(componentId)
243
+ override fun getScreenRegions(componentId: String): WritableMap = ScreenEnvironment.getScreenRegions(componentId)
244
244
 
245
245
  // With the activity the user sees, if there is one; the hinge remembers it and goes on with the next activity
246
246
  // (`Hinge.attach`).
@@ -23,11 +23,15 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListe
23
23
  * is made with the view, from the app's React host, and fills it; it starts once, as the view joins a window, and stops
24
24
  * once, on the main thread, as the view is destroyed.
25
25
  *
26
- * Ready once the safe area view of the screen told [ScreenReadiness] that the content rendered into it; a screen
27
- * without a safe area view (no view ever told anything of its id) once the root view of its surface has children, read
28
- * on this view's own layout passes. Ready once, ready for good; what waits for it ([whenReady]) runs then, once.
29
- * Without either, the limits of what waits decide: the view has no timer of its own. It owns the record of its
30
- * appearance, which the dispatcher of the appearance events holds until the content is ready (`AppearanceDispatcher`).
26
+ * Ready once the safe area view of the screen, in this view's own surface, told [ScreenReadiness] that the content
27
+ * rendered into it: the signal of another root with the same id (an old one, another live root of a bar component)
28
+ * never makes this one ready. A screen without a safe area view (no view ever told anything of its id) is ready once
29
+ * the root view of its surface has children, read on this view's own layout passes. A root that renders at once
30
+ * ([rendersAtOnce]: a component of a bar, a tab's cell) marks its id so from before its surface starts until it stops.
31
+ * Ready once, ready for good; what waits for it ([whenReady]) runs then, once.
32
+ * Without a signal or children, the limits of what waits decide: the view has no timer of its own. It owns the
33
+ * record of its appearance, which the dispatcher of the appearance events holds until the content is ready
34
+ * (`AppearanceDispatcher`).
31
35
  */
32
36
  @SuppressLint("ViewConstructor")
33
37
  class ReactView @JvmOverloads constructor(
@@ -36,6 +40,8 @@ class ReactView @JvmOverloads constructor(
36
40
  override val componentName: String,
37
41
  /** Props given by the native side, over `componentId`: the state of a tab's cell. */
38
42
  initialProps: Bundle? = null,
43
+ /** The root renders at once, without waiting for its layout: a component of a bar or a tab's cell. */
44
+ private val rendersAtOnce: Boolean = false,
39
45
  ) : FrameLayout(context), IReactView {
40
46
  private val scrollEventListener = ScrollEventListener()
41
47
  private var destroyed = false
@@ -44,7 +50,14 @@ class ReactView @JvmOverloads constructor(
44
50
  // What waited for the readiness was told; a question of isReady alone tells nothing, so nothing runs inside it.
45
51
  private var readyTold = false
46
52
  private val readyListeners = mutableListOf<Runnable>()
47
- private var readinessListener: ScreenReadiness.Listener? = null
53
+ private var listening = false
54
+
55
+ // What this root hears of its id: only the signals of its own surface.
56
+ private val readiness = object : ScreenReadiness.Listener {
57
+ override fun accepts(source: Int): Boolean = ownSource()?.let { it == source } ?: true
58
+
59
+ override fun onContentRendered() = checkReadiness()
60
+ }
48
61
 
49
62
  // None without the app's host (the unit tests).
50
63
  private val surface: ReactSurface?
@@ -59,8 +72,17 @@ class ReactView @JvmOverloads constructor(
59
72
  */
60
73
  internal var contentChildCount: () -> Int = { (surface?.view as? ViewGroup)?.childCount ?: 0 }
61
74
 
75
+ /**
76
+ * The id of this root's React surface, which the signals of its safe area view carry: React Native gives it as
77
+ * the surface starts (none matches before) and never gives it again. Null without a surface: every signal of the
78
+ * id is its own. The unit tests give their own.
79
+ */
80
+ internal var ownSource: () -> Int? = { surface?.surfaceID }
81
+
62
82
  init {
63
83
  require(componentId.isNotEmpty()) { "a React root needs the id of its screen" }
84
+ // Before the surface exists, so its safe area view never asks before the mark is there.
85
+ if (rendersAtOnce) ScreenReadiness.setRendersAtOnce(componentId, this, true)
64
86
  val props = Bundle().apply {
65
87
  initialProps?.let(::putAll)
66
88
  putString(COMPONENT_ID, componentId)
@@ -103,14 +125,9 @@ class ReactView @JvmOverloads constructor(
103
125
  if (readyTold) return listener.run()
104
126
  readyListeners += listener
105
127
  if (isReady()) return checkReadiness()
106
- if (readinessListener != null) return
107
- val readiness = object : ScreenReadiness.Listener {
108
- override fun onLaidOut() = Unit
109
-
110
- override fun onContentRendered() = checkReadiness()
111
- }
112
- readinessListener = readiness
113
- ScreenReadiness.setListener(componentId, readiness)
128
+ if (listening) return
129
+ listening = true
130
+ ScreenReadiness.addListener(componentId, this, readiness)
114
131
  }
115
132
 
116
133
  /**
@@ -126,23 +143,24 @@ class ReactView @JvmOverloads constructor(
126
143
  waiting.forEach(Runnable::run)
127
144
  }
128
145
 
129
- // The safe area view said so; without one, the root view of the surface has children.
130
- private fun hasRendered(): Boolean = when (ScreenReadiness.isContentRendered(componentId)) {
146
+ // The safe area view of this root's surface said so; without one, the root view of the surface has children.
147
+ private fun hasRendered(): Boolean = when (ScreenReadiness.isContentRendered(componentId, readiness)) {
131
148
  true -> true
132
149
  false -> false
133
150
  null -> contentChildCount() > 0
134
151
  }
135
152
 
136
153
  private fun stopListening() {
137
- val readiness = readinessListener ?: return
138
- readinessListener = null
139
- ScreenReadiness.removeListener(componentId, readiness)
154
+ if (!listening) return
155
+ listening = false
156
+ ScreenReadiness.removeListener(componentId, this)
140
157
  }
141
158
 
142
159
  /**
143
160
  * Stops the content, once, on the main thread: its appearance ends first (DidDisappear if its WillAppear went,
144
161
  * else what is held goes with it), then the surface stops and leaves the host, so a reload doesn't start it again
145
- * in the new JavaScript.
162
+ * in the new JavaScript. A root that renders at once takes its mark away: the last one of its id takes what was
163
+ * said of the id with it.
146
164
  */
147
165
  override fun destroy() {
148
166
  if (destroyed) return
@@ -150,6 +168,7 @@ class ReactView @JvmOverloads constructor(
150
168
  destroyed = true
151
169
  readyListeners.clear()
152
170
  stopListening()
171
+ if (rendersAtOnce) ScreenReadiness.setRendersAtOnce(componentId, this, false)
153
172
  val surface = surface ?: return
154
173
  if (started) surface.stop()
155
174
  surface.detach()
@@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReactContext
4
4
  import dev.apexnative.navigation.NavigationActivity
5
5
  import dev.apexnative.navigation.nativescreen.NativeScreenCalls
6
6
  import dev.apexnative.navigation.react.events.EventEmitter
7
- import dev.apexnative.navigation.react.events.NavigationStateReporter
7
+ import dev.apexnative.navigation.react.events.NavigationState
8
8
  import dev.apexnative.navigation.utils.UiThread
9
9
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
10
10
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
@@ -39,7 +39,7 @@ internal object ReloadCoordinator {
39
39
  /**
40
40
  * The JavaScript goes (a reload, or the host is destroyed). Any thread: the number grows at once; the layouts of
41
41
  * every navigation activity go on the main thread, with the "reloading" mark, and so do the listeners of the
42
- * JavaScript that went (back, hinge).
42
+ * JavaScript that went (back, hinge) and the holds of the navigation state its `setRoot` left.
43
43
  */
44
44
  fun onJavaScriptGoes() {
45
45
  advance()
@@ -48,6 +48,7 @@ internal object ReloadCoordinator {
48
48
  launches.contextGone()
49
49
  try {
50
50
  Navigators.all().forEach { it.onReload() }
51
+ NavigationState.javaScriptWent()
51
52
  BackCallbackState.setJsListeners(false)
52
53
  Hinge.setEnabled(Navigators.current(), false)
53
54
  } finally {
@@ -59,13 +60,15 @@ internal object ReloadCoordinator {
59
60
  /**
60
61
  * The developer tools of [activity] reload the JavaScript ("R R", the reload broadcast): the number grows first, so
61
62
  * the navigation of the activity goes without a word to the JavaScript that goes, then its layers go
62
- * ([NavigationActivity.onReload]). The host's own reset follows as the reload destroys it. Main thread.
63
+ * ([NavigationActivity.onReload]) with the holds of the navigation state. The host's own reset follows as the
64
+ * reload destroys it. Main thread.
63
65
  */
64
66
  fun resetForDeveloperReload(activity: NavigationActivity) {
65
67
  advance()
66
68
  Navigator.setReloading(true)
67
69
  try {
68
70
  activity.onReload()
71
+ NavigationState.javaScriptWent()
69
72
  } finally {
70
73
  Navigator.setReloading(false)
71
74
  }
@@ -79,8 +82,6 @@ internal object ReloadCoordinator {
79
82
  UiThread.runOnMain {
80
83
  launches.contextReady(context)
81
84
  NativeScreenCalls.failAll { name, componentId -> ErrorTexts.nativeCallReloaded(name, componentId) }
82
- // Scroll listeners that found no event dispatcher listen now.
83
- ScrollEvents.dispatcherReady()
84
85
  Navigators.all().filter { it.isNavigationResumed }.forEach(::launchIfDue)
85
86
  }
86
87
  }
@@ -92,7 +93,7 @@ internal object ReloadCoordinator {
92
93
  */
93
94
  fun launchIfDue(activity: NavigationActivity) {
94
95
  val context = launches.takeDue(activity, activity.isNavigationResumed) ?: return
95
- NavigationStateReporter.reset()
96
+ NavigationState.reset()
96
97
  EventEmitter(context).emitAppLaunched()
97
98
  }
98
99
  }
@@ -0,0 +1,177 @@
1
+ package dev.apexnative.navigation.react
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableArray
5
+ import com.facebook.react.bridge.WritableMap
6
+ import dev.apexnative.navigation.react.events.EventContract
7
+ import dev.apexnative.navigation.react.events.EventEmitter
8
+ import java.util.concurrent.ConcurrentHashMap
9
+
10
+ /**
11
+ * What JavaScript knows of each screen's place, by the screen's id, in one record: its safe area (the insets JavaScript
12
+ * gets and the padding of its safe area view, `useSafeAreaInsets`), its reserved regions (`useReservedRegions`) and,
13
+ * in a pair, its arrangement (`useArrangement`). The core measures them (`ScreenSafeArea`, [ScreenRegions], the pair's
14
+ * plan) on the main thread and keeps them here; JavaScript reads them from its own thread for a screen's first render
15
+ * and gets every change as an event. The record is immutable and replaced at once, so a reader on another thread sees
16
+ * one whole record. When the screen goes, [remove] clears all of it. As on iOS (`ANNScreenEnvironment`).
17
+ */
18
+ internal object ScreenEnvironment {
19
+ /** How much covers each edge of a screen's content, in dp. */
20
+ data class Insets(val top: Double, val right: Double, val bottom: Double, val left: Double)
21
+
22
+ /** A reserved region, in dp from the top left of the screen's content. */
23
+ data class Region(val x: Double, val y: Double, val width: Double, val height: Double)
24
+
25
+ /** Where a screen divides (`division`) and where hardware covers it (`occlusion`). */
26
+ data class Regions(val division: List<Region>, val occlusion: List<Region>) {
27
+ val isEmpty: Boolean get() = division.isEmpty() && occlusion.isEmpty()
28
+ }
29
+
30
+ /** Which screen of its pair a screen is, how the pair lies, whether it lies over the other one and is hidden. */
31
+ data class Arrangement(val placement: String, val layout: String, val front: Boolean, val hidden: Boolean)
32
+
33
+ /** Where the changes JavaScript hears go: the events of the screen's JavaScript, or a test's record. */
34
+ interface Changes {
35
+ fun insetsChanged(screenId: String, insets: Insets)
36
+
37
+ fun regionsChanged(screenId: String, regions: Regions)
38
+
39
+ fun arrangementChanged(screenId: String, arrangement: Arrangement)
40
+ }
41
+
42
+ // One screen's record; null for a fact not known yet (not laid out, outside a pair).
43
+ private data class Record(
44
+ val insets: Insets? = null,
45
+ val padding: Insets? = null,
46
+ val regions: Regions? = null,
47
+ val arrangement: Arrangement? = null,
48
+ )
49
+
50
+ private val NO_REGIONS = Regions(emptyList(), emptyList())
51
+ private val records = ConcurrentHashMap<String, Record>()
52
+ // Main thread only.
53
+ private val paddingListeners = HashMap<String, MutableSet<() -> Unit>>()
54
+
55
+ /**
56
+ * Keeps the screen's [insets] for JavaScript (what still covers its content) and the [padding] of its safe area
57
+ * view. Calls the screen's padding listeners when the padding changed, then tells JavaScript when the insets did.
58
+ * Main thread only.
59
+ */
60
+ fun setInsets(screenId: String, insets: Insets, padding: Insets, changes: Changes) {
61
+ val before = update(screenId) { it.copy(insets = insets, padding = padding) }
62
+ if (before?.padding != padding) paddingListeners[screenId]?.toList()?.forEach { it() }
63
+ if (before?.insets != insets) changes.insetsChanged(screenId, insets)
64
+ }
65
+
66
+ /**
67
+ * Keeps the screen's [regions], and tells JavaScript when they changed; none at first is what JavaScript reads
68
+ * before the first layout, so it isn't told. Main thread only.
69
+ */
70
+ fun setRegions(screenId: String, regions: Regions, changes: Changes) {
71
+ val before = update(screenId) { it.copy(regions = regions) }?.regions
72
+ if (before == regions || (before == null && regions.isEmpty)) return
73
+ changes.regionsChanged(screenId, regions)
74
+ }
75
+
76
+ /** Keeps the screen's [arrangement], and tells JavaScript when it changed and [notify] holds. Main thread only. */
77
+ fun setArrangement(screenId: String, arrangement: Arrangement, notify: Boolean, changes: Changes) {
78
+ val before = update(screenId) { it.copy(arrangement = arrangement) }?.arrangement
79
+ if (before == arrangement || !notify) return
80
+ changes.arrangementChanged(screenId, arrangement)
81
+ }
82
+
83
+ /** The padding of the screen's safe area view; none before the screen was laid out. Any thread. */
84
+ fun padding(screenId: String): Insets? = records[screenId]?.padding
85
+
86
+ /** The insets JavaScript gets; none before the screen was laid out. Any thread. */
87
+ fun insets(screenId: String): Insets? = records[screenId]?.insets
88
+
89
+ /** The screen's reserved regions; none before its first layout. Any thread. */
90
+ fun regions(screenId: String): Regions? = records[screenId]?.regions
91
+
92
+ /** The screen's arrangement; none outside a pair. Any thread. */
93
+ fun arrangement(screenId: String): Arrangement? = records[screenId]?.arrangement
94
+
95
+ /**
96
+ * Calls `listener` when the padding of the screen changes; returns the function that stops it. Main thread only.
97
+ */
98
+ fun listenToPadding(screenId: String, listener: () -> Unit): () -> Unit {
99
+ paddingListeners.getOrPut(screenId) { LinkedHashSet() }.add(listener)
100
+ return {
101
+ paddingListeners[screenId]?.let {
102
+ it.remove(listener)
103
+ if (it.isEmpty()) paddingListeners.remove(screenId)
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Forgets everything kept for the screen. Its padding listeners stay: a live safe area view keeps listening while
110
+ * it keeps the id, and stops itself.
111
+ */
112
+ fun remove(screenId: String) {
113
+ records.remove(screenId)
114
+ }
115
+
116
+ /** `getScreenInsets`: `{ top, right, bottom, left }`, empty before the screen was laid out. Any thread. */
117
+ fun getScreenInsets(screenId: String): WritableMap = insets(screenId)?.let(::toMap) ?: Arguments.createMap()
118
+
119
+ /** `getScreenRegions`: `{ division, occlusion }`, empty lists before the first layout. Any thread. */
120
+ fun getScreenRegions(screenId: String): WritableMap = toMap(regions(screenId) ?: NO_REGIONS)
121
+
122
+ /** `getArrangementState`: `{ placement, layout, front, hidden }`, empty outside a pair. Any thread. */
123
+ fun getArrangementState(screenId: String): WritableMap =
124
+ arrangement(screenId)?.let(::toMap) ?: Arguments.createMap()
125
+
126
+ /** The changes as events through [emitter], the one of the JavaScript the screen was made for. */
127
+ fun changesOf(emitter: EventEmitter): Changes = object : Changes {
128
+ override fun insetsChanged(screenId: String, insets: Insets) =
129
+ emitter.emitScreenInsetsChanged(screenId, toMap(insets))
130
+
131
+ override fun regionsChanged(screenId: String, regions: Regions) =
132
+ emitter.emitScreenRegionsChanged(screenId, toMap(regions))
133
+
134
+ override fun arrangementChanged(screenId: String, arrangement: Arrangement) =
135
+ emitter.emitArrangementChanged(screenId, toMap(arrangement))
136
+ }
137
+
138
+ // Replaces the screen's record at once with [change] of it; returns the record before, null for a new screen.
139
+ private fun update(screenId: String, change: (Record) -> Record): Record? {
140
+ var before: Record? = null
141
+ records.compute(screenId) { _, old ->
142
+ before = old
143
+ change(old ?: Record())
144
+ }
145
+ return before
146
+ }
147
+
148
+ private fun toMap(value: Insets): WritableMap = Arguments.createMap().apply {
149
+ putDouble(EventContract.ScreenInsetsChanged.TOP, value.top)
150
+ putDouble(EventContract.ScreenInsetsChanged.RIGHT, value.right)
151
+ putDouble(EventContract.ScreenInsetsChanged.BOTTOM, value.bottom)
152
+ putDouble(EventContract.ScreenInsetsChanged.LEFT, value.left)
153
+ }
154
+
155
+ private fun toMap(value: Regions): WritableMap = Arguments.createMap().apply {
156
+ putArray(EventContract.ScreenRegionsChanged.DIVISION, toArray(value.division))
157
+ putArray(EventContract.ScreenRegionsChanged.OCCLUSION, toArray(value.occlusion))
158
+ }
159
+
160
+ private fun toArray(regions: List<Region>): WritableArray = Arguments.createArray().apply {
161
+ for (region in regions) {
162
+ pushMap(Arguments.createMap().apply {
163
+ putDouble(EventContract.ScreenRegionRect.X, region.x)
164
+ putDouble(EventContract.ScreenRegionRect.Y, region.y)
165
+ putDouble(EventContract.ScreenRegionRect.WIDTH, region.width)
166
+ putDouble(EventContract.ScreenRegionRect.HEIGHT, region.height)
167
+ })
168
+ }
169
+ }
170
+
171
+ private fun toMap(value: Arrangement): WritableMap = Arguments.createMap().apply {
172
+ putString(EventContract.ArrangementChanged.PLACEMENT, value.placement)
173
+ putString(EventContract.ArrangementChanged.LAYOUT, value.layout)
174
+ putBoolean(EventContract.ArrangementChanged.FRONT, value.front)
175
+ putBoolean(EventContract.ArrangementChanged.HIDDEN, value.hidden)
176
+ }
177
+ }
@@ -3,38 +3,33 @@ package dev.apexnative.navigation.react
3
3
  import android.app.Activity
4
4
  import android.graphics.Rect
5
5
  import androidx.core.view.WindowInsetsCompat
6
- import com.facebook.react.ReactApplication
7
- import com.facebook.react.bridge.Arguments
8
- import com.facebook.react.bridge.WritableArray
9
- import com.facebook.react.bridge.WritableMap
10
- import dev.apexnative.navigation.react.events.EventContract
6
+ import dev.apexnative.navigation.react.ScreenEnvironment.Region
7
+ import dev.apexnative.navigation.react.ScreenEnvironment.Regions
11
8
  import dev.apexnative.navigation.react.events.EventEmitter
12
9
  import dev.apexnative.navigation.utils.WindowFolds
13
- import java.util.concurrent.ConcurrentHashMap
14
10
 
15
11
  /**
16
- * The reserved regions of each screen (`useReservedRegions`), in dp from the top left of its content: where it divides
17
- * (a fold the window is divided at, `division`) and where hardware covers it (a fold that hides what is under it, the
18
- * camera cutout: `occlusion`). Only those on the screen, cut to it: a fold at the edge of a screen of a pair is not on
19
- * it. Measured on the main thread as each screen lays out, and again after a change of the window's folds (which lays
20
- * the window out); JavaScript reads them from its own thread for a screen's first render, and gets every change as
21
- * `ANN.ScreenRegionsChanged`.
12
+ * Measures the reserved regions of each screen (`useReservedRegions`), in dp from the top left of its content: where
13
+ * it divides (a fold the window is divided at, `division`) and where hardware covers it (a fold that hides what is
14
+ * under it, the camera cutout: `occlusion`). Only those on the screen, cut to it: a fold at the edge of a screen of a
15
+ * pair is not on it. Measured on the main thread as each screen lays out, and again after a change of the window's
16
+ * folds (which lays the window out); [ScreenEnvironment] keeps them, answers JavaScript's reads and tells it of every
17
+ * change as `ANN.ScreenRegionsChanged`.
22
18
  */
23
19
  internal object ScreenRegions {
24
- data class Region(val x: Double, val y: Double, val width: Double, val height: Double)
25
-
26
- data class Regions(val division: List<Region>, val occlusion: List<Region>) {
27
- val isEmpty: Boolean get() = division.isEmpty() && occlusion.isEmpty()
28
- }
29
-
30
- private val NONE = Regions(emptyList(), emptyList())
31
- private val regions = ConcurrentHashMap<String, Regions>()
32
-
33
20
  /**
34
21
  * Measures the regions of the screen whose content the layout puts at [content] in the window (pixels), with the
35
- * window's [insets] (the camera cutout), and tells JavaScript when they changed. Main thread only.
22
+ * window's [insets] (the camera cutout), and keeps them in [ScreenEnvironment], which tells JavaScript when they
23
+ * changed through [emitter], the one of the JavaScript the screen was made for: silent once it went. Main thread
24
+ * only.
36
25
  */
37
- fun update(activity: Activity, screenId: String, content: Rect, insets: WindowInsetsCompat?) {
26
+ fun update(
27
+ activity: Activity,
28
+ screenId: String,
29
+ content: Rect,
30
+ insets: WindowInsetsCompat?,
31
+ emitter: EventEmitter,
32
+ ) {
38
33
  val folds = WindowFolds.of(activity)?.folds.orEmpty()
39
34
  val cutouts = insets?.displayCutout?.boundingRects.orEmpty()
40
35
  val density = activity.resources.displayMetrics.density
@@ -43,18 +38,7 @@ internal object ScreenRegions {
43
38
  occlusion = (folds.filter { it.occludes }.map { it.bounds } + cutouts)
44
39
  .mapNotNull { onScreen(it, content, density) },
45
40
  )
46
- val previous = regions.put(screenId, value)
47
- // None at first is what JavaScript reads before the first layout.
48
- if (previous == value || (previous == null && value.isEmpty)) return
49
- val reactContext = (activity.application as? ReactApplication)?.reactHost?.currentReactContext ?: return
50
- EventEmitter(reactContext).emitScreenRegionsChanged(screenId, toMap(value))
51
- }
52
-
53
- /** `{ division, occlusion }`, empty lists for an unknown screen and before its first layout. Any thread. */
54
- fun get(screenId: String): WritableMap = toMap(regions[screenId] ?: NONE)
55
-
56
- fun remove(screenId: String) {
57
- regions.remove(screenId)
41
+ ScreenEnvironment.setRegions(screenId, value, ScreenEnvironment.changesOf(emitter))
58
42
  }
59
43
 
60
44
  // The part of [bounds] (window pixels) on the screen, in dp from its top left; null off it. A fold without width
@@ -69,20 +53,4 @@ internal object ScreenRegions {
69
53
  height = part.height() / density.toDouble(),
70
54
  )
71
55
  }
72
-
73
- private fun toMap(value: Regions): WritableMap = Arguments.createMap().apply {
74
- putArray(EventContract.ScreenRegionsChanged.DIVISION, toArray(value.division))
75
- putArray(EventContract.ScreenRegionsChanged.OCCLUSION, toArray(value.occlusion))
76
- }
77
-
78
- private fun toArray(regions: List<Region>): WritableArray = Arguments.createArray().apply {
79
- for (region in regions) {
80
- pushMap(Arguments.createMap().apply {
81
- putDouble(EventContract.ScreenRegionRect.X, region.x)
82
- putDouble(EventContract.ScreenRegionRect.Y, region.y)
83
- putDouble(EventContract.ScreenRegionRect.WIDTH, region.width)
84
- putDouble(EventContract.ScreenRegionRect.HEIGHT, region.height)
85
- })
86
- }
87
- }
88
56
  }