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

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 (226) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/android/CLAUDE.md +38 -9
  3. package/android/CORE.md +11 -4
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  8. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  9. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  12. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  14. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  16. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  19. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  23. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  27. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  28. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +92 -19
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +79 -13
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +45 -6
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +35 -9
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +10 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -1
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +72 -16
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  53. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  54. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +164 -13
  55. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +26 -1
  56. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +136 -40
  57. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  58. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +107 -4
  59. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  60. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  61. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  62. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +131 -9
  63. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +75 -21
  64. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  65. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  68. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  69. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  70. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  71. package/cli/doctor/checks/app.js +61 -13
  72. package/cli/migrate/native.js +18 -0
  73. package/cli/setup/source.js +2 -1
  74. package/cli/setup/steps/mainApplication.js +134 -19
  75. package/docs-md/architecture/transitions.md +12 -7
  76. package/docs-md/containers/modals.md +4 -2
  77. package/docs-md/containers/overlays.md +3 -1
  78. package/docs-md/containers/side-menu.md +4 -2
  79. package/docs-md/containers/tabs.md +2 -0
  80. package/docs-md/flows/forms-in-a-modal.md +1 -1
  81. package/docs-md/help/glossary.md +2 -2
  82. package/docs-md/help/troubleshooting.md +20 -3
  83. package/docs-md/index.json +12 -0
  84. package/docs-md/migration/from-react-native-navigation.md +8 -2
  85. package/docs-md/screen-ui/back-and-gestures.md +8 -2
  86. package/docs-md/screen-ui/system-ui.md +1 -1
  87. package/docs-md/start/installation.md +3 -1
  88. package/ios/CLAUDE.md +17 -5
  89. package/ios/CORE.md +13 -8
  90. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  91. package/ios/Contract/ANNCommandContract.h +15 -0
  92. package/ios/Contract/ANNCommandContract.mm +78 -0
  93. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  94. package/ios/Core/ANNErrorHandler.h +1 -1
  95. package/ios/Core/ANNErrorHandler.mm +3 -1
  96. package/ios/Core/ANNEventEmitter.h +1 -1
  97. package/ios/Core/ANNEventEmitter.mm +2 -2
  98. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  99. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  100. package/ios/Core/ANNNavigationState.h +7 -3
  101. package/ios/Core/ANNNavigationState.mm +10 -5
  102. package/ios/Core/ANNNodeHelper.h +32 -12
  103. package/ios/Core/ANNNodeHelper.mm +100 -3
  104. package/ios/Core/ANNRenderWait.h +15 -1
  105. package/ios/Core/ANNRenderWait.mm +119 -17
  106. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  107. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  108. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  109. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  110. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  111. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  112. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  113. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  114. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  115. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  116. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  117. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  118. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  119. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  120. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  121. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  122. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  123. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  124. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  125. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  126. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  127. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  128. package/ios/Options/ANNOptionsCache.h +7 -6
  129. package/ios/Options/ANNSearchBarOptions.h +4 -3
  130. package/ios/Options/ANNUserChoice.h +57 -0
  131. package/ios/Options/ANNUserChoice.mm +44 -0
  132. package/ios/Options/Params/ANNImageSource.h +61 -0
  133. package/ios/Options/Params/ANNImageSource.mm +101 -0
  134. package/ios/Options/Params/ANNOptionImages.h +37 -0
  135. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  136. package/ios/Options/Params/Image.h +11 -2
  137. package/ios/Options/Params/Image.mm +15 -2
  138. package/ios/Options/Params/ImageParser.h +5 -5
  139. package/ios/Options/Params/ImageParser.mm +28 -46
  140. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  141. package/ios/RECIPES.md +11 -5
  142. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  143. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  144. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  145. package/ios/TopBar/ANNBarButtons.h +9 -5
  146. package/ios/TopBar/ANNBarButtons.mm +211 -63
  147. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  148. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  149. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  150. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  151. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  152. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  153. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  154. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  155. package/lib/module/ARCHITECTURE.md +18 -18
  156. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  157. package/lib/module/api/hooks/useNavigation.js +8 -3
  158. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  159. package/lib/module/api/navigation.js.map +1 -1
  160. package/lib/module/engine/commands.js +9 -7
  161. package/lib/module/engine/commands.js.map +1 -1
  162. package/lib/module/engine/layout.js +13 -9
  163. package/lib/module/engine/layout.js.map +1 -1
  164. package/lib/module/engine/options.js +44 -11
  165. package/lib/module/engine/options.js.map +1 -1
  166. package/lib/module/engine/places.js +44 -7
  167. package/lib/module/engine/places.js.map +1 -1
  168. package/lib/module/engine/props.js +30 -1
  169. package/lib/module/engine/props.js.map +1 -1
  170. package/lib/module/engine/rememberedOptions.js +1 -1
  171. package/lib/module/engine/rememberedOptions.js.map +1 -1
  172. package/lib/module/engine/screens.js +22 -12
  173. package/lib/module/engine/screens.js.map +1 -1
  174. package/lib/module/interfaces/contract/commands.json +101 -0
  175. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  176. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  177. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  178. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  179. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  180. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  181. package/lib/typescript/api/navigation.d.ts +4 -1
  182. package/lib/typescript/api/navigation.d.ts.map +1 -1
  183. package/lib/typescript/engine/commands.d.ts.map +1 -1
  184. package/lib/typescript/engine/options.d.ts +5 -2
  185. package/lib/typescript/engine/options.d.ts.map +1 -1
  186. package/lib/typescript/engine/places.d.ts +5 -2
  187. package/lib/typescript/engine/places.d.ts.map +1 -1
  188. package/lib/typescript/engine/props.d.ts +8 -1
  189. package/lib/typescript/engine/props.d.ts.map +1 -1
  190. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  191. package/lib/typescript/engine/screens.d.ts.map +1 -1
  192. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  193. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  194. package/lib/typescript/interfaces/Events.d.ts +19 -10
  195. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  196. package/lib/typescript/interfaces/options/animations.d.ts +15 -7
  197. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  198. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  199. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  200. package/lib/typescript/interfaces/options/modals.d.ts +18 -13
  201. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  202. package/lib/typescript/interfaces/options/system.d.ts +6 -3
  203. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  204. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  205. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  206. package/package.json +1 -1
  207. package/src/ARCHITECTURE.md +18 -18
  208. package/src/adapters/NativeANNTurboModule.ts +5 -2
  209. package/src/api/events/navigationEvents.ts +1 -1
  210. package/src/api/hooks/useNavigation.ts +9 -4
  211. package/src/api/navigation.ts +4 -1
  212. package/src/engine/commands.ts +19 -7
  213. package/src/engine/layout.ts +16 -9
  214. package/src/engine/options.ts +39 -11
  215. package/src/engine/places.ts +56 -7
  216. package/src/engine/props.ts +30 -1
  217. package/src/engine/rememberedOptions.ts +4 -1
  218. package/src/engine/screens.tsx +23 -13
  219. package/src/interfaces/ComponentEvents.ts +2 -2
  220. package/src/interfaces/Events.ts +19 -10
  221. package/src/interfaces/contract/commands.json +101 -0
  222. package/src/interfaces/options/animations.ts +15 -7
  223. package/src/interfaces/options/buttons.ts +13 -5
  224. package/src/interfaces/options/modals.ts +18 -13
  225. package/src/interfaces/options/system.ts +6 -3
  226. package/src/interfaces/options/topBar.ts +22 -9
@@ -15,6 +15,7 @@ import dev.apexnative.navigation.options.SplitViewGroupOptions
15
15
  import dev.apexnative.navigation.options.SplitViewOptions
16
16
  import dev.apexnative.navigation.react.CommandListener
17
17
  import dev.apexnative.navigation.react.CommandListenerAdapter
18
+ import dev.apexnative.navigation.react.ErrorTexts
18
19
  import dev.apexnative.navigation.react.events.EventEmitter
19
20
  import dev.apexnative.navigation.system.SystemUiOwner
20
21
  import dev.apexnative.navigation.utils.TestIds
@@ -24,10 +25,11 @@ import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
24
25
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
25
26
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
26
27
  import dev.apexnative.navigation.viewcontrollers.parent.ChildOffsetFromRest
28
+ import dev.apexnative.navigation.viewcontrollers.parent.OptionsScreen
27
29
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
28
30
  import dev.apexnative.navigation.viewcontrollers.parent.ReturnsScreenFromColumn
29
- import dev.apexnative.navigation.viewcontrollers.parent.ShowsColumns
30
31
  import dev.apexnative.navigation.viewcontrollers.parent.ShownChildren
32
+ import dev.apexnative.navigation.viewcontrollers.parent.ShowsColumns
31
33
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
32
34
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
33
35
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
@@ -45,8 +47,9 @@ class SplitViewController(
45
47
  presenter: Presenter,
46
48
  initialOptions: Options,
47
49
  private var split: SplitViewOptions,
48
- private val primary: ViewController<*>,
49
- private val secondary: ViewController<*>,
50
+ // The panes' content; `showInColumn` may give a pane a new one.
51
+ private var primary: ViewController<*>,
52
+ private var secondary: ViewController<*>,
50
53
  private val compact: ViewController<*>?,
51
54
  private val eventEmitter: EventEmitter,
52
55
  ) : ParentController<SplitViewLayout>(activity, childRegistry, id, presenter, initialOptions),
@@ -366,6 +369,19 @@ class SplitViewController(
366
369
  return can || super.canHandleBack()
367
370
  }
368
371
 
372
+ // As handleBack: the pane whose back goes first decides; an open sliding pane that would close previews nothing.
373
+ override fun stackPoppedByBack(): StackController? {
374
+ if (!isViewCreated) return super.stackPoppedByBack()
375
+ val layout = getView()
376
+ return when {
377
+ layout.isCompact -> compact?.stackPoppedByBack()
378
+ isSliding && !layout.panes.isOpen -> primary.stackPoppedByBack()
379
+ isSliding -> if (secondary.canHandleBack()) secondary.stackPoppedByBack() else null
380
+ secondary.canHandleBack() -> secondary.stackPoppedByBack()
381
+ else -> primary.stackPoppedByBack()
382
+ }
383
+ }
384
+
369
385
  /**
370
386
  * How far the pane of [child] is from where it rests: the secondary pane sliding over the primary one rests over
371
387
  * it, closed or on its way. Zero for the other children.
@@ -408,27 +424,67 @@ class SplitViewController(
408
424
  }
409
425
 
410
426
  /**
411
- * Shows [screen] in the secondary pane in place of what it shows and brings the pane into view. False with the
412
- * compact layout on screen: the screen goes over [from], the screen the command comes from.
427
+ * Shows [screen] in [column] (`primary` or `secondary`; Android has no other) in place of what the pane shows, and
428
+ * brings the pane into view. A stack pane's screens are replaced by it, unless it is a container
429
+ * (![goesInStack]): `columnIsStack`. Any other pane is taken by it, and its content goes. A column the split view
430
+ * doesn't have: `noSuchColumn`. False with the compact layout on screen: the screen goes over [from], the screen
431
+ * the command comes from.
413
432
  */
414
- override fun showScreen(screen: ViewController<*>, from: ViewController<*>, listener: CommandListener): Boolean {
433
+ override fun showScreen(
434
+ screen: ViewController<*>,
435
+ column: String,
436
+ goesInStack: Boolean,
437
+ from: ViewController<*>,
438
+ listener: CommandListener,
439
+ ): Boolean {
415
440
  if (isViewCreated && getView().isCompact) return false
416
- val stack = secondary as? StackController
417
- if (stack == null) {
418
- listener.onError("the 'secondary' column of the split view of the screen '${from.id}' is not a stack")
419
- return true
441
+ val pane = when (column) {
442
+ PRIMARY -> primary
443
+ SECONDARY -> secondary
444
+ else -> null
420
445
  }
421
- stack.setRoot(listOf(screen), false, object : CommandListenerAdapter() {
422
- override fun onSuccess(childId: String) {
446
+ when {
447
+ pane == null -> listener.onError(ErrorTexts.noSuchColumn(from.id, column))
448
+ pane is StackController && !goesInStack -> listener.onError(ErrorTexts.columnIsStack(from.id, column))
449
+ pane is StackController -> pane.setRoot(listOf(screen), false, object : CommandListenerAdapter() {
450
+ override fun onSuccess(childId: String) {
451
+ updateBackButton()
452
+ bringIntoView(column) { listener.onSuccess(childId) }
453
+ }
454
+
455
+ override fun onError(message: String) = listener.onError(message)
456
+ })
457
+ else -> {
458
+ replacePane(pane, screen)
423
459
  updateBackButton()
424
- showSecondary { listener.onSuccess(childId) }
460
+ val shownId = ((screen as? OptionsScreen)?.optionsScreen ?: screen).id
461
+ bringIntoView(column) { listener.onSuccess(shownId) }
425
462
  }
426
-
427
- override fun onError(message: String) = listener.onError(message)
428
- })
463
+ }
429
464
  return true
430
465
  }
431
466
 
467
+ // [next] takes the pane of [old], a screen alone or a container, as iOS puts a screen in place of a column's: its
468
+ // view goes where the old one's was, the children shown are told as a move of the panes tells them (DidDisappear
469
+ // of what left, DidAppear of what came), and [old] goes with its screens.
470
+ private fun replacePane(old: ViewController<*>, next: ViewController<*>) {
471
+ next.setParentController(this)
472
+ if (old === primary) primary = next else secondary = next
473
+ peekView()?.let { layout ->
474
+ layout.panes.replacePane(old.getView(), next.getView())
475
+ applyTestIds(primary.getView(), secondary.getView())
476
+ hideCoveredPane()
477
+ shownChildren.moved()
478
+ tellShownChanged()
479
+ }
480
+ old.destroy()
481
+ }
482
+
483
+ // The pane of [column] comes into view; [done] once the panes stopped.
484
+ private fun bringIntoView(column: String, done: Runnable) {
485
+ if (column == PRIMARY) setColumnShown(PRIMARY, true, done) else showSecondary(done)
486
+ }
487
+
432
488
  private fun showSecondary(done: Runnable) {
433
489
  if (!isSliding || getView().panes.isOpen) {
434
490
  done.run()
@@ -0,0 +1,287 @@
1
+ package dev.apexnative.navigation.viewcontrollers.stack
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.content.res.Configuration
7
+ import android.graphics.Bitmap
8
+ import android.graphics.Canvas
9
+ import android.graphics.Color
10
+ import android.graphics.Outline
11
+ import android.graphics.Rect
12
+ import android.os.Build
13
+ import android.os.Handler
14
+ import android.os.Looper
15
+ import android.view.PixelCopy
16
+ import android.view.View
17
+ import android.view.ViewGroup
18
+ import android.view.ViewOutlineProvider
19
+ import android.view.Window
20
+ import android.view.animation.DecelerateInterpolator
21
+ import android.view.animation.LinearInterpolator
22
+ import android.view.animation.PathInterpolator
23
+ import android.widget.ImageView
24
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
25
+ import dev.apexnative.navigation.views.animations.SystemTransition
26
+ import dev.apexnative.navigation.views.stack.StackLayout
27
+
28
+ /**
29
+ * The preview of a pop while the user's back gesture runs (predictive back), as the system shows the activity under
30
+ * the one that closes: the screen on top with its bar, a picture taken as the gesture starts, shrinks toward the
31
+ * gesture's side with rounded corners and follows the finger up and down; the screen under it shows behind it with
32
+ * its own bar, smaller and dimmed. Released, the pop plays its end ([commit]); cancelled, everything goes back
33
+ * ([cancel]). The values are those of the system's cross-activity back animation (WMShell
34
+ * `DefaultCrossActivityBackAnimation` of Android 15 and 16, the smallest scale as Android 17 shows it), also on Android
35
+ * 14, whose own preview differs. Main thread only.
36
+ */
37
+ internal class BackPreview(
38
+ private val layout: StackLayout,
39
+ private val top: View,
40
+ private val target: View,
41
+ /** The gesture started at the left edge: the screen moves right; from the right edge it stays centered. */
42
+ private val fromLeft: Boolean,
43
+ private val startY: Float,
44
+ /**
45
+ * The room the tabs take at the bottom of the top screen: the card is the stack above them, rounded on all four
46
+ * corners, while the tab bar stays, as Gmail shows it.
47
+ */
48
+ private val bottomRoom: Int,
49
+ /** The picture is there: the stack shows the screen under the top, with its bar, before the preview draws. */
50
+ private val onReveal: () -> Unit,
51
+ ) {
52
+ private val density = layout.resources.displayMetrics.density
53
+ private val bar = layout.topBar
54
+ private val scrim = View(layout.context)
55
+ private var card: ImageView? = null
56
+ private var progress = 0f
57
+ private var touchY = startY
58
+ private var animator: Animator? = null
59
+ private var ended = false
60
+ private var topAlpha = 1f
61
+ // What puts the stack as it was after a cancel: once, as the preview ends.
62
+ private var onCancelled: (() -> Unit)? = null
63
+ // Forward is to the end of the line: right to left, the screens move the other way, as the stack's pop does.
64
+ private val forward = if (layout.layoutDirection == View.LAYOUT_DIRECTION_RTL) -1f else 1f
65
+
66
+ /** The picture of the top is there and the screen under it shows: the preview draws. */
67
+ val isReady: Boolean
68
+ get() = card != null && !ended
69
+
70
+ /**
71
+ * Takes the picture of the stack as it shows now, drawn from its views (what lies over the stack, an overlay or a
72
+ * tab bar drawn over it, stays out of it); with a hardware bitmap in it, copied from the window a frame later. The
73
+ * preview draws once it is there.
74
+ */
75
+ fun start(window: Window) {
76
+ val width = layout.width
77
+ val height = areaHeight
78
+ if (width <= 0 || height <= 0) return
79
+ val picture = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
80
+ try {
81
+ layout.draw(Canvas(picture))
82
+ show(picture)
83
+ return
84
+ } catch (e: IllegalArgumentException) {
85
+ picture.eraseColor(Color.TRANSPARENT)
86
+ }
87
+ val place = IntArray(2)
88
+ layout.getLocationInWindow(place)
89
+ val area = Rect(place[0], place[1], place[0] + width, place[1] + height)
90
+ PixelCopy.request(window, area, picture, { result ->
91
+ if (result == PixelCopy.SUCCESS && !ended) show(picture)
92
+ }, Handler(Looper.getMainLooper()))
93
+ }
94
+
95
+ /** The gesture moved: [value] is its progress, 0 … 1, and [y] the finger's height in the window. */
96
+ fun progress(value: Float, y: Float) {
97
+ progress = value
98
+ touchY = y
99
+ if (isReady) drawGesture(GESTURE_CURVE.getInterpolation(value))
100
+ }
101
+
102
+ /**
103
+ * The gesture was cancelled: the preview springs back, then [restore] puts the stack as it was; at once when the
104
+ * preview ends earlier (a new gesture, a command).
105
+ */
106
+ fun cancel(restore: () -> Unit) {
107
+ onCancelled = restore
108
+ if (!isReady) return endAtOnce()
109
+ val from = progress
110
+ animator?.cancel()
111
+ animator = ValueAnimator.ofFloat(from, 0f).apply {
112
+ duration = CANCEL_MILLIS
113
+ interpolator = DecelerateInterpolator()
114
+ addUpdateListener { drawGesture(GESTURE_CURVE.getInterpolation(it.animatedValue as Float)) }
115
+ addListener(object : AnimatorListenerAdapter() {
116
+ override fun onAnimationEnd(animation: Animator) {
117
+ if (animator === animation) endAtOnce()
118
+ }
119
+ })
120
+ start()
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The gesture was released and the pop runs: the top goes back to its size while it moves away by 96 dp and fades
126
+ * in the first fifth, the screen under it grows to its place, the dim fades; [done] once at the end, after the
127
+ * preview took its views away.
128
+ */
129
+ fun commit(done: () -> Unit) {
130
+ val card = card ?: return done()
131
+ val distance = SystemTransition.DISTANCE_DP * density
132
+ val q = GESTURE_CURVE.getInterpolation(progress)
133
+ val scale = scaleAt(q)
134
+ val cardX = card.translationX
135
+ val cardY = card.translationY
136
+ val dim = scrim.alpha
137
+ animator?.cancel()
138
+ animator = ValueAnimator.ofFloat(0f, 1f).apply {
139
+ duration = SystemTransition.DURATION
140
+ interpolator = LinearInterpolator()
141
+ addUpdateListener {
142
+ val t = it.animatedValue as Float
143
+ val e = SystemTransition.CURVE.getInterpolation(t)
144
+ card.scaleX = lerp(scale, 1f, e)
145
+ card.scaleY = card.scaleX
146
+ card.translationX = lerp(cardX, distance * forward, e)
147
+ card.translationY = lerp(cardY, 0f, e)
148
+ card.alpha = (1f - 5f * t).coerceAtLeast(0f)
149
+ entering(lerp(scale, 1f, e), lerp(-distance * forward, 0f, e))
150
+ scrim.alpha = lerp(dim, 0f, t)
151
+ }
152
+ addListener(object : AnimatorListenerAdapter() {
153
+ override fun onAnimationEnd(animation: Animator) {
154
+ if (animator !== animation) return
155
+ endAtOnce()
156
+ done()
157
+ }
158
+ })
159
+ start()
160
+ }
161
+ }
162
+
163
+ /**
164
+ * The preview ends where it is: its views go, the screens and the bar take their own values again, and a cancel's
165
+ * restore runs.
166
+ */
167
+ fun endAtOnce() {
168
+ if (ended) return
169
+ ended = true
170
+ animator?.let {
171
+ animator = null
172
+ it.cancel()
173
+ }
174
+ card?.let { (it.parent as? ViewGroup)?.removeView(it) }
175
+ card = null
176
+ (scrim.parent as? ViewGroup)?.removeView(scrim)
177
+ top.alpha = topAlpha
178
+ entering(1f, 0f)
179
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
180
+ target.resetPivot()
181
+ bar.resetPivot()
182
+ }
183
+ onCancelled?.let {
184
+ onCancelled = null
185
+ it()
186
+ }
187
+ }
188
+
189
+ // The picture came: the screen under the top shows with its bar, dimmed, and the picture lies over everything in
190
+ // the top's place, cut to rounded corners.
191
+ private fun show(picture: Bitmap) {
192
+ onReveal()
193
+ val dark = layout.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
194
+ Configuration.UI_MODE_NIGHT_YES
195
+ scrim.setBackgroundColor(Color.BLACK)
196
+ scrim.alpha = if (dark) SCRIM_DARK else SCRIM_LIGHT
197
+ scrim.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
198
+ layout.addOverAll(scrim, fill())
199
+ val radius = CORNER_DP * density
200
+ val area = CoordinatorLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, areaHeight)
201
+ card = ImageView(layout.context).apply {
202
+ setImageBitmap(picture)
203
+ scaleType = ImageView.ScaleType.FIT_XY
204
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
205
+ outlineProvider = object : ViewOutlineProvider() {
206
+ override fun getOutline(view: View, outline: Outline) =
207
+ outline.setRoundRect(0, 0, view.width, view.height, radius)
208
+ }
209
+ clipToOutline = true
210
+ layout.addOverAll(this, area)
211
+ }
212
+ // Transparent, not invisible: a surface in it (a video, a map) stays, and so does the focus.
213
+ topAlpha = top.alpha
214
+ top.alpha = 0f
215
+ drawGesture(GESTURE_CURVE.getInterpolation(progress))
216
+ }
217
+
218
+ // The values of the gesture at [q], its progress on the system's curve.
219
+ private fun drawGesture(q: Float) {
220
+ val card = card ?: return
221
+ val width = layout.width.toFloat()
222
+ val height = areaHeight.toFloat()
223
+ val margin = MARGIN_DP * density
224
+ val scale = scaleAt(q)
225
+ card.scaleX = scale
226
+ card.scaleY = scale
227
+ // From the left edge the screen's right side ends 8 dp from the window's right edge; from the right edge it stays
228
+ // centered.
229
+ card.translationX = if (fromLeft) (width - width * scale) / 2f - margin * q else 0f
230
+ // Up and down with the finger, never closer than 8 dp to the window's edges.
231
+ val moved = touchY - startY
232
+ val ratio = FOLLOW_CURVE.getInterpolation((kotlin.math.abs(moved) / (height / 2f)).coerceAtMost(1f))
233
+ card.translationY = ((height - height * scale) / 2f - margin).coerceAtLeast(0f) * ratio *
234
+ Math.signum(moved)
235
+ entering(scale, -SystemTransition.DISTANCE_DP * density * forward)
236
+ }
237
+
238
+ // The screen under the top and its bar at [scale] around the stack's center, [x] along the line.
239
+ private fun entering(scale: Float, x: Float) {
240
+ for (view in listOf(target, bar)) {
241
+ view.pivotX = layout.width / 2f - view.left
242
+ view.pivotY = areaHeight / 2f - view.top
243
+ view.scaleX = scale
244
+ view.scaleY = scale
245
+ view.translationX = x
246
+ }
247
+ }
248
+
249
+ // The stack above the tabs' room: what the card shows and what the screens shrink around.
250
+ private val areaHeight: Int
251
+ get() = (layout.height - bottomRoom).coerceAtLeast(0)
252
+
253
+ private fun scaleAt(q: Float): Float = 1f - (1f - minScale()) * q
254
+
255
+ private fun fill() = CoordinatorLayout.LayoutParams(
256
+ ViewGroup.LayoutParams.MATCH_PARENT,
257
+ ViewGroup.LayoutParams.MATCH_PARENT,
258
+ )
259
+
260
+ private fun lerp(from: Float, to: Float, fraction: Float): Float = from + (to - from) * fraction
261
+
262
+ private companion object {
263
+ /** The system's curve of the back gesture's progress (`BackGestureInterpolator`). */
264
+ val GESTURE_CURVE = PathInterpolator(0.1f, 0.1f, 0f, 1f)
265
+
266
+ /** How the screen follows the finger up and down. */
267
+ val FOLLOW_CURVE = DecelerateInterpolator()
268
+
269
+ /** The room the shrunk screen keeps from the window's edges. */
270
+ const val MARGIN_DP = 8f
271
+
272
+ /** The corners of the shrunk screen, as Android 17 draws them on a Pixel (6.5 dp at its smallest). */
273
+ const val CORNER_DP = 7.5f
274
+
275
+ const val SCRIM_LIGHT = 0.2f
276
+ const val SCRIM_DARK = 0.8f
277
+
278
+ /** The cancel springs back about as fast as the system's (`BackProgressAnimator`'s spring, no bounce). */
279
+ const val CANCEL_MILLIS = 350L
280
+
281
+ /**
282
+ * The smallest scale of the screens: 0.9 in WMShell of Android 15 and 16 (and here on Android 14); Android 17
283
+ * shrinks the window further (0.86, measured on a Pixel 6).
284
+ */
285
+ fun minScale(): Float = if (android.os.Build.VERSION.SDK_INT >= 37) 0.86f else 0.9f
286
+ }
287
+ }
@@ -105,33 +105,42 @@ internal class StackCommands(
105
105
  }
106
106
 
107
107
  /**
108
- * `showInColumn` (Android): [layout] in the secondary pane of the split view of the screen [componentId], or over
109
- * that screen in its stack. Before any node: the screen is there, else `the screen '<id>' is not shown`; the push's
110
- * checks of the layout (an id that shows already; a container, which the stack of the screen, else its split view,
111
- * doesn't take); a screen prepared with the id goes. The screen is made at once, a native screen's view too. The
112
- * split view takes it (the answer once its pane stopped, or the rejection of the pane); with the compact layout on
113
- * screen, or without a split view, it is pushed over [componentId] in its stack, with the push's answer and errors;
114
- * neither: `the screen '<id>' is neither in a split view nor in a stack`. Every rejection after the screen was made
115
- * destroys it. Two calls in a row each make their screen the root of the pane in turn, and each answers its own id.
108
+ * `showInColumn` (Android): [layout] in [column] of the split view of the screen [componentId], or over that screen
109
+ * in its stack. Before any node: the screen is there, else `the screen '<id>' is not shown`; the layout has no id
110
+ * that shows already; out of a split view, the push's check of the layout (a container, which the stack doesn't
111
+ * take); a screen prepared with the id goes. The screen is made at once, a native screen's view too. The split view
112
+ * takes it — in place of a stack column's screens or of a column's content — with the answer once its pane
113
+ * stopped, or rejects (a column it doesn't have, a container for a stack column); with the compact layout on
114
+ * screen, or without a split view, it is pushed over [componentId] in its stack, with the push's checks, answer and
115
+ * errors; neither: `the screen '<id>' is neither in a split view nor in a stack`. Every rejection after the screen
116
+ * was made destroys it. Two calls in a row each make their screen the column's content in turn, and each answers
117
+ * its own id.
116
118
  */
117
- fun showInColumn(componentId: String, layout: LayoutNode, answer: CommandAnswer, what: String) {
119
+ fun showInColumn(componentId: String, column: String, layout: LayoutNode, answer: CommandAnswer, what: String) {
118
120
  val source = find(componentId)?.takeIf { !prepared.isPrepared(it) }
119
121
  ?: return answer.reject(ErrorTexts.screenNotShown(componentId))
120
122
  val shown = LayoutChecks.firstShownId(listOf(layout), ::isShown) { false }
121
123
  if (shown != null) return answer.reject(ErrorTexts.alreadyShown(shown))
122
124
  val columns = nodeWith<ShowsColumns>(source)
123
125
  val stack = source as? StackController ?: source.findParentStack()
124
- val placeId = stack?.id ?: (columns as? ViewController<*>)?.id
125
- placeId?.let { id -> LayoutChecks.checkStackChild(layout, id)?.let { return answer.reject(it) } }
126
+ // Out of a split view it can only be a push: the stack's check comes before any node.
127
+ if (columns == null && stack != null) {
128
+ LayoutChecks.checkStackChild(layout, stack.id)?.let { return answer.reject(it) }
129
+ }
126
130
  if (!factory.canMake(layout)) return answer.reject(ErrorTexts.notDoneYet("showInColumn"))
127
131
  prepared.discard(layout.id)
128
132
  val node = make(layout)
129
133
  try {
130
- if (columns?.showScreen(node, source, destroyingOnError(node, answer)) == true) return
134
+ val goesInStack = LayoutChecks.isStackChild(layout)
135
+ if (columns?.showScreen(node, column, goesInStack, source, destroyingOnError(node, answer)) == true) return
131
136
  if (stack == null) {
132
137
  node.destroy()
133
138
  return answer.reject(ErrorTexts.neitherSplitViewNorStack(componentId))
134
139
  }
140
+ LayoutChecks.checkStackChild(layout, stack.id)?.let {
141
+ node.destroy()
142
+ return answer.reject(it)
143
+ }
135
144
  pushMade(stack, source, node, layout, answer, what)
136
145
  } catch (e: Throwable) {
137
146
  node.destroy()
@@ -225,8 +234,9 @@ internal class StackCommands(
225
234
  fun setStackRoot(componentId: String, layouts: List<LayoutNode>, answer: CommandAnswer, what: String) {
226
235
  if (layouts.isEmpty()) return answer.reject(ErrorTexts.noScreensForStack(componentId))
227
236
  val stack = stackOf(componentId, answer) ?: return
228
- // An id of this stack may come again: the new screen replaces it (`setStackRoot` step 3).
229
- val shown = LayoutChecks.firstShownId(layouts, { isShown(it) && !isOf(stack, it) }) { false }
237
+ // An id of an entry of this stack may come again: the new screen replaces it (`setStackRoot` step 3). The
238
+ // stack's own id, and a screen an accepted cut or replace took away, show elsewhere or still go: as on iOS.
239
+ val shown = LayoutChecks.firstShownId(layouts, ::isShown) { isInEntryOf(stack, it.id) }
230
240
  if (shown != null) return answer.reject(ErrorTexts.alreadyShown(shown))
231
241
  for (layout in layouts) LayoutChecks.checkStackChild(layout, stack.id)?.let { return answer.reject(it) }
232
242
  if (!layouts.all(factory::canMake)) return answer.reject(ErrorTexts.notDoneYet("setStackRoot"))
@@ -357,10 +367,10 @@ internal class StackCommands(
357
367
  // A node of the registry has [id], and it isn't only prepared for a push (`push` step 1, `preparePush`).
358
368
  private fun isShown(id: String): Boolean = registry.find(id)?.let { !prepared.isPrepared(it) } == true
359
369
 
360
- // The node with [id] is [stack] or in it.
361
- private fun isOf(stack: StackController, id: String): Boolean {
362
- val node = find(id) ?: return false
363
- return node === stack || node.findParentStack() === stack
370
+ // The node with [id] is in an entry of [stack] as JavaScript sees it (`StackQueue.entries`), not the stack itself.
371
+ private fun isInEntryOf(stack: StackController, id: String): Boolean {
372
+ val node = find(id)?.takeIf { it !== stack } ?: return false
373
+ return stack.entryOf(node)?.let { it in stack.queue.entries } == true
364
374
  }
365
375
 
366
376
  // A node of [layout], with its view made now when it has a native screen: its creator's failure rejects the