@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
@@ -1,6 +1,5 @@
1
1
  package dev.apexnative.navigation.views.stack.topbar.titlebar
2
2
 
3
- import android.annotation.SuppressLint
4
3
  import android.content.Context
5
4
  import android.graphics.Color
6
5
  import android.graphics.Paint
@@ -15,20 +14,12 @@ import android.text.style.MetricAffectingSpan
15
14
  import android.transition.AutoTransition
16
15
  import android.transition.TransitionManager
17
16
  import android.util.TypedValue
17
+ import android.view.ContextThemeWrapper
18
18
  import android.view.Gravity
19
- import android.view.Menu
20
- import android.view.MenuItem
21
19
  import android.view.View
22
- import android.view.ViewGroup
23
20
  import androidx.appcompat.R as AppCompatR
24
21
  import androidx.appcompat.content.res.AppCompatResources
25
- import androidx.appcompat.view.menu.MenuView
26
- import androidx.appcompat.widget.ActionMenuView
27
- import androidx.appcompat.widget.PopupMenu
28
- import androidx.appcompat.widget.TooltipCompat
29
22
  import androidx.core.graphics.drawable.DrawableCompat
30
- import androidx.core.view.MenuItemCompat
31
- import com.google.android.material.appbar.MaterialToolbar
32
23
  import dev.apexnative.navigation.R
33
24
  import dev.apexnative.navigation.options.ButtonMenuOptions
34
25
  import dev.apexnative.navigation.options.ButtonOptions
@@ -36,19 +27,17 @@ import dev.apexnative.navigation.options.parsers.TypefaceLoader
36
27
  import dev.apexnative.navigation.utils.MaterialTheme
37
28
  import dev.apexnative.navigation.utils.TestIds
38
29
  import dev.apexnative.navigation.utils.UiUtils
39
- import java.util.WeakHashMap
40
30
  import kotlin.math.roundToInt
41
31
 
42
32
  /**
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
44
- * a React button's cell as the item's action view —, in the bar or in "⋮" as [set] says, with the `overflowMenu` items
45
- * first in "⋮". A press sends the button's id; a button with a `menu` opens it under the button (in "⋮" or not laid
46
- * out: at the bar's end), or on a long press with `menuOnLongPress`. The views of the items get their `testID`s after
47
- * each layout of the toolbar, which makes them again. Main thread only.
33
+ * The buttons of one side of the bar in its [ButtonRow]: each button a view of its own — a text or an icon (on its
34
+ * background) as a toolbar draws its items ([BarButtonView]), or a React button's cell —, in the bar or in "⋮" as
35
+ * [set] says, with the `overflowMenu` items first in "⋮". A press sends the button's id; a button with a `menu` opens
36
+ * it under the button (in "⋮": at the bar's end), or on a long press with `menuOnLongPress`. A button keeps its view
37
+ * while its id stays, and the view takes its `testID` as it is set. Main thread only.
48
38
  */
49
39
  internal class ButtonBar(
50
- private val toolbar: MaterialToolbar,
51
- private val left: Boolean,
40
+ private val row: ButtonRow,
52
41
  private val loadIcon: (String) -> Drawable?,
53
42
  private val host: Host,
54
43
  ) {
@@ -63,97 +52,139 @@ internal class ButtonBar(
63
52
  /** The menu of the button [buttonId] as the screen's options give it now; none. */
64
53
  fun menuOf(buttonId: String): ButtonMenuOptions?
65
54
 
66
- /** The `testID` of an item the side holds besides its buttons (the search's magnifier); none. */
67
- fun testIdOf(item: MenuItem): String? = null
55
+ /** The colors of the bar's menus as the screen's options give them now (`topBar.menu*Color`). */
56
+ fun menuColors(): BarMenu.Colors
68
57
  }
69
58
 
70
59
  /** One button as the bar shows it: its options with the side's colors on them, and its React cell if it has one. */
71
60
  class Button(val options: ButtonOptions, val cell: BarButtonComponentCell?)
72
61
 
73
- private val context: Context = toolbar.context
62
+ private val context: Context = row.context
74
63
  private val typefaces = TypefaceLoader(context)
75
- private val itemIds = HashMap<MenuItem, ButtonOptions>()
76
- private var overflowTestId: String? = null
77
- private var popup: PopupMenu? = null
78
- private var watched: ActionMenuView? = null
79
- // The item views that have the long press of a menu.
80
- private val hooked = WeakHashMap<View, Boolean>()
81
-
82
- init {
83
- toolbar.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> applyViewIds() }
84
- }
64
+ // The views of the buttons in the bar by their ids, kept from one set to the next.
65
+ private var views = HashMap<String, BarButtonView>()
66
+ private var overflowMenu: ButtonMenuOptions? = null
67
+ private var inOverflow: List<ButtonOptions> = emptyList()
68
+ private var popup: BarMenu? = null
69
+ private var overflowPopup: BarMenu? = null
85
70
 
86
71
  /**
87
- * The buttons of the side: [inBar] in the bar, [inOverflow] in "⋮" (the right side; the left one keeps all in
88
- * the bar), each in the list's order; [overflow], the items of `overflowMenu` (right side). [animated]: the change
89
- * goes with an `AutoTransition` (`animateLeftButtons` / `animateRightButtons`).
72
+ * The buttons of the side: [line] in the bar, in the line's order from its start; [inOverflow] in "⋮" (the right
73
+ * side; the left one keeps all in the bar), in the list's order; [overflow], the items of `overflowMenu` (right
74
+ * side). [animated]: the change goes with an `AutoTransition` (`animateLeftButtons` / `animateRightButtons`).
90
75
  */
91
- fun set(inBar: List<Button>, inOverflow: List<Button>, overflow: ButtonMenuOptions?, animated: Boolean) {
92
- if (animated && toolbar.isLaidOut) TransitionManager.beginDelayedTransition(toolbar, AutoTransition())
93
- val menu = toolbar.menu
94
- menu.clear()
95
- itemIds.clear()
96
- var order = 0
97
- overflow?.let {
98
- ButtonMenus.fill(context, menu, OVERFLOW_GROUP, 0, it, MenuItem.SHOW_AS_ACTION_NEVER, loadIcon) { id ->
99
- host.picked(null, id)
100
- }
101
- order = it.items.size
76
+ fun set(line: List<Button>, inOverflow: List<Button>, overflow: ButtonMenuOptions?, animated: Boolean) {
77
+ if (animated && row.isLaidOut) TransitionManager.beginDelayedTransition(row, AutoTransition())
78
+ val kept = this.views
79
+ this.views = HashMap()
80
+ val views = line.mapNotNull { button ->
81
+ val id = button.options.id.get(null) ?: return@mapNotNull null
82
+ button.cell?.let { return@mapNotNull cellOf(it, button.options) }
83
+ val view = kept.remove(id)?.takeIf { id !in this.views } ?: BarButtonView(context)
84
+ this.views[id] = view
85
+ bind(view, id, button.options)
102
86
  }
103
- overflowTestId = overflow?.testID
104
- inOverflow.forEachIndexed { index, button -> add(menu, button, OVERFLOW_ORDER + order + index, false) }
105
- inBar.forEachIndexed { index, button ->
106
- // The first right button is the last of the line: the menu lays its items out in their order.
107
- val place = if (left) index else inBar.size - 1 - index
108
- add(menu, button, BAR_ORDER + place, true)
87
+ row.setButtons(views)
88
+ overflowMenu = overflow
89
+ this.inOverflow = inOverflow.map { it.options }.filter { it.id.get(null) != null }
90
+ val showsOverflow = overflow != null || this.inOverflow.isNotEmpty()
91
+ if (showsOverflow) {
92
+ val button = row.overflowButton
93
+ button.setOnClickListener { openOverflow(it) }
94
+ TestIds.apply(button, overflow?.testID)
95
+ }
96
+ row.setOverflowShown(showsOverflow)
97
+ // An open "⋮" shows what it holds now.
98
+ overflowPopup?.let { menu ->
99
+ if (showsOverflow) {
100
+ menu.update(overflowEntries(menu.context, menu.colors.text), overflowIcons())
101
+ } else {
102
+ menu.dismiss()
103
+ }
109
104
  }
110
- toolbar.post { applyViewIds() }
111
105
  }
112
106
 
113
107
  /** The color of "⋮" (`rightButtonColor`, else `colorOnSurfaceVariant`). */
114
- fun setOverflowColor(color: Int) {
115
- val icon = toolbar.overflowIcon ?: return
116
- val tinted = DrawableCompat.wrap(icon.mutate())
117
- DrawableCompat.setTint(tinted, color)
118
- toolbar.overflowIcon = tinted
119
- }
108
+ fun setOverflowColor(color: Int) = row.setOverflowColor(color)
120
109
 
121
110
  /** An open menu of the side closes: the bar shows another screen. */
122
111
  fun closeMenus() {
123
112
  popup?.dismiss()
124
113
  popup = null
125
- toolbar.dismissPopupMenus()
114
+ overflowPopup?.dismiss()
115
+ overflowPopup = null
116
+ }
117
+
118
+ // A React button's cell: its label and its `testID` on the cell, the press its content's own.
119
+ private fun cellOf(cell: BarButtonComponentCell, options: ButtonOptions): View {
120
+ cell.contentDescription = options.accessibilityLabel.get(null)
121
+ TestIds.apply(cell, options.testID.get(null))
122
+ return cell
126
123
  }
127
124
 
128
- private fun add(menu: Menu, button: Button, order: Int, inBar: Boolean) {
129
- val options = button.options
130
- val id = options.id.get(null) ?: return
131
- val item = menu.add(BUTTONS_GROUP, Menu.NONE, order, titleOf(options))
132
- itemIds[item] = options
125
+ // A text or icon button: its look, its press and long press, its `testID`. The long press shows the tooltip, or
126
+ // opens the menu of a button with `menuOnLongPress`.
127
+ private fun bind(view: BarButtonView, id: String, options: ButtonOptions): View {
133
128
  val enabled = !options.enabled.isFalse
134
- item.isEnabled = enabled
135
- if (inBar) {
136
- item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
137
- } else {
138
- item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
139
- }
140
- val cell = button.cell
141
- if (cell != null) {
142
- item.actionView = cell
143
- cell.contentDescription = options.accessibilityLabel.get(null)
144
- } else {
145
- iconOf(options, enabled)?.let { item.icon = it }
146
- options.accessibilityLabel.get(null)?.let { MenuItemCompat.setContentDescription(item, it) }
147
- }
148
- item.setOnMenuItemClickListener {
149
- val menuOptions = host.menuOf(id)
150
- if (cell == null && menuOptions != null && !options.menuOnLongPress.isTrue) {
151
- openMenu(id, menuOptions, viewOf(item))
152
- } else if (cell == null) {
153
- host.pressed(id)
129
+ view.isEnabled = enabled
130
+ view.restyle()
131
+ val longPressMenu = options.menuOnLongPress.isTrue && host.menuOf(id) != null
132
+ view.setOnLongClickListener(null)
133
+ // A listener, even none, makes the view long-clickable: TalkBack would offer a long press that does nothing.
134
+ view.isLongClickable = false
135
+ view.show(titleOf(options), iconOf(options, enabled), options.accessibilityLabel.get(null), !longPressMenu)
136
+ if (longPressMenu) {
137
+ view.setOnLongClickListener { pressed ->
138
+ val menu = host.menuOf(id) ?: return@setOnLongClickListener false
139
+ openMenu(id, menu, pressed)
140
+ true
154
141
  }
155
- true
156
142
  }
143
+ view.setOnClickListener { pressed -> press(id, options, pressed) }
144
+ TestIds.apply(view, options.testID.get(null))
145
+ return view
146
+ }
147
+
148
+ // A press: the button's menu opens under [anchor] (at the bar's end without one), or the press goes to the screen.
149
+ private fun press(id: String, options: ButtonOptions, anchor: View?) {
150
+ val menu = host.menuOf(id)
151
+ if (menu != null && !options.menuOnLongPress.isTrue) openMenu(id, menu, anchor) else host.pressed(id)
152
+ }
153
+
154
+ // "⋮" opens as a toolbar opens it: the theme's overflow popup from the end of the button, in the bar's popup theme.
155
+ private fun openOverflow(anchor: View) {
156
+ closeMenus()
157
+ val popupTheme = MaterialTheme.popupTheme(context)
158
+ val themed = if (popupTheme != 0) ContextThemeWrapper(context, popupTheme) else context
159
+ val colors = host.menuColors()
160
+ val menu = BarMenu(themed, anchor, Gravity.END, AppCompatR.attr.actionOverflowMenuStyle, colors)
161
+ menu.onDismiss = { if (overflowPopup === menu) overflowPopup = null }
162
+ overflowPopup = menu
163
+ menu.show(overflowEntries(themed, colors.text), overflowIcons())
164
+ }
165
+
166
+ // The icons of "⋮" show when an `overflowMenu` item has one, the moved buttons' icons with them.
167
+ private fun overflowIcons(): Boolean = overflowMenu?.items.orEmpty().any { it.icon != null }
168
+
169
+ // The `overflowMenu` items, then the buttons that don't fit in the bar, in the list's order; a button keeps its own
170
+ // color.
171
+ private fun overflowEntries(themed: Context, textColor: Int?): List<BarMenu.Entry> {
172
+ val items = overflowMenu?.let {
173
+ ButtonMenus.entries(themed, it, textColor, loadIcon) { id -> host.picked(null, id) }
174
+ }.orEmpty()
175
+ val buttons = inOverflow.mapNotNull { options ->
176
+ val id = options.id.get(null) ?: return@mapNotNull null
177
+ val enabled = !options.enabled.isFalse
178
+ BarMenu.Entry(
179
+ titleOf(options),
180
+ iconOf(options, enabled),
181
+ enabled = enabled,
182
+ testId = options.testID.get(null),
183
+ label = options.accessibilityLabel.get(null),
184
+ onPick = { press(id, options, null) },
185
+ )
186
+ }
187
+ return items + buttons
157
188
  }
158
189
 
159
190
  // The text of a button: its font (missing weight or italic made synthetically), its size in sp, its color, its
@@ -218,93 +249,12 @@ internal class ButtonBar(
218
249
  if (options.items.isEmpty()) return
219
250
  popup?.dismiss()
220
251
  val themed = MaterialTheme.context(context)
221
- val menu = if (anchor != null) {
222
- PopupMenu(themed, anchor)
223
- } else {
224
- PopupMenu(themed, toolbar, Gravity.END)
225
- }
226
- ButtonMenus.fill(themed, menu.menu, Menu.NONE, 0, options, null, loadIcon) { id -> host.picked(buttonId, id) }
227
- menu.setOnDismissListener { if (popup === menu) popup = null }
252
+ val colors = host.menuColors()
253
+ val gravity = if (anchor != null) Gravity.START else Gravity.END
254
+ val menu = BarMenu(themed, anchor ?: row, gravity, AppCompatR.attr.popupMenuStyle, colors)
255
+ menu.onDismiss = { if (popup === menu) popup = null }
228
256
  popup = menu
229
- menu.show()
230
- }
231
-
232
- // The view of [item] in the bar; none in "⋮" or before the bar laid it out.
233
- private fun viewOf(item: MenuItem): View? {
234
- val menuView = actionMenuView() ?: return null
235
- for (index in 0 until menuView.childCount) {
236
- val child = menuView.getChildAt(index)
237
- if (itemOf(child) === item || item.actionView === child) return child
238
- }
239
- return null
240
- }
241
-
242
- private fun actionMenuView(): ActionMenuView? {
243
- for (index in 0 until toolbar.childCount) {
244
- val child = toolbar.getChildAt(index)
245
- if (child is ActionMenuView) return child
246
- }
247
- return null
248
- }
249
-
250
- // AppCompat gives an item's view no public way back to its item: the item view's own interface, which only
251
- // AppCompat's own views implement, is read and never changed.
252
- @SuppressLint("RestrictedApi")
253
- private fun itemOf(view: View): MenuItem? = (view as? MenuView.ItemView)?.itemData
254
-
255
- // The `testID`s of the item views: the bar makes its views again, so after every layout; a view used again without
256
- // one loses the one it had. "⋮" by AppCompat's description of it. A long press opens the menu of a button with
257
- // `menuOnLongPress`: the handler finds the item by the view pressed.
258
- private fun applyViewIds() {
259
- val menuView = actionMenuView() ?: return
260
- if (watched !== menuView) {
261
- watched = menuView
262
- menuView.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
263
- override fun onChildViewAdded(parent: View?, child: View?) {
264
- toolbar.post { applyViewIds() }
265
- }
266
-
267
- override fun onChildViewRemoved(parent: View?, child: View?) = Unit
268
- })
269
- }
270
- val overflowDescription = context.getString(AppCompatR.string.abc_action_menu_overflow_description)
271
- for (index in 0 until menuView.childCount) {
272
- val child = menuView.getChildAt(index)
273
- val item = itemOf(child) ?: itemIds.keys.firstOrNull { it.actionView === child }
274
- val options = item?.let(itemIds::get)
275
- when {
276
- options != null -> {
277
- TestIds.apply(child, options.testID.get(null))
278
- if (child !is BarButtonComponentCell) longPressOf(child, options)
279
- }
280
- child.contentDescription == overflowDescription -> TestIds.apply(child, overflowTestId)
281
- item != null -> TestIds.apply(child, host.testIdOf(item))
282
- else -> Unit
283
- }
284
- }
285
- }
286
-
287
- // The toolbar uses its item views again for other items: the handler finds the button by the view pressed, at the
288
- // press, and a view that no longer needs it gets the toolbar's tooltip back.
289
- private fun longPressOf(view: View, options: ButtonOptions) {
290
- val id = options.id.get(null)
291
- if (id != null && options.menuOnLongPress.isTrue && host.menuOf(id) != null) {
292
- hooked[view] = true
293
- view.setOnLongClickListener { pressed -> openOnLongPress(pressed) }
294
- return
295
- }
296
- if (hooked.remove(view) == null) return
297
- view.setOnLongClickListener(null)
298
- itemOf(view)?.let { TooltipCompat.setTooltipText(view, MenuItemCompat.getTooltipText(it) ?: it.title) }
299
- }
300
-
301
- private fun openOnLongPress(pressed: View): Boolean {
302
- val options = itemOf(pressed)?.let(itemIds::get) ?: return false
303
- val id = options.id.get(null) ?: return false
304
- if (!options.menuOnLongPress.isTrue) return false
305
- val menu = host.menuOf(id) ?: return false
306
- openMenu(id, menu, pressed)
307
- return true
257
+ menu.show(ButtonMenus.entries(themed, options, colors.text, loadIcon) { id -> host.picked(buttonId, id) })
308
258
  }
309
259
 
310
260
  // A font of the options on a button's text, with the weight and the slant the typeface lacks drawn synthetically.
@@ -323,10 +273,6 @@ internal class ButtonBar(
323
273
  }
324
274
 
325
275
  private companion object {
326
- const val BUTTONS_GROUP = 1
327
- const val OVERFLOW_GROUP = 2
328
- const val OVERFLOW_ORDER = 100
329
- const val BAR_ORDER = 1000
330
276
  const val CLOSE = "close"
331
277
  const val BOLD_WEIGHT = 600
332
278
  const val ITALIC_SKEW = -0.25f
@@ -1,15 +1,11 @@
1
1
  package dev.apexnative.navigation.views.stack.topbar.titlebar
2
2
 
3
- import android.annotation.SuppressLint
4
3
  import android.content.Context
5
4
  import android.graphics.Color
6
5
  import android.graphics.drawable.Drawable
7
6
  import android.text.SpannableString
8
7
  import android.text.Spanned
9
8
  import android.text.style.ForegroundColorSpan
10
- import android.view.Menu
11
- import android.view.MenuItem
12
- import androidx.appcompat.view.menu.MenuBuilder
13
9
  import com.google.android.material.color.MaterialColors
14
10
  import dev.apexnative.navigation.options.ButtonMenuOptions
15
11
  import dev.apexnative.navigation.utils.MaterialTheme
@@ -17,78 +13,52 @@ import androidx.appcompat.R as AppCompatR
17
13
  import com.google.android.material.R as MaterialR
18
14
 
19
15
  /**
20
- * Puts the items of a bar menu (the overflow menu, the menu of a button) into an Android menu: their titles and icons,
21
- * the destructive ones red, the checked ones with a checkmark. A pick of an item calls `onPick` with its id; with
22
- * `singleSelection` the checkmark moves to it in the open menu, and the caller writes it into the screen's options.
16
+ * Turns the items of a bar menu (the overflow menu, the menu of a button) into the entries of a [BarMenu]: their
17
+ * titles and icons, the destructive ones red, the checked ones with a check mark, the items of a `singleSelection` menu
18
+ * with radio buttons. Icons take `textColor` (`topBar.menuTextColor`), else the theme's on-surface-variant color. A
19
+ * pick of an item calls `onPick` with its id; the caller writes a `singleSelection` choice into the screen's options.
23
20
  */
24
- object ButtonMenus {
25
- fun fill(
21
+ internal object ButtonMenus {
22
+ fun entries(
26
23
  context: Context,
27
- menu: Menu,
28
- group: Int,
29
- firstOrder: Int,
30
24
  options: ButtonMenuOptions,
31
- showAsAction: Int?,
25
+ textColor: Int?,
32
26
  loadIcon: (String) -> Drawable?,
33
27
  onPick: (String) -> Unit,
34
- ) {
35
- addElements(context, menu, group, firstOrder, options.items, options.singleSelection, showAsAction, loadIcon, onPick)
36
- }
28
+ ): List<BarMenu.Entry> = entriesOf(context, options.items, options.singleSelection, textColor, loadIcon, onPick)
37
29
 
38
- private fun addElements(
30
+ private fun entriesOf(
39
31
  context: Context,
40
- menu: Menu,
41
- group: Int,
42
- firstOrder: Int,
43
32
  elements: List<ButtonMenuOptions.Element>,
44
33
  singleSelection: Boolean,
45
- showAsAction: Int?,
34
+ textColor: Int?,
46
35
  loadIcon: (String) -> Drawable?,
47
36
  onPick: (String) -> Unit,
48
- ) {
49
- val submenus = mutableListOf<MenuItem>()
50
- elements.forEachIndexed { index, element ->
51
- val order = firstOrder + index
52
- val item = when (element) {
53
- is ButtonMenuOptions.Submenu -> menu.addSubMenu(group, Menu.NONE, order, element.title).also {
54
- addElements(context, it, group, 0, element.items, element.singleSelection, null, loadIcon, onPick)
55
- }.item.also { submenus += it }
56
- is ButtonMenuOptions.Item -> menu.add(group, Menu.NONE, order, title(context, element)).apply {
57
- isEnabled = !element.disabled
58
- if (singleSelection || element.checked) {
59
- isCheckable = true
60
- isChecked = element.checked
61
- }
62
- setOnMenuItemClickListener { picked ->
63
- if (singleSelection) picked.isChecked = true
64
- onPick(element.id)
65
- true
66
- }
67
- }
68
- }
69
- val destructive = element is ButtonMenuOptions.Item && element.destructive
70
- val tint = if (destructive) errorColor(context) else iconColor(context)
71
- element.icon?.let { uri -> item.icon = loadIcon(uri)?.mutate()?.also { it.setTint(tint) } }
72
- if (showAsAction != null) item.setShowAsAction(showAsAction)
73
- }
74
- if (singleSelection) {
75
- menu.setGroupCheckable(group, true, true)
76
- // The group made the submenus checkable too: they open, and have no checkmark.
77
- submenus.forEach { it.isCheckable = false }
37
+ ): List<BarMenu.Entry> = elements.map { element ->
38
+ val destructive = element is ButtonMenuOptions.Item && element.destructive
39
+ val tint = if (destructive) errorColor(context) else textColor ?: iconColor(context)
40
+ val icon = element.icon?.let(loadIcon)?.mutate()?.also { it.setTint(tint) }
41
+ when (element) {
42
+ is ButtonMenuOptions.Submenu -> BarMenu.Entry(
43
+ element.title,
44
+ icon,
45
+ submenu = entriesOf(context, element.items, element.singleSelection, textColor, loadIcon, onPick),
46
+ testId = element.testID,
47
+ )
48
+ is ButtonMenuOptions.Item -> BarMenu.Entry(
49
+ title(context, element),
50
+ icon,
51
+ enabled = !element.disabled,
52
+ check = when {
53
+ singleSelection -> BarMenu.Check.Radio
54
+ element.checked -> BarMenu.Check.Box
55
+ else -> BarMenu.Check.None
56
+ },
57
+ checked = element.checked,
58
+ testId = element.testID,
59
+ onPick = { onPick(element.id) },
60
+ )
78
61
  }
79
- if (elements.any { it.icon != null }) showIcons(menu)
80
- }
81
-
82
- /** Some item of the menu, or of a submenu, has an icon. */
83
- fun hasIcons(options: ButtonMenuOptions): Boolean = hasIcons(options.items)
84
-
85
- private fun hasIcons(elements: List<ButtonMenuOptions.Element>): Boolean =
86
- elements.any { it.icon != null || (it is ButtonMenuOptions.Submenu && hasIcons(it.items)) }
87
-
88
- // Material's menus have their icons at the start of the items; the toolbar's menus leave them out by default.
89
- @SuppressLint("RestrictedApi")
90
- private fun showIcons(menu: Menu) {
91
- (menu as? MenuBuilder)?.setOptionalIconsVisible(true)
92
62
  }
93
63
 
94
64
  private fun title(context: Context, item: ButtonMenuOptions.Item): CharSequence {