@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
@@ -1,25 +1,17 @@
1
1
  package dev.apexnative.navigation.react
2
2
 
3
- import android.content.Context
4
- import android.util.Log
5
- import com.facebook.react.ReactApplication
6
- import com.facebook.react.uimanager.UIManagerHelper
7
- import com.facebook.react.uimanager.events.Event
8
- import com.facebook.react.uimanager.events.EventDispatcherListener
9
- import com.facebook.react.views.scroll.ScrollEvent
3
+ import android.view.ViewGroup
4
+ import android.widget.HorizontalScrollView
5
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
10
6
  import com.facebook.react.views.scroll.ScrollEventType
11
- import dev.apexnative.navigation.utils.NavigationLog
12
- import dev.apexnative.navigation.utils.UiThread
13
- import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListener
14
- import java.lang.reflect.Field
15
- import java.util.Collections
16
- import java.util.WeakHashMap
17
7
 
18
8
  /**
19
- * The scroll events of React Native's lists, as the screens' scroll listeners hear them: from the event dispatcher of
20
- * the running React Native, every list of the app. React Native keeps the offset, the speed and the kind of a scroll
21
- * event in private fields: they are looked up once, with their types; if React Native renames them, one warning goes to
22
- * the log and the screens hear no scroll, with no error at each event.
9
+ * The scroll events of React Native's scroll views, as the screens' scroll listeners hear them: through React Native's
10
+ * public scroll listener ([ReactScrollViewHelper.addScrollListener]), which every `ReactScrollView`,
11
+ * `ReactHorizontalScrollView` and `ReactNestedScrollView` of the process calls on the main thread just before it
12
+ * dispatches the same event to JavaScript, after the same throttle. One listener serves every screen and every React
13
+ * context; it is added while a screen listens and removed when the last one stops. Horizontal scroll views are left
14
+ * out: a pager or a carousel doesn't move the bars. Main thread only.
23
15
  */
24
16
  internal object ScrollEvents {
25
17
  /** What a scroll event is. */
@@ -28,133 +20,62 @@ internal object ScrollEvents {
28
20
  /** One scroll event: the list's tag (its view's id), its kind, its vertical offset and speed, in pixels. */
29
21
  class Reading(val viewTag: Int, val kind: Kind, val offsetY: Float, val velocityY: Float)
30
22
 
31
- private class Fields(val type: Field, val offsetY: Field, val velocityY: Field)
23
+ // One screen's listening: its own object, so that a stop takes away that listening and no other.
24
+ private class Listening(val handler: (Reading) -> Unit)
32
25
 
33
- private var fields: Fields? = null
34
- private var lookedUp = false
26
+ // The screens' listenings, in the order they came.
27
+ private val listenings = mutableListOf<Listening>()
35
28
 
36
- // The screens' listeners that found no event dispatcher, held weakly until React Native has one. Main thread.
37
- private val waiting: MutableSet<ScrollEventListener> = Collections.newSetFromMap(WeakHashMap())
29
+ // React Native holds its scroll listeners weakly: this object keeps the one it added for as long as it is added.
30
+ private val listener = object : ReactScrollViewHelper.ScrollListener {
31
+ override fun onScroll(
32
+ scrollView: ViewGroup?,
33
+ scrollEventType: ScrollEventType?,
34
+ xVelocity: Float,
35
+ yVelocity: Float,
36
+ ) = hear(scrollView, scrollEventType, yVelocity)
38
37
 
39
- /** [listener] found no event dispatcher: it is asked to listen again once React Native has one. */
40
- fun waitForDispatcher(listener: ScrollEventListener) {
41
- waiting += listener
42
- }
43
-
44
- /** A React context is ready (`ReloadCoordinator`): the listeners waiting for an event dispatcher listen now. */
45
- fun dispatcherReady() {
46
- val listeners = waiting.toList()
47
- waiting.clear()
48
- listeners.forEach(ScrollEventListener::attachIfNeeded)
38
+ override fun onLayout(scrollView: ViewGroup?) = Unit
49
39
  }
50
40
 
51
41
  /**
52
- * Calls [handler] on the main thread with each scroll event of the app's React Native, until the function returned
53
- * is called. None when React Native has no event dispatcher now (around a reload, the caller asks again later) or
54
- * its scroll events can't be read.
42
+ * Calls [handler] with each scroll event of a vertical scroll view of the app, until the function returned is
43
+ * called.
55
44
  */
56
- fun listen(context: Context, handler: (Reading) -> Unit): (() -> Unit)? {
57
- if (fields() == null) return null
58
- val reactContext = (context.applicationContext as? ReactApplication)?.reactHost?.currentReactContext
59
- ?: return null
60
- val dispatcher = try {
61
- UIManagerHelper.getEventDispatcher(reactContext)
62
- } catch (e: RuntimeException) {
63
- null
64
- } ?: return null
65
- val listener = EventDispatcherListener { event ->
66
- // The event goes back to its pool after the dispatch: it is read now, on the thread that dispatches.
67
- val reading = read(event) ?: return@EventDispatcherListener
68
- if (UiThread.isMainThread()) handler(reading) else UiThread.post { handler(reading) }
45
+ fun listen(handler: (Reading) -> Unit): () -> Unit {
46
+ val listening = Listening(handler)
47
+ if (listenings.isEmpty()) ReactScrollViewHelper.addScrollListener(listener)
48
+ listenings += listening
49
+ return {
50
+ // By identity: the second call of a stop finds nothing to take away.
51
+ if (listenings.removeAll { it === listening } && listenings.isEmpty()) {
52
+ ReactScrollViewHelper.removeScrollListener(listener)
53
+ }
69
54
  }
70
- dispatcher.addListener(listener)
71
- return { dispatcher.removeListener(listener) }
72
55
  }
73
56
 
74
- private fun read(event: Event<*>): Reading? {
75
- if (event !is ScrollEvent) return null
76
- val fields = fields() ?: return null
77
- return try {
78
- val type = fields.type.get(event) as? ScrollEventType ?: return null
79
- val offsetY = readFloat(fields.offsetY, event) ?: return null
80
- val velocityY = readFloat(fields.velocityY, event) ?: return null
81
- Reading(event.viewTag, kindOf(type), offsetY, velocityY)
82
- } catch (e: ReflectiveOperationException) {
83
- null
84
- } catch (e: IllegalArgumentException) {
85
- null
86
- }
57
+ /** One call of React Native's listener: the handlers listening now hear its reading, if it has one. */
58
+ fun hear(scrollView: ViewGroup?, type: ScrollEventType?, velocityY: Float) {
59
+ val reading = read(scrollView, type, velocityY) ?: return
60
+ listenings.toList().forEach { it.handler(reading) }
87
61
  }
88
62
 
89
- private fun kindOf(type: ScrollEventType): Kind = when (type) {
63
+ /**
64
+ * The reading of one call of React Native's listener: none for a horizontal scroll view, or when React Native gives
65
+ * no view or no kind. The offset is the view's `scrollY` and the speed React Native's own, in pixels, as the event
66
+ * sent to JavaScript carries them before it converts them to dp.
67
+ */
68
+ fun read(scrollView: ViewGroup?, type: ScrollEventType?, velocityY: Float): Reading? {
69
+ if (scrollView == null || type == null || scrollView is HorizontalScrollView) return null
70
+ return Reading(scrollView.id, kindOf(type), scrollView.scrollY.toFloat(), velocityY)
71
+ }
72
+
73
+ /** The kind of a reading for React Native's [type]. */
74
+ fun kindOf(type: ScrollEventType): Kind = when (type) {
90
75
  ScrollEventType.BEGIN_DRAG -> Kind.BeginDrag
91
76
  ScrollEventType.END_DRAG -> Kind.EndDrag
92
77
  ScrollEventType.SCROLL -> Kind.Scroll
93
78
  ScrollEventType.MOMENTUM_BEGIN -> Kind.MomentumBegin
94
79
  ScrollEventType.MOMENTUM_END -> Kind.MomentumEnd
95
80
  }
96
-
97
- @Synchronized
98
- private fun fields(): Fields? {
99
- if (lookedUp) return fields
100
- lookedUp = true
101
- fields = try {
102
- val owner = ScrollEvent::class.java
103
- val type = declaredField(owner, TYPE_NAMES) { ScrollEventType::class.java.isAssignableFrom(it) }
104
- val offsetY = declaredField(owner, OFFSET_NAMES, ::isNumeric)
105
- val velocityY = declaredField(owner, VELOCITY_NAMES, ::isNumeric)
106
- if (type != null && offsetY != null && velocityY != null) Fields(type, offsetY, velocityY) else null
107
- } catch (e: ReflectiveOperationException) {
108
- null
109
- } catch (e: SecurityException) {
110
- null
111
- }
112
- if (fields == null) {
113
- Log.w(
114
- NavigationLog.TAG,
115
- "[ApexNavigation] React Native's scroll events changed: hideOnScroll and the large title " +
116
- "don't follow the scroll",
117
- )
118
- }
119
- return fields
120
- }
121
-
122
- // The names of the fields: React Native 0.87 first, then the earlier ones with the `m` prefix.
123
- private val TYPE_NAMES = listOf("scrollEventType", "mScrollEventType")
124
- private val OFFSET_NAMES = listOf("scrollY", "mScrollY")
125
- private val VELOCITY_NAMES = listOf("yVelocity", "mYVelocity")
126
-
127
- /**
128
- * The first of the fields [names] that [owner] declares with a type [accepted] takes, made readable; none when no
129
- * name fits. React Native renamed them and changed their types between versions.
130
- */
131
- fun declaredField(owner: Class<*>, names: List<String>, accepted: (Class<*>) -> Boolean): Field? {
132
- for (name in names) {
133
- val field = try {
134
- owner.getDeclaredField(name)
135
- } catch (e: NoSuchFieldException) {
136
- continue
137
- }
138
- if (!accepted(field.type)) continue
139
- field.isAccessible = true
140
- return field
141
- }
142
- return null
143
- }
144
-
145
- /** A number of any kind: a primitive one (`int`, `float`, `double`, …) or a boxed one. */
146
- fun isNumeric(type: Class<*>): Boolean =
147
- type in PRIMITIVE_NUMBERS || Number::class.java.isAssignableFrom(type)
148
-
149
- /** [field] of [target] as a float, whatever numeric type it has; none when it holds no number. */
150
- fun readFloat(field: Field, target: Any): Float? = (field.get(target) as? Number)?.toFloat()
151
-
152
- private val PRIMITIVE_NUMBERS = setOf<Class<*>?>(
153
- Byte::class.javaPrimitiveType,
154
- Short::class.javaPrimitiveType,
155
- Int::class.javaPrimitiveType,
156
- Long::class.javaPrimitiveType,
157
- Float::class.javaPrimitiveType,
158
- Double::class.javaPrimitiveType,
159
- )
160
81
  }
@@ -90,7 +90,7 @@ internal object AppearanceDispatcher {
90
90
  val appearanceRecord: ScreenAppearance?
91
91
  }
92
92
 
93
- /** The navigation state (`NavigationStateReporter`): one update in this pass of the main loop, or one now. */
93
+ /** The navigation state ([NavigationState]): one update in this pass of the main loop, or one now. */
94
94
  interface StateUpdates {
95
95
  /**
96
96
  * Asks for one update in this pass of the main loop: the calls of one pass make one, after a DidAppear or a
@@ -98,7 +98,10 @@ internal object AppearanceDispatcher {
98
98
  */
99
99
  fun setNeedsUpdate()
100
100
 
101
- /** Computes the state now and sends it when it changed: before an answer, at the end of a transition. */
101
+ /**
102
+ * Computes the state now and sends it when it changed: a command's answer, once
103
+ * (`CommandAnswer.reportStateNow`), the end of a user's transition, a tab chosen from code.
104
+ */
102
105
  fun updateNow()
103
106
 
104
107
  /**
@@ -157,33 +160,6 @@ internal object AppearanceDispatcher {
157
160
  EventEmitter.current().emitSheetDetentChanged(componentId, detent)
158
161
  }
159
162
 
160
- /** The state of `NavigationStateReporter`, sent through the JavaScript that runs now, when it can be. */
161
- object ReporterState : StateUpdates {
162
- private val handler by lazy { Handler(Looper.getMainLooper()) }
163
-
164
- // The one posted update of a pass of the main loop, which checks the hold when it runs.
165
- private val held = StateHold(
166
- post = { task -> handler.post(task) },
167
- generation = { ReloadCoordinator.generation },
168
- ) {
169
- val emitter = EventEmitter.current()
170
- if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
171
- }
172
-
173
- override fun setNeedsUpdate() {
174
- if (EventEmitter.current().canSend) held.setNeedsUpdate()
175
- }
176
-
177
- override fun hold() = held.hold()
178
-
179
- override fun release() = held.release()
180
-
181
- override fun updateNow() {
182
- val emitter = EventEmitter.current()
183
- if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
184
- }
185
- }
186
-
187
163
  /** The main thread's loop. */
188
164
  object MainScheduler : Scheduler {
189
165
  private val handler by lazy { Handler(Looper.getMainLooper()) }
@@ -198,8 +174,8 @@ internal object AppearanceDispatcher {
198
174
  /** Where the events go: [EmitterSender]; the tests put a journal here. */
199
175
  var sender: Sender = EmitterSender
200
176
 
201
- /** The navigation state the sent appearance events and transitions update: [ReporterState]; a journal in tests. */
202
- var state: StateUpdates = ReporterState
177
+ /** The navigation state the appearance events and transitions update: [NavigationState]; a journal in tests. */
178
+ var state: StateUpdates = NavigationState
203
179
 
204
180
  /** The clock of the hold limit: [MainScheduler]; the tests put a clock that moves when they say. */
205
181
  var scheduler: Scheduler = MainScheduler
@@ -337,12 +313,15 @@ internal object AppearanceDispatcher {
337
313
  }
338
314
 
339
315
  /**
340
- * A transition ended,: what went disappears ([hiding]), what stands appears ([showing], with its WillAppear first
316
+ * A transition ended: what went disappears ([hiding]), what stands appears ([showing], with its WillAppear first
341
317
  * where it wasn't asked), the navigation state goes when it changed, then [events] (the transition's own:
342
318
  * `ANN.ScreenPopped`, `ANN.ModalDismissed`, and what must follow them, like stopping the content of what went),
343
- * then [answer] resolves with [value] (the promise, then `ANN.CommandCompleted`). A user's action has no answer. A
344
- * state that fails is logged ([log]) and the transition goes on: [events] and the answer always come. Without
345
- * [reportsState] the caller sends the state itself, later (a tab switch: after `ANN.BottomTabSelected`).
319
+ * then [answer] resolves with [value] (the promise, then `ANN.CommandCompleted`; no second state). The state is the
320
+ * answer's one state step (`CommandAnswer.reportStateNow`), which sends nothing for a JavaScript that went; a
321
+ * user's action has no answer, and a command answered before its transition ended had its state then: the state
322
+ * of the end goes all the same, unless its JavaScript went. A state that fails is logged and the transition goes
323
+ * on: [events] and the answer always come. Without [reportsState] the caller sends the state itself, later (a tab
324
+ * switch: after `ANN.BottomTabSelected`; a `setRoot` with layers: from its answer).
346
325
  */
347
326
  fun endTransition(
348
327
  hiding: List<ViewController<*>>,
@@ -360,18 +339,30 @@ internal object AppearanceDispatcher {
360
339
  holdsState = false
361
340
  }
362
341
  showing.forEach { it.onViewDidAppear() }
363
- if (reportsState) reportState(answer?.commandName)
342
+ if (reportsState) {
343
+ // The state before the transition's events, once per answer: the answer's own step before its promise
344
+ // finds it sent. Without an answer to wait for, the dispatcher sends it; nothing for a JavaScript that
345
+ // went.
346
+ when {
347
+ answer == null -> reportState()
348
+ answer.isAnswered -> if (ReloadCoordinator.isCurrent(answer.generation)) reportState()
349
+ else -> answer.reportStateNow()
350
+ }
351
+ }
364
352
  events?.invoke()
365
353
  answer?.resolve(value)
366
354
  }
367
355
 
368
- /** The navigation state now, when it changed; a failure is logged ([log]), nothing else stops for it. */
369
- fun reportState(commandName: String? = null) {
356
+ /**
357
+ * The navigation state now, when it changed, without a command's answer (a command's goes through its answer,
358
+ * `CommandAnswer.reportStateNow`); a failure is logged ([log]), nothing else stops for it.
359
+ */
360
+ fun reportState() {
370
361
  try {
371
362
  state.updateNow()
372
363
  } catch (e: Throwable) {
373
364
  val reason = ErrorTexts.couldNot("report the navigation state", ErrorTexts.reasonOf(e))
374
- log("[ApexNavigation] ${commandName ?: "transition"}: $reason")
365
+ log("[ApexNavigation] transition: $reason")
375
366
  }
376
367
  }
377
368
 
@@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactContext
6
6
  import com.facebook.react.bridge.WritableMap
7
7
  import dev.apexnative.navigation.NavigationApplication
8
8
  import dev.apexnative.navigation.nativescreen.NativeScreenCalls
9
+ import dev.apexnative.navigation.react.ReloadCoordinator
9
10
  import dev.apexnative.navigation.utils.NavigationLog
10
11
  import org.json.JSONArray
11
12
 
@@ -13,12 +14,26 @@ import org.json.JSONArray
13
14
  * Sends the events `ANN.<Name>` to the JavaScript of [reactContext] through React Native's device event emitter, in
14
15
  * the order they are sent: sent from the main thread only. Without a context, or when its React instance is gone, an
15
16
  * event is dropped with one line in the log. The names and the keys come from [EventContract], made from the types of
16
- * `src/interfaces/`. The core sends through [current]; modules keep making their own.
17
+ * `src/interfaces/`.
18
+ *
19
+ * An emitter belongs to one JavaScript, as on iOS (`ANNEventEmitter`): it takes the number of the JavaScript
20
+ * ([generation], `ReloadCoordinator`) when it is made and drops every event silently once that number moved. React
21
+ * Native keeps one context across a reload (bridgeless), so without it an event of a screen of the JavaScript that
22
+ * went would reach the new one, which may have a screen with the same id. The core and its modules send through
23
+ * [current], or [of] with the number of the JavaScript a node was made for.
17
24
  */
18
- class EventEmitter(private val reactContext: ReactContext?) {
19
- /** Whether an event sent now reaches JavaScript. */
25
+ class EventEmitter @JvmOverloads constructor(
26
+ private val reactContext: ReactContext?,
27
+ /** The number of the JavaScript this emitter sends to: by default the one that runs as it is made. */
28
+ val generation: Int = ReloadCoordinator.generation,
29
+ ) {
30
+ /** Whether the JavaScript of this emitter still runs: its events go only then. Any thread. */
31
+ val isCurrent: Boolean
32
+ get() = ReloadCoordinator.isCurrent(generation)
33
+
34
+ /** Whether an event sent now reaches JavaScript: its JavaScript still runs, in a live React instance. */
20
35
  val canSend: Boolean
21
- get() = reactContext?.hasActiveReactInstance() == true
36
+ get() = isCurrent && reactContext?.hasActiveReactInstance() == true
22
37
 
23
38
  fun emitAppLaunched() = emit(EventContract.AppLaunched.EVENT, Arguments.createMap())
24
39
 
@@ -167,6 +182,8 @@ class EventEmitter(private val reactContext: ReactContext?) {
167
182
  Arguments.createMap().apply { putString(EventContract.ScreenPopped.COMPONENT_ID, componentId) }
168
183
 
169
184
  private fun emit(name: String, data: WritableMap) {
185
+ // The JavaScript of this emitter went: the new one never had its screens.
186
+ if (!isCurrent) return
170
187
  val context = reactContext
171
188
  if (context == null || !context.hasActiveReactInstance()) {
172
189
  Log.w(NavigationLog.TAG, "Could not send event $name. React context is null!")
@@ -176,10 +193,20 @@ class EventEmitter(private val reactContext: ReactContext?) {
176
193
  }
177
194
 
178
195
  companion object {
179
- /** The sender to the JavaScript that runs now: the React context of the app's `ReactGateway`. */
196
+ /**
197
+ * The sender to the JavaScript that runs now: the React context of the app's `ReactGateway`. It goes silent
198
+ * once that JavaScript goes (a reload): one kept for later sends nothing to the next.
199
+ */
180
200
  @JvmStatic
181
- fun current(): EventEmitter =
182
- EventEmitter(NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext)
201
+ fun current(): EventEmitter = of(ReloadCoordinator.generation)
202
+
203
+ /**
204
+ * The sender to the JavaScript [generation] (a node's `jsGeneration`): the React context of the app's
205
+ * `ReactGateway` while that JavaScript runs, silent once it went, so an event of a screen made for the
206
+ * JavaScript that went never reaches the new one.
207
+ */
208
+ internal fun of(generation: Int): EventEmitter =
209
+ EventEmitter(NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext, generation)
183
210
 
184
211
  // The names of the events, for modules outside the library; inside it, [EventContract] has them with their
185
212
  // keys.
@@ -0,0 +1,154 @@
1
+ package dev.apexnative.navigation.react.events
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import dev.apexnative.navigation.react.ReloadCoordinator
6
+ import dev.apexnative.navigation.viewcontrollers.bottomtabs.BottomTabsController
7
+ import dev.apexnative.navigation.viewcontrollers.navigator.Navigators
8
+ import dev.apexnative.navigation.viewcontrollers.parent.ParentController
9
+ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
10
+
11
+ /**
12
+ * The one place of the navigation state JavaScript hears (`ANN.NavigationStateChanged`): the screen the user sees and
13
+ * the selected tab, the last state sent, the update posted for a pass of the main loop and its holds. Every change of
14
+ * the screen the user sees comes with a screen appearing: a push, a pop, a modal, a tab, a menu.
15
+ *
16
+ * Two ways ask for it. The dispatcher asks for one update after the events of a pass ([setNeedsUpdate], coalesced and
17
+ * held while a `setRoot` shows the modals and the overlays of its layout). A command's answer sends it at once
18
+ * ([updateNow], never held), once per answer (`CommandAnswer.reportStateNow`): at the end of its transition, else
19
+ * right before its promise. Each goes only when the state differs from the last one sent, through the JavaScript that
20
+ * runs now. Main thread only.
21
+ */
22
+ internal object NavigationState : AppearanceDispatcher.StateUpdates {
23
+ /** What JavaScript hears: the screen the user sees (`null` for none) and the selected tab (`null` out of tabs). */
24
+ data class State(val screenId: String?, val screenName: String?, val tab: Int?)
25
+
26
+ private val handler by lazy { Handler(Looper.getMainLooper()) }
27
+
28
+ // The last state sent to the JavaScript that runs now; none before its first.
29
+ private var sent: State? = null
30
+
31
+ // The one posted update of a pass of the main loop, which checks the holds when it runs.
32
+ private val updates = Updates(
33
+ post = { task -> handler.post(task) },
34
+ generation = { ReloadCoordinator.generation },
35
+ update = ::updateNow,
36
+ )
37
+
38
+ override fun setNeedsUpdate() {
39
+ if (EventEmitter.current().canSend) updates.setNeedsUpdate()
40
+ }
41
+
42
+ override fun hold() = updates.hold()
43
+
44
+ override fun release() = updates.release()
45
+
46
+ /**
47
+ * Sends the state at once when it differs from the last one sent. A command's answer calls it once, at the end of
48
+ * its transition or right before its promise: the event and the resolution reach JavaScript in that order, so the
49
+ * state is new when the `await` of the command goes on. The update posted for the pass then finds nothing new.
50
+ */
51
+ override fun updateNow() {
52
+ val emitter = EventEmitter.current()
53
+ if (!emitter.canSend) return
54
+ // The navigation of the activity the commands run in (`Navigators.current`).
55
+ val navigator = Navigators.current()?.navigator ?: return
56
+ val next = stateOf(navigator.visibleLayout)
57
+ if (next == sent) return
58
+ sent = next
59
+ emitter.emitNavigationStateChanged(next.screenId, next.screenName, next.tab)
60
+ }
61
+
62
+ /** The new JavaScript has no state yet (`ANN.AppLaunched`): the next one goes to it whatever it is. */
63
+ fun reset() {
64
+ sent = null
65
+ }
66
+
67
+ /**
68
+ * The JavaScript went with its layouts: a hold its `setRoot` left, and an update wanted under it, go with it, so
69
+ * the new JavaScript's updates are never held by the old one's.
70
+ */
71
+ fun javaScriptWent() = updates.dropHolds()
72
+
73
+ // The top modal, or the root ([layout]), down to the screen the user sees; overlays don't count. The tab is the
74
+ // selected one of the nearest tabs the screen is in, the last met on the way down, in a modal too: the tabs
75
+ // `selectTab` acts on from it. None for a screen out of tabs.
76
+ internal fun stateOf(layout: ViewController<*>?): State {
77
+ var tab: Int? = null
78
+ var current = layout ?: return State(null, null, null)
79
+ while (current is ParentController<*>) {
80
+ if (current is BottomTabsController) tab = current.selectedIndex
81
+ current = current.currentChild ?: return State(null, null, null)
82
+ }
83
+ return State(current.id, current.currentComponentName, tab)
84
+ }
85
+
86
+ /**
87
+ * The posted update and its holds. [setNeedsUpdate] posts one update ([post]), coalesced: the asks of one pass of
88
+ * the main loop make one, which runs [update] in a later pass. While held, an ask, or a posted update that runs
89
+ * while held (asked before the hold was taken), only marks one as wanted, and the last release posts it. Released
90
+ * right before the answer, that update finds nothing new: the state before the answer is sent at once, never
91
+ * held, and an identical state is never sent twice. Every [hold] has one [release]; a release without a hold does
92
+ * nothing, so the count never goes below zero. An update keeps the JavaScript it was asked for ([generation]): one
93
+ * asked by a JavaScript that went before it runs is dropped; a newer ask of the new JavaScript still goes.
94
+ */
95
+ internal class Updates(
96
+ private val post: (() -> Unit) -> Unit,
97
+ private val generation: () -> Int = { 0 },
98
+ private val update: () -> Unit,
99
+ ) {
100
+ /** The holds not released yet. */
101
+ var count = 0
102
+ private set
103
+
104
+ // An update was asked while held.
105
+ private var wanted = false
106
+ // An update is posted and hasn't run yet.
107
+ private var scheduled = false
108
+ // The JavaScript the last ask was for.
109
+ private var askedIn = 0
110
+
111
+ /** Asks for one update in a later pass of the main loop; while held, once the last hold is released. */
112
+ fun setNeedsUpdate() {
113
+ askedIn = generation()
114
+ if (count > 0) wanted = true else schedule()
115
+ }
116
+
117
+ /** The updates asked from now on, and those posted that run from now on, wait for as many [release]s. */
118
+ fun hold() {
119
+ count++
120
+ }
121
+
122
+ /** One hold ends; with the last, the update asked while held is posted now. */
123
+ fun release() {
124
+ if (count == 0) return
125
+ count--
126
+ if (count > 0 || !wanted) return
127
+ wanted = false
128
+ schedule()
129
+ }
130
+
131
+ /**
132
+ * The holds and the update wanted under them go, unreleased: the JavaScript that took them went. An update
133
+ * already posted stays; it runs only for the JavaScript it was asked for.
134
+ */
135
+ fun dropHolds() {
136
+ count = 0
137
+ wanted = false
138
+ }
139
+
140
+ private fun schedule() {
141
+ if (scheduled) return
142
+ scheduled = true
143
+ post {
144
+ scheduled = false
145
+ // A hold taken since it was posted: the update waits for the release. The JavaScript it was asked for
146
+ // went: nothing goes to the new one, which hears its first state after its own changes.
147
+ when {
148
+ count > 0 -> wanted = true
149
+ askedIn == generation() -> update()
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
@@ -10,7 +10,7 @@ import com.facebook.react.uimanager.StateWrapper
10
10
  import com.facebook.react.uimanager.ThemedReactContext
11
11
  import com.facebook.react.uimanager.UIManagerHelper
12
12
  import com.facebook.react.views.view.ReactViewGroup
13
- import dev.apexnative.navigation.react.ScreenInsets
13
+ import dev.apexnative.navigation.react.ScreenEnvironment
14
14
  import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
15
15
 
16
16
  /**
@@ -20,7 +20,7 @@ import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
20
20
  @SuppressLint("ViewConstructor")
21
21
  class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
22
22
  private var stopListening: (() -> Unit)? = null
23
- private var applied: ScreenInsets.Insets? = null
23
+ private var applied: ScreenEnvironment.Insets? = null
24
24
  private var readySent = false
25
25
  private var contentSignalled = false
26
26
 
@@ -48,7 +48,7 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
48
48
  stopListening?.invoke()
49
49
  field = value
50
50
  applied = null
51
- stopListening = value?.let { id -> ScreenInsets.listenToPadding(id) { applyPadding() } }
51
+ stopListening = value?.let { id -> ScreenEnvironment.listenToPadding(id) { applyPadding() } }
52
52
  // The screen has a safe area view: it counts as rendered once this view says so.
53
53
  value?.let(ScreenReadiness::wrapped)
54
54
  applyPadding()
@@ -56,7 +56,7 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
56
56
 
57
57
  private fun applyPadding() {
58
58
  val state = stateWrapper ?: return
59
- val padding = screenId?.let(ScreenInsets::padding) ?: return
59
+ val padding = screenId?.let(ScreenEnvironment::padding) ?: return
60
60
  if (padding == applied) return
61
61
  applied = padding
62
62
  state.updateState(
@@ -69,9 +69,9 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
69
69
  )
70
70
  }
71
71
 
72
- private fun paddingOf(data: ReadableMap): ScreenInsets.Insets? {
72
+ private fun paddingOf(data: ReadableMap): ScreenEnvironment.Insets? {
73
73
  if (!data.hasKey("top")) return null
74
- return ScreenInsets.Insets(data.getDouble("top"), data.getDouble("right"), data.getDouble("bottom"),
74
+ return ScreenEnvironment.Insets(data.getDouble("top"), data.getDouble("right"), data.getDouble("bottom"),
75
75
  data.getDouble("left"))
76
76
  }
77
77
 
@@ -95,8 +95,9 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
95
95
  val reactContext = context as? ReactContext ?: return
96
96
  val dispatcher = UIManagerHelper.getEventDispatcher(reactContext) ?: return
97
97
  readySent = true
98
- dispatcher.dispatchEvent(SafeAreaReadyEvent(UIManagerHelper.getSurfaceId(this), id))
99
- screenId?.let(ScreenReadiness::laidOut)
98
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
99
+ dispatcher.dispatchEvent(SafeAreaReadyEvent(surfaceId, id))
100
+ screenId?.let { ScreenReadiness.laidOut(it, surfaceId) }
100
101
  }
101
102
 
102
103
  // The navigation tells whether this view's screen waits: a component of a bar (title, button, background, tab)
@@ -111,12 +112,13 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
111
112
  }
112
113
 
113
114
  // Once, by the screen's id: the container asks the screen's view for the layout React mounts the content without.
114
- // On attach too: content added before this view was attached is signalled then.
115
+ // On attach too: content added before this view was attached is signalled then. The signal names this view's
116
+ // surface: only the root that shows it takes it as its own.
115
117
  private fun signalContent() {
116
118
  if (contentSignalled) return
117
119
  val id = screenId ?: return
118
120
  contentSignalled = true
119
- ScreenReadiness.contentRendered(id)
121
+ ScreenReadiness.contentRendered(id, UIManagerHelper.getSurfaceId(this))
120
122
  }
121
123
 
122
124
  fun drop() {