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

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 (126) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +10 -4
  4. package/android/CORE.md +4 -1
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  7. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  8. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  9. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  11. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  12. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  13. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  16. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  17. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  18. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +12 -8
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +12 -11
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +2 -2
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +1 -1
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -3
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +2 -1
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +2 -1
  29. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  30. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  31. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  32. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  33. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  34. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  35. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  36. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  37. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  38. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  42. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  43. package/cli/app.js +1 -1
  44. package/cli/project.js +6 -2
  45. package/cli/setup/index.js +13 -0
  46. package/cli/setup/steps/appDelegate.js +66 -11
  47. package/cli/setup/steps/sceneDelegate.js +108 -0
  48. package/cli/upgrade/changelog.js +2 -2
  49. package/docs-md/architecture/transitions.md +1 -1
  50. package/docs-md/guides/deep-links.md +15 -33
  51. package/docs-md/start/installation.md +1 -0
  52. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  53. package/ios/Animations/ANNTransitionWait.mm +6 -7
  54. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  55. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  56. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  57. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  58. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  59. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  60. package/ios/CLAUDE.md +10 -2
  61. package/ios/CORE.md +3 -2
  62. package/ios/Contract/ANNCommandAnswer.h +4 -3
  63. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  64. package/ios/Contract/ANNCommandRunner.h +2 -2
  65. package/ios/Contract/ANNCommandRunner.mm +1 -1
  66. package/ios/Contract/ANNErrorTexts.h +6 -2
  67. package/ios/Contract/ANNErrorTexts.mm +18 -10
  68. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  69. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  70. package/ios/Core/ANNErrorHandler.h +8 -3
  71. package/ios/Core/ANNErrorHandler.mm +17 -1
  72. package/ios/Core/ANNEventContract.h +246 -0
  73. package/ios/Core/ANNEventContract.mm +225 -0
  74. package/ios/Core/ANNEventEmitter.h +5 -1
  75. package/ios/Core/ANNEventEmitter.mm +117 -80
  76. package/ios/Core/ANNHinge.mm +6 -4
  77. package/ios/Core/ANNSceneDelegate.mm +25 -15
  78. package/ios/Core/ANNSystemUI.h +3 -2
  79. package/ios/Core/ANNSystemUI.mm +7 -0
  80. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  81. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  82. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  83. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  84. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  85. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  86. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  87. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  88. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  89. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  90. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  91. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  92. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  93. package/ios/Options/ANNSheetOptions.h +4 -1
  94. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  95. package/ios/RECIPES.md +2 -1
  96. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  97. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  98. package/lib/module/ARCHITECTURE.md +7 -4
  99. package/lib/module/api/app/createNavigation.js +2 -0
  100. package/lib/module/api/app/createNavigation.js.map +1 -1
  101. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  102. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  103. package/lib/module/interfaces/contract/errors.json +275 -0
  104. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  105. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  106. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  107. package/lib/typescript/interfaces/Events.d.ts +9 -0
  108. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  109. package/lib/typescript/interfaces/options/animations.d.ts +17 -13
  110. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  111. package/lib/typescript/interfaces/options/modals.d.ts +15 -11
  112. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  113. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  114. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  115. package/lib/typescript/interfaces/options/system.d.ts +4 -3
  116. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  117. package/package.json +2 -25
  118. package/src/ARCHITECTURE.md +7 -4
  119. package/src/api/app/createNavigation.ts +2 -0
  120. package/src/api/linking/dispatchedLinks.ts +18 -0
  121. package/src/interfaces/Events.ts +10 -0
  122. package/src/interfaces/contract/errors.json +275 -0
  123. package/src/interfaces/options/animations.ts +17 -13
  124. package/src/interfaces/options/modals.ts +15 -11
  125. package/src/interfaces/options/sideMenu.ts +7 -6
  126. package/src/interfaces/options/system.ts +4 -3
@@ -36,7 +36,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
36
36
  ) : Call()
37
37
 
38
38
  /** The icon's size in dp, width over height. */
39
- data class IconSize(override val index: Int, val width: Int?, val height: Int?) : Call()
39
+ data class IconSize(override val index: Int, val width: Double?, val height: Double?) : Call()
40
40
 
41
41
  data class Font(override val index: Int, val font: FontOptions) : Call()
42
42
 
@@ -86,7 +86,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
86
86
  options.icon.get(null)?.let { Call.Icon(index, it) },
87
87
  options.selectedIcon.get(null)?.let { Call.SelectedIcon(index, it) },
88
88
  colors(index, options),
89
- Call.IconSize(index, options.iconWidth.get(null)?.toInt(), options.iconHeight.get(null)?.toInt()),
89
+ Call.IconSize(index, options.iconWidth.get(null), options.iconHeight.get(null)),
90
90
  Call.Font(index, options.font.copy()),
91
91
  Call.FontSizes(index, options.selectedFontSize.get(null)?.toFloat(), options.fontSize.get(null)?.toFloat()),
92
92
  Call.TestId(index, options.testID.get(null)),
@@ -110,7 +110,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
110
110
  calls += colors(index, resolved)
111
111
  }
112
112
  if (merge.iconWidth.hasValue() || merge.iconHeight.hasValue()) {
113
- calls += Call.IconSize(index, resolved.iconWidth.get(null)?.toInt(), resolved.iconHeight.get(null)?.toInt())
113
+ calls += Call.IconSize(index, resolved.iconWidth.get(null), resolved.iconHeight.get(null))
114
114
  }
115
115
  if (merge.font.hasValue()) calls += Call.Font(index, resolved.font.copy())
116
116
  if (merge.fontSize.hasValue() || merge.selectedFontSize.hasValue()) {
@@ -147,8 +147,11 @@ internal class ModalCoordinator(
147
147
  */
148
148
  fun carry(record: ModalRecord, oldRoot: ViewController<*>?)
149
149
 
150
- /** New [options] (resolved) of the record: its `testID`s, its sheet's `update` ([move]: to its selection). */
151
- fun update(record: ModalRecord, options: Options, move: Boolean)
150
+ /**
151
+ * New [options] (resolved) of the record: its `testID`s, its sheet's `update`; [merge] is the `mergeOptions`
152
+ * that brought them, which alone may move the sheet, or null when the shown screen changed.
153
+ */
154
+ fun update(record: ModalRecord, options: Options, merge: Options?)
152
155
 
153
156
  /** The colors of the record's own views again (a light/dark switch). */
154
157
  fun repaint(record: ModalRecord) = Unit
@@ -353,13 +356,14 @@ internal class ModalCoordinator(
353
356
  // Options.
354
357
 
355
358
  /**
356
- * A `mergeOptions` went into [node] (`mergeOptions` step 5): when it is in a modal, the modal's sheet takes its
357
- * options again (it re-reads the resolved ones at any merge) and its `testID`s apply again.
359
+ * A `mergeOptions` of [merge] went into [node] (`mergeOptions` step 5): when it is in a modal, the modal's sheet
360
+ * takes its resolved options again, moving only to a `selectedDetent` that [merge] names, and its `testID`s apply
361
+ * again.
358
362
  */
359
- fun optionsMergedInto(node: ViewController<*>) {
363
+ fun optionsMergedInto(node: ViewController<*>, merge: Options) {
360
364
  val record = recordOf(node) ?: return
361
365
  if (record.root.isDestroyed) return
362
- views.update(record, record.root.resolveShownOptions(DefaultOptions.current), move = true)
366
+ views.update(record, record.root.resolveShownOptions(DefaultOptions.current), merge)
363
367
  }
364
368
 
365
369
  /**
@@ -369,7 +373,7 @@ internal class ModalCoordinator(
369
373
  fun visibleScreenChanged(node: ViewController<*>) {
370
374
  val record = recordOf(node) ?: return
371
375
  if (record.state == State.Waiting || record.root.isDestroyed) return
372
- views.update(record, record.root.resolveShownOptions(DefaultOptions.current), move = false)
376
+ views.update(record, record.root.resolveShownOptions(DefaultOptions.current), merge = null)
373
377
  }
374
378
 
375
379
  /** The colors of the modals' own views again, after a light/dark switch. */
@@ -1230,7 +1234,7 @@ internal class ModalCoordinator(
1230
1234
  val screen = AppearanceDispatcher.visibleNode(record.root)
1231
1235
  if (screen.isDestroyed) return
1232
1236
  screen.mergeOptions(options)
1233
- optionsMergedInto(screen)
1237
+ optionsMergedInto(screen, options)
1234
1238
  }
1235
1239
 
1236
1240
  private inline fun whileTaking(work: () -> Unit) {
@@ -122,31 +122,32 @@ internal class ModalSheet(
122
122
  // where the sheet is laid out.
123
123
  layout.addView(container, CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).also { it.behavior = behavior })
124
124
  shown = onShown
125
- update(sheetOptions, swipeToDismiss, move = false)
125
+ update(sheetOptions, swipeToDismiss, merge = null)
126
+ selected = sheetOptions.selectedDetent?.takeIf { it in detents }
126
127
  // Laid out hidden first, so it slides up from the bottom.
127
128
  container.post { if (closing == null) behavior.state = stateOf(selected ?: detents.first()) }
128
129
  }
129
130
 
130
- /** New options from mergeOptions: the detents, the look, and the selected detent, which the sheet moves to. */
131
- fun update(sheetOptions: SheetOptions, swipeToDismiss: Boolean, move: Boolean = true) {
132
- val previous = selected
131
+ /**
132
+ * New resolved options: the detents and the look. The sheet moves only as [SheetStops] says, after a `mergeOptions`
133
+ * of [merge]; without one (the shown screen changed, the show) it stays where it is.
134
+ */
135
+ fun update(sheetOptions: SheetOptions, swipeToDismiss: Boolean, merge: SheetOptions?) {
133
136
  options = sheetOptions
134
137
  this.swipeToDismiss = swipeToDismiss
135
138
  detents = sheetOptions.detents?.takeIf { it.isNotEmpty() }?.sortedBy { heightOf(it) } ?: listOf(SheetDetent.Large)
136
- selected = sheetOptions.selectedDetent?.takeIf { it in detents }
137
139
  grabber.visibility = if (sheetOptions.grabber.isTrue) View.VISIBLE else View.GONE
138
140
  behavior.dragAnywhere = sheetOptions.expandsOnScroll.get(true)
139
141
  container.invalidateOutline()
140
142
  configure()
141
- if (!move || closing != null) return
143
+ if (merge == null || closing != null) return
142
144
  val settled = detentOf(behavior.state)
143
145
  if (settled != null) behavior.isHideable = swipeToDismiss && settled == detents.first()
144
- val target = when {
145
- selected != null && selected != previous -> selected
146
- settled != null && settled !in detents -> selected ?: detents.first()
147
- else -> null
146
+ val target = SheetStops.target(settled, detents, merge.selectedDetent, sheetOptions.selectedDetent)
147
+ if (target != null) {
148
+ selected = target
149
+ behavior.state = stateOf(target)
148
150
  }
149
- if (target != null) behavior.state = stateOf(target)
150
151
  updateScrim()
151
152
  }
152
153
 
@@ -245,10 +245,10 @@ internal class ModalViews(
245
245
  hideEmptyLayer()
246
246
  }
247
247
 
248
- override fun update(record: ModalRecord, options: Options, move: Boolean) {
248
+ override fun update(record: ModalRecord, options: Options, merge: Options?) {
249
249
  val parts = parts[record] ?: return
250
250
  applyTestIds(record, parts, options)
251
- parts.sheet?.update(options.modal.sheet, swipeToDismissOf(options), move)
251
+ parts.sheet?.update(options.modal.sheet, swipeToDismissOf(options), merge?.modal?.sheet)
252
252
  }
253
253
 
254
254
  override fun repaint(record: ModalRecord) {
@@ -0,0 +1,25 @@
1
+ package dev.apexnative.navigation.viewcontrollers.modal
2
+
3
+ import dev.apexnative.navigation.options.SheetDetent
4
+
5
+ /**
6
+ * Where a shown sheet goes at new options, as on iOS: the sheet keeps the stop it stands at (the user's last drag
7
+ * included) until a `selectedDetent` arrives in a `mergeOptions`, or until its stop is no longer among the detents.
8
+ */
9
+ internal object SheetStops {
10
+ /**
11
+ * The stop to move to, or null to stay: [arrived] (the `selectedDetent` the merge names) when it is among
12
+ * [detents]; else, when [settled] (the stop the sheet rests at) is no longer one of them, [selected] (the resolved
13
+ * `selectedDetent`) when it is, or the lowest.
14
+ */
15
+ fun target(
16
+ settled: SheetDetent?,
17
+ detents: List<SheetDetent>,
18
+ arrived: SheetDetent?,
19
+ selected: SheetDetent?,
20
+ ): SheetDetent? = when {
21
+ arrived != null && arrived in detents -> arrived
22
+ settled != null && settled !in detents -> selected?.takeIf { it in detents } ?: detents.first()
23
+ else -> null
24
+ }
25
+ }
@@ -214,7 +214,7 @@ class Navigator(
214
214
  * it itself, `ComponentPresenter`); the system UI follows (only what changed is written).
215
215
  */
216
216
  internal fun optionsMerged(node: ViewController<*>, options: Options = Options()) {
217
- modals.optionsMergedInto(node)
217
+ modals.optionsMergedInto(node, options)
218
218
  overlays.optionsMergedInto(node)
219
219
  val navigationBar = options.navigationBar
220
220
  val padding = navigationBar.drawBehind.hasValue() || navigationBar.backgroundColor.hasValue()
@@ -255,9 +255,7 @@ internal class SideMenuController(
255
255
  }
256
256
 
257
257
  // The system UI follows the screen the user sees now, at rest: the open menu's, the center's once a close
258
- // ends. The owner reads it before the next frame, when a menu that closed is hidden already. An open without
259
- // animation runs this inside `AppearanceDispatcher.withoutWillAppear`: what starts to appear from here would
260
- // send no WillAppear.
258
+ // ends. The owner reads it before the next frame, when a menu that closed is hidden already.
261
259
  override fun shownChanged() {
262
260
  navigator?.shownScreensChanged(this@SideMenuController)
263
261
  navigator?.systemUi?.invalidate()
@@ -16,8 +16,8 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
16
  * the drawer calls `onDrawerClosed` only for a drawer that had opened.
17
17
  * - The menu screen's events: WillAppear at the side's first movement, DidAppear when it opened, DidDisappear when it
18
18
  * rests closed; only while the side menu has appeared ([Host.appeared]) — otherwise they come with the side menu's
19
- * own appearance (its visible children). An open without animation sends DidAppear without WillAppear
20
- * (`AppearanceDispatcher.withoutWillAppear`).
19
+ * own appearance (its visible children). An open without animation sends both too, WillAppear first, as on iOS:
20
+ * the drawer jumps to its open offset before it calls the open.
21
21
  * - At each rest: one state update after the menu's events, the back callback, and the layers that follow the shown
22
22
  * screen.
23
23
  * - A drawer that doesn't call back (not laid out since it joined the window, or not in one: `DrawerLayout` only
@@ -246,11 +246,8 @@ internal class SideMenuMotion(
246
246
  private fun move(side: Side, open: Boolean, animate: Boolean) {
247
247
  val drawer = drawer()
248
248
  val silent = !drawer.callsBack
249
- val run = {
250
- if (open) drawer.open(side, animate) else drawer.close(side, animate)
251
- if (silent) record(side, open)
252
- }
253
- if (open && !animate && host.appeared) AppearanceDispatcher.withoutWillAppear(run) else run()
249
+ if (open) drawer.open(side, animate) else drawer.close(side, animate)
250
+ if (silent) record(side, open)
254
251
  }
255
252
 
256
253
  private fun record(side: Side, open: Boolean) {
@@ -18,15 +18,14 @@ import dev.apexnative.navigation.views.sidemenu.SideMenuView
18
18
  * `testID`s. Each moment applies its part, from the side menu's resolved options
19
19
  * (`SideMenuController.resolveShownOptions`: the current part's chain, the center's `sideMenu` over an open menu's) —
20
20
  * at creation everything with the heights, at every appearance of a screen inside everything but the heights, at a
21
- * merge the fields it names, at a repaint the background and the `testID`s. The locks of `enabled` and the command
21
+ * merge the fields it names (a width or a height at once, an open menu too), at a repaint the background and the
22
+ * `testID`s. The locks of `enabled` and the command
22
23
  * `visible` are the node's ([SideMenuMotion]). Main thread only.
23
24
  */
24
25
  internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Options()) {
25
- /** The side menu's [view] is made, with [options], resolved: the heights, read only now. */
26
+ /** The side menu's [view] is made, with [options], resolved: the heights too, which only a merge changes after. */
26
27
  fun applyOnCreation(view: SideMenuView, options: Options) {
27
- for (side in Side.entries) {
28
- view.setPartHeight(side, SideMenuRules.height(SideMenuRules.sideOf(options.sideMenu, side), ::toPixels))
29
- }
28
+ applyHeights(view, options.sideMenu)
30
29
  applyWidths(view, options.sideMenu)
31
30
  applyBackground(view, options)
32
31
  applyTestIds(view, options.sideMenu)
@@ -46,14 +45,18 @@ internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Optio
46
45
  /**
47
46
  * A merge reached the side menu, its own or through a screen inside (`mergeOptions` step 3): the fields [options]
48
47
  * name, with the values of the side menu's resolution now, so a screen off what shows changes nothing until it
49
- * appears. A width waits for the next appearance, a height for nothing.
48
+ * appears. A width or a height resizes its menu at once, open or closed, as on iOS.
50
49
  */
51
50
  override fun mergeOptions(viewController: ViewController<*>, options: Options) {
52
51
  val layout = viewController.peekView() as? SideMenuView ?: return
53
52
  val touchesBackground = SideMenuRules.touchesBackground(options)
54
53
  val touchesTestIds = SideMenuRules.touchesTestIds(options.sideMenu)
55
- if (!touchesBackground && !touchesTestIds) return
54
+ val touchesWidths = SideMenuRules.touchesWidths(options.sideMenu)
55
+ val touchesHeights = SideMenuRules.touchesHeights(options.sideMenu)
56
+ if (!touchesBackground && !touchesTestIds && !touchesWidths && !touchesHeights) return
56
57
  val shown = viewController.resolveShownOptions(DefaultOptions.current)
58
+ if (touchesWidths) applyWidths(layout, shown.sideMenu)
59
+ if (touchesHeights) applyHeights(layout, shown.sideMenu)
57
60
  if (touchesBackground) applyBackground(layout, shown)
58
61
  if (touchesTestIds) applyTestIds(layout, shown.sideMenu)
59
62
  }
@@ -71,6 +74,12 @@ internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Optio
71
74
  }
72
75
  }
73
76
 
77
+ private fun applyHeights(view: SideMenuView, options: SideMenuOptions) {
78
+ for (side in Side.entries) {
79
+ view.setPartHeight(side, SideMenuRules.height(SideMenuRules.sideOf(options, side), ::toPixels))
80
+ }
81
+ }
82
+
74
83
  // `layout.backgroundColor` on the drawer, its whole height, under the status bar; unset leaves the view as it is,
75
84
  // `'NoColor'` clears it.
76
85
  private fun applyBackground(view: View, options: Options) {
@@ -54,6 +54,13 @@ internal object SideMenuRules {
54
54
  fun touchesTestIds(merge: SideMenuOptions): Boolean =
55
55
  merge.testID.hasValue() || merge.left.testID.hasValue() || merge.right.testID.hasValue()
56
56
 
57
+ /** A merge names `width` of a side: the menus take their widths again. */
58
+ fun touchesWidths(merge: SideMenuOptions): Boolean = merge.left.width.hasValue() || merge.right.width.hasValue()
59
+
60
+ /** A merge names `height` of a side: the menus take their heights again. */
61
+ fun touchesHeights(merge: SideMenuOptions): Boolean =
62
+ merge.left.height.hasValue() || merge.right.height.hasValue()
63
+
57
64
  /** A merge names the background of the side menu (`layout.backgroundColor`). */
58
65
  fun touchesBackground(merge: Options): Boolean = merge.layout.backgroundColor.hasValue()
59
66
 
@@ -64,7 +71,7 @@ internal object SideMenuRules {
64
71
  fun wantedWidth(options: SideMenuSideOptions, toPixels: (Double) -> Int): Int? =
65
72
  options.width.get(null)?.let { max(0, toPixels(it)) }
66
73
 
67
- /** The height of a side, in pixels: `height` (dp) by [toPixels], read at creation only; none: the whole height. */
74
+ /** The height of a side, in pixels: `height` (dp) by [toPixels]; none: the whole height. */
68
75
  fun height(options: SideMenuSideOptions, toPixels: (Double) -> Int): Int? =
69
76
  options.height.get(null)?.let { max(0, toPixels(it)) }
70
77
 
@@ -31,6 +31,7 @@ import dev.apexnative.navigation.viewcontrollers.parent.ShownChildren
31
31
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
32
32
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
33
33
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
34
+ import kotlin.math.roundToInt
34
35
 
35
36
  /**
36
37
  * A split view: the primary pane beside the secondary one when the width takes both (a tablet, a phone in landscape).
@@ -489,7 +490,7 @@ class SplitViewController(
489
490
  root.mergeOptions(options)
490
491
  }
491
492
 
492
- private fun dp(value: Double) = UiUtils.dpToPx(activity, value.toFloat()).toInt()
493
+ private fun dp(value: Double) = UiUtils.dpToPx(activity, value.toFloat()).roundToInt()
493
494
 
494
495
  companion object {
495
496
  private const val PRIMARY = "primary"
@@ -60,6 +60,7 @@ import dev.apexnative.navigation.views.stack.topbar.TopBarDefaults
60
60
  import dev.apexnative.navigation.views.stack.topbar.titlebar.BarButtonComponentCell
61
61
  import dev.apexnative.navigation.views.stack.topbar.titlebar.ButtonBar
62
62
  import dev.apexnative.navigation.views.stack.topbar.titlebar.IconBackgroundDrawable
63
+ import kotlin.math.roundToInt
63
64
 
64
65
  /**
65
66
  * What the bar of one stack shows: the options of the screen on top of the stack, resolved (a pair's options screen),
@@ -881,7 +882,7 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
881
882
  else -> TitleBar.Alignment.Start
882
883
  }
883
884
 
884
- private fun dp(value: Double): Int = UiUtils.dpToPx(activity, value.toFloat()).toInt()
885
+ private fun dp(value: Double): Int = UiUtils.dpToPx(activity, value.toFloat()).roundToInt()
885
886
 
886
887
  // What makes a button's look, as a text: the parts that differ make it apply again.
887
888
  private fun buttonKey(context: Context, button: ButtonOptions): String = listOf(
@@ -98,8 +98,8 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
98
98
  var iconInactiveColor: Int? = null
99
99
  var titleActiveColor: Int? = null
100
100
  var titleInactiveColor: Int? = null
101
- var iconWidth: Int? = null
102
- var iconHeight: Int? = null
101
+ var iconWidth: Double? = null
102
+ var iconHeight: Double? = null
103
103
  var typeface: Typeface? = null
104
104
  var activeTextSize: Float? = null
105
105
  var inactiveTextSize: Float? = null
@@ -246,9 +246,9 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
246
246
 
247
247
  fun setTabTestId(index: Int, testId: String?) = updateStyle(index) { it.testId = testId }
248
248
 
249
- fun setIconWidth(index: Int, width: Int?) = updateStyle(index) { it.iconWidth = width }
249
+ fun setIconWidth(index: Int, width: Double?) = updateStyle(index) { it.iconWidth = width }
250
250
 
251
- fun setIconHeight(index: Int, height: Int?) = updateStyle(index) { it.iconHeight = height }
251
+ fun setIconHeight(index: Int, height: Double?) = updateStyle(index) { it.iconHeight = height }
252
252
 
253
253
  fun setTitleTypeface(index: Int, typeface: Typeface?) = updateStyle(index) { it.typeface = typeface }
254
254
 
@@ -475,7 +475,7 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
475
475
  view.setIconTintList(tint(style.iconActiveColor, style.iconInactiveColor, itemIconTintList))
476
476
  view.setTextColor(tint(style.titleActiveColor, style.titleInactiveColor, itemTextColor))
477
477
  val iconSize = style.iconWidth ?: style.iconHeight
478
- view.setIconSize(if (iconSize != null) UiUtils.dpToPx(context, iconSize) else itemIconSize)
478
+ view.setIconSize(if (iconSize != null) UiUtils.dpToPixels(context, iconSize) else itemIconSize)
479
479
  view.findViewById<TextView>(MaterialR.id.navigation_bar_item_small_label_view)?.let {
480
480
  style.typeface?.let { typeface -> it.typeface = typeface }
481
481
  style.inactiveTextSize?.let { size -> it.setTextSize(TypedValue.COMPLEX_UNIT_SP, size) }
@@ -0,0 +1,31 @@
1
+ package dev.apexnative.navigation.views.element
2
+
3
+ import dev.apexnative.navigation.utils.DevelopmentLog
4
+
5
+ /**
6
+ * The isolation of every element of the transitions: [work], a step of the element named [element] (its `nativeID` in
7
+ * quotes, or a pair's two), runs; an exception in it skips that element only — one line in the log
8
+ * ([elementSkipped]), and false — while the rest of the transition plays and its command completes.
9
+ */
10
+ internal inline fun elementStep(
11
+ element: String,
12
+ noinline log: (String) -> Unit = DevelopmentLog::warn,
13
+ work: () -> Unit,
14
+ ): Boolean = try {
15
+ work()
16
+ true
17
+ } catch (e: Exception) {
18
+ elementSkipped(element, e.toString(), log)
19
+ false
20
+ }
21
+
22
+ /** The line in the log for the element named [element], skipped for [reason]: it shows and hides with its screen. */
23
+ internal fun elementSkipped(element: String, reason: String, log: (String) -> Unit = DevelopmentLog::warn) =
24
+ log("[ApexNavigation] the transition's element $element is skipped, it shows and hides with its screen while the " +
25
+ "rest plays: $reason")
26
+
27
+ /** The name of an element in the log. */
28
+ internal fun elementName(nativeId: String) = "'$nativeId'"
29
+
30
+ /** The name of a shared pair in the log. */
31
+ internal fun pairName(fromId: String, toId: String) = "'$fromId' -> '$toId'"
@@ -7,7 +7,6 @@ import android.os.Build
7
7
  import android.util.Log
8
8
  import android.view.View
9
9
  import android.view.ViewGroup
10
- import com.facebook.react.uimanager.util.ReactFindViewUtil
11
10
  import com.facebook.react.views.view.ReactViewGroup
12
11
  import dev.apexnative.navigation.options.ElementTransitionOptions
13
12
  import dev.apexnative.navigation.options.LayoutAnimation
@@ -23,12 +22,13 @@ import dev.apexnative.navigation.views.element.animators.SharedAnimatorFactory
23
22
  * The elements of one transition: the element transitions and the targets of the shared elements found by their
24
23
  * `nativeID` (depth first, the first match) in the whole view of the screen each side shows — an element in the screen
25
24
  * that goes first, then in the one that comes; a pair's `fromId` in the screen that goes, its `toId` in the one that
26
- * comes. What isn't found, what is in another transition's layer and an image that isn't loaded is skipped; the rest
27
- * plays. Each view found moves into a [TransitionLayer] at its place on the screen, in the order its hierarchy draws
28
- * it, and an [ElementStub] takes its index in its parent. A pair's `from` stays where it is, hidden while its copy
29
- * flies. [finish] puts every view back at its index with its layout, its pivot and the values of its properties read
30
- * before — unless React removed its stub meanwhile: then it is gone for good —, shows the `from`s again and takes the
31
- * layer away. Main thread only.
25
+ * comes; each screen is walked once for all of them ([TransitionViews]). What isn't found, what is in another
26
+ * transition's layer and an image that isn't loaded is skipped; so is an element whose start fails ([elementStep]):
27
+ * it goes back at once and the rest plays. Each view found moves into a [TransitionLayer] at its place on the screen,
28
+ * in the order its hierarchy draws it, and an [ElementStub] takes its index in its parent. A pair's `from` stays where
29
+ * it is, hidden while its copy flies. [finish] puts every view back at its index with its layout, its pivot and the
30
+ * values of its properties read before — unless React removed its stub meanwhile: then it is gone for good —, shows
31
+ * the `from`s again and takes the layer away. Main thread only.
32
32
  */
33
33
  internal class ElementTransition private constructor(private val layer: TransitionLayer) {
34
34
  // A view in the layer: where it came from and what it was.
@@ -89,18 +89,41 @@ internal class ElementTransition private constructor(private val layer: Transiti
89
89
  view.rotationX, view.rotationY, view.pivotX, view.pivotY,
90
90
  )
91
91
  val stub = ElementStub(view.context, view)
92
- parent.removeViewAt(index)
93
- parent.addView(stub, index, params)
94
- stub.layout(frame.left, frame.top, frame.right, frame.bottom)
95
- val left = originX + frame.left
96
- val top = originY + frame.top
97
- layer.put(view, left, top, frame.width(), frame.height())
92
+ try {
93
+ parent.removeViewAt(index)
94
+ parent.addView(stub, index, params)
95
+ stub.layout(frame.left, frame.top, frame.right, frame.bottom)
96
+ val left = originX + frame.left
97
+ val top = originY + frame.top
98
+ layer.put(view, left, top, frame.width(), frame.height())
99
+ } catch (e: RuntimeException) {
100
+ putBack(view, stub, parent, index, params, frame)
101
+ throw e
102
+ }
98
103
  // Scale and rotations turn about the center.
99
104
  view.pivotX = frame.width() / 2f
100
105
  view.pivotY = frame.height() / 2f
101
106
  return Moved(view, stub, params, frame, values, pivotSet, originX, originY).also { moved += it }
102
107
  }
103
108
 
109
+ // A move that failed half way undone: the view back at its index in its parent, the stub gone.
110
+ private fun putBack(
111
+ view: View,
112
+ stub: ElementStub,
113
+ parent: ViewGroup,
114
+ index: Int,
115
+ params: ViewGroup.LayoutParams,
116
+ frame: Rect,
117
+ ) {
118
+ if (view.parent === layer) layer.removeView(view)
119
+ val stubIndex = parent.indexOfChild(stub)
120
+ if (stubIndex >= 0) parent.removeViewAt(stubIndex)
121
+ if (view.parent == null) {
122
+ parent.addView(view, index.coerceAtMost(parent.childCount), params)
123
+ view.layout(frame.left, frame.top, frame.right, frame.bottom)
124
+ }
125
+ }
126
+
104
127
  private fun giveBack(item: Moved) {
105
128
  val view = item.view
106
129
  // A commit of Fabric meanwhile laid it out in its parent's coordinates: the newest such frame holds.
@@ -156,13 +179,26 @@ internal class ElementTransition private constructor(private val layer: Transiti
156
179
  from, to, toParent, layer, options, fallbackDuration, DevelopmentLog::warn,
157
180
  ) ?: return false
158
181
  val alpha = item.values[ALPHA]
182
+ val fromAlpha = from.alpha
159
183
  to.alpha = 0f
160
- hidden += from to from.alpha
184
+ hidden += from to fromAlpha
161
185
  flight.animator.addListener(object : AnimatorListenerAdapter() {
186
+ private var stopped = false
187
+
162
188
  override fun onAnimationStart(animation: Animator) {
189
+ if (stopped) return
163
190
  from.alpha = 0f
164
191
  if (!flight.drawsPictures) to.alpha = alpha
165
192
  }
193
+
194
+ // A flight whose start or frame failed stops (`MoveAnimator`), maybe inside its own start: `from` shows
195
+ // again in its screen, and a `to` that moves itself in its place, as at any stop (a flight of pictures
196
+ // shows its `to` at its end).
197
+ override fun onAnimationCancel(animation: Animator) {
198
+ stopped = true
199
+ from.alpha = fromAlpha
200
+ if (!flight.drawsPictures) to.alpha = alpha
201
+ }
166
202
  })
167
203
  animators += flight.animator
168
204
  return true
@@ -195,20 +231,23 @@ internal class ElementTransition private constructor(private val layer: Transiti
195
231
  ): ElementTransition? {
196
232
  val fromView = from?.let { AppearanceDispatcher.visibleNode(it).peekView() }
197
233
  val toView = AppearanceDispatcher.visibleNode(to).peekView()
234
+ val transitions = animation.elementTransitions.transitions
235
+ val shared = animation.sharedElements.get()
236
+ val views = TransitionViews(shared, transitions, fromView, toView)
198
237
  val taken = HashSet<View>()
199
238
  val elements = ArrayList<Pair<ElementTransitionOptions, View>>()
200
- for (element in animation.elementTransitions.transitions) {
239
+ for (element in transitions) {
201
240
  if (element.id.isEmpty()) continue
202
- val view = find(fromView, element.id) ?: find(toView, element.id) ?: continue
241
+ val view = found(views.leaving(element.id)) ?: found(views.arriving(element.id)) ?: continue
203
242
  if (TransitionLayer.holds(view) || !taken.add(view)) continue
204
243
  elements += element to view
205
244
  }
206
245
  val pairs = ArrayList<Triple<SharedElementTransitionOptions, View, View>>()
207
- for (pair in animation.sharedElements.get()) {
246
+ for (pair in shared) {
208
247
  val fromId = pair.fromId.get(null) ?: continue
209
248
  val toId = pair.toId.get(null) ?: continue
210
- val source = find(fromView, fromId) ?: continue
211
- val target = find(toView, toId) ?: continue
249
+ val source = found(views.leaving(fromId)) ?: continue
250
+ val target = found(views.arriving(toId)) ?: continue
212
251
  if (source === target || TransitionLayer.holds(source) || TransitionLayer.holds(target)) continue
213
252
  if (target in taken || source in taken) continue
214
253
  if (!loaded(source, fromId) || !loaded(target, toId)) continue
@@ -246,8 +285,7 @@ internal class ElementTransition private constructor(private val layer: Transiti
246
285
  null
247
286
  }
248
287
 
249
- private fun find(root: View?, nativeId: String): View? =
250
- root?.let { ReactFindViewUtil.findView(it, nativeId) }?.takeUnless { it is ElementStub }
288
+ private fun found(view: View?): View? = view?.takeUnless { it is ElementStub }
251
289
 
252
290
  // An image not loaded drops out of the transition; its wait logged it already, else it is logged now.
253
291
  private fun loaded(view: View, id: String): Boolean {
@@ -258,24 +296,44 @@ internal class ElementTransition private constructor(private val layer: Transiti
258
296
  }
259
297
 
260
298
  // The views into the layer in the order they are drawn, then their animators; a pair that doesn't fly goes back at
261
- // once.
299
+ // once. Each element's steps run on their own ([elementStep]): one that fails goes back, the others play.
262
300
  private fun fill(
263
301
  host: ViewGroup,
264
302
  elements: List<Pair<ElementTransitionOptions, View>>,
265
303
  pairs: List<Triple<SharedElementTransitionOptions, View, View>>,
266
304
  fallbackDuration: Long,
267
305
  ) {
306
+ val names = HashMap<View, String>()
307
+ for ((options, view) in elements) names[view] = elementName(options.id)
308
+ for ((options, _, target) in pairs) names[target] = pairName(options.fromId.get(""), options.toId.get(""))
268
309
  val views = elements.map { it.second } + pairs.map { it.third }
269
310
  val parents = views.associateWith { it.parent as? View }
270
311
  val placed = HashMap<View, Moved>()
271
- for (view in DrawOrder.sorted(views, host) { it }) moveIntoLayer(view)?.let { placed[view] = it }
272
- for ((options, view) in elements) placed[view]?.let { addElement(options, it) }
312
+ for (view in DrawOrder.sorted(views, host) { it }) {
313
+ var item: Moved? = null
314
+ if (elementStep(names.getValue(view)) { item = moveIntoLayer(view) }) item?.let { placed[view] = it }
315
+ }
316
+ for ((options, view) in elements) {
317
+ val item = placed[view] ?: continue
318
+ if (!elementStep(names.getValue(view)) { addElement(options, item) }) goBack(item)
319
+ }
273
320
  for ((options, source, target) in pairs) {
274
321
  val item = placed[target] ?: continue
275
- if (!addPair(options, source, item, parents[target], fallbackDuration)) {
276
- moved -= item
277
- giveBack(item)
322
+ var flies = false
323
+ elementStep(names.getValue(target)) {
324
+ flies = addPair(options, source, item, parents[target], fallbackDuration)
278
325
  }
326
+ if (!flies) goBack(item)
327
+ }
328
+ }
329
+
330
+ // An element that doesn't play leaves the layer now; one that can't go back is logged, the others go on.
331
+ private fun goBack(item: Moved) {
332
+ moved -= item
333
+ try {
334
+ giveBack(item)
335
+ } catch (e: RuntimeException) {
336
+ Log.w(NavigationLog.TAG, "[ApexNavigation] An element of a transition couldn't go back", e)
279
337
  }
280
338
  }
281
339
  }
@@ -3,7 +3,6 @@ package dev.apexnative.navigation.views.element
3
3
  import android.util.Log
4
4
  import android.view.Choreographer
5
5
  import android.view.View
6
- import com.facebook.react.uimanager.util.ReactFindViewUtil
7
6
  import dev.apexnative.navigation.options.LayoutAnimation
8
7
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
9
8
  import dev.apexnative.navigation.utils.DevelopmentLog
@@ -72,12 +71,14 @@ internal object TargetsWait {
72
71
  ): () -> Unit {
73
72
  val fromView = from?.let { AppearanceDispatcher.visibleNode(it).peekView() }
74
73
  val toView = AppearanceDispatcher.visibleNode(to).peekView()
74
+ val pairs = animation.sharedElements.get()
75
+ val views = TransitionViews(pairs, emptyList(), fromView, toView)
75
76
  val images = ArrayList<ImageWait.Image?>()
76
- for (pair in animation.sharedElements.get()) {
77
+ for (pair in pairs) {
77
78
  val fromId = pair.fromId.get(null) ?: continue
78
79
  val toId = pair.toId.get(null) ?: continue
79
- val source = fromView?.let { ReactFindViewUtil.findView(it, fromId) } ?: continue
80
- val target = toView?.let { ReactFindViewUtil.findView(it, toId) } ?: continue
80
+ val source = views.leaving(fromId) ?: continue
81
+ val target = views.arriving(toId) ?: continue
81
82
  images += imageOf(source, fromId)
82
83
  images += imageOf(target, toId)
83
84
  }