@apexnative/navigation 1.0.0-alpha.5 → 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 (182) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +14 -12
  4. package/android/CORE.md +9 -6
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +36 -5
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  7. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  8. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +0 -2
  9. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  10. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +12 -49
  11. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  12. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -6
  13. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  14. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +9 -7
  15. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  16. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +13 -10
  17. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  18. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +7 -8
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -0
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +4 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  33. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  34. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  35. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  36. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  37. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  38. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  46. package/docs-md/architecture/transitions.md +4 -2
  47. package/docs-md/containers/modals.md +2 -0
  48. package/docs-md/containers/overlays.md +1 -1
  49. package/docs-md/containers/side-menu.md +1 -1
  50. package/docs-md/containers/tabs.md +2 -0
  51. package/docs-md/flows/bottom-sheets.md +1 -1
  52. package/docs-md/fundamentals/starting-the-app.md +1 -1
  53. package/docs-md/help/glossary.md +2 -2
  54. package/docs-md/help/troubleshooting.md +21 -0
  55. package/docs-md/index.json +12 -0
  56. package/docs-md/migration/from-expo-router.md +1 -1
  57. package/docs-md/migration/from-react-native-navigation.md +1 -1
  58. package/docs-md/migration/from-react-navigation.md +2 -2
  59. package/docs-md/screen-ui/back-and-gestures.md +14 -0
  60. package/docs-md/screen-ui/bar-buttons.md +4 -4
  61. package/docs-md/screen-ui/search.md +1 -1
  62. package/docs-md/screen-ui/system-ui.md +16 -11
  63. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  64. package/docs-md/screen-ui/top-bar.md +8 -4
  65. package/ios/Animations/ANNFrameDriver.mm +2 -0
  66. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  67. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  68. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  69. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  70. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  71. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  72. package/ios/CLAUDE.md +17 -15
  73. package/ios/CORE.md +24 -20
  74. package/ios/Contract/ANNCommandRunner.h +8 -7
  75. package/ios/Contract/ANNCommandRunner.mm +21 -27
  76. package/ios/Contract/ANNLayoutNode.h +3 -3
  77. package/ios/Contract/ANNLayoutReader.h +3 -3
  78. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  79. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  80. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  81. package/ios/Core/ApexNavigation+Internal.h +1 -1
  82. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  83. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  84. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  87. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  88. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  89. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  90. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  91. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  92. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  93. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  94. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  95. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  96. package/ios/Layouts/Stack/ANNPreviewPush.mm +57 -5
  97. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  98. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  100. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  103. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  104. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  105. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  106. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  107. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  108. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  109. package/ios/Options/ANNOptions.h +18 -0
  110. package/ios/Options/ANNOptions.mm +112 -1
  111. package/ios/Options/ANNOptionsReader.mm +110 -0
  112. package/ios/Options/Params/ANNParam.h +15 -0
  113. package/ios/Options/Params/ANNParam.mm +61 -0
  114. package/ios/Options/Params/Color.h +3 -0
  115. package/ios/Options/Params/Color.mm +21 -0
  116. package/ios/Options/Params/Image.h +4 -0
  117. package/ios/Options/Params/Image.mm +20 -0
  118. package/ios/React/ANNReactView.mm +6 -25
  119. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  120. package/ios/React/ANNSurfaceHostingView.h +6 -0
  121. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  122. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  123. package/ios/TopBar/ANNBarAppearance.h +2 -1
  124. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  125. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  126. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  127. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  128. package/ios/TopBar/ANNBarButtons.h +5 -3
  129. package/ios/TopBar/ANNBarButtons.mm +186 -17
  130. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  131. package/ios/TopBar/ANNMainList.h +3 -0
  132. package/ios/TopBar/ANNMainList.mm +4 -0
  133. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  134. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  135. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  136. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  137. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  138. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  139. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  140. package/lib/module/ARCHITECTURE.md +8 -4
  141. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  142. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  143. package/lib/module/api/options/OptionsChain.js +2 -2
  144. package/lib/module/engine/commands.js +21 -1
  145. package/lib/module/engine/commands.js.map +1 -1
  146. package/lib/module/engine/screens.js +29 -4
  147. package/lib/module/engine/screens.js.map +1 -1
  148. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  149. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  150. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  151. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  152. package/lib/typescript/engine/commands.d.ts.map +1 -1
  153. package/lib/typescript/engine/screens.d.ts +20 -6
  154. package/lib/typescript/engine/screens.d.ts.map +1 -1
  155. package/lib/typescript/interfaces/options/animations.d.ts +12 -7
  156. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  157. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  158. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  159. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  160. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  161. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  162. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  163. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  164. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  165. package/package.json +1 -1
  166. package/src/ARCHITECTURE.md +8 -4
  167. package/src/api/app/createNavigation.ts +3 -3
  168. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  169. package/src/api/options/OptionsChain.ts +2 -2
  170. package/src/engine/commands.ts +30 -6
  171. package/src/engine/screens.tsx +29 -8
  172. package/src/interfaces/options/animations.ts +12 -7
  173. package/src/interfaces/options/bottomTabs.ts +18 -5
  174. package/src/interfaces/options/buttons.ts +8 -8
  175. package/src/interfaces/options/system.ts +8 -5
  176. package/src/interfaces/options/toolbar.ts +2 -2
  177. package/src/interfaces/options/topBar.ts +32 -29
  178. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  179. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  180. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  181. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  182. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-alpha.6](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2026-10-25)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * leave the bottom edge of a screen free by default ([#49](https://github.com/apexnative/navigation/issues/49))
9
+
10
+ ### Features
11
+
12
+ * a sheet's back preview on android, lighter tabs and top bar, the compatibility mode on ios 27 ([#46](https://github.com/apexnative/navigation/issues/46)) ([24d333d](https://github.com/apexnative/navigation/commit/24d333dd43fc5e056786f22dc8d7f1a0ff82a1ad))
13
+ * leave the bottom edge of a screen free by default ([#49](https://github.com/apexnative/navigation/issues/49)) ([b6bb165](https://github.com/apexnative/navigation/commit/b6bb165152114fb2886bd3c4ee55560c418f0cbe))
14
+ * **tabs:** choose whether a tab switch animates with bottomTabs.switchAnimation ([#52](https://github.com/apexnative/navigation/issues/52)) ([c6185b6](https://github.com/apexnative/navigation/commit/c6185b6b42574cff671def1ebdf8aa4f5e4be681))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * an explicit top edge stays free under a large title ([#53](https://github.com/apexnative/navigation/issues/53)) ([188a89e](https://github.com/apexnative/navigation/commit/188a89e8d160c071f91cc010ed43ccc0683e12b3))
20
+ * shared elements fly turned and scaled views the same way on ios and android ([#50](https://github.com/apexnative/navigation/issues/50)) ([bf63ab6](https://github.com/apexnative/navigation/commit/bf63ab624d33ff9295af297ce6506206b1bfa0e6))
21
+ * the custom tab row moves with its tabs on android, and tab cells get selected and badge ([#51](https://github.com/apexnative/navigation/issues/51)) ([fd8f41d](https://github.com/apexnative/navigation/commit/fd8f41d2efd108741672220c54253a86704929a5))
22
+
23
+
24
+ ### Performance Improvements
25
+
26
+ * **ios:** render a preview's screen when a press on its view starts ([#48](https://github.com/apexnative/navigation/issues/48)) ([4a57d1a](https://github.com/apexnative/navigation/commit/4a57d1aee1df7690dbde4c7d08dca2a70773d1f5))
27
+
3
28
  ## [1.0.0-alpha.5](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2026-10-23)
4
29
 
5
30
 
package/README.md CHANGED
@@ -97,7 +97,7 @@ navigation.push('Details', { props: { title: 'Order 42' } });
97
97
  each other: tabs of stacks, a stack in a modal, tabs in a side menu.
98
98
  - **Screen UI as options:** the top bar, bar buttons, search, the toolbar and the floating action button, the status
99
99
  bar and the system bars.
100
- - **No safe area code:** every screen keeps its content clear of the bars, the cutout and the home indicator by itself.
100
+ - **No safe area code:** every screen keeps its top and sides clear of the bars and the cutout by itself; the bottom is free, and a list adds `useSafeAreaInsets().bottom`.
101
101
  - **Native screens:** SwiftUI, UIKit and Compose screens are pushed, presented and put in tabs the same way as React
102
102
  screens.
103
103
  - **One React tree when you want it:** each screen is its own React root by default; with `appProvider` all screens
package/android/CLAUDE.md CHANGED
@@ -22,8 +22,8 @@ top-level folder. Main thread only, unless a line says otherwise.
22
22
  `Variant`, `ThemeColour`); `parsers/` reads them tolerantly from React Native data.
23
23
  - `react/`: the way in and out: `NavigationTurboModule`, `CommandRunner`, `CommandAnswer`, `ErrorTexts`,
24
24
  `OptionsCommands`, `PendingCalls` (a task behind the calls JavaScript made), `ReloadCoordinator`, `ReactGateway`,
25
- `ReactView` (a React root), the stores (`ScreenInsets`, `ScreenRegions`, `ArrangementStates`, `Hinge`, `StartInfo`),
26
- `LaunchLink`, `ScrollEvents`.
25
+ `ReactView` (a React root), the stores (`ScreenEnvironment`: a screen's insets, padding, regions and pair state;
26
+ `Hinge`, `StartInfo`), `ScreenRegions` (measures a screen's regions), `LaunchLink`, `ScrollEvents`.
27
27
  - `react/events/`: `AppearanceDispatcher`, `ScreenAppearance`, `EventEmitter`, `EventContract` (made by
28
28
  `scripts/contract.mjs`), `NavigationState`, `ComponentType`.
29
29
  - `viewcontrollers/viewcontroller/`: `ViewController` (the base of every node), `Presenter`, `IReactView` (what a
@@ -44,7 +44,7 @@ top-level folder. Main thread only, unless a line says otherwise.
44
44
  - `viewcontrollers/sidemenu/`: `SideMenuController`, `SideMenuMotion`, `SideMenuPresenter`, `SideMenuRules`.
45
45
  - `viewcontrollers/splitview/`, `arrangement/`: the split view and the pair (modules, plus the core's
46
46
  `SplitViewPresenter`).
47
- - `viewcontrollers/modal/`, `overlay/`: `ModalCoordinator`, `ModalRecord`, `ModalViews`, `ModalSheet`;
47
+ - `viewcontrollers/modal/`, `overlay/`: `ModalCoordinator`, `ModalRecord`, `ModalViews`, `ModalSheet`, `SheetBack`;
48
48
  `OverlayCoordinator`, `OverlayRecord`, `OverlayViews`, `OverlayTouches`.
49
49
  - `views/`: Android views and animators, no rules: `component/`, `externalcomponent/`, `stack/` (`StackLayout`,
50
50
  `topbar/`, `fab/`), `bottomtabs/`, `sidemenu/`, `animations/` (curves, `SideAnimator`, `AnimationRules`), `element/`
@@ -78,7 +78,8 @@ top-level folder. Main thread only, unless a line says otherwise.
78
78
  `StackViews.kt`, `StackTransitions.kt`, `PreparedPushes.kt`; `views/stack/StackLayout.kt`, `ScreenEdge.kt` (the edge
79
79
  a moving screen extends); back: `NavigationActivity.kt`, `viewcontrollers/navigator/BackCallbackState.kt`; the
80
80
  preview of a pop during the back gesture (Android 14+): `StackController.BackGesture`, `BackPreview.kt`,
81
- `stackPoppedByBack` of the layouts.
81
+ `stackPoppedByBack` of the layouts; of a sheet's dismissal: `viewcontrollers/modal/SheetBack.kt`,
82
+ `ModalCoordinator.backTargetSheet`, `ModalSheet.BackPreview`.
82
83
  - Stack FAB: `viewcontrollers/stack/fab/FabPresenter.kt`, `FabPlan.kt`, `FabPlacement.kt`; `views/stack/fab/`.
83
84
  - Top bar (title, buttons, menus, large title): `viewcontrollers/stack/topbar/TopBarPresenter.kt`, `TopBarRoom.kt`,
84
85
  `TopBarVisibility.kt`, `BarComponents.kt`, `ButtonPlaces.kt`, `LargeTitleCollapse.kt`, `BarRowTransition.kt`;
@@ -87,7 +88,7 @@ top-level folder. Main thread only, unless a line says otherwise.
87
88
  `views/stack/topbar/search/`.
88
89
  - Tabs: `viewcontrollers/bottomtabs/BottomTabsController.kt`, `BottomTabsPresenter.kt`, `TabSelection.kt`,
89
90
  `TabItems.kt`, `TabsRoom.kt`; `views/bottomtabs/`; `customrow/BottomTabsCustomRowOptions.kt`,
90
- `viewcontrollers/bottomtabs/CustomTabRowPlacer.kt`.
91
+ `viewcontrollers/bottomtabs/CustomTabRowGeometry.kt`.
91
92
  - Modals and sheets: `viewcontrollers/modal/ModalCoordinator.kt`, `ModalRecord.kt`, `ModalViews.kt`, `ModalSheet.kt`,
92
93
  `ModalAnimations.kt`; `options/SheetOptions.kt`.
93
94
  - Overlays: `viewcontrollers/overlay/OverlayCoordinator.kt`, `OverlayViews.kt`, `OverlayTouches.kt`.
@@ -96,12 +97,13 @@ top-level folder. Main thread only, unless a line says otherwise.
96
97
  - Split view: `viewcontrollers/splitview/SplitViewController.kt`, `SplitViewLayout.kt`, `SplitPanes.kt`,
97
98
  `SplitViewPresenter.kt`; `StackCommands.showInColumn`.
98
99
  - Pair (arrangement): `viewcontrollers/arrangement/ArrangementController.kt`, `ArrangementPlanner.kt`,
99
- `ArrangementLayout.kt`, `ArrangementDefinition.kt`; `react/ArrangementStates.kt`.
100
+ `ArrangementLayout.kt`, `ArrangementDefinition.kt`; the pair's state in `react/ScreenEnvironment.kt`.
100
101
  - Preview, key commands: iOS only. The options are parsed and merged (`options/PreviewOptions.kt`,
101
102
  `options/KeyCommandOptions.kt`) and not applied; `dismissPreview` does nothing.
102
103
  - Animations and transitions: `views/animations/` (`AnimationRules`, `Curves`, `SideAnimator`, `ValueInterpolation`,
103
- `FadeThrough`, `SystemTransition`); `views/element/` (`ElementTransitions`, `TransitionLayer`, `TargetsWait`,
104
- `animators/`); `options/StackAnimationOptions.kt`, `ViewAnimationOptions.kt`.
104
+ `FadeThrough`, `SystemTransition`, `AnimationLayer`: the hardware layer of a fade); `views/element/`
105
+ (`ElementTransitions`, `TransitionLayer`, `TargetsWait`, `animators/`); `options/StackAnimationOptions.kt`,
106
+ `ViewAnimationOptions.kt`.
105
107
  - System bars: `system/SystemUiOwner.kt`, `SystemUiLayers.kt`, `SystemUiRules.kt`, `SystemUiWindow.kt`,
106
108
  `AppDirection.kt`; `viewcontrollers/parent/ContainerBackground.kt`.
107
109
  - Events and state: `react/events/AppearanceDispatcher.kt`, `ScreenAppearance.kt`, `EventEmitter.kt`,
@@ -111,21 +113,21 @@ top-level folder. Main thread only, unless a line says otherwise.
111
113
  - Error texts and answers: `react/ErrorTexts.kt` and `react/CommandContract.kt`, made by `scripts/contract.mjs` from
112
114
  `src/interfaces/contract/errors.json` and `commands.json`.
113
115
  - Links: `react/LaunchLink.kt`, `NavigationActivity.kt` (`onNewIntent`); `react/StartInfo.kt`.
114
- - Hinge and regions: `react/Hinge.kt`, `react/ScreenRegions.kt`, `react/ScreenInsets.kt`, `utils/WindowFolds.kt`.
116
+ - Hinge and regions: `react/Hinge.kt`, `react/ScreenRegions.kt`, `react/ScreenEnvironment.kt`, `utils/WindowFolds.kt`.
115
117
  - Native screens: `nativescreen/NativeScreenContext.kt`, `NativeScreenCalls.kt`, `NativeScreenCreator.kt`;
116
118
  `viewcontrollers/externalcomponent/`.
117
119
  - Slots: `slots/SlotHost.kt`, `SlotView.kt`, `SlotScreenView.kt`, `SlotContainers.kt` (containers and slot views by
118
120
  slot id: a change goes straight to the views of its id, `SlotViews.kt`), `SlotViewManager.kt`.
119
121
  - Safe area: `safearea/SafeAreaView.kt`, `SafeAreaViewManager.kt`; `viewcontrollers/component/ScreenReadiness.kt`;
120
- `react/ScreenInsets.kt`.
122
+ `react/ScreenEnvironment.kt`.
121
123
  - The C++ of `ANNSlot`, `ANNSafeAreaView`: `android/src/main/jni/CMakeLists.txt`, `ApexNavigationSpec.h`; sources in
122
124
  `ios/cpp/`.
123
125
 
124
126
  ## Threads
125
127
 
126
128
  - **JS thread.** The sync getters `getScreenInsets`, `getScreenRegions`, `getArrangementState`, `getHinge`,
127
- `getStartInfo` read stores (`ConcurrentHashMap` in `ScreenInsets`, `ScreenRegions`, `ArrangementStates`; `@Volatile`
128
- in `Hinge`). They never touch a node.
129
+ `getStartInfo` read stores (one `ConcurrentHashMap` of immutable records in `ScreenEnvironment`; `@Volatile` in
130
+ `Hinge`). They never touch a node.
129
131
  - **Module thread.** The `check` of every command with a promise: `LayoutReader` and, through it, `OptionsReader` (also
130
132
  for `setDefaultOptions`, `mergeOptions` and the options of `dismissModal`, in the runner's `prepare` step);
131
133
  `ReadableJson` for native-screen props. The registry of native screens is read under its lock. Images are not loaded
package/android/CORE.md CHANGED
@@ -10,7 +10,7 @@ otherwise.
10
10
  ```
11
11
  JS ─ ANNTurboModule (codegen NativeANNTurboModuleSpec)
12
12
  │ sync getters answer on the JS thread from stores the main thread fills
13
- │ (ScreenInsets, ScreenRegions, ArrangementStates, Hinge, StartInfo)
13
+ │ (ScreenEnvironment, Hinge, StartInfo)
14
14
  ▼ module thread
15
15
  react/NavigationTurboModule one override per method
16
16
  │ layout/LayoutReader ► options/OptionsReader the whole layout read and checked, its options read
@@ -188,7 +188,7 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
188
188
  - **Room for the bars.** A screen's room at the top and bottom is `ScreenFrame` with `ScreenSafeArea` and the
189
189
  `ParentController` room hooks (`getTopInset`, `keepsStatusBarRoomOf`, …). A screen in a pair forwards its room to
190
190
  the pair (`ScreenFrame.applyRoom`), and the pair's `getTopInset` uses the screens' rule (`ScreenFrame.topRoomOf`)
191
- with the options it shows with. A view's place in the window for the safe area (the React root, the stack's header
191
+ with the options it shows with. The bottom edge is not padded by default (only `'bottom'` in the edges pads it); `roomPadding` still shrinks the view for an opaque navigation bar and the keyboard. A view's place in the window for the safe area (the React root, the stack's header
192
192
  bottom in `headerBottomOf`, the tabs' top in `tabsTopOf`) is `RestPlace`: from layout, without translations, a sheet
193
193
  at rest.
194
194
  - **A container's own move.** A container that moves children in or out of the window without a command (the split
@@ -209,9 +209,9 @@ Modules are the feature classes that plug into the core through its contract (th
209
209
  (`SlotHost`, `SlotView`, `SlotContainers`), safe area (`SafeAreaView`), native screens (`nativescreen/`), splash
210
210
  (`splash/`), animations (`FadeThrough`, `SystemTransition`, `TransitionTargets`, three element animators), utils
211
211
  (`MaterialTheme`, `NavigationLog`, `awaitBarComponents`, `whenDrawn`).
212
- - **Stores and singletons:** regions (`Hinge`, `ScreenInsets`, `ScreenRegions`, `WindowFolds`), pair state
213
- (`ArrangementStates`), links (`LaunchLink`, `StartInfo`), events (`NavigationState`), commands
214
- (`BackCallbackState`), stack (`PreparedFrame`).
212
+ - **Stores and singletons:** a screen's environment (`ScreenEnvironment`: its insets and padding, its regions as
213
+ `ScreenRegions` measures them, its pair state), the window (`Hinge`, `WindowFolds`), links (`LaunchLink`,
214
+ `StartInfo`), events (`NavigationState`), commands (`BackCallbackState`), stack (`PreparedFrame`).
215
215
 
216
216
  How they plug in:
217
217
 
@@ -235,5 +235,8 @@ How they plug in:
235
235
  - **Core code in a module's folder:** `viewcontrollers/splitview/SplitViewPresenter.kt`, `slots/SlotScreenView.kt`,
236
236
  and in `views/animations/` and `views/element/` everything except the files named above.
237
237
  - **Stores** are written on the main thread by the core and read from any thread; they are cleared with their nodes and on
238
- reload. Their events, and the search's, go through the emitter of the JavaScript the screen was made for
238
+ reload. A screen's environment is one immutable record per id in `ScreenEnvironment` (insets for JavaScript, padding,
239
+ regions, pair state), replaced at once; the screen's node clears it with one `remove` when it is destroyed, unless a
240
+ later node has its id; the padding listeners of its safe area views stay until the views stop. Their events, and
241
+ the search's, go through the emitter of the JavaScript the screen was made for
239
242
  (`EventEmitter.of(node.jsGeneration)`); the hinge keeps the `EventEmitter.current()` of the call that turned it on.
@@ -35,6 +35,7 @@ import dev.apexnative.navigation.splash.SplashTheme
35
35
  import dev.apexnative.navigation.splash.SplashView
36
36
  import dev.apexnative.navigation.system.SystemUiWindow
37
37
  import dev.apexnative.navigation.utils.MaterialTheme
38
+ import dev.apexnative.navigation.viewcontrollers.modal.SheetBack
38
39
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackCheck
39
40
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
40
41
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
@@ -72,23 +73,33 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
72
73
  // Enabled while back has something to do in the app (BackCallbackState); otherwise the system does its own back.
73
74
  // From Android 14, where the app hears the progress of the back gesture, a gesture that would pop a stack with the
74
75
  // default transition previews the pop as the system previews back between activities (`StackController
75
- // .BackGesture`); JavaScript's listeners hear back first, so with them there is no preview.
76
+ // .BackGesture`), and one that would dismiss a sheet previews its hide as Material's sheets do (`SheetBack`), its
77
+ // release closing it as a drag down does; JavaScript's listeners hear back first, so with them there is no preview.
76
78
  private val backCallback = object : OnBackPressedCallback(false) {
77
79
  override fun handleOnBackStarted(backEvent: BackEventCompat) {
78
80
  backGesture = null
79
- navigator?.let { backCheck?.atBack(it) }
81
+ sheetGesture = null
82
+ val navigator = navigator ?: return
83
+ backCheck?.atBack(navigator)
80
84
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || BackCallbackState.hasJsListeners) return
81
- val stack = navigator?.stackPoppedByBack() ?: return
82
- backGesture = stack.startBackGesture(backEvent.swipeEdge == BackEventCompat.EDGE_LEFT, backEvent.touchY)
85
+ val stack = navigator.stackPoppedByBack()
86
+ if (stack != null) {
87
+ backGesture = stack.startBackGesture(backEvent.swipeEdge == BackEventCompat.EDGE_LEFT, backEvent.touchY)
88
+ return
89
+ }
90
+ sheetGesture = navigator.sheetDismissedByBack()?.startBackPreview(backEvent)
83
91
  }
84
92
 
85
93
  override fun handleOnBackProgressed(backEvent: BackEventCompat) {
86
94
  backGesture?.progress(backEvent.progress, backEvent.touchY)
95
+ sheetGesture?.progress(backEvent)
87
96
  }
88
97
 
89
98
  override fun handleOnBackCancelled() {
90
99
  backGesture?.cancel()
91
100
  backGesture = null
101
+ sheetGesture?.cancel()
102
+ sheetGesture = null
92
103
  navigator?.let(BackCallbackState::update)
93
104
  }
94
105
 
@@ -96,13 +107,33 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
96
107
  navigator?.let { backCheck?.atBack(it) }
97
108
  val gesture = backGesture
98
109
  backGesture = null
99
- if (gesture?.commit() == true) BackCallbackState.update() else passBack()
110
+ val sheet = sheetGesture
111
+ sheetGesture = null
112
+ when {
113
+ gesture?.commit() == true -> BackCallbackState.update()
114
+ sheet != null && commitSheet(sheet) -> BackCallbackState.update()
115
+ else -> passBack()
116
+ }
100
117
  }
101
118
  }
102
119
 
103
120
  // The back gesture that previews a pop now; none.
104
121
  private var backGesture: StackController.BackGesture? = null
105
122
 
123
+ // The back gesture that previews a sheet's hide now; none.
124
+ private var sheetGesture: SheetBack.Gesture? = null
125
+
126
+ // A sheet's preview released: the sheet goes as the user's drag down closes it, and back doesn't go on (no
127
+ // `passBack`, which would dismiss it a second time). A command that changed what back does since the gesture
128
+ // started (a push in the sheet, a modal over it, a hold) leaves it to the usual back, the sheet back in place.
129
+ private fun commitSheet(gesture: SheetBack.Gesture): Boolean {
130
+ if (navigator?.sheetDismissedByBack() !== gesture.sheet) {
131
+ gesture.cancel()
132
+ return false
133
+ }
134
+ return gesture.commit()
135
+ }
136
+
106
137
  // Debug builds only: the check that what back does never changes without its trigger (BackCallbackState), sampled
107
138
  // before frames and at back events. It only logs; release builds have neither the check nor the listener.
108
139
  private val backCheck = if (BuildConfig.DEBUG) BackCallbackCheck() else null
@@ -95,6 +95,7 @@ class BottomTabsAccessoryOptions {
95
95
  class BottomTabsOptions {
96
96
  @JvmField var visible: Bool = NullBool()
97
97
  @JvmField var animate: Bool = NullBool()
98
+ @JvmField var switchAnimation: Text = NullText()
98
99
  /** Selects a tab once; a whole number, truncated where it is read. */
99
100
  @JvmField var currentTabIndex: Fraction = NullFraction()
100
101
  /** Selects a tab once. */
@@ -124,18 +125,19 @@ class BottomTabsOptions {
124
125
  @JvmField var minimizeBehavior: Text = NullText()
125
126
  @JvmField var bottomAccessory = BottomTabsAccessoryOptions()
126
127
 
127
- fun hasValue(): Boolean = visible.hasValue() || animate.hasValue() || currentTabIndex.hasValue() ||
128
- currentTabId.hasValue() || testID.hasValue() || accessibilityLabel.hasValue() || moreTestID.hasValue() ||
129
- drawBehind.hasValue() || backgroundColor.hasValue() || tabsAttachMode.hasValue() || barStyle.hasValue() ||
130
- layoutStyle.hasValue() || cornerRadius.hasValue() || bottomMargin.hasValue() || translucent.hasValue() ||
131
- hideShadow.hasValue() || titleDisplayMode.hasValue() || elevation.hasValue() || hideOnScroll.hasValue() ||
132
- borderColor.hasValue() || borderWidth.hasValue() || shadow.hasValue() || customRow.hasValue() ||
133
- sidebar.hasValue() || sidebarLayout.hasValue() || customizationId.hasValue() || minimizeBehavior.hasValue() ||
134
- bottomAccessory.hasValue()
128
+ fun hasValue(): Boolean = visible.hasValue() || animate.hasValue() || switchAnimation.hasValue() ||
129
+ currentTabIndex.hasValue() || currentTabId.hasValue() || testID.hasValue() || accessibilityLabel.hasValue() ||
130
+ moreTestID.hasValue() || drawBehind.hasValue() || backgroundColor.hasValue() || tabsAttachMode.hasValue() ||
131
+ barStyle.hasValue() || layoutStyle.hasValue() || cornerRadius.hasValue() || bottomMargin.hasValue() ||
132
+ translucent.hasValue() || hideShadow.hasValue() || titleDisplayMode.hasValue() || elevation.hasValue() ||
133
+ hideOnScroll.hasValue() || borderColor.hasValue() || borderWidth.hasValue() || shadow.hasValue() ||
134
+ customRow.hasValue() || sidebar.hasValue() || sidebarLayout.hasValue() || customizationId.hasValue() ||
135
+ minimizeBehavior.hasValue() || bottomAccessory.hasValue()
135
136
 
136
137
  fun mergeWith(other: BottomTabsOptions) {
137
138
  if (other.visible.hasValue()) visible = other.visible
138
139
  if (other.animate.hasValue()) animate = other.animate
140
+ if (other.switchAnimation.hasValue()) switchAnimation = other.switchAnimation
139
141
  if (other.currentTabIndex.hasValue()) currentTabIndex = other.currentTabIndex
140
142
  if (other.currentTabId.hasValue()) currentTabId = other.currentTabId
141
143
  if (other.testID.hasValue()) testID = other.testID
@@ -167,6 +169,7 @@ class BottomTabsOptions {
167
169
  fun mergeWithDefault(defaults: BottomTabsOptions) {
168
170
  if (!visible.hasValue()) visible = defaults.visible
169
171
  if (!animate.hasValue()) animate = defaults.animate
172
+ if (!switchAnimation.hasValue()) switchAnimation = defaults.switchAnimation
170
173
  if (!currentTabIndex.hasValue()) currentTabIndex = defaults.currentTabIndex
171
174
  if (!currentTabId.hasValue()) currentTabId = defaults.currentTabId
172
175
  if (!testID.hasValue()) testID = defaults.testID
@@ -198,6 +201,7 @@ class BottomTabsOptions {
198
201
  fun copy() = BottomTabsOptions().also { it.mergeWith(this) }
199
202
 
200
203
  companion object {
204
+ private val SWITCH_ANIMATIONS = setOf("platform", "none")
201
205
  private val TABS_ATTACH_MODES = setOf("together", "afterInitialTab", "onSwitchToTab")
202
206
  private val BAR_STYLES = setOf("default", "black")
203
207
  private val LAYOUT_STYLES = setOf("stretch", "compact")
@@ -212,6 +216,7 @@ class BottomTabsOptions {
212
216
  if (map == null) return options
213
217
  options.visible = BoolParser.parse(map, "visible")
214
218
  options.animate = BoolParser.parse(map, "animate")
219
+ options.switchAnimation = TextParser.parse(map, "switchAnimation", SWITCH_ANIMATIONS)
215
220
  options.currentTabIndex = FractionParser.parse(map, "currentTabIndex")
216
221
  options.currentTabId = TextParser.parse(map, "currentTabId")
217
222
  options.testID = TextParser.parse(map, "testID")
@@ -236,11 +236,11 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
236
236
  check = {},
237
237
  ) { _, answer -> navigator().stackCommands.setColumnShown(componentId, column, shown, answer) }
238
238
 
239
- override fun getArrangementState(componentId: String): WritableMap = ArrangementStates.get(componentId)
239
+ override fun getArrangementState(componentId: String): WritableMap = ScreenEnvironment.getArrangementState(componentId)
240
240
 
241
- override fun getScreenInsets(componentId: String): WritableMap = ScreenInsets.get(componentId)
241
+ override fun getScreenInsets(componentId: String): WritableMap = ScreenEnvironment.getScreenInsets(componentId)
242
242
 
243
- override fun getScreenRegions(componentId: String): WritableMap = ScreenRegions.get(componentId)
243
+ override fun getScreenRegions(componentId: String): WritableMap = ScreenEnvironment.getScreenRegions(componentId)
244
244
 
245
245
  // With the activity the user sees, if there is one; the hinge remembers it and goes on with the next activity
246
246
  // (`Hinge.attach`).
@@ -82,8 +82,6 @@ internal object ReloadCoordinator {
82
82
  UiThread.runOnMain {
83
83
  launches.contextReady(context)
84
84
  NativeScreenCalls.failAll { name, componentId -> ErrorTexts.nativeCallReloaded(name, componentId) }
85
- // Scroll listeners that found no event dispatcher listen now.
86
- ScrollEvents.dispatcherReady()
87
85
  Navigators.all().filter { it.isNavigationResumed }.forEach(::launchIfDue)
88
86
  }
89
87
  }
@@ -0,0 +1,177 @@
1
+ package dev.apexnative.navigation.react
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableArray
5
+ import com.facebook.react.bridge.WritableMap
6
+ import dev.apexnative.navigation.react.events.EventContract
7
+ import dev.apexnative.navigation.react.events.EventEmitter
8
+ import java.util.concurrent.ConcurrentHashMap
9
+
10
+ /**
11
+ * What JavaScript knows of each screen's place, by the screen's id, in one record: its safe area (the insets JavaScript
12
+ * gets and the padding of its safe area view, `useSafeAreaInsets`), its reserved regions (`useReservedRegions`) and,
13
+ * in a pair, its arrangement (`useArrangement`). The core measures them (`ScreenSafeArea`, [ScreenRegions], the pair's
14
+ * plan) on the main thread and keeps them here; JavaScript reads them from its own thread for a screen's first render
15
+ * and gets every change as an event. The record is immutable and replaced at once, so a reader on another thread sees
16
+ * one whole record. When the screen goes, [remove] clears all of it. As on iOS (`ANNScreenEnvironment`).
17
+ */
18
+ internal object ScreenEnvironment {
19
+ /** How much covers each edge of a screen's content, in dp. */
20
+ data class Insets(val top: Double, val right: Double, val bottom: Double, val left: Double)
21
+
22
+ /** A reserved region, in dp from the top left of the screen's content. */
23
+ data class Region(val x: Double, val y: Double, val width: Double, val height: Double)
24
+
25
+ /** Where a screen divides (`division`) and where hardware covers it (`occlusion`). */
26
+ data class Regions(val division: List<Region>, val occlusion: List<Region>) {
27
+ val isEmpty: Boolean get() = division.isEmpty() && occlusion.isEmpty()
28
+ }
29
+
30
+ /** Which screen of its pair a screen is, how the pair lies, whether it lies over the other one and is hidden. */
31
+ data class Arrangement(val placement: String, val layout: String, val front: Boolean, val hidden: Boolean)
32
+
33
+ /** Where the changes JavaScript hears go: the events of the screen's JavaScript, or a test's record. */
34
+ interface Changes {
35
+ fun insetsChanged(screenId: String, insets: Insets)
36
+
37
+ fun regionsChanged(screenId: String, regions: Regions)
38
+
39
+ fun arrangementChanged(screenId: String, arrangement: Arrangement)
40
+ }
41
+
42
+ // One screen's record; null for a fact not known yet (not laid out, outside a pair).
43
+ private data class Record(
44
+ val insets: Insets? = null,
45
+ val padding: Insets? = null,
46
+ val regions: Regions? = null,
47
+ val arrangement: Arrangement? = null,
48
+ )
49
+
50
+ private val NO_REGIONS = Regions(emptyList(), emptyList())
51
+ private val records = ConcurrentHashMap<String, Record>()
52
+ // Main thread only.
53
+ private val paddingListeners = HashMap<String, MutableSet<() -> Unit>>()
54
+
55
+ /**
56
+ * Keeps the screen's [insets] for JavaScript (what still covers its content) and the [padding] of its safe area
57
+ * view. Calls the screen's padding listeners when the padding changed, then tells JavaScript when the insets did.
58
+ * Main thread only.
59
+ */
60
+ fun setInsets(screenId: String, insets: Insets, padding: Insets, changes: Changes) {
61
+ val before = update(screenId) { it.copy(insets = insets, padding = padding) }
62
+ if (before?.padding != padding) paddingListeners[screenId]?.toList()?.forEach { it() }
63
+ if (before?.insets != insets) changes.insetsChanged(screenId, insets)
64
+ }
65
+
66
+ /**
67
+ * Keeps the screen's [regions], and tells JavaScript when they changed; none at first is what JavaScript reads
68
+ * before the first layout, so it isn't told. Main thread only.
69
+ */
70
+ fun setRegions(screenId: String, regions: Regions, changes: Changes) {
71
+ val before = update(screenId) { it.copy(regions = regions) }?.regions
72
+ if (before == regions || (before == null && regions.isEmpty)) return
73
+ changes.regionsChanged(screenId, regions)
74
+ }
75
+
76
+ /** Keeps the screen's [arrangement], and tells JavaScript when it changed and [notify] holds. Main thread only. */
77
+ fun setArrangement(screenId: String, arrangement: Arrangement, notify: Boolean, changes: Changes) {
78
+ val before = update(screenId) { it.copy(arrangement = arrangement) }?.arrangement
79
+ if (before == arrangement || !notify) return
80
+ changes.arrangementChanged(screenId, arrangement)
81
+ }
82
+
83
+ /** The padding of the screen's safe area view; none before the screen was laid out. Any thread. */
84
+ fun padding(screenId: String): Insets? = records[screenId]?.padding
85
+
86
+ /** The insets JavaScript gets; none before the screen was laid out. Any thread. */
87
+ fun insets(screenId: String): Insets? = records[screenId]?.insets
88
+
89
+ /** The screen's reserved regions; none before its first layout. Any thread. */
90
+ fun regions(screenId: String): Regions? = records[screenId]?.regions
91
+
92
+ /** The screen's arrangement; none outside a pair. Any thread. */
93
+ fun arrangement(screenId: String): Arrangement? = records[screenId]?.arrangement
94
+
95
+ /**
96
+ * Calls `listener` when the padding of the screen changes; returns the function that stops it. Main thread only.
97
+ */
98
+ fun listenToPadding(screenId: String, listener: () -> Unit): () -> Unit {
99
+ paddingListeners.getOrPut(screenId) { LinkedHashSet() }.add(listener)
100
+ return {
101
+ paddingListeners[screenId]?.let {
102
+ it.remove(listener)
103
+ if (it.isEmpty()) paddingListeners.remove(screenId)
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Forgets everything kept for the screen. Its padding listeners stay: a live safe area view keeps listening while
110
+ * it keeps the id, and stops itself.
111
+ */
112
+ fun remove(screenId: String) {
113
+ records.remove(screenId)
114
+ }
115
+
116
+ /** `getScreenInsets`: `{ top, right, bottom, left }`, empty before the screen was laid out. Any thread. */
117
+ fun getScreenInsets(screenId: String): WritableMap = insets(screenId)?.let(::toMap) ?: Arguments.createMap()
118
+
119
+ /** `getScreenRegions`: `{ division, occlusion }`, empty lists before the first layout. Any thread. */
120
+ fun getScreenRegions(screenId: String): WritableMap = toMap(regions(screenId) ?: NO_REGIONS)
121
+
122
+ /** `getArrangementState`: `{ placement, layout, front, hidden }`, empty outside a pair. Any thread. */
123
+ fun getArrangementState(screenId: String): WritableMap =
124
+ arrangement(screenId)?.let(::toMap) ?: Arguments.createMap()
125
+
126
+ /** The changes as events through [emitter], the one of the JavaScript the screen was made for. */
127
+ fun changesOf(emitter: EventEmitter): Changes = object : Changes {
128
+ override fun insetsChanged(screenId: String, insets: Insets) =
129
+ emitter.emitScreenInsetsChanged(screenId, toMap(insets))
130
+
131
+ override fun regionsChanged(screenId: String, regions: Regions) =
132
+ emitter.emitScreenRegionsChanged(screenId, toMap(regions))
133
+
134
+ override fun arrangementChanged(screenId: String, arrangement: Arrangement) =
135
+ emitter.emitArrangementChanged(screenId, toMap(arrangement))
136
+ }
137
+
138
+ // Replaces the screen's record at once with [change] of it; returns the record before, null for a new screen.
139
+ private fun update(screenId: String, change: (Record) -> Record): Record? {
140
+ var before: Record? = null
141
+ records.compute(screenId) { _, old ->
142
+ before = old
143
+ change(old ?: Record())
144
+ }
145
+ return before
146
+ }
147
+
148
+ private fun toMap(value: Insets): WritableMap = Arguments.createMap().apply {
149
+ putDouble(EventContract.ScreenInsetsChanged.TOP, value.top)
150
+ putDouble(EventContract.ScreenInsetsChanged.RIGHT, value.right)
151
+ putDouble(EventContract.ScreenInsetsChanged.BOTTOM, value.bottom)
152
+ putDouble(EventContract.ScreenInsetsChanged.LEFT, value.left)
153
+ }
154
+
155
+ private fun toMap(value: Regions): WritableMap = Arguments.createMap().apply {
156
+ putArray(EventContract.ScreenRegionsChanged.DIVISION, toArray(value.division))
157
+ putArray(EventContract.ScreenRegionsChanged.OCCLUSION, toArray(value.occlusion))
158
+ }
159
+
160
+ private fun toArray(regions: List<Region>): WritableArray = Arguments.createArray().apply {
161
+ for (region in regions) {
162
+ pushMap(Arguments.createMap().apply {
163
+ putDouble(EventContract.ScreenRegionRect.X, region.x)
164
+ putDouble(EventContract.ScreenRegionRect.Y, region.y)
165
+ putDouble(EventContract.ScreenRegionRect.WIDTH, region.width)
166
+ putDouble(EventContract.ScreenRegionRect.HEIGHT, region.height)
167
+ })
168
+ }
169
+ }
170
+
171
+ private fun toMap(value: Arrangement): WritableMap = Arguments.createMap().apply {
172
+ putString(EventContract.ArrangementChanged.PLACEMENT, value.placement)
173
+ putString(EventContract.ArrangementChanged.LAYOUT, value.layout)
174
+ putBoolean(EventContract.ArrangementChanged.FRONT, value.front)
175
+ putBoolean(EventContract.ArrangementChanged.HIDDEN, value.hidden)
176
+ }
177
+ }
@@ -3,36 +3,25 @@ package dev.apexnative.navigation.react
3
3
  import android.app.Activity
4
4
  import android.graphics.Rect
5
5
  import androidx.core.view.WindowInsetsCompat
6
- import com.facebook.react.bridge.Arguments
7
- import com.facebook.react.bridge.WritableArray
8
- import com.facebook.react.bridge.WritableMap
9
- import dev.apexnative.navigation.react.events.EventContract
6
+ import dev.apexnative.navigation.react.ScreenEnvironment.Region
7
+ import dev.apexnative.navigation.react.ScreenEnvironment.Regions
10
8
  import dev.apexnative.navigation.react.events.EventEmitter
11
9
  import dev.apexnative.navigation.utils.WindowFolds
12
- import java.util.concurrent.ConcurrentHashMap
13
10
 
14
11
  /**
15
- * The reserved regions of each screen (`useReservedRegions`), in dp from the top left of its content: where it divides
16
- * (a fold the window is divided at, `division`) and where hardware covers it (a fold that hides what is under it, the
17
- * camera cutout: `occlusion`). Only those on the screen, cut to it: a fold at the edge of a screen of a pair is not on
18
- * it. Measured on the main thread as each screen lays out, and again after a change of the window's folds (which lays
19
- * the window out); JavaScript reads them from its own thread for a screen's first render, and gets every change as
20
- * `ANN.ScreenRegionsChanged`.
12
+ * Measures the reserved regions of each screen (`useReservedRegions`), in dp from the top left of its content: where
13
+ * it divides (a fold the window is divided at, `division`) and where hardware covers it (a fold that hides what is
14
+ * under it, the camera cutout: `occlusion`). Only those on the screen, cut to it: a fold at the edge of a screen of a
15
+ * pair is not on it. Measured on the main thread as each screen lays out, and again after a change of the window's
16
+ * folds (which lays the window out); [ScreenEnvironment] keeps them, answers JavaScript's reads and tells it of every
17
+ * change as `ANN.ScreenRegionsChanged`.
21
18
  */
22
19
  internal object ScreenRegions {
23
- data class Region(val x: Double, val y: Double, val width: Double, val height: Double)
24
-
25
- data class Regions(val division: List<Region>, val occlusion: List<Region>) {
26
- val isEmpty: Boolean get() = division.isEmpty() && occlusion.isEmpty()
27
- }
28
-
29
- private val NONE = Regions(emptyList(), emptyList())
30
- private val regions = ConcurrentHashMap<String, Regions>()
31
-
32
20
  /**
33
21
  * Measures the regions of the screen whose content the layout puts at [content] in the window (pixels), with the
34
- * window's [insets] (the camera cutout), and tells JavaScript when they changed, through [emitter], the one of the
35
- * JavaScript the screen was made for: silent once it went. Main thread only.
22
+ * window's [insets] (the camera cutout), and keeps them in [ScreenEnvironment], which tells JavaScript when they
23
+ * changed through [emitter], the one of the JavaScript the screen was made for: silent once it went. Main thread
24
+ * only.
36
25
  */
37
26
  fun update(
38
27
  activity: Activity,
@@ -49,17 +38,7 @@ internal object ScreenRegions {
49
38
  occlusion = (folds.filter { it.occludes }.map { it.bounds } + cutouts)
50
39
  .mapNotNull { onScreen(it, content, density) },
51
40
  )
52
- val previous = regions.put(screenId, value)
53
- // None at first is what JavaScript reads before the first layout.
54
- if (previous == value || (previous == null && value.isEmpty)) return
55
- emitter.emitScreenRegionsChanged(screenId, toMap(value))
56
- }
57
-
58
- /** `{ division, occlusion }`, empty lists for an unknown screen and before its first layout. Any thread. */
59
- fun get(screenId: String): WritableMap = toMap(regions[screenId] ?: NONE)
60
-
61
- fun remove(screenId: String) {
62
- regions.remove(screenId)
41
+ ScreenEnvironment.setRegions(screenId, value, ScreenEnvironment.changesOf(emitter))
63
42
  }
64
43
 
65
44
  // The part of [bounds] (window pixels) on the screen, in dp from its top left; null off it. A fold without width
@@ -74,20 +53,4 @@ internal object ScreenRegions {
74
53
  height = part.height() / density.toDouble(),
75
54
  )
76
55
  }
77
-
78
- private fun toMap(value: Regions): WritableMap = Arguments.createMap().apply {
79
- putArray(EventContract.ScreenRegionsChanged.DIVISION, toArray(value.division))
80
- putArray(EventContract.ScreenRegionsChanged.OCCLUSION, toArray(value.occlusion))
81
- }
82
-
83
- private fun toArray(regions: List<Region>): WritableArray = Arguments.createArray().apply {
84
- for (region in regions) {
85
- pushMap(Arguments.createMap().apply {
86
- putDouble(EventContract.ScreenRegionRect.X, region.x)
87
- putDouble(EventContract.ScreenRegionRect.Y, region.y)
88
- putDouble(EventContract.ScreenRegionRect.WIDTH, region.width)
89
- putDouble(EventContract.ScreenRegionRect.HEIGHT, region.height)
90
- })
91
- }
92
- }
93
56
  }