@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
@@ -31,7 +31,12 @@ class SlotScreenView(
31
31
  private val whenMounted = mutableListOf<Runnable>()
32
32
  private val whenRendered = mutableListOf<Runnable>()
33
33
  private val mountedHandlers = LinkedHashMap<String, Runnable>()
34
- private var readinessListener: ScreenReadiness.Listener? = null
34
+ private var listening = false
35
+
36
+ // The safe area view of a slot renders in the app's one React tree: every signal of the id is the screen's.
37
+ private val readiness = object : ScreenReadiness.Listener {
38
+ override fun onContentRendered() = checkReadiness()
39
+ }
35
40
  private var mounted = false
36
41
  private var readyTold = false
37
42
  private var destroyed = false
@@ -105,14 +110,9 @@ class SlotScreenView(
105
110
  if (readyTold) return listener.run()
106
111
  whenRendered += listener
107
112
  if (isReady()) return checkReadiness()
108
- if (readinessListener != null) return
109
- val readiness = object : ScreenReadiness.Listener {
110
- override fun onLaidOut() = Unit
111
-
112
- override fun onContentRendered() = checkReadiness()
113
- }
114
- readinessListener = readiness
115
- ScreenReadiness.setListener(componentId, readiness)
113
+ if (listening) return
114
+ listening = true
115
+ ScreenReadiness.addListener(componentId, this, readiness)
116
116
  }
117
117
 
118
118
  override fun whenSlotContentMounted(listener: Runnable) {
@@ -151,8 +151,8 @@ class SlotScreenView(
151
151
  }
152
152
 
153
153
  private fun stopListening() {
154
- val readiness = readinessListener ?: return
155
- readinessListener = null
156
- ScreenReadiness.removeListener(componentId, readiness)
154
+ if (!listening) return
155
+ listening = false
156
+ ScreenReadiness.removeListener(componentId, this)
157
157
  }
158
158
  }
@@ -26,16 +26,3 @@ val View.borderRadius: Float
26
26
  */
27
27
  internal fun resolveBorderRadius(radius: LengthPercentage, width: Int, density: Float): Float =
28
28
  (radius.resolve(width / density) * density).coerceAtLeast(0f)
29
-
30
- /** The scale the parents of [view] give it, along x and along y: a view in a scaled parent shows smaller. */
31
- fun computeInheritedScale(view: View): Pair<Float, Float> {
32
- var scaleX = 1f
33
- var scaleY = 1f
34
- var parent = view.parent as? View
35
- while (parent != null) {
36
- scaleX *= parent.scaleX
37
- scaleY *= parent.scaleY
38
- parent = parent.parent as? View
39
- }
40
- return scaleX to scaleY
41
- }
@@ -9,7 +9,8 @@ import androidx.window.layout.WindowMetricsCalculator
9
9
  import dev.apexnative.navigation.NavigationActivity
10
10
  import dev.apexnative.navigation.options.DefaultOptions
11
11
  import dev.apexnative.navigation.options.Options
12
- import dev.apexnative.navigation.react.ArrangementStates
12
+ import dev.apexnative.navigation.react.ScreenEnvironment
13
+ import dev.apexnative.navigation.react.events.EventEmitter
13
14
  import dev.apexnative.navigation.utils.SystemUiUtils
14
15
  import dev.apexnative.navigation.utils.WindowFolds
15
16
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
@@ -145,12 +146,14 @@ class ArrangementController internal constructor(
145
146
  val overlay = plan.layout == ArrangementPlan.Layout.OVERLAY
146
147
  val single = plan.layout == ArrangementPlan.Layout.SINGLE
147
148
  if (primary is ComponentViewController) {
148
- val state = ArrangementStates.State(PRIMARY, layout, front = overlay, hidden = false)
149
- ArrangementStates.set(activity, primary.id, state, notify)
149
+ val state = ScreenEnvironment.Arrangement(PRIMARY, layout, front = overlay, hidden = false)
150
+ val changes = ScreenEnvironment.changesOf(EventEmitter.of(primary.jsGeneration))
151
+ ScreenEnvironment.setArrangement(primary.id, state, notify, changes)
150
152
  }
151
153
  if (secondary is ComponentViewController) {
152
- val state = ArrangementStates.State(SECONDARY, layout, front = false, hidden = single)
153
- ArrangementStates.set(activity, secondary.id, state, notify)
154
+ val state = ScreenEnvironment.Arrangement(SECONDARY, layout, front = false, hidden = single)
155
+ val changes = ScreenEnvironment.changesOf(EventEmitter.of(secondary.jsGeneration))
156
+ ScreenEnvironment.setArrangement(secondary.id, state, notify, changes)
154
157
  }
155
158
  }
156
159
 
@@ -243,9 +246,9 @@ class ArrangementController internal constructor(
243
246
  // The stack keeps the bar's title, buttons and search by the view of its screen on top: they go with the pair.
244
247
  if (!isDestroyed && peekView() != null) performOnParentController { it.onChildDestroyed(this) }
245
248
  folds?.removeListener(onFoldsChanged)
246
- ArrangementStates.remove(primary.id)
247
- ArrangementStates.remove(secondary.id)
248
249
  shownChildren.containerDisappeared()
250
+ // The screens go with the pair: each clears its own environment, its arrangement too, unless its id is
251
+ // another's now.
249
252
  super.destroy()
250
253
  }
251
254
 
@@ -2,6 +2,7 @@ package dev.apexnative.navigation.viewcontrollers.bottomtabs
2
2
 
3
3
  import android.view.View
4
4
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListener
5
+ import dev.apexnative.navigation.views.bottomtabs.BottomTabsLayout
5
6
 
6
7
  /**
7
8
  * bottomTabs.hideOnScroll: the tabs hide after a drag that scrolls the screen's content down and come back after one
@@ -43,9 +44,10 @@ class BottomTabsCollapseBehavior(private val container: () -> View?) :
43
44
  override fun onHide() {
44
45
  val view = container() ?: return
45
46
  hiddenByScroll = true
46
- // Out of the screen with what is under it: a compact bar floats over its bottom margin and the navigation bar.
47
- val under = ((view.parent as? View)?.let { it.height - view.bottom } ?: 0).coerceAtLeast(0)
48
- view.animate().translationY((view.height + under).toFloat()).setDuration(DURATION).start()
47
+ // Out of the screen with what is under it (a compact bar floats over its bottom margin and the navigation bar)
48
+ // and the custom row that rises above it.
49
+ val distance = (view.parent as? BottomTabsLayout)?.hideDistance ?: view.height
50
+ view.animate().translationY(distance.toFloat()).setDuration(DURATION).start()
49
51
  }
50
52
 
51
53
  private companion object {
@@ -26,16 +26,15 @@ import dev.apexnative.navigation.utils.scrollToTop
26
26
  import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
27
27
  import dev.apexnative.navigation.viewcontrollers.component.ComponentViewController
28
28
  import dev.apexnative.navigation.viewcontrollers.component.RestPlace
29
- import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
30
29
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
31
30
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
32
31
  import dev.apexnative.navigation.viewcontrollers.parent.ContainerBackground
33
32
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
34
33
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
35
34
  import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
36
- import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
37
35
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
38
36
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
37
+ import dev.apexnative.navigation.views.animations.AnimationLayer
39
38
  import dev.apexnative.navigation.views.animations.FadeThrough
40
39
  import dev.apexnative.navigation.views.bottomtabs.BottomTabItem
41
40
  import dev.apexnative.navigation.views.bottomtabs.BottomTabsLayout
@@ -201,7 +200,9 @@ class BottomTabsController(
201
200
  }
202
201
  })
203
202
  val set = AnimatorSet()
204
- set.playTogether(exit, FadeThrough.enter(coming))
203
+ // Each tab fades whole through a hardware layer; a switch ended early ([set]'s end) gives them back too.
204
+ val enter = FadeThrough.enter(coming)
205
+ set.playTogether(AnimationLayer.during(going, exit), AnimationLayer.during(coming, enter))
205
206
  set.addListener(object : AnimatorListenerAdapter() {
206
207
  override fun onAnimationEnd(animation: Animator) {
207
208
  set.removeAllListeners()
@@ -352,8 +353,8 @@ class BottomTabsController(
352
353
 
353
354
  /**
354
355
  * Selects the tab at [index]: the bar shows it at once; on screen the tab it left fades through to it (Material's
355
- * fade through), off screen one container becomes invisible and the other visible at once; then [switched]. The
356
- * history of back follows.
356
+ * fade through); off screen, or when the new tab's `bottomTabs.switchAnimation` is `'none'`, one container becomes
357
+ * invisible and the other visible at once; then [switched]. The history of back follows.
357
358
  */
358
359
  private fun select(index: Int, how: How) {
359
360
  if (index == selectedIndex || index !in tabs.indices || isDestroyed) return
@@ -379,8 +380,11 @@ class BottomTabsController(
379
380
  // A layer that waits for the tabs waits for the tab they show now.
380
381
  navigator?.shownScreensChanged(this)
381
382
  if (onScreen) AppearanceDispatcher.beginTransition(listOf(shown))
383
+ // From code and by back the state goes at once, as on iOS: `selectTab` resolves on it, and the command after
384
+ // it must act in this tab, not in the one still fading out.
385
+ if (how != How.Press) AppearanceDispatcher.reportState()
382
386
  if (views == null) return switched(index, previous, how, onScreen, hasViews = false)
383
- endSwitch = views.switch(shown, left, animated = onScreen) {
387
+ endSwitch = views.switch(shown, left, animated = onScreen && fadesThrough(shown)) {
384
388
  endSwitch = null
385
389
  if (!isDestroyed) switched(index, previous, how, onScreen, hasViews = true)
386
390
  }
@@ -390,8 +394,8 @@ class BottomTabsController(
390
394
  * The switch to the tab at [index] from [previous] ended: what went disappears, what came appears — unless the tabs
391
395
  * stopped showing during the fade (a modal over them): it appears when they show again; then for a press
392
396
  * `ANN.BottomTabSelected` right after the new screen's DidAppear and the left tab's `popToRootOnBlur`, with the state
393
- * as one update after them; from code and by back, `popToRootOnBlur` and the state at once. [onScreen]: the switch
394
- * started on screen.
397
+ * as one update after them; from code and by back, `popToRootOnBlur` and the state, which went at the start too
398
+ * ([select]). [onScreen]: the switch started on screen.
395
399
  */
396
400
  private fun switched(index: Int, previous: Int, how: How, onScreen: Boolean, hasViews: Boolean) {
397
401
  val left = tabs[previous]
@@ -426,6 +430,11 @@ class BottomTabsController(
426
430
  if (screen == null) finish() else AppearanceDispatcher.afterDidAppear(screen, finish)
427
431
  }
428
432
 
433
+ // `bottomTabs.switchAnimation` of the tab that shows, its shown screen's over its own, the tabs' and the defaults:
434
+ // `'platform'`, the default, fades through; `'none'` switches at once.
435
+ private fun fadesThrough(tab: ViewController<*>): Boolean =
436
+ tab.resolveShownOptions(DefaultOptions.current).bottomTabs.switchAnimation.get(null) != "none"
437
+
429
438
  // A switch still running ends at once and a press's `ANN.BottomTabSelected` still waiting goes, before anything
430
439
  // newer on these tabs.
431
440
  private fun flushPendingSelected() {
@@ -547,9 +556,6 @@ class BottomTabsController(
547
556
  return TabsRoom.tabsTop(room(), screenOptions(screen), bottom)
548
557
  }
549
558
 
550
- override fun leavesBottomEdgeFree(child: ViewController<*>, screen: ViewController<*>): Boolean =
551
- TabsRoom.leavesBottomEdgeFree(screenOptions(screen)) || super.leavesBottomEdgeFree(child, screen)
552
-
553
559
  override fun tabsAtRestOf(child: ViewController<*>, screen: ViewController<*>): FabPlacement.Tabs? {
554
560
  if (peekView() == null) return super.tabsAtRestOf(child, screen)
555
561
  return TabsRoom.fabTabs(room(), screenOptions(screen))
@@ -630,7 +636,6 @@ class BottomTabsController(
630
636
  layout.row = row
631
637
  layout.bar.customRowOptionsListener = { applyRow(row, it) }
632
638
  layout.bar.customRowOptions = resolveShownOptions(DefaultOptions.current).bottomTabs.customRow.copy()
633
- CustomTabRowPlacer.track(activity)
634
639
  }
635
640
 
636
641
  private fun applyRow(row: CustomTabRow, options: BottomTabsCustomRowOptions) {
@@ -660,15 +665,12 @@ class BottomTabsController(
660
665
  private fun makeCell(index: Int): CustomBottomTabItemView {
661
666
  val component = itemOptions(index).component
662
667
  val componentId = component.componentId.get(null) ?: "$id.$index"
663
- ScreenReadiness.setRendersAtOnce(componentId, true)
664
- // Another live root has the id (the same layout set again before the old one went): what it rendered isn't
665
- // this one's; the id's readiness goes only with its last root (as the bar's roots).
666
- if (BarComponents.isLive(componentId)) ScreenReadiness.forgetRender(componentId)
667
- BarComponents.rootMade(componentId)
668
668
  val cell = CustomBottomTabItemView(activity)
669
669
  val state = cellState(index)
670
- cell.show(ReactView(activity, componentId, component.name.get(""), cell.bundleOf(state)), state)
671
- cell.onDestroyed = { if (BarComponents.rootStopped(componentId)) ScreenReadiness.remove(componentId) }
670
+ // Another live root may have the id (the same layout set again before the old one went): each answers by the
671
+ // render of its own surface, and the id's readiness goes with its last root.
672
+ val root = ReactView(activity, componentId, component.name.get(""), cell.bundleOf(state), rendersAtOnce = true)
673
+ cell.show(root, state)
672
674
  return cell
673
675
  }
674
676
 
@@ -685,7 +687,7 @@ class BottomTabsController(
685
687
 
686
688
  /**
687
689
  * Destroys the tabs once: every tab and the registry first, then the cells (each stops its root once), the row out
688
- * of the window, the scroll it followed. A press's event still waiting goes with them.
690
+ * of the tabs' view, the scroll it followed. A press's event still waiting goes with them.
689
691
  */
690
692
  override fun destroy() {
691
693
  if (isDestroyed) return
@@ -696,7 +698,6 @@ class BottomTabsController(
696
698
  endSwitch?.invoke()
697
699
  peekView()?.let { layout ->
698
700
  layout.bar.destroyCustomItemViews()
699
- layout.row?.let(CustomTabRowPlacer::forget)
700
701
  layout.row = null
701
702
  }
702
703
  cells = emptyList()
@@ -49,6 +49,22 @@ internal object CustomTabRowGeometry {
49
49
  content = content,
50
50
  )
51
51
 
52
+ /**
53
+ * The row over the bar as the tabs' view lays it out: the bar's [barLeft], [barRight] and [barBottom] in that
54
+ * view, whose height is [tabsHeight] and whose bottom [navigationBar] pixels lie under the navigation bar — the
55
+ * row's bottom never goes over them —, with [margin], [content] and [bottomMargin] as [place] takes them.
56
+ */
57
+ fun inTabs(
58
+ barLeft: Int,
59
+ barRight: Int,
60
+ barBottom: Int,
61
+ tabsHeight: Int,
62
+ navigationBar: Int,
63
+ margin: Int,
64
+ content: Int,
65
+ bottomMargin: Int,
66
+ ): Place = place(barLeft, barRight, minOf(barBottom, tabsHeight - navigationBar), margin, content, bottomMargin)
67
+
52
68
  /**
53
69
  * The part of the row's frame at [place] below its visible part, in pixels: `bottomMargin`. The row draws and takes
54
70
  * touches only above it.
@@ -61,6 +77,12 @@ internal object CustomTabRowGeometry {
61
77
  */
62
78
  fun needsPlacing(last: Place?, next: Place): Boolean = last != next
63
79
 
80
+ /**
81
+ * The row's own lift over its [elevation] while the bar's container stands at [barZ]: as high as the container, so
82
+ * the row, after it in the tabs' view, draws over it and takes the touches first even under a raised bar.
83
+ */
84
+ fun lift(barZ: Float, elevation: Float): Float = (barZ - elevation).coerceAtLeast(0f)
85
+
64
86
  /**
65
87
  * The row's background: `customRow.backgroundColor` ([color]; `null` and `'NoColor'` are none); without it
66
88
  * `backgroundEffect: 'none'` ([effect]) is clear, any other effect or none the theme's `android:colorBackground`
@@ -58,11 +58,6 @@ internal object TabsRoom {
58
58
  fun tabsTop(bar: Bar, screen: Screen, tabsBottom: Int): Int? =
59
59
  if (screen.hidden) null else tabsBottom - bar.reach - bar.rowOvershoot
60
60
 
61
- /**
62
- * The safe area of the screen leaves its bottom edge free: the bar hides on scroll and shows for it.
63
- */
64
- fun leavesBottomEdgeFree(screen: Screen): Boolean = screen.hideOnScroll && !screen.hidden
65
-
66
61
  /**
67
62
  * The bar at rest under a stack's FAB: also when the screen is under it (`drawBehind`, `hideOnScroll`); none when
68
63
  * the screen hides it.
@@ -8,12 +8,12 @@ import androidx.core.view.ViewCompat
8
8
  import androidx.core.view.WindowInsetsCompat
9
9
  import dev.apexnative.navigation.options.DefaultOptions
10
10
  import dev.apexnative.navigation.options.Options
11
- import dev.apexnative.navigation.react.ArrangementStates
12
11
  import dev.apexnative.navigation.react.ReactView
13
- import dev.apexnative.navigation.react.ScreenInsets
12
+ import dev.apexnative.navigation.react.ScreenEnvironment
14
13
  import dev.apexnative.navigation.react.ScreenRegions
15
14
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
16
15
  import dev.apexnative.navigation.react.events.ComponentType
16
+ import dev.apexnative.navigation.react.events.EventEmitter
17
17
  import dev.apexnative.navigation.slots.SlotScreenView
18
18
  import dev.apexnative.navigation.system.SystemUiOwner
19
19
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
@@ -156,7 +156,7 @@ class ComponentViewController(
156
156
  shown = layout.isShown || prepared,
157
157
  layoutPending = !prepared && isLayoutPending(content),
158
158
  topOfStack = parent?.isTopOfStack(this) ?: true,
159
- hasSafeArea = ScreenInsets.padding(id) != null,
159
+ hasSafeArea = ScreenEnvironment.padding(id) != null,
160
160
  )
161
161
  if (!measures) return
162
162
  val windowInsets = ViewCompat.getRootWindowInsets(content) ?: return
@@ -175,10 +175,15 @@ class ComponentViewController(
175
175
  val edges = ScreenSafeArea.paddedEdges(
176
176
  safeAreaEdges = resolveCurrentOptions(DefaultOptions.current).layout.safeAreaEdges,
177
177
  topFree = parent?.leavesTopEdgeFree(this) == true,
178
- bottomFree = parent?.leavesBottomEdgeFree(this) == true,
178
+ largeTitle = parent?.showsLargeTitleOver(this) == true,
179
179
  )
180
- ScreenInsets.update(activity, id, insets, ScreenSafeArea.padding(insets, edges))
181
- ScreenRegions.update(activity, id, Rect(place.left, place.top, place.right, place.bottom), windowInsets)
180
+ // The events of the screen go to the JavaScript it was made for, and to no other.
181
+ val emitter = EventEmitter.of(jsGeneration)
182
+ val padding = ScreenSafeArea.padding(insets, edges)
183
+ val uncovered = ScreenSafeArea.uncovered(insets, padding)
184
+ ScreenEnvironment.setInsets(id, uncovered, padding, ScreenEnvironment.changesOf(emitter))
185
+ val area = Rect(place.left, place.top, place.right, place.bottom)
186
+ ScreenRegions.update(activity, id, area, windowInsets, emitter)
182
187
  }
183
188
 
184
189
  // A layout asked for on the content or on a view around it: it isn't in its place yet.
@@ -241,9 +246,7 @@ class ComponentViewController(
241
246
  }
242
247
  setParentController(null)
243
248
  if (isIdTakenByAnother) return
244
- ScreenInsets.remove(id)
245
- ScreenRegions.remove(id)
246
- ArrangementStates.remove(id)
249
+ ScreenEnvironment.remove(id)
247
250
  ScreenReadiness.remove(id)
248
251
  }
249
252
 
@@ -5,7 +5,7 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
5
5
  /**
6
6
  * Whether a screen's content rendered, which a command waits for before the screen comes in: a new root (`setRoot`),
7
7
  * a push (unless `waitForRender: false`), the new top of a `setStackRoot`. A wait never listens to the readiness store
8
- * itself: the content keeps the one listener of its id and tells every waiter. Main thread only.
8
+ * itself: the content keeps its own listener of its id and tells every waiter. Main thread only.
9
9
  */
10
10
  internal interface Readiness {
11
11
  /** The content of [screen] rendered; a screen without content to wait for is ready. */
@@ -1,93 +1,128 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.component
2
2
 
3
3
  /**
4
- * The readiness of each screen by its id: the safe area view of a screen (`ANNSafeAreaView`) tells it here that it is
5
- * laid out in its place and that the screen's content rendered into it, and asks whether the screen waits for that at
6
- * all. The screen's container listens by the screen's id: no view walks up or down the views to find the other. Main
7
- * thread only.
4
+ * The readiness of each screen by its id: the safe area view of a screen (`ANNSafeAreaView`) tells it here that it
5
+ * has one, that it is laid out in its place and that the screen's content rendered into it, and asks whether the
6
+ * screen waits for that at all. The content of a screen or of a component listens by the id: no view walks up or down
7
+ * the views to find the other. Main thread only.
8
+ *
9
+ * As on iOS (`ANNScreenReadiness`), what a safe area view said is kept: a listener that comes later hears it at once.
10
+ * Several listeners may wait on one id (the roots of a bar component that several screens show with one id), each
11
+ * under a key of its own, which removes it. Each signal carries its source, the React surface of the safe area view
12
+ * that gave it, and a listener hears only the sources it [accepts][Listener.accepts]: React Native never gives a
13
+ * surface's id twice in a process, so the late signal of another root with the same id (a root of the JavaScript that
14
+ * went, a destroyed screen, another live root of a bar component) never makes a new root ready.
8
15
  */
9
16
  object ScreenReadiness {
10
- /** What a screen's container hears from the safe area view of its screen. */
17
+ /** The source of a signal that names none: a listener that accepts every source hears it. */
18
+ const val NO_SOURCE = -1
19
+
20
+ /** What the content of a screen or of a component hears from the safe area view of its id. */
11
21
  interface Listener {
22
+ /**
23
+ * Whether a signal of [source] (the id of the React surface of the safe area view that gave it) is this
24
+ * listener's: by default every one is.
25
+ */
26
+ fun accepts(source: Int): Boolean = true
27
+
12
28
  /** The safe area view is laid out in its place, with a size. */
13
- fun onLaidOut()
29
+ fun onLaidOut() = Unit
14
30
 
15
31
  /** The screen rendered its content into its safe area view, also when it renders nothing (`null`). */
16
32
  fun onContentRendered()
17
33
  }
18
34
 
19
- private val atOnce = HashSet<String>()
20
- private val wrapped = HashSet<String>()
21
- private val laidOut = HashSet<String>()
22
- private val rendered = HashSet<String>()
23
- private val listeners = HashMap<String, Listener>()
35
+ // What the safe area views of one id said: that one exists, and the sources that are laid out and rendered.
36
+ private class Entry {
37
+ var wrapped = false
38
+ val laidOut = HashSet<Int>()
39
+ val rendered = HashSet<Int>()
40
+ }
41
+
42
+ private val entries = HashMap<String, Entry>()
43
+ private val listeners = HashMap<String, LinkedHashMap<Any, Listener>>()
44
+ // The roots that render at once, by id: an id renders at once while one of them lives.
45
+ private val atOnce = HashMap<String, MutableSet<Any>>()
24
46
 
25
47
  /**
26
48
  * Whether the screen [screenId] waits for its safe area view to be laid out before its content renders: every
27
- * screen does, as on iOS, unless the core marked it as one that renders at once ([setRendersAtOnce]) — a React
28
- * component of a bar or a tab item, whose size comes from its content.
49
+ * screen does, as on iOS, unless a root of the id renders at once ([setRendersAtOnce]) — a React component of a
50
+ * bar or a tab item, whose size comes from its content.
29
51
  */
30
- fun waitsForReadiness(screenId: String): Boolean = screenId !in atOnce
52
+ fun waitsForReadiness(screenId: String): Boolean = atOnce[screenId].isNullOrEmpty()
31
53
 
32
- /** The core marks a React root it made as one that renders at once (a component of a bar, a tab item), or not. */
33
- fun setRendersAtOnce(screenId: String, rendersAtOnce: Boolean) {
34
- if (rendersAtOnce) atOnce += screenId else atOnce -= screenId
54
+ /**
55
+ * The root [key] of [screenId] renders at once (a component of a bar, a tab item), or stopped. The id renders at
56
+ * once while a root marks it; when the last one stops, what was said of the id goes too, so a later root with it
57
+ * starts anew.
58
+ */
59
+ fun setRendersAtOnce(screenId: String, key: Any, rendersAtOnce: Boolean) {
60
+ if (rendersAtOnce) {
61
+ atOnce.getOrPut(screenId, ::HashSet).add(key)
62
+ return
63
+ }
64
+ val keys = atOnce[screenId] ?: return
65
+ if (!keys.remove(key) || keys.isNotEmpty()) return
66
+ atOnce.remove(screenId)
67
+ entries.remove(screenId)
35
68
  }
36
69
 
37
70
  /** The screen [screenId] has a safe area view: it is rendered only once that view says so. */
38
71
  fun wrapped(screenId: String) {
39
- wrapped += screenId
72
+ entryOf(screenId).wrapped = true
40
73
  }
41
74
 
42
- /** The safe area view of [screenId] is laid out in its place, with a size. */
43
- fun laidOut(screenId: String) {
44
- if (laidOut.add(screenId)) listeners[screenId]?.onLaidOut()
75
+ /** The safe area view of [screenId], in the React surface [source], is laid out in its place, with a size. */
76
+ fun laidOut(screenId: String, source: Int = NO_SOURCE) {
77
+ val entry = entryOf(screenId)
78
+ entry.wrapped = true
79
+ if (entry.laidOut.add(source)) tell(screenId, source, Listener::onLaidOut)
45
80
  }
46
81
 
47
- /** The screen [screenId] rendered its content into its safe area view. */
48
- fun contentRendered(screenId: String) {
49
- if (rendered.add(screenId)) listeners[screenId]?.onContentRendered()
82
+ /** The screen [screenId] rendered its content into its safe area view, in the React surface [source]. */
83
+ fun contentRendered(screenId: String, source: Int = NO_SOURCE) {
84
+ val entry = entryOf(screenId)
85
+ entry.wrapped = true
86
+ if (entry.rendered.add(source)) tell(screenId, source, Listener::onContentRendered)
50
87
  }
51
88
 
52
89
  /**
53
- * Whether the screen [screenId] rendered its content into its safe area view; null when no safe area view told
54
- * anything of it (a screen without one, of the old API: its container decides by its own content).
90
+ * Whether the screen [screenId] rendered its content into its safe area view, in a source [listener] accepts (any
91
+ * source without one); null when no safe area view told anything of the id (a screen without one, of the old API:
92
+ * its content decides by its own children).
55
93
  */
56
- fun isContentRendered(screenId: String): Boolean? = when (screenId) {
57
- in rendered -> true
58
- in laidOut, in wrapped -> false
59
- else -> null
60
- }
61
-
62
- /** [listener] hears the signals of [screenId] from now on; null stops it. */
63
- fun setListener(screenId: String, listener: Listener?) {
64
- if (listener == null) listeners.remove(screenId) else listeners[screenId] = listener
94
+ fun isContentRendered(screenId: String, listener: Listener? = null): Boolean? {
95
+ val entry = entries[screenId] ?: return null
96
+ return entry.rendered.any { listener?.accepts(it) ?: true }
65
97
  }
66
98
 
67
99
  /**
68
- * [listener] stops hearing [screenId], if it is the one that listens: a later screen with the same id (a `setRoot`
69
- * makes the new root before the old one goes) keeps its own.
100
+ * [listener] hears the signals of [screenId] under [key] (its owner) until [removeListener] with the key: a key
101
+ * has one listener per id, the other keys keep theirs. What the safe area views said before reaches it at once, in
102
+ * order, for the sources it accepts.
70
103
  */
71
- fun removeListener(screenId: String, listener: Listener) {
72
- if (listeners[screenId] === listener) listeners.remove(screenId)
104
+ fun addListener(screenId: String, key: Any, listener: Listener) {
105
+ listeners.getOrPut(screenId, ::LinkedHashMap)[key] = listener
106
+ val entry = entries[screenId] ?: return
107
+ if (entry.laidOut.any(listener::accepts) && isListening(screenId, key, listener)) listener.onLaidOut()
108
+ if (entry.rendered.any(listener::accepts) && isListening(screenId, key, listener)) listener.onContentRendered()
73
109
  }
74
110
 
75
- /**
76
- * A new root takes [screenId] while another with it lives (a component of a bar on two screens): what the other
77
- * rendered isn't the new one's, which waits for its own render. Its mark of rendering at once stays.
78
- */
79
- fun forgetRender(screenId: String) {
80
- laidOut -= screenId
81
- rendered -= screenId
111
+ /** The listener of [key] stops hearing [screenId]; those of other keys (a later root with the id) keep theirs. */
112
+ fun removeListener(screenId: String, key: Any) {
113
+ val keys = listeners[screenId] ?: return
114
+ keys.remove(key)
115
+ if (keys.isEmpty()) listeners.remove(screenId)
82
116
  }
83
117
 
84
- /** The screen is destroyed: what was kept of it goes, so a later screen with the same id starts anew. */
118
+ /**
119
+ * The screen [screenId] owns the id from now on (made), or went while no other node has it (destroyed): what its
120
+ * safe area views said and the marks of rendering at once go, so a later screen with the id starts anew. The
121
+ * listeners stay with their keys, which remove them.
122
+ */
85
123
  fun remove(screenId: String) {
86
- atOnce -= screenId
87
- wrapped -= screenId
88
- laidOut -= screenId
89
- rendered -= screenId
90
- listeners -= screenId
124
+ entries.remove(screenId)
125
+ atOnce.remove(screenId)
91
126
  }
92
127
 
93
128
  /**
@@ -95,10 +130,22 @@ object ScreenReadiness {
95
130
  * "rendered" kept for a screen of the old one would make its new screen with the same id ready at once.
96
131
  */
97
132
  fun reset() {
98
- atOnce.clear()
99
- wrapped.clear()
100
- laidOut.clear()
101
- rendered.clear()
133
+ entries.clear()
102
134
  listeners.clear()
135
+ atOnce.clear()
136
+ }
137
+
138
+ private fun entryOf(screenId: String): Entry = entries.getOrPut(screenId, ::Entry)
139
+
140
+ private fun isListening(screenId: String, key: Any, listener: Listener): Boolean =
141
+ listeners[screenId]?.get(key) === listener
142
+
143
+ // Each listener of the id that accepts the source, as they are now: one that a listener before it removed hears
144
+ // nothing.
145
+ private fun tell(screenId: String, source: Int, signal: (Listener) -> Unit) {
146
+ val now = listeners[screenId]?.entries?.toList() ?: return
147
+ for ((key, listener) in now) {
148
+ if (isListening(screenId, key, listener) && listener.accepts(source)) signal(listener)
149
+ }
103
150
  }
104
151
  }