@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
@@ -29,11 +29,12 @@ import dev.apexnative.navigation.utils.dp
29
29
  import kotlin.math.roundToInt
30
30
 
31
31
  /**
32
- * A modal shown as a Material bottom sheet (`modal.sheet`): the modal's view in a container with a
33
- * [BottomSheetBehavior], in the modals' CoordinatorLayout, over a scrim. It stops at the detents of its options —
34
- * `medium` at half the height, `large` at the full height under the status bar, a number at that height in dp — and the
35
- * user drags it between them. A drag down past the smallest one closes it, unless `swipeToDismiss` is false: then the
36
- * sheet stays, and the try is told. The screen under it stays in place.
32
+ * A modal shown as a Material bottom sheet (`pageSheet`, `formSheet`, `popover`, or `modal.sheet` without a style): the
33
+ * modal's view in a container with a [BottomSheetBehavior], in the modals' CoordinatorLayout, over a scrim. It stops at
34
+ * the detents of its options — `medium` at half the height, `large` at the full height under the status bar, a number
35
+ * at that height in dp — and the user drags it between them. In a window wider than [MAX_WIDTH_DP] it stands in the
36
+ * middle at that width. A drag down past the smallest one closes it, unless `swipeToDismiss` is false: then the sheet
37
+ * stays, and the try is told. The screen under it stays in place.
37
38
  */
38
39
  internal class ModalSheet(
39
40
  private val layout: CoordinatorLayout,
@@ -96,11 +97,19 @@ internal class ModalSheet(
96
97
  outline.setRoundRect(0, 0, view.width, view.height + radius.roundToInt(), radius)
97
98
  }
98
99
  }
99
- // The sheet is under the status bar: its screens leave no room for it at their top.
100
+ // The sheet is under the status bar: its screens leave no room for it at their top. In the middle of a wide
101
+ // window it reaches no side, and keeps room only for the part of a side bar that is over it.
100
102
  ViewCompat.setOnApplyWindowInsetsListener(container) { _, insets ->
101
103
  val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
102
104
  val bars = insets.getInsets(types)
103
- WindowInsetsCompat.Builder(insets).setInsets(types, Insets.of(bars.left, 0, bars.right, bars.bottom)).build()
105
+ val laidOut = container.width > 0
106
+ val left = if (laidOut) (bars.left - container.left).coerceAtLeast(0) else bars.left
107
+ val right = if (laidOut) (bars.right - (layout.width - container.right)).coerceAtLeast(0) else bars.right
108
+ WindowInsetsCompat.Builder(insets).setInsets(types, Insets.of(left, 0, right, bars.bottom)).build()
109
+ }
110
+ // Its sides move with the window's width (a rotation, a resize): the room for the side bars follows.
111
+ container.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
112
+ if (left != oldLeft || right != oldRight) container.post { ViewCompat.requestApplyInsets(container) }
104
113
  }
105
114
  container.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
106
115
  container.addView(grabber, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.TOP or Gravity.CENTER_HORIZONTAL))
@@ -120,7 +129,12 @@ internal class ModalSheet(
120
129
  layout.addView(scrim, CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
121
130
  // As tall as its largest detent (maxHeight), not moved down by a margin: the behavior counts its states from
122
131
  // where the sheet is laid out.
123
- layout.addView(container, CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).also { it.behavior = behavior })
132
+ // In a wide window it stands in the middle, as wide as a Material modal sheet at most.
133
+ behavior.maxWidth = MAX_WIDTH_DP.dp
134
+ val params = CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
135
+ params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
136
+ params.behavior = behavior
137
+ layout.addView(container, params)
124
138
  shown = onShown
125
139
  update(sheetOptions, swipeToDismiss, merge = null)
126
140
  selected = sheetOptions.selectedDetent?.takeIf { it in detents }
@@ -334,10 +348,13 @@ internal class ModalSheet(
334
348
  }
335
349
  }
336
350
 
337
- private companion object {
351
+ companion object {
352
+ /** The widest a sheet is, in dp: the Material modal bottom sheet's `maxWidth`. */
353
+ const val MAX_WIDTH_DP = 640
354
+
338
355
  /** The dim of a Material modal bottom sheet's scrim. */
339
- const val SCRIM_ALPHA = 0.32f
340
- const val GRAB_AREA_DP = 48
341
- const val ATTEMPT_DISTANCE_DP = 48
356
+ private const val SCRIM_ALPHA = 0.32f
357
+ private const val GRAB_AREA_DP = 48
358
+ private const val ATTEMPT_DISTANCE_DP = 48
342
359
  }
343
360
  }
@@ -4,9 +4,9 @@ import android.animation.Animator
4
4
  import android.animation.AnimatorListenerAdapter
5
5
  import android.animation.AnimatorSet
6
6
  import android.animation.ObjectAnimator
7
- import android.animation.PropertyValuesHolder
8
7
  import android.animation.TimeInterpolator
9
8
  import android.content.Context
9
+ import android.view.Gravity
10
10
  import android.view.View
11
11
  import android.view.ViewGroup
12
12
  import android.view.animation.LinearInterpolator
@@ -17,6 +17,7 @@ import androidx.core.view.WindowInsetsCompat
17
17
  import dev.apexnative.navigation.options.ModalAnimationOptions
18
18
  import dev.apexnative.navigation.options.Options
19
19
  import dev.apexnative.navigation.utils.TestIds
20
+ import dev.apexnative.navigation.utils.dp
20
21
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
21
22
  import dev.apexnative.navigation.views.animations.AnimationRules
22
23
  import dev.apexnative.navigation.views.animations.SideAnimator
@@ -62,9 +63,15 @@ internal class ModalViews(
62
63
  try {
63
64
  val view = record.root.view
64
65
  (view.parent as? ViewGroup)?.removeView(view)
65
- // A sheet's content waits at the sheet's full height, under the status bar.
66
+ // A sheet's content waits at the sheet's full height, under the status bar, and at its width.
66
67
  val params = CoordinatorLayout.LayoutParams(MATCH, MATCH)
67
- if (record.isSheet) params.topMargin = statusBarTop()
68
+ if (record.isSheet) {
69
+ params.topMargin = statusBarTop()
70
+ params.gravity = Gravity.CENTER_HORIZONTAL
71
+ // The modals layer is hidden while it has no modal: the window's content gives the width.
72
+ val maxWidth = ModalSheet.MAX_WIDTH_DP.dp
73
+ if (((modalsLayer.parent as? View)?.width ?: 0) > maxWidth) params.width = maxWidth
74
+ }
68
75
  container.addView(view, params)
69
76
  modalsLayer.addView(container, FrameLayout.LayoutParams(MATCH, MATCH))
70
77
  } catch (e: Throwable) {
@@ -100,14 +107,13 @@ internal class ModalViews(
100
107
  container.translationY = 0f
101
108
  return playOwn(record, parts, motion, showing = true, onEnd)
102
109
  }
110
+ // The system's slide over the window below: the modal comes up whole, without a fade.
103
111
  val distance = distanceOf(container)
104
- val animator = ObjectAnimator.ofPropertyValuesHolder(
105
- container,
106
- PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, distance, 0f),
107
- PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
108
- )
109
- animator.interpolator = TimeInterpolator { ModalAnimations.decelerate(it) }
110
- return play(parts, animator, onEnd)
112
+ container.alpha = 1f
113
+ container.translationY = distance
114
+ val animator = ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, distance, 0f)
115
+ animator.interpolator = TimeInterpolator { ModalAnimations.linearOutSlowIn(it) }
116
+ return play(parts, animator, ModalAnimations.SHOW_DURATION, onEnd)
111
117
  }
112
118
 
113
119
  // The root's view leaves the container and goes into the sheet's, in one turn; the sheet slides to its first
@@ -163,13 +169,11 @@ internal class ModalViews(
163
169
  if (!animated || !seen) return none(onEnd)
164
170
  if (motion != null) return playOwn(record, parts, motion, showing = false, onEnd)
165
171
  val container = parts.container
166
- val animator = ObjectAnimator.ofPropertyValuesHolder(
167
- container,
168
- PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, container.translationY, distanceOf(container)),
169
- PropertyValuesHolder.ofFloat(View.ALPHA, container.alpha, 0f),
172
+ val animator = ObjectAnimator.ofFloat(
173
+ container, View.TRANSLATION_Y, container.translationY, distanceOf(container),
170
174
  )
171
- animator.interpolator = TimeInterpolator { ModalAnimations.accelerateDecelerate(it) }
172
- return play(parts, animator, onEnd)
175
+ animator.interpolator = TimeInterpolator { ModalAnimations.accelerate(it) }
176
+ return play(parts, animator, ModalAnimations.DISMISS_DURATION, onEnd)
173
177
  }
174
178
 
175
179
  override fun awaitTargets(
@@ -355,8 +359,8 @@ internal class ModalViews(
355
359
  private fun viewOf(node: ViewController<*>): View? =
356
360
  parts.entries.firstOrNull { it.key.root === node }?.value?.container ?: node.peekView()
357
361
 
358
- private fun play(parts: Parts, animator: Animator, onEnd: () -> Unit): () -> Unit {
359
- animator.duration = ModalAnimations.DURATION
362
+ private fun play(parts: Parts, animator: Animator, duration: Long, onEnd: () -> Unit): () -> Unit {
363
+ animator.duration = duration
360
364
  animator.addListener(object : AnimatorListenerAdapter() {
361
365
  override fun onAnimationEnd(animation: Animator) {
362
366
  if (parts.animator === animation) parts.animator = null
@@ -41,6 +41,10 @@ internal object BackCallbackState {
41
41
  entries.removeAll { it.navigator.get().let { attached -> attached == null || attached === navigator } }
42
42
  }
43
43
 
44
+ /** JavaScript listens to back (`BackHandler`): it hears a press before the navigation does. */
45
+ val hasJsListeners: Boolean
46
+ get() = jsListeners
47
+
44
48
  /** `setBackListeners` of JavaScript; false again when its JavaScript goes. */
45
49
  fun setJsListeners(active: Boolean) {
46
50
  jsListeners = active
@@ -31,6 +31,7 @@ import dev.apexnative.navigation.viewcontrollers.component.ContentReadiness
31
31
  import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
32
32
  import dev.apexnative.navigation.viewcontrollers.externalcomponent.ExternalComponentViewController
33
33
  import dev.apexnative.navigation.viewcontrollers.modal.ModalCoordinator
34
+ import dev.apexnative.navigation.viewcontrollers.modal.ModalRecord
34
35
  import dev.apexnative.navigation.viewcontrollers.modal.ModalViews
35
36
  import dev.apexnative.navigation.viewcontrollers.overlay.OverlayCoordinator
36
37
  import dev.apexnative.navigation.viewcontrollers.overlay.OverlayRecord
@@ -38,6 +39,7 @@ import dev.apexnative.navigation.viewcontrollers.overlay.OverlayViews
38
39
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
39
40
  import dev.apexnative.navigation.viewcontrollers.stack.PreparedPushes
40
41
  import dev.apexnative.navigation.viewcontrollers.stack.StackCommands
42
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
41
43
  import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
42
44
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
43
45
  import dev.apexnative.navigation.views.animations.FadeThrough
@@ -153,6 +155,15 @@ class Navigator(
153
155
  fun handleBack(listener: CommandListener): Boolean =
154
156
  modals.handleBack(listener) ?: (root?.handleBack(listener) == true)
155
157
 
158
+ /**
159
+ * The stack whose top [handleBack] would take away with the default pop: in the top modal, else in the root; none
160
+ * when back would do anything else.
161
+ */
162
+ internal fun stackPoppedByBack(): StackController? {
163
+ val modalRoot = modals.backTargetRoot()
164
+ return if (modalRoot != null) modalRoot.stackPoppedByBack() else root?.stackPoppedByBack()
165
+ }
166
+
156
167
  /**
157
168
  * [layer] may wait to show from now on ([WaitingLayer]): the nodes in it ask it through [isInWaitingLayer] and
158
169
  * [shownScreensChanged]. The root's layer is there from the start.
@@ -264,9 +275,7 @@ class Navigator(
264
275
  if (!made) return answer.reject(ErrorTexts.notDoneYet("setRoot"))
265
276
  val steps = layout.modals.mapIndexed { index, node ->
266
277
  val last = index == layout.modals.lastIndex
267
- RootCoordinator.LayerStep { done ->
268
- modals.showInChain(node.id, { makeLayer(node, isOverlay = false) }, last, done)
269
- }
278
+ ModalLayerStep(node, last)
270
279
  } + layout.overlays.map { node ->
271
280
  RootCoordinator.LayerStep { done ->
272
281
  overlays.showInChain(node.id, { makeLayer(node, isOverlay = true) }, done)
@@ -275,6 +284,35 @@ class Navigator(
275
284
  roots.setRoot({ factory.create(layout.root) }, answer, steps)
276
285
  }
277
286
 
287
+ // A modal of a `setRoot`'s layout: made with the root and held until its turn, so it renders while the root waits
288
+ // and comes in; made in its turn instead while an id of it still shows (a modal of the old root with that id goes
289
+ // with the old root). A modal that couldn't be made is skipped in its turn.
290
+ private inner class ModalLayerStep(private val node: LayoutNode, private val animated: Boolean) :
291
+ RootCoordinator.LayerStep {
292
+ private var prepared = false
293
+ private var held: ModalRecord? = null
294
+
295
+ override fun prepare() {
296
+ if (LayoutChecks.firstShownId(listOf(node), ::isShownNow) { false } != null) return
297
+ prepared = true
298
+ held = modals.hold(node.id, { factory.create(node) }, animated)
299
+ }
300
+
301
+ override fun discard() {
302
+ held?.let(modals::drop)
303
+ held = null
304
+ }
305
+
306
+ override fun show(done: () -> Unit): () -> Unit {
307
+ val record = held
308
+ held = null
309
+ if (record != null) return modals.release(record, done)
310
+ if (!prepared) return modals.showInChain(node.id, { makeLayer(node, isOverlay = false) }, animated, done)
311
+ done()
312
+ return {}
313
+ }
314
+ }
315
+
278
316
  // A modal or an overlay of a `setRoot`'s layout, made as its turn comes: an id that shows by then fails it.
279
317
  private fun makeLayer(node: LayoutNode, isOverlay: Boolean): ViewController<*> {
280
318
  val shown = LayoutChecks.firstShownId(listOf(node), ::isShownNow) { isOverlay && isReplacedOverlay(node, it) }
@@ -401,9 +439,10 @@ class Navigator(
401
439
  }
402
440
 
403
441
  /**
404
- * The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot` on its way resolves with
405
- * `''` (JavaScript sets its root again in the next activity), a modal's or an overlay's show and the commands that
406
- * wait for a modal with their roots' ids. Main thread only.
442
+ * The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot` whose root still waited
443
+ * resolves with `''` (JavaScript sets its root again in the next activity), one whose root showed resolves with
444
+ * its id, and a modal's or an overlay's show and the commands that wait for a modal resolve with their roots' ids.
445
+ * Main thread only.
407
446
  */
408
447
  internal fun destroyWithActivity() {
409
448
  destroyLayers(answerPending = true)
@@ -36,7 +36,8 @@ import dev.apexnative.navigation.views.animations.AnimationRules
36
36
  * The modals over the old root ([ModalsOverRoot]): taken as the `setRoot` is accepted (commands don't find them), given
37
37
  * back when it ends without replacing the root, carried with the old root into its transition and destroyed with it,
38
38
  * without `ANN.ModalDismissed`. The modals and the overlays of the layout ([LayerStep]) show after the root, one by
39
- * one, before the answer: a newer `setRoot` stops them.
39
+ * one, before the answer: a newer `setRoot` stops them. Its modals are made with the root ([LayerStep.prepare]) and
40
+ * render unseen while it waits and comes in, so each shows in its turn without a wait of its own.
40
41
  *
41
42
  * The steps that come later (the wait's timers and readiness signals, the end of the transition, the end of a layer,
42
43
  * a stack of the waiting root that changed its top) run under no runner: each goes through one guard, and a failure
@@ -128,11 +129,20 @@ internal class RootCoordinator(
128
129
  }
129
130
 
130
131
  /**
131
- * A modal or an overlay of the layout of a `setRoot`, shown after its root: [show] makes it and shows it, and calls
132
- * its argument once it showed or went (a failure is logged and skipped); what it returns stops it while it waits —
133
- * a newer `setRoot`: it goes without a word.
132
+ * A modal or an overlay of the layout of a `setRoot`, shown after its root: [show] shows it (made then, unless
133
+ * [prepare] made it), and calls its argument once it showed or went (a failure is logged and skipped); what it
134
+ * returns stops it while it waits — a newer `setRoot`: it goes without a word.
134
135
  */
135
136
  fun interface LayerStep {
137
+ /**
138
+ * Right after the root is placed, before its wait: a step may make its layer now, so that it renders unseen
139
+ * while the root waits and comes in, and shows without a wait of its own in its turn.
140
+ */
141
+ fun prepare() = Unit
142
+
143
+ /** The `setRoot` ended before this step's turn: what [prepare] made goes without a word. */
144
+ fun discard() = Unit
145
+
136
146
  fun show(done: () -> Unit): () -> Unit
137
147
  }
138
148
 
@@ -221,6 +231,7 @@ internal class RootCoordinator(
221
231
  modals.rootAccepted()
222
232
  val waitForRender = animationsOf(root).enter.waitForRender
223
233
  this.pending = pending
234
+ layers.forEach { it.prepare() }
224
235
  firstRoot.rootSet(waitForRender)
225
236
  await(pending, waitForRender)
226
237
  }
@@ -229,8 +240,9 @@ internal class RootCoordinator(
229
240
  /**
230
241
  * The activity's layers go: the `setRoot` on its way ends, then the root it replaced, the current root and its root
231
242
  * are destroyed, as at any destroy (their events reach only their own JavaScript). [answerPending]: the activity
232
- * went, the waiting `setRoot` resolves with `''` (JavaScript sets the root again in the next activity); otherwise
233
- * (a reload) it gets no answer.
243
+ * went; a `setRoot` whose root still waited resolves with `''` (JavaScript sets the root again in the next
244
+ * activity), one whose root showed (its transition ran, or the modals and overlays of its layout were showing)
245
+ * resolves with the root's id. Otherwise (a reload) it gets no answer.
234
246
  */
235
247
  fun destroyAll(answerPending: Boolean) {
236
248
  val pending = pending
@@ -245,7 +257,9 @@ internal class RootCoordinator(
245
257
  modals.replacedRootGone()
246
258
  pending?.let(::releaseState)
247
259
  if (pending != null && pending.phase == Phase.Waiting) firstRoot.rootDestroyed()
248
- if (answerPending) pending?.answer?.resolve("")
260
+ if (!answerPending || pending == null) return
261
+ // An empty id tells JavaScript the root didn't show: its start waits for the next activity and sets it there.
262
+ pending.answer.resolve(if (pending.phase == Phase.Waiting) "" else pending.root.id)
249
263
  }
250
264
 
251
265
  // Waits for the screens that show when the root shows, then runs its transition. Without `waitForRender` one limit
@@ -367,6 +381,14 @@ internal class RootCoordinator(
367
381
  if (!done) pending.stopLayer = stop
368
382
  }
369
383
 
384
+ // The layers of [pending] not shown yet go: what they made ahead goes unseen, the others aren't made. Each on its
385
+ // own: one that fails doesn't keep the others.
386
+ private fun discardLayers(pending: Pending) {
387
+ val rest = pending.layers.drop(pending.nextLayer)
388
+ pending.nextLayer = pending.layers.size
389
+ rest.forEach { attempt { it.discard() } }
390
+ }
391
+
370
392
  // A new `setRoot` came while this one was on its way.
371
393
  private fun supersede() {
372
394
  val pending = pending ?: return
@@ -389,14 +411,15 @@ internal class RootCoordinator(
389
411
  pending.replaced = null
390
412
  modals.replacedRootGone()
391
413
  }
392
- // Its root is the current root: the modal or the overlay that waits now goes without a word, the others
393
- // aren't made; those shown stay over its root, and go with it when the new root replaces it.
414
+ // Its root is the current root: the modal or the overlay that waits now goes without a word, the others go
415
+ // unseen or aren't made; those shown stay over its root, and go with it when the new root replaces it.
394
416
  Phase.Layers -> {
395
417
  pending.stopLayer?.invoke()
396
418
  pending.stopLayer = null
397
419
  releaseState(pending)
398
420
  }
399
421
  }
422
+ discardLayers(pending)
400
423
  pending.answer.reject(ErrorTexts.setRootReplaced(pending.root.id))
401
424
  }
402
425
 
@@ -426,6 +449,7 @@ internal class RootCoordinator(
426
449
  pending.wait = null
427
450
  when (pending.phase) {
428
451
  Phase.Waiting -> {
452
+ discardLayers(pending)
429
453
  attempt { takeAway(pending.root) }
430
454
  attempt { modals.rootKept() }
431
455
  attempt { firstRoot.rootDestroyed() }
@@ -451,6 +475,7 @@ internal class RootCoordinator(
451
475
  pending.stopLayer = null
452
476
  }
453
477
  }
478
+ discardLayers(pending)
454
479
  // A root that is up takes the splash away, as at its show.
455
480
  if (pending.phase != Phase.Waiting) attempt { onRootShown() }
456
481
  attempt { releaseState(pending) }
@@ -482,6 +507,7 @@ internal class RootCoordinator(
482
507
  // The root was destroyed while it waited, by something other than a newer `setRoot` or the activity going.
483
508
  private fun rootGone(pending: Pending) {
484
509
  this.pending = null
510
+ discardLayers(pending)
485
511
  releaseState(pending)
486
512
  layer.remove(pending.root)
487
513
  modals.rootKept()
@@ -124,18 +124,18 @@ internal class OverlayCoordinator(
124
124
  }
125
125
 
126
126
  /**
127
- * `dismissOverlay`: the overlays with the root id [componentId] go — one that waits is cancelled, one that shows
128
- * goes with its events (both, when a replacement of a shown one waits) — and [answer] resolves with the id. An id
129
- * no node has: `not shown`; a node that is no overlay's root (Android takes only the root's id): `not in an
130
- * overlay`.
127
+ * `dismissOverlay`: the overlay [componentId] is in — its root or any screen inside it — goes with every overlay of
128
+ * the same root id: one that waits is cancelled, one that shows goes with its events (both, when a replacement of
129
+ * a shown one waits). [answer] resolves with the overlay's root id, as on iOS. An id no node has: `not shown`; a
130
+ * node in no overlay: `not in an overlay`.
131
131
  */
132
132
  fun dismiss(componentId: String, answer: CommandAnswer) {
133
- if (host.find(componentId) == null) return answer.reject(ErrorTexts.screenNotShown(componentId))
134
- val matches = records.filter { it.rootId == componentId }
135
- if (matches.isEmpty()) return answer.reject(ErrorTexts.notInOverlay(componentId))
133
+ val node = host.find(componentId) ?: return answer.reject(ErrorTexts.screenNotShown(componentId))
134
+ val rootId = recordOf(node)?.rootId ?: return answer.reject(ErrorTexts.notInOverlay(componentId))
135
+ val matches = records.filter { it.rootId == rootId }
136
136
  matches.filter { it.state == OverlayRecord.State.Waiting }.forEach(::cancel)
137
137
  val shown = matches.filter { it.state == OverlayRecord.State.Shown }
138
- if (shown.isEmpty()) return answer.resolve(componentId)
138
+ if (shown.isEmpty()) return answer.resolve(rootId)
139
139
  shown.dropLast(1).forEach { remove(it, null) }
140
140
  remove(shown.last(), answer)
141
141
  }
@@ -185,6 +185,8 @@ internal class OverlayCoordinator(
185
185
 
186
186
  private fun start(make: () -> ViewController<*>, answer: CommandAnswer?, chainDone: (() -> Unit)?): OverlayRecord {
187
187
  val root = make()
188
+ // Before the root's view is made: its screens are drawn under the status bar from their first layout.
189
+ root.shownAsOverlay = true
188
190
  val record = OverlayRecord(root)
189
191
  // A waiting layer before its views join the window: what is in it isn't on screen from its first layout.
190
192
  record.screensChanged = { screensChanged(record) }
@@ -41,11 +41,18 @@ interface KeepsStatusBarRoom
41
41
  /** A node that shows screens in its columns (the split view): `showInColumn` and `setColumnShown` go to it. */
42
42
  interface ShowsColumns {
43
43
  /**
44
- * Shows [screen] in the column after the first one, in place of what it shows; [listener] once it shows, or with
45
- * the error reason, which names [from], the screen the command comes from. False when the node doesn't take it now
46
- * (its compact layout shows): the core pushes the screen over [from] instead.
44
+ * Shows [screen] in [column], in place of what it shows; [listener] once it shows, or with the error reason, which
45
+ * names [from], the screen the command comes from. [goesInStack] says whether [screen]'s layout is what a stack
46
+ * takes (a screen, a native screen, a pair), not a container. False when the node doesn't take it now (its compact
47
+ * layout shows): the core pushes the screen over [from] instead.
47
48
  */
48
- fun showScreen(screen: ViewController<*>, from: ViewController<*>, listener: CommandListener): Boolean
49
+ fun showScreen(
50
+ screen: ViewController<*>,
51
+ column: String,
52
+ goesInStack: Boolean,
53
+ from: ViewController<*>,
54
+ listener: CommandListener,
55
+ ): Boolean
49
56
 
50
57
  /** Shows or hides [column]; [done] once the columns have stopped moving. */
51
58
  fun setColumnShown(column: String, shown: Boolean, done: Runnable)
@@ -31,9 +31,9 @@ internal object ContainerBackground {
31
31
  if (!colour.hasValue()) return null
32
32
  val color = colour.get(node.activity, Color.TRANSPARENT) ?: Color.TRANSPARENT
33
33
  // The screens' band rule (`ScreenSafeArea.backgroundBand`) for a layout, which keeps no room at its top: the
34
- // status bar's height, but none when it is hidden or the layout is drawn under it, in a side menu, a sheet or
35
- // a pair.
36
- val full = options.statusBar.isHiddenOrDrawBehind || node.isInSideMenu || node.isInSheet ||
34
+ // status bar's height, but none when it is hidden or the layout is drawn under it (in an overlay by default),
35
+ // in a side menu, a sheet or a pair.
36
+ val full = options.statusBar.isHiddenOrDrawBehind(node.isInOverlay) || node.isInSideMenu || node.isInSheet ||
37
37
  node.parentController is KeepsStatusBarRoom
38
38
  val band = if (full) 0 else SystemUiUtils.getStatusBarHeight(node.activity)
39
39
  if (band <= 0) return ColorDrawable(color)
@@ -8,6 +8,7 @@ import dev.apexnative.navigation.react.CommandListener
8
8
  import dev.apexnative.navigation.viewcontrollers.bottomtabs.TabBarMotion
9
9
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
10
10
  import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
11
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
11
12
  import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
12
13
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
13
14
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
@@ -210,6 +211,8 @@ abstract class ParentController<V : ViewGroup>(
210
211
 
211
212
  override fun canHandleBack(): Boolean = getCurrentChild()?.canHandleBack() == true
212
213
 
214
+ override fun stackPoppedByBack(): StackController? = getCurrentChild()?.stackPoppedByBack()
215
+
213
216
  override fun destroy() {
214
217
  if (isDestroyed) return
215
218
  getChildControllers().forEach { it.destroy() }
@@ -16,6 +16,7 @@ import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
16
16
  import dev.apexnative.navigation.viewcontrollers.parent.ChildOffsetFromRest
17
17
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
18
18
  import dev.apexnative.navigation.viewcontrollers.sidemenu.SideMenuMotion.Side
19
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
19
20
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
20
21
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
21
22
  import dev.apexnative.navigation.views.sidemenu.SideMenuView
@@ -130,7 +131,7 @@ internal class SideMenuController(
130
131
 
131
132
  // `mergeOptions` with the side menu's id: its own options, then what a merge does, then the layouts around.
132
133
  override fun mergeOptions(options: Options) {
133
- this.options = this.options.copy().mergeWith(options)
134
+ mergeOwnOptions(options)
134
135
  applyMerge(options)
135
136
  performOnParentController { it.mergeChildOptions(SideMenuRules.withoutSideMenu(options), this) }
136
137
  }
@@ -183,6 +184,9 @@ internal class SideMenuController(
183
184
 
184
185
  override fun canHandleBack(): Boolean = motion.canGoBack || center.canHandleBack()
185
186
 
187
+ // An open menu closes first: no preview of a pop then.
188
+ override fun stackPoppedByBack(): StackController? = if (motion.canGoBack) null else center.stackPoppedByBack()
189
+
186
190
  // A button's press addressed to the side menu goes to the center.
187
191
  override fun sendOnNavigationButtonPressed(buttonId: String) = center.sendOnNavigationButtonPressed(buttonId)
188
192
 
@@ -1,6 +1,8 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.splitview
2
2
 
3
3
  import android.content.Context
4
+ import android.view.View
5
+ import android.view.ViewGroup
4
6
  import android.view.WindowInsets
5
7
  import androidx.core.graphics.Insets
6
8
  import androidx.core.view.WindowInsetsCompat
@@ -14,6 +16,27 @@ class SplitPanes(context: Context) : SlidingPaneLayout(context) {
14
16
  /** The panes are side by side. */
15
17
  var isSideBySide = false
16
18
 
19
+ /**
20
+ * Puts [next] in the place of the pane [old], with the structure and the params [old] had. The library wraps the
21
+ * second view it is given in a view of its own that holds back touches (`addView` with one child there), and its
22
+ * `removeView` takes that wrapper away: a plain remove and add would wrap a new primary pane, or give a new
23
+ * secondary one the wrapper's params, and lose its width and weight. So the secondary pane's content changes
24
+ * inside the wrapper, and a new primary pane goes in before the old one leaves, while two children are there.
25
+ */
26
+ internal fun replacePane(old: View, next: View) {
27
+ val holder = old.parent as? ViewGroup ?: return
28
+ val params = old.layoutParams
29
+ if (holder === this) {
30
+ // A copy: the width that `splitView.width` changes later is the new pane's own.
31
+ addView(next, indexOfChild(old), if (params is LayoutParams) LayoutParams(params) else params)
32
+ removeView(old)
33
+ } else {
34
+ val index = holder.indexOfChild(old)
35
+ holder.removeView(old)
36
+ holder.addView(next, index, params)
37
+ }
38
+ }
39
+
17
40
  override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
18
41
  if (!isSideBySide) return super.dispatchApplyWindowInsets(insets)
19
42
  val own = onApplyWindowInsets(insets)