@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
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
+
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)
29
+
30
+
31
+ ### Features
32
+
33
+ * android back options, a launch screen copy from UILaunchScreen, modal and back fixes ([#43](https://github.com/apexnative/navigation/issues/43)) ([29f311e](https://github.com/apexnative/navigation/commit/29f311e318134f284a0c6279e6c86e15612c9343))
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * **android:** send the state of a tab switch from code at its start ([#44](https://github.com/apexnative/navigation/issues/44)) ([34d03d0](https://github.com/apexnative/navigation/commit/34d03d0214cf09e14329d0bae51e47d46063f054))
39
+ * **hooks:** measure agent folder scopes from the worktree a file is in ([#42](https://github.com/apexnative/navigation/issues/42)) ([d306137](https://github.com/apexnative/navigation/commit/d306137534d3ba551fab8a7346c7ad7300c0faf7))
40
+
3
41
  ## [1.0.0-alpha.4](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2026-10-23)
4
42
 
5
43
 
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,10 +22,10 @@ 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
- `scripts/contract.mjs`), `NavigationStateReporter`, `ComponentType`.
28
+ `scripts/contract.mjs`), `NavigationState`, `ComponentType`.
29
29
  - `viewcontrollers/viewcontroller/`: `ViewController` (the base of every node), `Presenter`, `IReactView` (what a
30
30
  screen shows), `ScrollEventListener`.
31
31
  - `viewcontrollers/child/`, `parent/`: `ChildController` and the registry; `ParentController` (the base of layouts),
@@ -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,36 +97,37 @@ 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`,
108
- `NavigationStateReporter.kt`; `react/CommandAnswer.kt`.
110
+ `NavigationState.kt`; `react/CommandAnswer.kt`.
109
111
  - Options (parsing, merging): `options/Options.kt`, `OptionsReader.kt`, `OptionsCache.kt`, `DefaultOptions.kt`,
110
112
  `params/`, `parsers/`; `viewcontrollers/viewcontroller/ViewController.kt` (resolution).
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
@@ -172,6 +174,9 @@ top-level folder. Main thread only, unless a line says otherwise.
172
174
  `cancel` it on every exit.
173
175
  - A layer that waits to show must be a `WaitingLayer` and leave on every exit (show, supersede, destroy, root gone).
174
176
  - Never resolve a promise yourself: only `CommandAnswer` knows whether its JavaScript went and sends the state first.
177
+ - An event of a screen goes to the JavaScript the screen was made for: `EventEmitter.of(node.jsGeneration)`, or an
178
+ emitter kept from then (it goes silent after a reload). React Native keeps one context across a reload, so a sender
179
+ taken from the context at event time tells the new JavaScript about a screen it never had.
175
180
  - A signal that comes inside a layout pass or a view's attach must not move views there; the stack posts readiness
176
181
  (`deliver`).
177
182
  - A step that runs later (a timer, a readiness signal, an animator's end, a user callback) runs under no runner: it
@@ -206,6 +211,10 @@ top-level folder. Main thread only, unless a line says otherwise.
206
211
  - The back gesture's preview shows the screen under the top outside any operation of the queue: it may only reveal
207
212
  it and put it back, and its release goes through `StackQueue.cut(gesture = true)`. A command that comes meanwhile
208
213
  stops it through `StackDisplay.stop`, like any transition it takes over.
214
+ - What back does (`Navigator.canHandleBack`, `keepsBackFromSystem`) is computed on events, not before each frame: a
215
+ new path that changes it without a command (a gesture, an animator's end, a layout's switch) calls
216
+ `BackCallbackState.update`, or the system's back leaves the app where it should go back. Debug builds log a missed
217
+ one (`BackCallbackCheck`).
209
218
  - Without edge to edge the window resizes for the keyboard by the manifest's soft input mode, so
210
219
  `layout.adjustResize: false` goes through the system UI owner's keyboard group (`SystemUiWindow.setKeyboardCovers`),
211
220
  not through the screen's padding.
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
@@ -31,7 +31,7 @@ coordinators: RootCoordinator · StackQueue (one per stack) · ModalCoordinator
31
31
  presenters: ComponentPresenter · StackPresenter + TopBarPresenter · BottomTabsPresenter · SideMenuPresenter ·
32
32
  SplitViewPresenter · system/SystemUiOwner (status bar, navigation bar, orientation, keyboard)
33
33
  ▼
34
- react/events/AppearanceDispatcher ─► NavigationStateReporter ─► EventEmitter ─► ANN.* events ─► JS
34
+ react/events/AppearanceDispatcher ─► NavigationState ─► EventEmitter ─► ANN.* events ─► JS
35
35
  ```
36
36
 
37
37
  - **Way in.** `NavigationTurboModule` only turns a method into a `CommandRunner` call; it holds no navigation logic. It
@@ -84,11 +84,12 @@ react/events/AppearanceDispatcher ─► NavigationStateReporter ─► EventEmi
84
84
  `AppearanceDispatcher.beginTransition`: **`ComponentWillAppear`** of Profile. The transition runs
85
85
  (`StackTransitions`, `SystemTransition`, or the screens' own animation, `AnimationRules.stackIsOwn`).
86
86
  8. **End.** `finishPush` calls `AppearanceDispatcher.endTransition`: **`ComponentDidDisappear`** of Home, then
87
- **`ComponentDidAppear`** of Profile, then `reportState` (**`NavigationStateChanged`**, only if it changed), then the
88
- transition's own events (none for a push), then `CommandAnswer.resolve` with Profile's id.
89
- 9. **Answer.** `resolve` sends the state once more if it changed (`AnswerEvents.beforeAnswer`), resolves the promise,
90
- then sends **`CommandCompleted`** (`CommandContract.COMPLETED`). After the task, `BackCallbackState.update` recomputes what
91
- back does.
87
+ **`ComponentDidAppear`** of Profile, then the answer's one state step (`CommandAnswer.reportStateNow`:
88
+ **`NavigationStateChanged`**, only if it changed), then the transition's own events (none for a push), then
89
+ `CommandAnswer.resolve` with Profile's id.
90
+ 9. **Answer.** `resolve` sends the state only when no transition sent it (`reportStateNow` runs once per answer: a
91
+ `setRoot` with layout modals sends it here), resolves the promise, then sends **`CommandCompleted`**
92
+ (`CommandContract.COMPLETED`). After the task, `BackCallbackState.update` recomputes what back does.
92
93
 
93
94
  The events go from the main thread, synchronously, through `EventEmitter.current()`; the promise is answered once.
94
95
 
@@ -127,13 +128,16 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
127
128
  `beginTransition` starts the appearances; `endTransition` runs what left, then what came, then the state, then the
128
129
  transition's events, then the answer. Events asked before a screen's content is ready are held in its
129
130
  `ScreenAppearance` (until the content mounts or `HOLD_LIMIT_MILLIS`).
130
- - **Navigation state.** `AppearanceDispatcher.state` (`StateUpdates`) is the one way to ask for it: `StateHold`
131
- coalesces the asks to one posted update per pass of the main loop and keeps it while held; the update calls
132
- `NavigationStateReporter.updateNow`. `updateNow` also runs before every answer of a command that changes what shows
133
- (`CommandAnswer`) and is never held. It reads the navigator through `Navigators.current()`, and `stateOf` finds the
131
+ - **Navigation state.** `NavigationState` owns it: the last state sent, the walk (`stateOf`), the posted update and its
132
+ holds (`NavigationState.Updates`). `AppearanceDispatcher.state` (`StateUpdates`) is the one way to ask for it:
133
+ `setNeedsUpdate` coalesces the asks to one posted update per pass of the main loop and keeps it while held; the
134
+ update calls `updateNow`. A command's answer sends it once, at once and never held (`CommandAnswer.reportStateNow`):
135
+ at the end of its transition, else right before its promise for the commands that change what shows; nothing for a
136
+ JavaScript that went. It reads the navigator through `Navigators.current()`, and `stateOf` finds the
134
137
  `tab` as the nearest tabs of the visible screen (the last tabs met walking down from the visible layout). A `setRoot`
135
138
  with layout modals or overlays takes `AppearanceDispatcher.StateUpdates.hold()` and releases it right before its
136
- answer, so JavaScript hears one state; every hold has one `release()`.
139
+ answer, so JavaScript hears one state; every hold has one `release()`. The holds go when the JavaScript goes
140
+ (`NavigationState.javaScriptWent`), so one left by its `setRoot` never holds the next JavaScript's updates.
137
141
  - **Answer.** `CommandAnswer`: resolves or rejects once, and nothing once the JavaScript went. The id a stack entry
138
142
  answers with is one rule, `StackController.answerIdOf` (the queue asks it through `StackQueue.Host.answerIdOf` with
139
143
  an `AnswerKind`): `push` answers the entry's own id (a pair's), every other command the screen it stands for (a
@@ -141,7 +145,11 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
141
145
  - **Stack queue.** `StackQueue`: every command, back and return of a container goes through it; it keeps `entries`
142
146
  (what JavaScript sees) and `shown` (what the container shows) and answers once.
143
147
  - **Screen readiness.** `ScreenReadiness` (by id; written by `SafeAreaView`) and `ContentReadiness` (the `Readiness`
144
- the waits ask, through `IReactView.isReady`).
148
+ the waits ask, through `IReactView.isReady`). As on iOS, the signals are kept (a listener that comes later hears them
149
+ at once), an id has a listener per key (several roots of one bar component), and each signal carries the React
150
+ surface it came from: a `ReactView` takes only its own surface's, so an old root with the same id never makes a new
151
+ one ready. An id renders at once while a root marks it (`ReactView`'s `rendersAtOnce`); the last one takes the id's
152
+ signals with it. A screen's id is cleared as the screen is made and destroyed, everything on reload.
145
153
  - **Render wait.** `RenderWait`: the one wait for screens to render, used by `RootCoordinator`, `ModalCoordinator`,
146
154
  `OverlayCoordinator` and `StackQueue`; `RenderWait.waitedScreens` walks `ParentController.waitedChildren()` at every
147
155
  level. What differs between users are values (limit, bar phase, unknown-content rule), never steps.
@@ -171,7 +179,8 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
171
179
  - **Back.** `NavigationActivity.passBack` → `ReactGateway.onBackPressed` (JavaScript's `BackHandler`) →
172
180
  `invokeDefaultOnBackPressed` → `Navigator.handleBack` → `ModalCoordinator.handleBack` → the root's
173
181
  `ParentController.handleBack` → `StackController.handleBack` → `StackQueue.cut`. `BackCallbackState` keeps the
174
- system callback enabled only while back has something to do.
182
+ system callback enabled only while back has something to do, computed again on the events that change it (its KDoc
183
+ lists them), never before each frame; debug builds check it (`BackCallbackCheck`).
175
184
  - **Error texts.** `ErrorTexts`: one function per text, the same words as iOS, made with iOS's from
176
185
  `src/interfaces/contract/errors.json` (`scripts/contract.mjs`). JavaScript adds `[navigation] <command>:`.
177
186
  - **Event names and keys.** `EventContract`, made from the event types of `src/interfaces/`; `EventEmitter` and the
@@ -179,7 +188,7 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
179
188
  - **Room for the bars.** A screen's room at the top and bottom is `ScreenFrame` with `ScreenSafeArea` and the
180
189
  `ParentController` room hooks (`getTopInset`, `keepsStatusBarRoomOf`, …). A screen in a pair forwards its room to
181
190
  the pair (`ScreenFrame.applyRoom`), and the pair's `getTopInset` uses the screens' rule (`ScreenFrame.topRoomOf`)
182
- 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
183
192
  bottom in `headerBottomOf`, the tabs' top in `tabsTopOf`) is `RestPlace`: from layout, without translations, a sheet
184
193
  at rest.
185
194
  - **A container's own move.** A container that moves children in or out of the window without a command (the split
@@ -200,9 +209,9 @@ Modules are the feature classes that plug into the core through its contract (th
200
209
  (`SlotHost`, `SlotView`, `SlotContainers`), safe area (`SafeAreaView`), native screens (`nativescreen/`), splash
201
210
  (`splash/`), animations (`FadeThrough`, `SystemTransition`, `TransitionTargets`, three element animators), utils
202
211
  (`MaterialTheme`, `NavigationLog`, `awaitBarComponents`, `whenDrawn`).
203
- - **Stores and singletons:** regions (`Hinge`, `ScreenInsets`, `ScreenRegions`, `WindowFolds`), pair state
204
- (`ArrangementStates`), links (`LaunchLink`, `StartInfo`), events (`NavigationStateReporter`), commands
205
- (`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`).
206
215
 
207
216
  How they plug in:
208
217
 
@@ -226,4 +235,8 @@ How they plug in:
226
235
  - **Core code in a module's folder:** `viewcontrollers/splitview/SplitViewPresenter.kt`, `slots/SlotScreenView.kt`,
227
236
  and in `views/animations/` and `views/element/` everything except the files named above.
228
237
  - **Stores** are written on the main thread by the core and read from any thread; they are cleared with their nodes and on
229
- reload.
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
242
+ (`EventEmitter.of(node.jsGeneration)`); the hinge keeps the `EventEmitter.current()` of the call that turned it on.
@@ -35,6 +35,8 @@ 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
39
+ import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackCheck
38
40
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
39
41
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
40
42
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigators
@@ -71,38 +73,73 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
71
73
  // Enabled while back has something to do in the app (BackCallbackState); otherwise the system does its own back.
72
74
  // From Android 14, where the app hears the progress of the back gesture, a gesture that would pop a stack with the
73
75
  // default transition previews the pop as the system previews back between activities (`StackController
74
- // .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.
75
78
  private val backCallback = object : OnBackPressedCallback(false) {
76
79
  override fun handleOnBackStarted(backEvent: BackEventCompat) {
77
80
  backGesture = null
81
+ sheetGesture = null
82
+ val navigator = navigator ?: return
83
+ backCheck?.atBack(navigator)
78
84
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || BackCallbackState.hasJsListeners) return
79
- val stack = navigator?.stackPoppedByBack() ?: return
80
- 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)
81
91
  }
82
92
 
83
93
  override fun handleOnBackProgressed(backEvent: BackEventCompat) {
84
94
  backGesture?.progress(backEvent.progress, backEvent.touchY)
95
+ sheetGesture?.progress(backEvent)
85
96
  }
86
97
 
87
98
  override fun handleOnBackCancelled() {
88
99
  backGesture?.cancel()
89
100
  backGesture = null
101
+ sheetGesture?.cancel()
102
+ sheetGesture = null
103
+ navigator?.let(BackCallbackState::update)
90
104
  }
91
105
 
92
106
  override fun handleOnBackPressed() {
107
+ navigator?.let { backCheck?.atBack(it) }
93
108
  val gesture = backGesture
94
109
  backGesture = null
95
- 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
+ }
96
117
  }
97
118
  }
98
119
 
99
120
  // The back gesture that previews a pop now; none.
100
121
  private var backGesture: StackController.BackGesture? = null
101
122
 
102
- // Before each frame of the window: what back does may have changed, and the system decides at the start of a
103
- // gesture whether the app takes it.
104
- private val backBeforeFrame = ViewTreeObserver.OnPreDrawListener {
105
- navigator?.let(BackCallbackState::update)
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
+
137
+ // Debug builds only: the check that what back does never changes without its trigger (BackCallbackState), sampled
138
+ // before frames and at back events. It only logs; release builds have neither the check nor the listener.
139
+ private val backCheck = if (BuildConfig.DEBUG) BackCallbackCheck() else null
140
+ private val backCheckBeforeFrame = ViewTreeObserver.OnPreDrawListener {
141
+ val navigator = navigator
142
+ if (navigator != null) backCheck?.sample(navigator, SystemClock.uptimeMillis())
106
143
  true
107
144
  }
108
145
  private var frameObserver: ViewTreeObserver? = null
@@ -143,7 +180,9 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
143
180
  SlotHost.attachIfStarted(this)
144
181
  reactGateway.onActivityCreated(this)
145
182
  onBackPressedDispatcher.addCallback(this, backCallback)
146
- frameObserver = window.decorView.viewTreeObserver.also { it.addOnPreDrawListener(backBeforeFrame) }
183
+ if (backCheck != null) {
184
+ frameObserver = window.decorView.viewTreeObserver.also { it.addOnPreDrawListener(backCheckBeforeFrame) }
185
+ }
147
186
  // The activity's system UI: its owner remembers the theme's colors and the manifest's orientation now, before
148
187
  // anything changes them.
149
188
  navigator.systemUi.start(SystemUiWindow.of(this))
@@ -218,6 +257,8 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
218
257
  isNavigationResumed = true
219
258
  // The root, the modals and the overlays lay out again; nothing is sent.
220
259
  navigator?.onResume()
260
+ // A safety net: what back does changes on its own triggers, and the system asks the callback from here on.
261
+ navigator?.let(BackCallbackState::update)
221
262
  if (developerSupport() != null) reloadBroadcast.register()
222
263
  ReloadCoordinator.launchIfDue(this)
223
264
  }
@@ -239,6 +280,8 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
239
280
  override fun onWindowFocusChanged(hasFocus: Boolean) {
240
281
  super.onWindowFocusChanged(hasFocus)
241
282
  if (reactGateway.currentReactContext != null) reactGateway.host.onWindowFocusChange(hasFocus)
283
+ // A safety net, as at onResume: back after a dialog or another window over the activity.
284
+ if (hasFocus) navigator?.let(BackCallbackState::update)
242
285
  }
243
286
 
244
287
  override fun onUserLeaveHint() {
@@ -302,9 +345,11 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
302
345
  splashHold = null
303
346
  reloadBroadcast.release()
304
347
  navigator?.let(BackCallbackState::detach)
305
- // The window's observer now, and the one the listener was added to, if they differ.
306
- val observers = listOfNotNull(frameObserver, window?.peekDecorView()?.viewTreeObserver).distinct()
307
- observers.forEach { if (it.isAlive) it.removeOnPreDrawListener(backBeforeFrame) }
348
+ // The debug check's listener: from the window's observer now, and the one it was added to, if they differ.
349
+ if (backCheck != null) {
350
+ val observers = listOfNotNull(frameObserver, window?.peekDecorView()?.viewTreeObserver).distinct()
351
+ observers.forEach { if (it.isAlive) it.removeOnPreDrawListener(backCheckBeforeFrame) }
352
+ }
308
353
  frameObserver = null
309
354
  SlotHost.detach(this)
310
355
  navigator?.destroyWithActivity()
@@ -317,7 +362,9 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
317
362
  /**
318
363
  * Back that JavaScript's listeners didn't take: the navigation does its step first — a stack's top leaves or holds,
319
364
  * a pane closes —; with nothing to do there, the activity's own back, as without the navigation. An app may
320
- * override it (e.g. `moveTaskToBack(true)`).
365
+ * override it (e.g. `moveTaskToBack(true)`). The callback is off for that back only: the dispatcher's own back is
366
+ * the activity's, not [onBackPressed]; then it is computed again, and `hardwareBackButton.systemAnimation: false`
367
+ * turns it on again.
321
368
  */
322
369
  override fun invokeDefaultOnBackPressed() {
323
370
  val handled = navigator?.handleBack(CommandListenerAdapter()) == true
@@ -8,7 +8,7 @@ import com.facebook.react.bridge.ReadableMap
8
8
  */
9
9
  class AnimationsOptions {
10
10
  @JvmField var push = StackPushAnimationOptions()
11
- @JvmField var pop = StackAnimationOptions()
11
+ @JvmField var pop = StackPopAnimationOptions()
12
12
  @JvmField var setStackRoot = SetStackRootAnimationOptions()
13
13
  @JvmField var setRoot = EnterExitAnimationOptions()
14
14
  @JvmField var showModal = ModalAnimationOptions()
@@ -43,7 +43,7 @@ class AnimationsOptions {
43
43
  val options = AnimationsOptions()
44
44
  if (map == null) return options
45
45
  options.push = map.group("push") { StackPushAnimationOptions.parse(it) }
46
- options.pop = map.group("pop") { StackAnimationOptions.parse(it) }
46
+ options.pop = map.group("pop") { StackPopAnimationOptions.parse(it) }
47
47
  options.setStackRoot = map.group("setStackRoot") { SetStackRootAnimationOptions.parse(it) }
48
48
  options.setRoot = map.group("setRoot") { EnterExitAnimationOptions.parse(it) }
49
49
  options.showModal = map.group("showModal") { ModalAnimationOptions.parse(it) }
@@ -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")
@@ -15,20 +15,27 @@ class HardwareBackButtonOptions {
15
15
  @JvmField var popStackOnPress: Bool = NullBool()
16
16
  /** Internal. */
17
17
  @JvmField var dismissModalOnPress: Bool = NullBool()
18
+ /**
19
+ * `systemAnimation`: `false` keeps back in the app on the first screen too, so the system doesn't play its
20
+ * animation of going home; the press then does the activity's own back.
21
+ */
22
+ @JvmField var systemAnimation: Bool = NullBool()
18
23
 
19
24
  fun hasValue(): Boolean = bottomTabsOnPress.hasValue() || popStackOnPress.hasValue() ||
20
- dismissModalOnPress.hasValue()
25
+ dismissModalOnPress.hasValue() || systemAnimation.hasValue()
21
26
 
22
27
  fun mergeWith(other: HardwareBackButtonOptions) {
23
28
  if (other.bottomTabsOnPress.hasValue()) bottomTabsOnPress = other.bottomTabsOnPress
24
29
  if (other.popStackOnPress.hasValue()) popStackOnPress = other.popStackOnPress
25
30
  if (other.dismissModalOnPress.hasValue()) dismissModalOnPress = other.dismissModalOnPress
31
+ if (other.systemAnimation.hasValue()) systemAnimation = other.systemAnimation
26
32
  }
27
33
 
28
34
  fun mergeWithDefault(defaults: HardwareBackButtonOptions) {
29
35
  if (!bottomTabsOnPress.hasValue()) bottomTabsOnPress = defaults.bottomTabsOnPress
30
36
  if (!popStackOnPress.hasValue()) popStackOnPress = defaults.popStackOnPress
31
37
  if (!dismissModalOnPress.hasValue()) dismissModalOnPress = defaults.dismissModalOnPress
38
+ if (!systemAnimation.hasValue()) systemAnimation = defaults.systemAnimation
32
39
  }
33
40
 
34
41
  fun copy() = HardwareBackButtonOptions().also { it.mergeWith(this) }
@@ -43,6 +50,7 @@ class HardwareBackButtonOptions {
43
50
  options.bottomTabsOnPress = TextParser.parse(map, "bottomTabsOnPress", BOTTOM_TABS_ON_PRESS)
44
51
  options.popStackOnPress = BoolParser.parse(map, "popStackOnPress")
45
52
  options.dismissModalOnPress = BoolParser.parse(map, "dismissModalOnPress")
53
+ options.systemAnimation = BoolParser.parse(map, "systemAnimation")
46
54
  return options
47
55
  }
48
56
  }
@@ -99,8 +99,8 @@ class BottomTabsEnterExitAnimationOptions {
99
99
  }
100
100
 
101
101
  /**
102
- * `StackAnimationOptions`: the animation of `pop`, and the base of `push` and `setStackRoot`; over the shared elements
103
- * and element transitions of [LayoutAnimation].
102
+ * `StackAnimationOptions`: the base of the animations of `push`, `pop` and `setStackRoot`; over the shared elements and
103
+ * element transitions of [LayoutAnimation].
104
104
  */
105
105
  open class StackAnimationOptions : LayoutAnimation() {
106
106
  @JvmField var enabled: Bool = NullBool()
@@ -188,6 +188,44 @@ class StackPushAnimationOptions : StackAnimationOptions() {
188
188
  }
189
189
  }
190
190
 
191
+ /**
192
+ * `StackPopAnimationOptions`: the animation of `pop`, and `preview`: whether a back gesture over the stack previews the
193
+ * pop of its top (Android 14+); `false` pops it when the gesture is released, as on Android 13.
194
+ */
195
+ class StackPopAnimationOptions : StackAnimationOptions() {
196
+ @JvmField var preview: Bool = NullBool()
197
+
198
+ override fun hasValue(): Boolean = super.hasValue() || preview.hasValue()
199
+
200
+ fun mergeWith(other: StackPopAnimationOptions) {
201
+ super.mergeWith(other)
202
+ if (other.preview.hasValue()) preview = other.preview
203
+ }
204
+
205
+ fun mergeWithDefault(defaults: StackPopAnimationOptions) {
206
+ super.mergeWithDefault(defaults)
207
+ if (!preview.hasValue()) preview = defaults.preview
208
+ }
209
+
210
+ override fun copy(): StackPopAnimationOptions = StackPopAnimationOptions().also { it.mergeWith(this) }
211
+
212
+ // Reads the fields of this class from [map], after those of its parent.
213
+ private fun readStackPopAnimationOptions(map: ReadableMap) {
214
+ readStackAnimationOptions(map)
215
+ preview = BoolParser.parse(map, "preview")
216
+ }
217
+
218
+ companion object {
219
+ @JvmStatic
220
+ fun parse(map: ReadableMap?): StackPopAnimationOptions {
221
+ val options = StackPopAnimationOptions()
222
+ if (map == null) return options
223
+ options.readStackPopAnimationOptions(map)
224
+ return options
225
+ }
226
+ }
227
+ }
228
+
191
229
  /** `SetStackRootAnimationOptions`: the animation of `setStackRoot`. */
192
230
  class SetStackRootAnimationOptions : StackAnimationOptions() {
193
231
  @JvmField var asPush: Bool = NullBool()