@apexnative/navigation 1.0.0-alpha.1 → 1.0.0-alpha.3

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 (163) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +31 -6
  4. package/android/CORE.md +7 -2
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  8. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  9. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  10. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  11. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  12. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  13. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  17. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  18. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +76 -13
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +86 -16
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +41 -23
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +24 -20
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +42 -4
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +29 -6
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +2 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -3
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +16 -2
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +148 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +24 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +51 -11
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +105 -2
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +123 -8
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +19 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  51. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  52. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  53. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  54. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  55. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  56. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  57. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  63. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  64. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  65. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  68. package/cli/app.js +1 -1
  69. package/cli/project.js +6 -2
  70. package/cli/setup/index.js +13 -0
  71. package/cli/setup/steps/appDelegate.js +66 -11
  72. package/cli/setup/steps/sceneDelegate.js +108 -0
  73. package/docs-md/architecture/transitions.md +7 -1
  74. package/docs-md/containers/modals.md +4 -2
  75. package/docs-md/containers/overlays.md +3 -1
  76. package/docs-md/containers/side-menu.md +4 -2
  77. package/docs-md/containers/tabs.md +2 -0
  78. package/docs-md/flows/forms-in-a-modal.md +1 -1
  79. package/docs-md/guides/deep-links.md +15 -33
  80. package/docs-md/help/glossary.md +1 -1
  81. package/docs-md/index.json +8 -0
  82. package/docs-md/screen-ui/back-and-gestures.md +6 -0
  83. package/docs-md/screen-ui/system-ui.md +1 -1
  84. package/docs-md/start/installation.md +1 -0
  85. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  86. package/ios/Animations/ANNTransitionWait.mm +6 -7
  87. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  88. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  89. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  90. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  91. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  92. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  93. package/ios/CLAUDE.md +10 -2
  94. package/ios/CORE.md +3 -2
  95. package/ios/Contract/ANNCommandAnswer.h +4 -3
  96. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  97. package/ios/Contract/ANNCommandRunner.h +2 -2
  98. package/ios/Contract/ANNCommandRunner.mm +1 -1
  99. package/ios/Contract/ANNErrorTexts.h +6 -2
  100. package/ios/Contract/ANNErrorTexts.mm +18 -10
  101. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  102. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  103. package/ios/Core/ANNErrorHandler.h +8 -3
  104. package/ios/Core/ANNErrorHandler.mm +17 -1
  105. package/ios/Core/ANNEventContract.h +246 -0
  106. package/ios/Core/ANNEventContract.mm +225 -0
  107. package/ios/Core/ANNEventEmitter.h +5 -1
  108. package/ios/Core/ANNEventEmitter.mm +117 -80
  109. package/ios/Core/ANNHinge.mm +6 -4
  110. package/ios/Core/ANNSceneDelegate.mm +25 -15
  111. package/ios/Core/ANNSystemUI.h +3 -2
  112. package/ios/Core/ANNSystemUI.mm +7 -0
  113. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  114. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  115. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  116. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  117. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  118. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  119. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  120. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  121. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  122. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  123. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  124. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  125. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  126. package/ios/Options/ANNSheetOptions.h +4 -1
  127. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  128. package/ios/RECIPES.md +2 -1
  129. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  130. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  131. package/lib/module/ARCHITECTURE.md +7 -4
  132. package/lib/module/api/app/createNavigation.js +2 -0
  133. package/lib/module/api/app/createNavigation.js.map +1 -1
  134. package/lib/module/api/hooks/useNavigation.js +8 -3
  135. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  136. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  137. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  138. package/lib/module/interfaces/contract/errors.json +275 -0
  139. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  140. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  141. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  142. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  143. package/lib/typescript/interfaces/Events.d.ts +9 -0
  144. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  145. package/lib/typescript/interfaces/options/animations.d.ts +27 -18
  146. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  147. package/lib/typescript/interfaces/options/modals.d.ts +33 -24
  148. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  149. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  150. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  151. package/lib/typescript/interfaces/options/system.d.ts +10 -6
  152. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  153. package/package.json +1 -1
  154. package/src/ARCHITECTURE.md +7 -4
  155. package/src/api/app/createNavigation.ts +2 -0
  156. package/src/api/hooks/useNavigation.ts +8 -3
  157. package/src/api/linking/dispatchedLinks.ts +18 -0
  158. package/src/interfaces/Events.ts +10 -0
  159. package/src/interfaces/contract/errors.json +275 -0
  160. package/src/interfaces/options/animations.ts +27 -18
  161. package/src/interfaces/options/modals.ts +33 -24
  162. package/src/interfaces/options/sideMenu.ts +7 -6
  163. package/src/interfaces/options/system.ts +10 -6
@@ -1,8 +1,6 @@
1
1
  package dev.apexnative.navigation.views.element.animators
2
2
 
3
3
  import android.animation.Animator
4
- import androidx.core.animation.doOnEnd
5
- import androidx.core.animation.doOnStart
6
4
  import com.facebook.drawee.drawable.ScalingUtils
7
5
  import com.facebook.react.views.image.ReactImageView
8
6
  import dev.apexnative.navigation.views.animations.ValueInterpolation
@@ -45,14 +43,14 @@ internal class ReactImageAnimator(
45
43
  to.rotation = ValueInterpolation.number(fromRotation, toRotation, f, time)
46
44
  to.invalidate()
47
45
  }
48
- doOnStart {
46
+ onStart {
49
47
  savedFade = to.hierarchy.fadeDuration
50
48
  to.hierarchy.fadeDuration = 0
51
49
  to.hierarchy.setActualImageScaleType(scale)
52
50
  frame(0f)
53
51
  }
54
- addUpdateListener { frame(it.animatedFraction) }
55
- doOnEnd {
52
+ onFrame { frame(it) }
53
+ onEnd {
56
54
  to.hierarchy.setActualImageScaleType(toType)
57
55
  to.hierarchy.fadeDuration = savedFade
58
56
  to.translationX = 0f
@@ -10,6 +10,7 @@ import dev.apexnative.navigation.options.SharedElementTransitionOptions
10
10
  import dev.apexnative.navigation.views.animations.AnimationRules
11
11
  import dev.apexnative.navigation.views.animations.Curves
12
12
  import dev.apexnative.navigation.views.element.TransitionLayer
13
+ import dev.apexnative.navigation.views.element.pairName
13
14
 
14
15
  /**
15
16
  * The animator of a shared element by the kinds of its two views, chosen by explicit classes, never by reflection:
@@ -55,6 +56,7 @@ internal object SharedAnimatorFactory {
55
56
  else -> moves(PlainViewAnimator(from, to, layer).create())
56
57
  } ?: return null
57
58
  val animator = flight.animator
59
+ (animator as? MoveAnimator)?.element = pairName(options.fromId.get(""), options.toId.get(""))
58
60
  val duration = AnimationRules.durationOf(options.duration.get(null), fallbackDuration)
59
61
  animator.duration = duration
60
62
  animator.startDelay = AnimationRules.delayOf(options.startDelay.get(null))
@@ -11,8 +11,6 @@ import android.text.Layout
11
11
  import android.view.View
12
12
  import android.view.ViewGroup
13
13
  import android.widget.TextView
14
- import androidx.core.animation.doOnEnd
15
- import androidx.core.animation.doOnStart
16
14
  import com.facebook.react.views.text.ReactTextView
17
15
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
18
16
  import dev.apexnative.navigation.utils.TextViewUtils
@@ -42,16 +40,16 @@ class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTe
42
40
  val drawable = TextMorphDrawable(start, end)
43
41
  // Everything changes between the start and the end of the animation: a cancelled one ends too.
44
42
  return MoveAnimator().apply {
45
- doOnStart {
43
+ onStart {
46
44
  drawable.setBounds(0, 0, layer.width, layer.height)
47
45
  layer.overlay.add(drawable)
48
46
  to.alpha = 0f
49
47
  }
50
- addUpdateListener {
51
- drawable.update(it.animatedFraction, curve.getInterpolation(it.animatedFraction))
48
+ onFrame {
49
+ drawable.update(it, curve.getInterpolation(it))
52
50
  layer.invalidate()
53
51
  }
54
- doOnEnd {
52
+ onEnd {
55
53
  layer.overlay.remove(drawable)
56
54
  layer.invalidate()
57
55
  to.alpha = end.alpha
@@ -12,8 +12,6 @@ import android.graphics.RectF
12
12
  import android.graphics.drawable.Drawable
13
13
  import android.view.View
14
14
  import android.view.ViewGroup
15
- import androidx.core.animation.doOnEnd
16
- import androidx.core.animation.doOnStart
17
15
  import com.facebook.react.views.view.ReactViewGroup
18
16
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
19
17
  import dev.apexnative.navigation.utils.ViewUtils
@@ -38,16 +36,16 @@ class ViewChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactVi
38
36
  val drawable = ViewMorphDrawable(start, end)
39
37
  // Everything changes between the start and the end of the animation: a cancelled one ends too.
40
38
  return MoveAnimator().apply {
41
- doOnStart {
39
+ onStart {
42
40
  drawable.setBounds(0, 0, layer.width, layer.height)
43
41
  layer.overlay.add(drawable)
44
42
  to.alpha = 0f
45
43
  }
46
- addUpdateListener {
47
- drawable.progress = curve.getInterpolation(it.animatedFraction)
44
+ onFrame {
45
+ drawable.progress = curve.getInterpolation(it)
48
46
  layer.invalidate()
49
47
  }
50
- doOnEnd {
48
+ onEnd {
51
49
  layer.overlay.remove(drawable)
52
50
  layer.invalidate()
53
51
  to.alpha = end.alpha
@@ -0,0 +1,15 @@
1
+ package dev.apexnative.navigation.views.stack
2
+
3
+ import android.graphics.drawable.Drawable
4
+ import android.view.View
5
+
6
+ /**
7
+ * What fills the room beside a moving screen in a transition (`StackLayout.extendEdge`), where the system's
8
+ * transitions between activities stretch a window's edge (`<extend>` of `activity_open_*` and `activity_close_*`): the
9
+ * screen's own background, which is what its edge shows unless its content covers it. Taken without drawing the
10
+ * screen, at the start of every push and pop. Main thread only.
11
+ */
12
+ internal object ScreenEdge {
13
+ /** A copy of [view]'s background, stretched across the room; none without one. */
14
+ fun pictureOf(view: View): Drawable? = view.background?.constantState?.newDrawable(view.resources)?.mutate()
15
+ }
@@ -2,6 +2,8 @@ package dev.apexnative.navigation.views.stack
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.drawable.Drawable
5
7
  import android.view.Gravity
6
8
  import android.view.View
7
9
  import android.view.ViewGroup
@@ -18,7 +20,8 @@ import java.util.WeakHashMap
18
20
  * moves between its places without leaving it, so it doesn't leave and join the window again; a covered screen that
19
21
  * comes back gets the last window insets the stack got before it joins, and nothing asks the window to send them again
20
22
  * (CoordinatorLayout asks for insets only as it joins the window itself and only with `fitsSystemWindows`, which this
21
- * view doesn't set). Main thread only.
23
+ * view doesn't set). In a transition of the stack's own the bar is drawn with the screen it goes with
24
+ * ([barTravels]). Main thread only.
22
25
  */
23
26
  @SuppressLint("ViewConstructor")
24
27
  class StackLayout(context: Context) : CoordinatorLayout(context) {
@@ -134,4 +137,137 @@ class StackLayout(context: Context) : CoordinatorLayout(context) {
134
137
  requestLayout()
135
138
  invalidate()
136
139
  }
140
+
141
+ // The edges of the screens in a transition: drawn beside each, under it, where it moves and as transparent as it.
142
+ private val edges = HashMap<View, Edge>()
143
+
144
+ private class Edge(val picture: Drawable, val width: Int, val atStart: Boolean)
145
+
146
+ /**
147
+ * [view] extends its start edge ([atStart]) or its end edge by [width] pixels while it moves, as the system's
148
+ * transitions between activities extend a window: its background fills the room it moves away from, so the screen
149
+ * under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls [edgesMoved].
150
+ */
151
+ fun extendEdge(view: View, atStart: Boolean, width: Int) {
152
+ val left = atStart != (layoutDirection == LAYOUT_DIRECTION_RTL)
153
+ val picture = ScreenEdge.pictureOf(view) ?: return
154
+ edges[view] = Edge(picture, width, left)
155
+ invalidate()
156
+ }
157
+
158
+ /** [view] moves no more: its edge goes. */
159
+ fun dropEdge(view: View) {
160
+ if (edges.remove(view) != null) invalidate()
161
+ }
162
+
163
+ /** A screen with an extended edge moved: the edges are drawn again where the screens are now. */
164
+ fun edgesMoved() {
165
+ if (edges.isNotEmpty()) invalidate()
166
+ }
167
+
168
+ // The bar's travel in a transition of the stack's own: the screen the bar goes with, the screen that leaves with the
169
+ // picture of the bar as it was, and whether that one lies over the bar (a pop).
170
+ private var barScreen: View? = null
171
+ private var barLeaving: View? = null
172
+ private var barPicture: Drawable? = null
173
+ private var leavingOver = false
174
+
175
+ /**
176
+ * The bar goes with [screen] in a transition of the stack's own: it is drawn as [screen] moves (its translation,
177
+ * scale and rotation around its pivot, its alpha). [picture], laid where the bar is, goes the same way with
178
+ * [leaving]: it is drawn right over that screen, outside its bounds too (a screen laid out under the bar). With
179
+ * [over] the leaving screen and its picture lie over the bar (a pop), else under it. Only the drawing moves: the
180
+ * bar's views and touches stay in place. [endBarTravel] ends it.
181
+ */
182
+ fun barTravels(screen: View, leaving: View?, picture: Drawable?, over: Boolean) {
183
+ barScreen = screen
184
+ barLeaving = leaving
185
+ barPicture = picture
186
+ leavingOver = over && leaving != null
187
+ invalidate()
188
+ }
189
+
190
+ /** The bar is drawn in its place again, over the screens; the picture goes. */
191
+ fun endBarTravel() {
192
+ if (barScreen == null) return
193
+ barScreen = null
194
+ barLeaving = null
195
+ barPicture = null
196
+ leavingOver = false
197
+ invalidate()
198
+ }
199
+
200
+ override fun dispatchDraw(canvas: Canvas) {
201
+ super.dispatchDraw(canvas)
202
+ // The screens' animators change only their own properties, which don't draw this view again: each frame of
203
+ // the bar's travel asks for the next.
204
+ if (barScreen != null) postInvalidateOnAnimation()
205
+ }
206
+
207
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
208
+ edges[child]?.let { drawEdge(canvas, child, it) }
209
+ val screen = barScreen ?: return super.drawChild(canvas, child, drawingTime)
210
+ // The leaving screen, while it is still here, and its picture; on a pop the bar right under them.
211
+ val leaving = barLeaving?.takeIf(::holds)
212
+ val under = leaving.takeIf { leavingOver }
213
+ if (child === topBar) {
214
+ if (under != null) return false
215
+ return drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
216
+ topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
217
+ super.drawChild(canvas, topBar, drawingTime)
218
+ }
219
+ }
220
+ if (child === under && topBar.visibility == VISIBLE) {
221
+ drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
222
+ topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
223
+ super.drawChild(canvas, topBar, drawingTime)
224
+ }
225
+ }
226
+ val drawn = super.drawChild(canvas, child, drawingTime)
227
+ val picture = barPicture
228
+ if (child === leaving && picture != null) {
229
+ val bounds = picture.bounds
230
+ drawWith(canvas, child, bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat(),
231
+ bounds.bottom.toFloat()) {
232
+ picture.draw(canvas)
233
+ false
234
+ }
235
+ }
236
+ return drawn
237
+ }
238
+
239
+ // Draws with [screen]'s transform and alpha what [draw] draws within the bounds given, in this view's coordinates.
240
+ private fun drawWith(
241
+ canvas: Canvas,
242
+ screen: View,
243
+ left: Float,
244
+ top: Float,
245
+ right: Float,
246
+ bottom: Float,
247
+ draw: () -> Boolean,
248
+ ): Boolean {
249
+ val alpha = if (screen.visibility == VISIBLE) screen.alpha else 0f
250
+ if (alpha <= 0f) return false
251
+ val saved = canvas.save()
252
+ canvas.translate(screen.left.toFloat(), screen.top.toFloat())
253
+ canvas.concat(screen.matrix)
254
+ canvas.translate(-screen.left.toFloat(), -screen.top.toFloat())
255
+ if (alpha < 1f) canvas.saveLayerAlpha(left, top, right, bottom, (alpha * 255).toInt())
256
+ val drawn = draw()
257
+ canvas.restoreToCount(saved)
258
+ return drawn
259
+ }
260
+
261
+ // At the child's place to the fraction of a pixel, as its own translation draws it, and one pixel under it: no seam
262
+ // of the screen below shows between them.
263
+ private fun drawEdge(canvas: Canvas, child: View, edge: Edge) {
264
+ if (child.visibility != VISIBLE || child.alpha <= 0f) return
265
+ val x = if (edge.atStart) child.left + child.translationX - edge.width else child.right + child.translationX - 1f
266
+ val saved = canvas.save()
267
+ canvas.translate(x, child.top + child.translationY)
268
+ edge.picture.setBounds(0, 0, edge.width + 1, child.height)
269
+ edge.picture.alpha = (child.alpha * 255).toInt()
270
+ edge.picture.draw(canvas)
271
+ canvas.restoreToCount(saved)
272
+ }
137
273
  }
@@ -118,6 +118,8 @@ class TitleBar(context: Context) : ViewGroup(context) {
118
118
  val height = MeasureSpec.getSize(heightMeasureSpec)
119
119
  val inner = max(width - sideInsetsBox[0] - sideInsetsBox[1], 0)
120
120
  val exactHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
121
+ alignButtons(leftButtonBar, height)
122
+ alignButtons(rightButtonBar, height)
121
123
  leftButtonBar.measure(MeasureSpec.makeMeasureSpec(inner, MeasureSpec.AT_MOST), exactHeight)
122
124
  val rightRoom = max(inner - leftButtonBar.measuredWidth, 0)
123
125
  rightButtonBar.measure(MeasureSpec.makeMeasureSpec(rightRoom, MeasureSpec.AT_MOST), exactHeight)
@@ -192,6 +194,12 @@ class TitleBar(context: Context) : ViewGroup(context) {
192
194
  return min(max(middle, startBar), max(inner - endBar - width, startBar))
193
195
  }
194
196
 
197
+ // A toolbar stands its back button and its menu at the top of its minimum height, centered in it (`buttonGravity`
198
+ // is top): with the row's height as that minimum, they stand in the middle of the row, as the title does.
199
+ private fun alignButtons(toolbar: MaterialToolbar, height: Int) {
200
+ if (toolbar.minimumHeight != height) toolbar.minimumHeight = height
201
+ }
202
+
195
203
  // A toolbar that holds only buttons: no insets of its own around them (the row places them).
196
204
  private fun plain(toolbar: MaterialToolbar) {
197
205
  // Before the toolbar makes its menu, which takes the popup theme once: "⋮" in the current mode.
@@ -200,7 +208,6 @@ class TitleBar(context: Context) : ViewGroup(context) {
200
208
  toolbar.contentInsetStartWithNavigation = 0
201
209
  toolbar.contentInsetEndWithActions = 0
202
210
  toolbar.setPadding(0, 0, 0, 0)
203
- toolbar.minimumHeight = 0
204
211
  }
205
212
 
206
213
  private companion object {
@@ -55,6 +55,8 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
55
55
  private var heightChanged = false
56
56
  // How far the large title is collapsed, in pixels.
57
57
  private var largeTitleShift = 0
58
+ // `topBar.elevation`, in pixels: the bar's elevation unless it is flat.
59
+ private var shadow = 0f
58
60
 
59
61
  /** The toolbar of the right buttons: its menu holds them, the overflow items and the search's magnifier. */
60
62
  val rightButtonBar: MaterialToolbar
@@ -187,14 +189,27 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
187
189
  * The shadow of `topBar.elevation`, in pixels: its colors are black (Android 9 and later read them), none at 0.
188
190
  */
189
191
  fun setShadow(elevation: Float) {
190
- if (this.elevation == elevation) return
191
- this.elevation = elevation
192
+ if (shadow == elevation) return
193
+ shadow = elevation
194
+ if (!isFlat) this.elevation = elevation
192
195
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
193
196
  outlineAmbientShadowColor = Color.BLACK
194
197
  outlineSpotShadowColor = Color.BLACK
195
198
  }
196
199
  }
197
200
 
201
+ /**
202
+ * The bar draws without its shadow, at the level of the screens, in a transition that draws it with a screen: a
203
+ * screen drawn after it covers it, and an alpha layer around it fades it (with an elevation the renderer would draw
204
+ * the bar over every screen and out of the layer).
205
+ */
206
+ var isFlat = false
207
+ set(value) {
208
+ if (field == value) return
209
+ field = value
210
+ elevation = if (value) 0f else shadow
211
+ }
212
+
198
213
  /** The line under the bar: [height] pixels of [color]. */
199
214
  fun setBorder(color: Int, height: Int) {
200
215
  border.setBackgroundColor(color)
@@ -335,8 +350,15 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
335
350
  * A picture of the bar under its row — the background, the large title, the search, the line — and where it starts
336
351
  * in the bar, for a push to a screen without a large title; none when the bar can't be drawn in software.
337
352
  */
338
- fun pictureUnderRow(): Pair<Bitmap, Int>? {
339
- val top = content.top + rowArea.bottom
353
+ fun pictureUnderRow(): Pair<Bitmap, Int>? = pictureFrom(content.top + rowArea.bottom)
354
+
355
+ /**
356
+ * A picture of the whole bar, for the screen it leaves with in a transition of the stack's own; none when the bar
357
+ * can't be drawn in software.
358
+ */
359
+ fun picture(): Bitmap? = pictureFrom(0)?.first
360
+
361
+ private fun pictureFrom(top: Int): Pair<Bitmap, Int>? {
340
362
  val width = width
341
363
  val height = height - top
342
364
  if (width <= 0 || height <= 0) return null
@@ -37,6 +37,7 @@ import dev.apexnative.navigation.utils.MaterialTheme
37
37
  import dev.apexnative.navigation.utils.TestIds
38
38
  import dev.apexnative.navigation.utils.UiUtils
39
39
  import java.util.WeakHashMap
40
+ import kotlin.math.roundToInt
40
41
 
41
42
  /**
42
43
  * The buttons of one side of the bar in its toolbar: each button a menu item — a text, an icon (on its background), or
@@ -171,7 +172,7 @@ internal class ButtonBar(
171
172
  options.fontSize.get(0.0).toFloat(),
172
173
  context.resources.displayMetrics,
173
174
  )
174
- spannable.setSpan(AbsoluteSizeSpan(size.toInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
175
+ spannable.setSpan(AbsoluteSizeSpan(size.roundToInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
175
176
  }
176
177
  if (options.font.hasValue()) {
177
178
  val typeface = options.font.getTypeface(typefaces, Typeface.DEFAULT)
@@ -207,8 +208,8 @@ internal class ButtonBar(
207
208
  icon,
208
209
  color,
209
210
  UiUtils.dpToPx(context, background.cornerRadius.get(0.0).toFloat()),
210
- UiUtils.dpToPx(context, background.width.get(0.0).toFloat()).toInt(),
211
- UiUtils.dpToPx(context, background.height.get(0.0).toFloat()).toInt(),
211
+ UiUtils.dpToPx(context, background.width.get(0.0).toFloat()).roundToInt(),
212
+ UiUtils.dpToPx(context, background.height.get(0.0).toFloat()).roundToInt(),
212
213
  )
213
214
  }
214
215
 
package/cli/app.js CHANGED
@@ -9,7 +9,7 @@ const { exec } = require('./exec');
9
9
  const MINIMUM = [0, 85];
10
10
 
11
11
  /** The newest React Native the navigation was checked with: `init` makes apps of its newest patch. */
12
- const TESTED = [0, 87];
12
+ const TESTED = [0, 88];
13
13
 
14
14
  /** The name the navigation is installed under. */
15
15
  const PACKAGE = '@apexnative/navigation';
package/cli/project.js CHANGED
@@ -8,8 +8,9 @@ const { exec } = require('./exec');
8
8
  /**
9
9
  * The files of the app the CLI sets up, checks and adds to, where the app has them; `undefined` for one it couldn't
10
10
  * find. `styles` is the file of the app's Android theme, `styles.xml` or `themes.xml`; `manifest` its
11
- * `AndroidManifest.xml`. `xcodeProject` is the app's `project.pbxproj`; `androidPackage` the package of its Kotlin code (the `namespace`
12
- * of `android/app/build.gradle`), and `androidSources` the folder it is in.
11
+ * `AndroidManifest.xml`. `sceneDelegate` is the `SceneDelegate.swift` next to `AppDelegate.swift`, which apps made
12
+ * from React Native 0.88's template have. `xcodeProject` is the app's `project.pbxproj`; `androidPackage` the package
13
+ * of its Kotlin code (the `namespace` of `android/app/build.gradle`), and `androidSources` the folder it is in.
13
14
  * @typedef {{
14
15
  * root: string;
15
16
  * mainApplication?: string;
@@ -17,6 +18,7 @@ const { exec } = require('./exec');
17
18
  * styles?: string;
18
19
  * manifest?: string;
19
20
  * appDelegate?: string;
21
+ * sceneDelegate?: string;
20
22
  * infoPlist?: string;
21
23
  * entry?: string;
22
24
  * app?: string;
@@ -64,6 +66,8 @@ function findProject(root, run = exec) {
64
66
  existing(path.join(androidMain, 'res', 'values', 'themes.xml')),
65
67
  manifest: existing(path.join(androidMain, 'AndroidManifest.xml')),
66
68
  appDelegate,
69
+ sceneDelegate:
70
+ appDelegate && existing(path.join(path.dirname(appDelegate), 'SceneDelegate.swift')),
67
71
  infoPlist: appDelegate && existing(path.join(path.dirname(appDelegate), 'Info.plist')),
68
72
  entry: ['index.js', 'index.ts', 'index.tsx']
69
73
  .map((name) => path.join(root, name))
@@ -16,6 +16,7 @@ const { planEntry, navigationFile, HOW_TO: ENTRY_HOW_TO } = require('./steps/ent
16
16
  const { planEnv, targetOf } = require('./steps/env');
17
17
  const { planInfoPlist } = require('./steps/infoPlist');
18
18
  const { planMainActivity } = require('./steps/mainActivity');
19
+ const { planSceneDelegate } = require('./steps/sceneDelegate');
19
20
  const { planMainApplication } = require('./steps/mainApplication');
20
21
  const { planManifest } = require('./steps/manifest');
21
22
  const { planStyles } = require('./steps/styles');
@@ -83,6 +84,18 @@ function planProject(project) {
83
84
  planAppDelegate,
84
85
  notFound('AppDelegate.swift')
85
86
  ),
87
+ // React Native 0.88's template starts React Native in its scene delegate; the earlier templates have none.
88
+ ...(project.sceneDelegate
89
+ ? [
90
+ planFile(
91
+ 'SceneDelegate.swift',
92
+ 'ios',
93
+ project.sceneDelegate,
94
+ planSceneDelegate,
95
+ notFound('SceneDelegate.swift')
96
+ ),
97
+ ]
98
+ : []),
86
99
  planFile('Info.plist', 'ios', project.infoPlist, planInfoPlist, notFound('Info.plist')),
87
100
  planFile('index.js', 'js', project.entry, planEntry, ENTRY_HOW_TO),
88
101
  ];
@@ -19,7 +19,10 @@ its delegate (the bundle URL) before calling super.
19
19
  self.reactNativeDelegate = ReactNativeDelegate()
20
20
  return super.application(application, didFinishLaunchingWithOptions: launchOptions)
21
21
  }
22
- }`;
22
+ }
23
+
24
+ ReactNativeDelegate is the app's RCTDefaultReactNativeFactoryDelegate, which gives the bundle URL (bundleURL()); in
25
+ React Native 0.88's template that method is in SceneDelegate.`;
23
26
 
24
27
  // Lines of the template's launch: ANNAppDelegate does all of it.
25
28
  const TEMPLATE_LINES = [
@@ -42,6 +45,20 @@ const LAUNCH_OVERRIDE =
42
45
  const DELEGATE = /^([ \t]*)let\s+delegate\s*=\s*ReactNativeDelegate\(\)[ \t]*$/m;
43
46
  const RETURN_TRUE = /^([ \t]*)return\s+true[ \t]*$/m;
44
47
 
48
+ // React Native 0.88's template starts React Native in its scene delegate: the app delegate only returns true. The
49
+ // bundle URL moves into a delegate of React Native's factory, as the earlier templates had it.
50
+ const REACT_NATIVE_DELEGATE = `class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
51
+ override func bundleURL() -> URL? {
52
+ #if DEBUG
53
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
54
+ #else
55
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
56
+ #endif
57
+ }
58
+ }
59
+ `;
60
+ const RETURN_TRUE_OR_TRUE = /^([ \t]*)(?:return\s+)?true[ \t]*$/m;
61
+
45
62
  /**
46
63
  * AppDelegate.swift: the app delegate becomes an ANNAppDelegate.
47
64
  * @param {string} text
@@ -55,13 +72,30 @@ function planAppDelegate(text) {
55
72
  note: 'changed by hand, see below',
56
73
  howTo: HOW_TO,
57
74
  });
58
- if (!HEADER.test(text) || !LAUNCH.test(text) || !DELEGATE.test(text)) return manual;
75
+ if (!HEADER.test(text) || !LAUNCH.test(text)) return manual;
76
+ let next = DELEGATE.test(text) ? fromFactoryLaunch(text) : fromSceneLaunch(text);
77
+ if (next === undefined) return manual;
59
78
 
79
+ // The dependency provider is ANNAppDelegate's now.
80
+ if (!/\bRCTAppDependencyProvider\b/.test(next)) {
81
+ next = next.replace(/^import\s+ReactAppDependencyProvider[ \t]*\n/m, '');
82
+ }
83
+ next = addImport(next, 'import ApexNavigation', { sorted: false });
84
+ return { status: 'change', next, note: 'ANNAppDelegate' };
85
+ }
86
+
87
+ /**
88
+ * The app delegate of the templates before React Native 0.88, which makes React Native's factory and window in its
89
+ * launch; undefined when the launch has code of the app's own that uses them.
90
+ * @param {string} text
91
+ * @returns {string | undefined}
92
+ */
93
+ function fromFactoryLaunch(text) {
60
94
  let next = text
61
95
  .replace(HEADER, 'class $1: ANNAppDelegate {')
62
96
  .replace(LAUNCH, '$1override func$2');
63
97
  for (const line of TEMPLATE_LINES) {
64
- if (!line.test(next)) return manual;
98
+ if (!line.test(next)) return undefined;
65
99
  next = next.replace(line, '');
66
100
  }
67
101
  next = next.replace(DELEGATE, '$1self.reactNativeDelegate = ReactNativeDelegate()');
@@ -69,10 +103,10 @@ function planAppDelegate(text) {
69
103
  // The launch now ends in super's, which starts React Native.
70
104
  const launchAt = next.search(LAUNCH_OVERRIDE);
71
105
  const returnAt = next.slice(launchAt).search(RETURN_TRUE);
72
- if (returnAt < 0) return manual;
106
+ if (returnAt < 0) return undefined;
73
107
  const body = next.slice(launchAt, launchAt + returnAt).replace(/self\.reactNativeDelegate/g, '');
74
108
  // Code of the app's own that used the template's factory, delegate or window: it has to move by hand.
75
- if (/\b(factory|delegate|window)\b/.test(body)) return manual;
109
+ if (/\b(factory|delegate|window)\b/.test(body)) return undefined;
76
110
  const rest = next
77
111
  .slice(launchAt + returnAt)
78
112
  .replace(
@@ -82,17 +116,38 @@ function planAppDelegate(text) {
82
116
  next = next.slice(0, launchAt + returnAt) + rest;
83
117
 
84
118
  // The blank lines the template's launch leaves behind.
85
- next = next
119
+ return next
86
120
  .replace(/(class\s+\w+:\s*ANNAppDelegate\s*\{\n)(?:[ \t]*\n)+/, '$1')
87
121
  .replace(/(self\.reactNativeDelegate = ReactNativeDelegate\(\)\n)(?:[ \t]*\n)+/, '$1')
88
122
  .replace(/\n(?:[ \t]*\n)+([ \t]*return super\.application\()/, '\n$1');
123
+ }
89
124
 
90
- // The dependency provider is ANNAppDelegate's now.
91
- if (!/\bRCTAppDependencyProvider\b/.test(next)) {
92
- next = next.replace(/^import\s+ReactAppDependencyProvider[ \t]*\n/m, '');
125
+ /**
126
+ * The app delegate of React Native 0.88's template, whose launch only returns true; undefined when the app delegate
127
+ * makes React Native or a window of its own.
128
+ * @param {string} text
129
+ * @returns {string | undefined}
130
+ */
131
+ function fromSceneLaunch(text) {
132
+ if (/\b(RCTReactNativeFactory|ReactNativeDelegate|window)\b/.test(text)) return undefined;
133
+ let next = text
134
+ .replace(HEADER, 'class $1: ANNAppDelegate {')
135
+ .replace(LAUNCH, '$1override func$2');
136
+ const launchAt = next.search(LAUNCH_OVERRIDE);
137
+ const returnAt = next.slice(launchAt).search(RETURN_TRUE_OR_TRUE);
138
+ if (returnAt < 0) return undefined;
139
+ const rest = next
140
+ .slice(launchAt + returnAt)
141
+ .replace(
142
+ RETURN_TRUE_OR_TRUE,
143
+ '$1self.reactNativeDelegate = ReactNativeDelegate()\n' +
144
+ '$1return super.application(application, didFinishLaunchingWithOptions: launchOptions)'
145
+ );
146
+ next = `${next.slice(0, launchAt + returnAt)}${rest.trimEnd()}\n\n${REACT_NATIVE_DELEGATE}`;
147
+ for (const module of ['React', 'React_RCTAppDelegate']) {
148
+ next = addImport(next, `import ${module}`, { sorted: false });
93
149
  }
94
- next = addImport(next, 'import ApexNavigation', { sorted: false });
95
- return { status: 'change', next, note: 'ANNAppDelegate' };
150
+ return next;
96
151
  }
97
152
 
98
153
  module.exports = { planAppDelegate };