@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -0,0 +1,216 @@
1
+ package dev.apexnative.navigation.views.stack.topbar.titlebar
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.ColorStateList
6
+ import android.graphics.drawable.Drawable
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import androidx.appcompat.R as AppCompatR
11
+ import androidx.appcompat.widget.AppCompatImageButton
12
+ import androidx.appcompat.widget.AppCompatImageView
13
+ import androidx.appcompat.widget.TooltipCompat
14
+ import androidx.core.graphics.drawable.DrawableCompat
15
+ import androidx.core.widget.ImageViewCompat
16
+ import com.google.android.material.R as MaterialR
17
+ import com.google.android.material.theme.overlay.MaterialThemeOverlay
18
+ import dev.apexnative.navigation.utils.MaterialTheme
19
+ import dev.apexnative.navigation.utils.UiUtils
20
+ import kotlin.math.max
21
+ import kotlin.math.min
22
+ import kotlin.math.roundToInt
23
+
24
+ /**
25
+ * The buttons of one side of the bar's row, from the start of the line: the back button ([navigationIcon]), the
26
+ * buttons ([setButtons]), what the core always puts after them ([setTrailing]: the search's magnifier) and "⋮"
27
+ * ([overflowButton]). They stand in the middle of the row, at most 56 dp high, as a Material 3 toolbar stands its
28
+ * buttons, with the toolbar's theme (its ripples); right to left the line mirrors. Main thread only.
29
+ */
30
+ @SuppressLint("ViewConstructor")
31
+ class ButtonRow(context: Context) : ViewGroup(themed(context)) {
32
+ private val maxButtonHeight = UiUtils.dpToPx(this.context, MAX_BUTTON_HEIGHT_DP).roundToInt()
33
+ private var navigation: AppCompatImageButton? = null
34
+ private var buttons: List<View> = emptyList()
35
+ private var trailing: View? = null
36
+ private var overflow: OverflowButton? = null
37
+ private var overflowColor: Int? = null
38
+
39
+ /** The back button's icon; none hides the button. */
40
+ var navigationIcon: Drawable?
41
+ get() = navigation?.takeIf { it.visibility == VISIBLE }?.drawable
42
+ set(value) {
43
+ if (value == null) {
44
+ navigation?.let {
45
+ it.setImageDrawable(null)
46
+ it.visibility = GONE
47
+ }
48
+ return
49
+ }
50
+ val button = navigation ?: AppCompatImageButton(context, null, AppCompatR.attr.toolbarNavigationButtonStyle)
51
+ .also {
52
+ navigation = it
53
+ arrange()
54
+ }
55
+ button.setImageDrawable(value)
56
+ button.visibility = VISIBLE
57
+ }
58
+
59
+ /** The back button's label: its accessibility label and tooltip. */
60
+ var navigationContentDescription: CharSequence?
61
+ get() = navigation?.contentDescription
62
+ set(value) {
63
+ val button = navigation ?: return
64
+ button.contentDescription = value
65
+ TooltipCompat.setTooltipText(button, value)
66
+ }
67
+
68
+ /** The back button's view while it shows; none. */
69
+ val navigationButton: View?
70
+ get() = navigation?.takeIf { it.visibility == VISIBLE }
71
+
72
+ private var navigationListener: OnClickListener? = null
73
+
74
+ /** What a press of the back button does. */
75
+ fun setNavigationOnClickListener(listener: OnClickListener?) {
76
+ navigationListener = listener
77
+ navigation?.setOnClickListener(listener)
78
+ }
79
+
80
+ /** The buttons, in the line's order from its start. A view in another parent moves here. */
81
+ fun setButtons(views: List<View>) {
82
+ buttons = views
83
+ arrange()
84
+ }
85
+
86
+ /** What stands after the buttons, before "⋮"; none. */
87
+ fun setTrailing(view: View?) {
88
+ if (trailing === view) return
89
+ trailing = view
90
+ arrange()
91
+ }
92
+
93
+ /** What stands after the buttons now. */
94
+ val trailingView: View?
95
+ get() = trailing
96
+
97
+ /**
98
+ * "⋮", made the first time it is asked for and shown with [setOverflowShown]: the theme's
99
+ * `actionOverflowButtonStyle`, labelled "More options" in the user's language.
100
+ */
101
+ val overflowButton: View
102
+ get() = overflow ?: OverflowButton(context).also {
103
+ it.visibility = GONE
104
+ overflowColor?.let { color -> ImageViewCompat.setImageTintList(it, ColorStateList.valueOf(color)) }
105
+ overflow = it
106
+ arrange()
107
+ }
108
+
109
+ /** "⋮" shows at the end of the line, or not. */
110
+ fun setOverflowShown(shown: Boolean) {
111
+ if (!shown && overflow == null) return
112
+ overflowButton.visibility = if (shown) VISIBLE else GONE
113
+ }
114
+
115
+ /** The color of "⋮", now or once it is made. */
116
+ fun setOverflowColor(color: Int) {
117
+ overflowColor = color
118
+ overflow?.let { ImageViewCompat.setImageTintList(it, ColorStateList.valueOf(color)) }
119
+ }
120
+
121
+ // The children in the line's order, so accessibility reads them in it too; a view that stays where it is isn't
122
+ // moved, which would detach it.
123
+ private fun arrange() {
124
+ val wanted = listOfNotNull(navigation) + buttons + listOfNotNull(trailing, overflow)
125
+ for (index in childCount - 1 downTo 0) {
126
+ if (getChildAt(index) !in wanted) removeViewAt(index)
127
+ }
128
+ wanted.forEachIndexed { index, view ->
129
+ if (index < childCount && getChildAt(index) === view) return@forEachIndexed
130
+ (view.parent as? ViewGroup)?.removeView(view)
131
+ addView(view, index, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
132
+ }
133
+ navigation?.setOnClickListener(navigationListener)
134
+ }
135
+
136
+ // As a toolbar, the row takes the touches around its buttons: they don't reach a screen under the bar.
137
+ @SuppressLint("ClickableViewAccessibility")
138
+ override fun onTouchEvent(event: MotionEvent?): Boolean = true
139
+
140
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
141
+ val height = MeasureSpec.getSize(heightMeasureSpec)
142
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
143
+ val widthSize = MeasureSpec.getSize(widthMeasureSpec)
144
+ val buttonHeight = min(height, maxButtonHeight)
145
+ var used = 0
146
+ for (index in 0 until childCount) {
147
+ val child = getChildAt(index)
148
+ if (child.visibility == GONE) continue
149
+ val widthSpec = if (widthMode == MeasureSpec.UNSPECIFIED) {
150
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
151
+ } else {
152
+ MeasureSpec.makeMeasureSpec(max(widthSize - used, 0), MeasureSpec.AT_MOST)
153
+ }
154
+ // The back button takes the whole height a toolbar gives its buttons; the others as much as they need.
155
+ val heightMode = if (child === navigation) MeasureSpec.EXACTLY else MeasureSpec.AT_MOST
156
+ child.measure(widthSpec, MeasureSpec.makeMeasureSpec(buttonHeight, heightMode))
157
+ used += child.measuredWidth
158
+ }
159
+ setMeasuredDimension(resolveSize(used, widthMeasureSpec), height)
160
+ }
161
+
162
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
163
+ val width = r - l
164
+ val height = b - t
165
+ val rtl = layoutDirection == LAYOUT_DIRECTION_RTL
166
+ var start = 0
167
+ for (index in 0 until childCount) {
168
+ val child = getChildAt(index)
169
+ if (child.visibility == GONE) continue
170
+ val w = child.measuredWidth
171
+ val x = if (rtl) width - start - w else start
172
+ val top = (height - child.measuredHeight) / 2
173
+ child.layout(x, top, x + w, top + child.measuredHeight)
174
+ start += w
175
+ }
176
+ }
177
+
178
+ // "⋮" as a toolbar draws it: its ripple a square around the icon, its label as its tooltip.
179
+ @SuppressLint("ViewConstructor")
180
+ private class OverflowButton(context: Context) :
181
+ AppCompatImageView(context, null, AppCompatR.attr.actionOverflowButtonStyle) {
182
+ init {
183
+ isClickable = true
184
+ isFocusable = true
185
+ TooltipCompat.setTooltipText(this, contentDescription)
186
+ }
187
+
188
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
189
+ super.onSizeChanged(w, h, oldw, oldh)
190
+ val background = background ?: return
191
+ if (drawable == null) return
192
+ val half = max(w, h) / 2
193
+ val centerX = (w + paddingLeft - paddingRight) / 2
194
+ val centerY = (h + paddingTop - paddingBottom) / 2
195
+ DrawableCompat.setHotspotBounds(background, centerX - half, centerY - half, centerX + half, centerY + half)
196
+ }
197
+ }
198
+
199
+ private companion object {
200
+ // A toolbar's `maxButtonHeight`.
201
+ const val MAX_BUTTON_HEIGHT_DP = 56f
202
+
203
+ // The theme a Material 3 toolbar gives its buttons (`toolbarStyle`'s overlay: its ripples), kept in the
204
+ // current light or dark mode.
205
+ fun themed(context: Context): Context {
206
+ val wrapped = MaterialThemeOverlay.wrap(
207
+ context,
208
+ null,
209
+ AppCompatR.attr.toolbarStyle,
210
+ MaterialR.style.Widget_Material3_Toolbar,
211
+ )
212
+ if (wrapped !== context) MaterialTheme.followOverlay(wrapped)
213
+ return wrapped
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,82 @@
1
+ // The height of React content laid out at a width without a height limit, for the Kotlin side
2
+ // (`dev.apexnative.navigation.react.ContentHeights`): a sheet with the 'content' detent. Fabric lays out a copy of the
3
+ // tree for it, so the shown layout stays as it is: a React root's surface here, a slot's content in the slot's own
4
+ // layout (`ANNSlotShadowNode`), which keeps the height in `ANNSlotContentHeights`.
5
+
6
+ #include <fbjni/fbjni.h>
7
+ #include <jni.h>
8
+ #include <react/fabric/SurfaceHandlerBinding.h>
9
+ #include <react/renderer/components/ApexNavigationSpec/ANNSlotState.h>
10
+ #include <react/renderer/core/LayoutConstraints.h>
11
+ #include <react/renderer/core/LayoutContext.h>
12
+
13
+ #include <limits>
14
+
15
+ using namespace facebook;
16
+
17
+ namespace {
18
+
19
+ std::string slotIdOf(jstring slotId) {
20
+ return slotId ? jni::wrap_alias(slotId)->toStdString() : std::string{};
21
+ }
22
+
23
+ // A React root's surface: React Native keeps the getter of its surface handler unmangled for the native side.
24
+ struct JReactSurfaceImpl : jni::JavaClass<JReactSurfaceImpl> {
25
+ static constexpr auto kJavaDescriptor = "Lcom/facebook/react/runtime/ReactSurfaceImpl;";
26
+
27
+ const react::SurfaceHandler &surfaceHandler() const {
28
+ static const auto method =
29
+ javaClassStatic()->getMethod<react::SurfaceHandlerBinding::javaobject()>("getSurfaceHandler");
30
+ return method(self())->cthis()->getSurfaceHandler();
31
+ }
32
+ };
33
+
34
+ } // namespace
35
+
36
+ /**
37
+ * The height of the surface's content at [width] points without a height limit, in points: `SurfaceHandler::measure`,
38
+ * as iOS's surface measures for `sizeThatFits`. 0 for a surface that doesn't run; -1 on a failure.
39
+ */
40
+ extern "C" JNIEXPORT jfloat JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeSurfaceHeight(
41
+ JNIEnv * /*env*/, jclass /*type*/, jobject surface, jfloat width, jfloat pointScale, jfloat fontScale, jboolean rtl,
42
+ jboolean swapLeftAndRight) {
43
+ try {
44
+ auto ref = jni::wrap_alias(static_cast<JReactSurfaceImpl::javaobject>(surface));
45
+ react::LayoutConstraints constraints{};
46
+ constraints.minimumSize = react::Size{.width = width, .height = 0};
47
+ constraints.maximumSize = react::Size{.width = width, .height = std::numeric_limits<react::Float>::infinity()};
48
+ constraints.layoutDirection = rtl ? react::LayoutDirection::RightToLeft : react::LayoutDirection::LeftToRight;
49
+ react::LayoutContext context{};
50
+ context.pointScaleFactor = pointScale;
51
+ context.fontSizeMultiplier = fontScale;
52
+ context.swapLeftAndRightInRTL = swapLeftAndRight;
53
+ return ref->surfaceHandler().measure(constraints, context).height;
54
+ } catch (...) {
55
+ jni::translatePendingCppExceptionToJavaException();
56
+ return -1;
57
+ }
58
+ }
59
+
60
+ /** The slot's content is measured from its next layout on (`ANNSlotContentHeights::request`). */
61
+ extern "C" JNIEXPORT void JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeRequestSlot(
62
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
63
+ react::ANNSlotContentHeights::request(slotIdOf(slotId));
64
+ }
65
+
66
+ /** Whether the slot's content is measured. */
67
+ extern "C" JNIEXPORT jboolean JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeIsSlotRequested(
68
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
69
+ return react::ANNSlotContentHeights::isRequested(slotIdOf(slotId));
70
+ }
71
+
72
+ /** The height of the slot's content at its last layout, in points; negative before it. */
73
+ extern "C" JNIEXPORT jfloat JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeSlotHeight(
74
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
75
+ return react::ANNSlotContentHeights::height(slotIdOf(slotId));
76
+ }
77
+
78
+ /** The slot's screen went: nothing more is measured for it. */
79
+ extern "C" JNIEXPORT void JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeForgetSlot(
80
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
81
+ react::ANNSlotContentHeights::forget(slotIdOf(slotId));
82
+ }
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Material Icons "arrow_right" (Apache 2.0, android/MATERIAL_SYMBOLS_LICENSE.txt): the arrow of a submenu item in a
3
+ top bar menu, mirrored right to left. -->
4
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
5
+ android:width="24dp"
6
+ android:height="24dp"
7
+ android:autoMirrored="true"
8
+ android:tint="?attr/colorControlNormal"
9
+ android:viewportWidth="24"
10
+ android:viewportHeight="24">
11
+ <path
12
+ android:fillColor="@android:color/white"
13
+ android:pathData="M10,17l5,-5 -5,-5v10z" />
14
+ </vector>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- The title over the items of a submenu of a top bar menu (BarMenu), as AppCompat shows a submenu's title. -->
3
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="?attr/listPreferredItemHeightSmall"
6
+ android:minWidth="196dp"
7
+ android:paddingStart="16dp"
8
+ android:paddingEnd="16dp">
9
+
10
+ <TextView
11
+ android:id="@+id/apex_navigation_menu_title"
12
+ android:layout_width="match_parent"
13
+ android:layout_height="wrap_content"
14
+ android:layout_gravity="center_vertical"
15
+ android:ellipsize="end"
16
+ android:singleLine="true"
17
+ android:textAlignment="viewStart"
18
+ android:textAppearance="?attr/textAppearancePopupMenuHeader" />
19
+ </FrameLayout>
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ An item of a menu of the top bar (BarMenu), as AppCompat lays out a popup menu's item: the icon, the title, the
4
+ submenu's arrow, the check mark of a checkable item. The inflater of the activity turns the check box and the radio
5
+ button into Material ones, as it does for AppCompat's menus.
6
+ -->
7
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
8
+ android:layout_width="match_parent"
9
+ android:layout_height="?attr/listPreferredItemHeightSmall"
10
+ android:gravity="center_vertical"
11
+ android:minWidth="196dp"
12
+ android:orientation="horizontal"
13
+ android:paddingEnd="16dp">
14
+
15
+ <ImageView
16
+ android:id="@+id/apex_navigation_menu_icon"
17
+ android:layout_width="wrap_content"
18
+ android:layout_height="wrap_content"
19
+ android:layout_marginStart="8dp"
20
+ android:layout_marginTop="8dp"
21
+ android:layout_marginEnd="-8dp"
22
+ android:layout_marginBottom="8dp"
23
+ android:duplicateParentState="true"
24
+ android:importantForAccessibility="no"
25
+ android:scaleType="centerInside"
26
+ android:visibility="gone" />
27
+
28
+ <TextView
29
+ android:id="@+id/apex_navigation_menu_title"
30
+ android:layout_width="0dp"
31
+ android:layout_height="wrap_content"
32
+ android:layout_marginStart="16dp"
33
+ android:layout_weight="1"
34
+ android:duplicateParentState="true"
35
+ android:ellipsize="end"
36
+ android:singleLine="true"
37
+ android:textAlignment="viewStart"
38
+ android:textAppearance="?attr/textAppearanceLargePopupMenu" />
39
+
40
+ <ImageView
41
+ android:id="@+id/apex_navigation_menu_arrow"
42
+ android:layout_width="wrap_content"
43
+ android:layout_height="wrap_content"
44
+ android:layout_marginStart="8dp"
45
+ android:duplicateParentState="true"
46
+ android:importantForAccessibility="no"
47
+ android:scaleType="center"
48
+ android:src="@drawable/apex_navigation_ic_arrow_right"
49
+ android:visibility="gone" />
50
+
51
+ <CheckBox
52
+ android:id="@+id/apex_navigation_menu_check"
53
+ android:layout_width="wrap_content"
54
+ android:layout_height="wrap_content"
55
+ android:clickable="false"
56
+ android:duplicateParentState="true"
57
+ android:focusable="false"
58
+ android:visibility="gone" />
59
+
60
+ <RadioButton
61
+ android:id="@+id/apex_navigation_menu_radio"
62
+ android:layout_width="wrap_content"
63
+ android:layout_height="wrap_content"
64
+ android:clickable="false"
65
+ android:duplicateParentState="true"
66
+ android:focusable="false"
67
+ android:visibility="gone" />
68
+ </LinearLayout>
@@ -8,7 +8,7 @@
8
8
  <style name="ThemeOverlay.ApexNavigation.TopBar" parent="ThemeOverlay.Material3.Toolbar.Surface" />
9
9
 
10
10
  <!--
11
- The popup theme of the bar's toolbars, for the "⋮" menu, when the app's Material 3 theme is fixed to light or to
11
+ The popup theme of the bar's "⋮" menu, when the app's Material 3 theme is fixed to light or to
12
12
  dark and doesn't follow the device's mode: a day/night overlay that gives the menu Material 3's colors of the
13
13
  current mode where the app's theme shows the other one, and leaves the app's colors in the mode it shows
14
14
  (values-night has the dark side). An app theme that follows the mode needs none: the menu takes the bar's theme.
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <resources>
3
- <!-- The popup themes of the bar's toolbars in dark mode: see values/styles.xml. -->
3
+ <!-- The popup themes of the bar's "⋮" in dark mode: see values/styles.xml. -->
4
4
  <style name="ThemeOverlay.ApexNavigation.TopBar.PopupOverLight" parent="ThemeOverlay.Material3.Dark" />
5
5
  <style name="ThemeOverlay.ApexNavigation.TopBar.PopupOverDark" parent="" />
6
6
  </resources>
@@ -100,11 +100,12 @@ function appChecks({ root, run }) {
100
100
  const project = findProject(root, run);
101
101
  const plans = planProject(project);
102
102
  const app = plans.find(({ file }) => file !== undefined && file === project.app);
103
+ const metro = plans.find(({ file }) => file !== undefined && file === project.metroConfig);
103
104
  const manifest = plans.find(({ file }) => file !== undefined && file === project.manifest);
104
105
  const native = plans.filter((plan) => plan.platform !== 'js' && plan !== manifest);
105
106
  const types = plans.filter(({ name }) => name === ENV_FILE || name === 'tsconfig.json');
106
107
  const entry = plans.filter(
107
- (plan) => plan.platform === 'js' && plan !== app && !types.includes(plan)
108
+ (plan) => plan.platform === 'js' && plan !== app && plan !== metro && !types.includes(plan)
108
109
  );
109
110
  checks.push(filesCheck('Native files', native), filesCheck('Entry point', entry));
110
111
  const typed = typesCheck(root, types);
@@ -120,6 +121,20 @@ function appChecks({ root, run }) {
120
121
  });
121
122
  }
122
123
 
124
+ // Without the navigation's Metro setup an edit that isn't a boundary of Fast Refresh reloads the app.
125
+ if (metro && metro.status !== 'done') {
126
+ checks.push({
127
+ group: 'app',
128
+ title: 'Fast Refresh',
129
+ status: 'warn',
130
+ detail: `${metro.name} doesn't use withApexNavigation: some edits reload the app`,
131
+ fix:
132
+ metro.status === 'manual'
133
+ ? { label: 'Wrap the config metro.config.js exports in withApexNavigation' }
134
+ : SETUP,
135
+ });
136
+ }
137
+
123
138
  // Predictive back is optional: without it the navigation works, and Android plays no back animation of its own.
124
139
  if (manifest && manifest.status !== 'done') {
125
140
  checks.push({
package/cli/project.js CHANGED
@@ -22,6 +22,7 @@ const { exec } = require('./exec');
22
22
  * infoPlist?: string;
23
23
  * entry?: string;
24
24
  * app?: string;
25
+ * metroConfig?: string;
25
26
  * typescript: boolean;
26
27
  * xcodeProject?: string;
27
28
  * androidPackage?: string;
@@ -75,6 +76,9 @@ function findProject(root, run = exec) {
75
76
  app: ['App.tsx', 'App.ts', 'App.jsx', 'App.js']
76
77
  .map((name) => path.join(root, name))
77
78
  .find((file) => fs.existsSync(file)),
79
+ metroConfig: ['metro.config.js', 'metro.config.cjs']
80
+ .map((name) => path.join(root, name))
81
+ .find((file) => fs.existsSync(file)),
78
82
  typescript: ['App.tsx', 'App.ts', 'tsconfig.json'].some((name) =>
79
83
  fs.existsSync(path.join(root, name))
80
84
  ),
@@ -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 { planMetro } = require('./steps/metro');
19
20
  const { planSceneDelegate } = require('./steps/sceneDelegate');
20
21
  const { planMainApplication } = require('./steps/mainApplication');
21
22
  const { planManifest } = require('./steps/manifest');
@@ -134,6 +135,10 @@ function planProject(project) {
134
135
  plans.push(...planEnv(project.root, navigation, ts));
135
136
  // The template's App renders React Native's status bar; one moved elsewhere or written anew has nothing to change.
136
137
  if (project.app) plans.push(planFile('App.tsx', 'js', project.app, planApp, APP_HOW_TO));
138
+ // Fast Refresh for every edit; an app without a Metro config of its own has nothing to change.
139
+ if (project.metroConfig) {
140
+ plans.push(planFile('metro.config.js', 'js', project.metroConfig, planMetro, ''));
141
+ }
137
142
  return plans;
138
143
  }
139
144
 
@@ -0,0 +1,46 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /** @typedef {import('./types').StepResult} StepResult */
5
+
6
+ const REQUIRE = "const { withApexNavigation } = require('@apexnative/navigation/metro');";
7
+
8
+ // The config the file exports: `module.exports = …;`, its last statement.
9
+ const EXPORT = /^module\.exports\s*=\s*([\s\S]+?);?\s*$/m;
10
+
11
+ const HOW_TO = `Wrap the config metro.config.js exports, so an edit updates the app by Fast Refresh instead of
12
+ reloading it:
13
+
14
+ ${REQUIRE}
15
+
16
+ module.exports = withApexNavigation(mergeConfig(getDefaultConfig(__dirname), config));`;
17
+
18
+ /**
19
+ * metro.config.js: the config it exports goes through withApexNavigation, which puts the navigation's Babel
20
+ * transformer in front of the app's own.
21
+ * @param {string} text
22
+ * @returns {StepResult}
23
+ */
24
+ function planMetro(text) {
25
+ if (text.includes('withApexNavigation')) return { status: 'done', note: 'already set up' };
26
+ const found = EXPORT.exec(text);
27
+ // Only an export that ends the file: one in the middle may be changed by what comes after it.
28
+ if (!found || text.slice(found.index + found[0].length).trim() !== '') {
29
+ return { status: 'manual', note: 'changed by hand, see below', howTo: HOW_TO };
30
+ }
31
+ const exported = found[1].trim();
32
+ const before = text.slice(0, found.index);
33
+ const requires = [...before.matchAll(/^.*require\(.*\);?$/gm)];
34
+ const last = requires[requires.length - 1];
35
+ const head =
36
+ last === undefined
37
+ ? `${REQUIRE}\n\n${before}`
38
+ : `${before.slice(0, last.index + last[0].length)}\n${REQUIRE}${before.slice(last.index + last[0].length)}`;
39
+ return {
40
+ status: 'change',
41
+ next: `${head}module.exports = withApexNavigation(${exported});\n`,
42
+ note: 'Fast Refresh for every edit',
43
+ };
44
+ }
45
+
46
+ module.exports = { planMetro, HOW_TO };
@@ -255,6 +255,10 @@ A wrong name or a missing prop is an error of the compiler, not of the app at ru
255
255
  - **Which are left out:** a screen with a required prop that is a function, and one with a prop whose type has no native form (see [Types](#types)). `codegen` names each in a warning. Native code still opens it by its name, `push("Settings", props: [:])`, with its props as a plain dictionary or map.
256
256
  - **After a change** of the screens or their props, run `codegen` again; `doctor` says when the generated code is behind.
257
257
 
258
+ ## Fast Refresh
259
+
260
+ `nativeScreen` returns a component that runs `useProps`. An edit of the hook or of the options applies in place, with the app running. A change of the native name, or adding or removing `useProps`, restarts the app (a reload).
261
+
258
262
  ## UIKit and Android views
259
263
 
260
264
  A screen of UIKit or Android views gets the same `screen`, and follows its props as they change: