@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
@@ -12,147 +12,159 @@ import org.json.JSONArray
12
12
  /**
13
13
  * Sends the events `ANN.<Name>` to the JavaScript of [reactContext] through React Native's device event emitter, in
14
14
  * the order they are sent: sent from the main thread only. Without a context, or when its React instance is gone, an
15
- * event is dropped with one line in the log. The names and the fields are the contract. The core sends through
16
- * [current]; modules keep making their own.
15
+ * event is dropped with one line in the log. The names and the keys come from [EventContract], made from the types of
16
+ * `src/interfaces/`. The core sends through [current]; modules keep making their own.
17
17
  */
18
18
  class EventEmitter(private val reactContext: ReactContext?) {
19
19
  /** Whether an event sent now reaches JavaScript. */
20
20
  val canSend: Boolean
21
21
  get() = reactContext?.hasActiveReactInstance() == true
22
22
 
23
- fun emitAppLaunched() = emit(APP_LAUNCHED, Arguments.createMap())
23
+ fun emitAppLaunched() = emit(EventContract.AppLaunched.EVENT, Arguments.createMap())
24
24
 
25
25
  fun emitCommandCompleted(commandName: String, commandId: String, completionTime: Long) =
26
- emit(COMMAND_COMPLETED, Arguments.createMap().apply {
27
- putString("commandName", commandName)
28
- putString("commandId", commandId)
29
- putDouble("completionTime", completionTime.toDouble())
26
+ emit(EventContract.CommandCompleted.EVENT, Arguments.createMap().apply {
27
+ putString(EventContract.CommandCompleted.COMMAND_NAME, commandName)
28
+ putString(EventContract.CommandCompleted.COMMAND_ID, commandId)
29
+ putDouble(EventContract.CommandCompleted.COMPLETION_TIME, completionTime.toDouble())
30
30
  })
31
31
 
32
32
  fun emitComponentWillAppear(componentId: String, componentName: String, type: ComponentType) =
33
- emit(COMPONENT_WILL_APPEAR, component(componentId, componentName, type))
33
+ emit(EventContract.ComponentWillAppear.EVENT, component(componentId, componentName, type))
34
34
 
35
35
  fun emitComponentDidAppear(componentId: String, componentName: String, type: ComponentType) =
36
- emit(COMPONENT_DID_APPEAR, component(componentId, componentName, type))
36
+ emit(EventContract.ComponentDidAppear.EVENT, component(componentId, componentName, type))
37
37
 
38
38
  fun emitComponentDidDisappear(componentId: String, componentName: String, type: ComponentType) =
39
- emit(COMPONENT_DID_DISAPPEAR, component(componentId, componentName, type))
39
+ emit(EventContract.ComponentDidDisappear.EVENT, component(componentId, componentName, type))
40
40
 
41
41
  /** The screen the user sees (`null` for none) and the selected tab of the tabs it is in (`null` out of tabs). */
42
42
  fun emitNavigationStateChanged(screenId: String?, screenName: String?, tab: Int?) =
43
- emit(NAVIGATION_STATE_CHANGED, Arguments.createMap().apply {
43
+ emit(EventContract.NavigationStateChanged.EVENT, Arguments.createMap().apply {
44
+ val keys = EventContract.NavigationStateChanged
44
45
  if (screenId == null) {
45
- putNull("screen")
46
+ putNull(keys.SCREEN)
46
47
  } else {
47
- putMap("screen", Arguments.createMap().apply {
48
- putString("id", screenId)
49
- putString("name", screenName)
48
+ putMap(keys.SCREEN, Arguments.createMap().apply {
49
+ putString(EventContract.NavigationStateChangedScreen.ID, screenId)
50
+ putString(EventContract.NavigationStateChangedScreen.NAME, screenName)
50
51
  })
51
52
  }
52
- if (tab == null) putNull("tab") else putInt("tab", tab)
53
+ if (tab == null) putNull(keys.TAB) else putInt(keys.TAB, tab)
53
54
  })
54
55
 
55
- fun emitScreenPopped(componentId: String) = emit(SCREEN_POPPED, withId(componentId))
56
+ fun emitScreenPopped(componentId: String) = emit(EventContract.ScreenPopped.EVENT, withId(componentId))
56
57
 
57
58
  fun emitNavigationButtonPressed(componentId: String, buttonId: String) =
58
- emit(NAVIGATION_BUTTON_PRESSED, withId(componentId).apply { putString("buttonId", buttonId) })
59
+ emit(EventContract.NavigationButtonPressed.EVENT, withId(componentId).apply {
60
+ putString(EventContract.NavigationButtonPressed.BUTTON_ID, buttonId)
61
+ })
59
62
 
60
63
  fun emitBottomTabSelected(selectedTabIndex: Int, unselectedTabIndex: Int, bottomTabsId: String) =
61
- emit(BOTTOM_TAB_SELECTED, Arguments.createMap().apply {
62
- putInt("selectedTabIndex", selectedTabIndex)
63
- putInt("unselectedTabIndex", unselectedTabIndex)
64
- putString("bottomTabsId", bottomTabsId)
64
+ emit(EventContract.BottomTabSelected.EVENT, Arguments.createMap().apply {
65
+ putInt(EventContract.BottomTabSelected.SELECTED_TAB_INDEX, selectedTabIndex)
66
+ putInt(EventContract.BottomTabSelected.UNSELECTED_TAB_INDEX, unselectedTabIndex)
67
+ putString(EventContract.BottomTabSelected.BOTTOM_TABS_ID, bottomTabsId)
65
68
  })
66
69
 
67
70
  fun emitBottomTabPressed(tabIndex: Int, bottomTabsId: String, screenId: String?, wasSelected: Boolean) =
68
- emit(BOTTOM_TAB_PRESSED, Arguments.createMap().apply {
69
- putInt("tabIndex", tabIndex)
70
- putString("bottomTabsId", bottomTabsId)
71
- screenId?.let { putString("screenId", it) }
72
- putBoolean("wasSelected", wasSelected)
71
+ emit(EventContract.BottomTabPressed.EVENT, Arguments.createMap().apply {
72
+ putInt(EventContract.BottomTabPressed.TAB_INDEX, tabIndex)
73
+ putString(EventContract.BottomTabPressed.BOTTOM_TABS_ID, bottomTabsId)
74
+ screenId?.let { putString(EventContract.BottomTabPressed.SCREEN_ID, it) }
75
+ putBoolean(EventContract.BottomTabPressed.WAS_SELECTED, wasSelected)
73
76
  })
74
77
 
75
78
  fun emitBottomTabLongPressed(selectedTabIndex: Int, bottomTabsId: String) =
76
- emit(BOTTOM_TAB_LONG_PRESSED, Arguments.createMap().apply {
77
- putInt("selectedTabIndex", selectedTabIndex)
78
- putString("bottomTabsId", bottomTabsId)
79
+ emit(EventContract.BottomTabLongPressed.EVENT, Arguments.createMap().apply {
80
+ putInt(EventContract.BottomTabLongPressed.SELECTED_TAB_INDEX, selectedTabIndex)
81
+ putString(EventContract.BottomTabLongPressed.BOTTOM_TABS_ID, bottomTabsId)
79
82
  })
80
83
 
81
84
  /** The id and the name (without `slot:`) of the screen the modal showed, and how many modals went. */
82
85
  fun emitModalDismissed(componentId: String, componentName: String, modalsDismissed: Int) =
83
- emit(MODAL_DISMISSED, withId(componentId).apply {
84
- putString("componentName", componentName)
85
- putInt("modalsDismissed", modalsDismissed)
86
+ emit(EventContract.ModalDismissed.EVENT, withId(componentId).apply {
87
+ putString(EventContract.ModalDismissed.COMPONENT_NAME, componentName)
88
+ putInt(EventContract.ModalDismissed.MODALS_DISMISSED, modalsDismissed)
86
89
  })
87
90
 
88
- fun emitModalAttemptedToDismiss(componentId: String) = emit(MODAL_ATTEMPTED_TO_DISMISS, withId(componentId))
91
+ fun emitModalAttemptedToDismiss(componentId: String) =
92
+ emit(EventContract.ModalAttemptedToDismiss.EVENT, withId(componentId))
89
93
 
90
94
  /** [detent]: `medium`, `large`, or a height in dp. */
91
95
  fun emitSheetDetentChanged(componentId: String, detent: Any) =
92
- emit(SHEET_DETENT_CHANGED, withId(componentId).apply {
96
+ emit(EventContract.SheetDetentChanged.EVENT, withId(componentId).apply {
93
97
  when (detent) {
94
- is Number -> putDouble("detent", detent.toDouble())
95
- else -> putString("detent", detent.toString())
98
+ is Number -> putDouble(EventContract.SheetDetentChanged.DETENT, detent.toDouble())
99
+ else -> putString(EventContract.SheetDetentChanged.DETENT, detent.toString())
96
100
  }
97
101
  })
98
102
 
99
103
  fun emitSearchBarUpdated(componentId: String, text: String, isFocused: Boolean, scope: String?) =
100
- emit(SEARCH_BAR_UPDATED, withId(componentId).apply {
101
- putString("text", text)
102
- putBoolean("isFocused", isFocused)
103
- scope?.let { putString("scope", it) }
104
+ emit(EventContract.SearchBarUpdated.EVENT, withId(componentId).apply {
105
+ putString(EventContract.SearchBarUpdated.TEXT, text)
106
+ putBoolean(EventContract.SearchBarUpdated.IS_FOCUSED, isFocused)
107
+ scope?.let { putString(EventContract.SearchBarUpdated.SCOPE, it) }
104
108
  })
105
109
 
106
110
  fun emitSearchBarSubmitted(componentId: String, text: String, scope: String?) =
107
- emit(SEARCH_BAR_SUBMITTED, withId(componentId).apply {
108
- putString("text", text)
109
- scope?.let { putString("scope", it) }
111
+ emit(EventContract.SearchBarSubmitted.EVENT, withId(componentId).apply {
112
+ putString(EventContract.SearchBarSubmitted.TEXT, text)
113
+ scope?.let { putString(EventContract.SearchBarSubmitted.SCOPE, it) }
110
114
  })
111
115
 
112
- fun emitSearchBarCancelPressed(componentId: String) = emit(SEARCH_BAR_CANCEL_PRESSED, withId(componentId))
116
+ fun emitSearchBarCancelPressed(componentId: String) =
117
+ emit(EventContract.SearchBarCancelPressed.EVENT, withId(componentId))
113
118
 
114
119
  fun emitSearchSuggestionPicked(componentId: String, suggestionId: String) =
115
- emit(SEARCH_SUGGESTION_PICKED, withId(componentId).apply { putString("suggestionId", suggestionId) })
120
+ emit(EventContract.SearchSuggestionPicked.EVENT, withId(componentId).apply {
121
+ putString(EventContract.SearchSuggestionPicked.SUGGESTION_ID, suggestionId)
122
+ })
116
123
 
117
124
  fun emitSplitViewColumnsStacked(componentId: String, stacked: Boolean) =
118
- emit(SPLIT_VIEW_COLUMNS_STACKED, withId(componentId).apply { putBoolean("stacked", stacked) })
125
+ emit(EventContract.SplitViewColumnsStacked.EVENT, withId(componentId).apply {
126
+ putBoolean(EventContract.SplitViewColumnsStacked.STACKED, stacked)
127
+ })
119
128
 
120
- fun emitSlotDestroyed(componentId: String) = emit(SLOT_DESTROYED, withId(componentId))
129
+ fun emitSlotDestroyed(componentId: String) = emit(EventContract.SlotDestroyed.EVENT, withId(componentId))
121
130
 
122
131
  /** [insets]: `{ top, right, bottom, left }`. */
123
132
  fun emitScreenInsetsChanged(componentId: String, insets: WritableMap) =
124
- emit(SCREEN_INSETS_CHANGED, withId(componentId).apply { merge(insets) })
133
+ emit(EventContract.ScreenInsetsChanged.EVENT, withId(componentId).apply { merge(insets) })
125
134
 
126
135
  /** [regions]: `{ division, occlusion }`. */
127
136
  fun emitScreenRegionsChanged(componentId: String, regions: WritableMap) =
128
- emit(SCREEN_REGIONS_CHANGED, withId(componentId).apply { merge(regions) })
137
+ emit(EventContract.ScreenRegionsChanged.EVENT, withId(componentId).apply { merge(regions) })
129
138
 
130
139
  /** [state]: `{ placement, layout, front, hidden }`. */
131
140
  fun emitArrangementChanged(componentId: String, state: WritableMap) =
132
- emit(ARRANGEMENT_CHANGED, withId(componentId).apply { merge(state) })
141
+ emit(EventContract.ArrangementChanged.EVENT, withId(componentId).apply { merge(state) })
133
142
 
134
143
  /** [value]: `{ state, angle? }`, or `{}` without a hinge. */
135
- fun emitHingeChanged(value: WritableMap) = emit(HINGE_CHANGED, value)
144
+ fun emitHingeChanged(value: WritableMap) = emit(EventContract.HingeChanged.EVENT, value)
136
145
 
137
146
  /** [kind]: `function` or `command`; [arguments] as JSON, which JavaScript gets as `args`. */
138
147
  fun emitNativeScreenCall(componentId: String, callId: String, kind: String, name: String, arguments: JSONArray) =
139
- emit(NATIVE_SCREEN_CALL, withId(componentId).apply {
140
- putString("callId", callId)
141
- putString("kind", kind)
142
- putString("name", name)
143
- putArray("args", NativeScreenCalls.toArguments(arguments))
148
+ emit(EventContract.NativeScreenCall.EVENT, withId(componentId).apply {
149
+ putString(EventContract.NativeScreenCall.CALL_ID, callId)
150
+ putString(EventContract.NativeScreenCall.KIND, kind)
151
+ putString(EventContract.NativeScreenCall.NAME, name)
152
+ putArray(EventContract.NativeScreenCall.ARGS, NativeScreenCalls.toArguments(arguments))
144
153
  })
145
154
 
146
- fun emitNativeScreenReleased(componentId: String) = emit(NATIVE_SCREEN_RELEASED, withId(componentId))
155
+ fun emitNativeScreenReleased(componentId: String) =
156
+ emit(EventContract.NativeScreenReleased.EVENT, withId(componentId))
147
157
 
158
+ // The three appearance events have the same keys.
148
159
  private fun component(componentId: String, componentName: String, type: ComponentType): WritableMap =
149
160
  withId(componentId).apply {
150
- putString("componentName", componentName)
151
- putString("componentType", type.jsName)
161
+ putString(EventContract.ComponentDidAppear.COMPONENT_NAME, componentName)
162
+ putString(EventContract.ComponentDidAppear.COMPONENT_TYPE, type.jsName)
152
163
  }
153
164
 
165
+ // The events of a screen carry its id under the same key.
154
166
  private fun withId(componentId: String): WritableMap =
155
- Arguments.createMap().apply { putString("componentId", componentId) }
167
+ Arguments.createMap().apply { putString(EventContract.ScreenPopped.COMPONENT_ID, componentId) }
156
168
 
157
169
  private fun emit(name: String, data: WritableMap) {
158
170
  val context = reactContext
@@ -169,31 +181,33 @@ class EventEmitter(private val reactContext: ReactContext?) {
169
181
  fun current(): EventEmitter =
170
182
  EventEmitter(NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext)
171
183
 
172
- const val APP_LAUNCHED = "ANN.AppLaunched"
173
- const val COMMAND_COMPLETED = "ANN.CommandCompleted"
174
- const val COMPONENT_WILL_APPEAR = "ANN.ComponentWillAppear"
175
- const val COMPONENT_DID_APPEAR = "ANN.ComponentDidAppear"
176
- const val COMPONENT_DID_DISAPPEAR = "ANN.ComponentDidDisappear"
177
- const val NAVIGATION_STATE_CHANGED = "ANN.NavigationStateChanged"
178
- const val SCREEN_POPPED = "ANN.ScreenPopped"
179
- const val NAVIGATION_BUTTON_PRESSED = "ANN.NavigationButtonPressed"
180
- const val BOTTOM_TAB_SELECTED = "ANN.BottomTabSelected"
181
- const val BOTTOM_TAB_PRESSED = "ANN.BottomTabPressed"
182
- const val BOTTOM_TAB_LONG_PRESSED = "ANN.BottomTabLongPressed"
183
- const val MODAL_DISMISSED = "ANN.ModalDismissed"
184
- const val MODAL_ATTEMPTED_TO_DISMISS = "ANN.ModalAttemptedToDismiss"
185
- const val SHEET_DETENT_CHANGED = "ANN.SheetDetentChanged"
186
- const val SEARCH_BAR_UPDATED = "ANN.SearchBarUpdated"
187
- const val SEARCH_BAR_SUBMITTED = "ANN.SearchBarSubmitted"
188
- const val SEARCH_BAR_CANCEL_PRESSED = "ANN.SearchBarCancelPressed"
189
- const val SEARCH_SUGGESTION_PICKED = "ANN.SearchSuggestionPicked"
190
- const val SPLIT_VIEW_COLUMNS_STACKED = "ANN.SplitViewColumnsStacked"
191
- const val SLOT_DESTROYED = "ANN.SlotDestroyed"
192
- const val SCREEN_INSETS_CHANGED = "ANN.ScreenInsetsChanged"
193
- const val SCREEN_REGIONS_CHANGED = "ANN.ScreenRegionsChanged"
194
- const val ARRANGEMENT_CHANGED = "ANN.ArrangementChanged"
195
- const val HINGE_CHANGED = "ANN.HingeChanged"
196
- const val NATIVE_SCREEN_CALL = "ANN.NativeScreenCall"
197
- const val NATIVE_SCREEN_RELEASED = "ANN.NativeScreenReleased"
184
+ // The names of the events, for modules outside the library; inside it, [EventContract] has them with their
185
+ // keys.
186
+ const val APP_LAUNCHED = EventContract.AppLaunched.EVENT
187
+ const val COMMAND_COMPLETED = EventContract.CommandCompleted.EVENT
188
+ const val COMPONENT_WILL_APPEAR = EventContract.ComponentWillAppear.EVENT
189
+ const val COMPONENT_DID_APPEAR = EventContract.ComponentDidAppear.EVENT
190
+ const val COMPONENT_DID_DISAPPEAR = EventContract.ComponentDidDisappear.EVENT
191
+ const val NAVIGATION_STATE_CHANGED = EventContract.NavigationStateChanged.EVENT
192
+ const val SCREEN_POPPED = EventContract.ScreenPopped.EVENT
193
+ const val NAVIGATION_BUTTON_PRESSED = EventContract.NavigationButtonPressed.EVENT
194
+ const val BOTTOM_TAB_SELECTED = EventContract.BottomTabSelected.EVENT
195
+ const val BOTTOM_TAB_PRESSED = EventContract.BottomTabPressed.EVENT
196
+ const val BOTTOM_TAB_LONG_PRESSED = EventContract.BottomTabLongPressed.EVENT
197
+ const val MODAL_DISMISSED = EventContract.ModalDismissed.EVENT
198
+ const val MODAL_ATTEMPTED_TO_DISMISS = EventContract.ModalAttemptedToDismiss.EVENT
199
+ const val SHEET_DETENT_CHANGED = EventContract.SheetDetentChanged.EVENT
200
+ const val SEARCH_BAR_UPDATED = EventContract.SearchBarUpdated.EVENT
201
+ const val SEARCH_BAR_SUBMITTED = EventContract.SearchBarSubmitted.EVENT
202
+ const val SEARCH_BAR_CANCEL_PRESSED = EventContract.SearchBarCancelPressed.EVENT
203
+ const val SEARCH_SUGGESTION_PICKED = EventContract.SearchSuggestionPicked.EVENT
204
+ const val SPLIT_VIEW_COLUMNS_STACKED = EventContract.SplitViewColumnsStacked.EVENT
205
+ const val SLOT_DESTROYED = EventContract.SlotDestroyed.EVENT
206
+ const val SCREEN_INSETS_CHANGED = EventContract.ScreenInsetsChanged.EVENT
207
+ const val SCREEN_REGIONS_CHANGED = EventContract.ScreenRegionsChanged.EVENT
208
+ const val ARRANGEMENT_CHANGED = EventContract.ArrangementChanged.EVENT
209
+ const val HINGE_CHANGED = EventContract.HingeChanged.EVENT
210
+ const val NATIVE_SCREEN_CALL = EventContract.NativeScreenCall.EVENT
211
+ const val NATIVE_SCREEN_RELEASED = EventContract.NativeScreenReleased.EVENT
198
212
  }
199
213
  }
@@ -45,12 +45,6 @@ internal class ScreenAppearance(
45
45
  /** Cancels the hold limit running for [held]; none when nothing is held. */
46
46
  internal var cancelLimit: (() -> Unit)? = null
47
47
 
48
- /**
49
- * This appearance started without its WillAppear (`AppearanceDispatcher.withoutWillAppear`: a side menu's drawer
50
- * that jumped open); its DidAppear is its first event. Cleared as it is hidden.
51
- */
52
- internal var skippedWill = false
53
-
54
48
  private var ready = content == null
55
49
  private var waitsForContent = false
56
50
 
@@ -22,8 +22,8 @@ object UiUtils {
22
22
  fun pxToDp(context: Context, px: Float): Float = px / context.resources.displayMetrics.density
23
23
 
24
24
  /**
25
- * A size of the options in dp, in whole pixels of the screen of [context]: times `densityDpi / 160`, the fraction
26
- * of a pixel dropped; a size of 0 or less is not converted.
25
+ * A size of the options in dp, in whole pixels of the screen of [context]: times `densityDpi / 160`, rounded to the
26
+ * nearest pixel only here, as iOS places a size in points on whole pixels; a size of 0 or less is not converted.
27
27
  */
28
28
  @JvmStatic
29
29
  fun dpToPixels(context: Context, dp: Double): Int = dpToPixels(dp, context.resources.displayMetrics.densityDpi)
@@ -31,7 +31,7 @@ object UiUtils {
31
31
  /** [dp] in whole pixels at [densityDpi], as [dpToPixels] converts it. */
32
32
  @JvmStatic
33
33
  fun dpToPixels(dp: Double, densityDpi: Int): Int =
34
- if (dp <= 0) dp.toInt() else (dp * densityDpi / BASE_DENSITY_DPI).toInt()
34
+ if (dp <= 0) dp.toInt() else (dp * densityDpi / BASE_DENSITY_DPI).roundToInt()
35
35
 
36
36
  /**
37
37
  * A font size of the options in sp, in pixels of the screen of [context]: it follows the system's text scale,
@@ -2,24 +2,25 @@ package dev.apexnative.navigation.viewcontrollers.arrangement
2
2
 
3
3
  import android.app.Activity
4
4
  import dev.apexnative.navigation.options.Options
5
+ import dev.apexnative.navigation.viewcontrollers.component.ComponentPresenter
5
6
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
6
7
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
7
8
 
8
9
  /**
9
10
  * The presenter of the secondary screen of a pair ([ArrangementController]). The status bar, the navigation bar and
10
- * the orientation are the primary's, as the top bar is: the secondary applies only its background, and brought to the
11
- * front it gives them back to the primary.
11
+ * the orientation are the primary's, as the top bar is: the secondary applies only its background (the theme's
12
+ * without `layout.backgroundColor`, as any React screen), and brought to the front it gives them back to the primary.
12
13
  */
13
14
  internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Options) :
14
15
  Presenter(activity, defaultOptions) {
15
16
 
16
17
  override fun applyOptions(view: ViewController<*>, options: Options) =
17
- applyBackgroundColor(view, options.copy().withDefaultOptions(defaultOptions))
18
+ applyBackground(view, options.copy().withDefaultOptions(defaultOptions))
18
19
 
19
20
  override fun mergeOptions(viewController: ViewController<*>, options: Options) {
20
21
  if (!options.layout.backgroundColor.hasValue()) return
21
22
  val merged = viewController.resolveCurrentOptions().copy().mergeWith(options)
22
- applyBackgroundColor(viewController, merged.withDefaultOptions(defaultOptions))
23
+ applyBackground(viewController, merged.withDefaultOptions(defaultOptions))
23
24
  }
24
25
 
25
26
  override fun onViewBroughtToFront(viewController: ViewController<*>, options: Options) {
@@ -27,5 +28,12 @@ internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Opti
27
28
  }
28
29
 
29
30
  override fun onConfigurationChanged(controller: ViewController<*>, options: Options) =
30
- applyBackgroundColor(controller, options.copy().withDefaultOptions(defaultOptions))
31
+ applyBackground(controller, options.copy().withDefaultOptions(defaultOptions))
32
+
33
+ // `layout.backgroundColor`, else the theme's background (`ComponentPresenter.themeBackgroundOf`).
34
+ private fun applyBackground(view: ViewController<*>, options: Options) {
35
+ if (options.layout.backgroundColor.hasValue()) return applyBackgroundColor(view, options)
36
+ val color = ComponentPresenter.themeBackgroundOf(activity) ?: return
37
+ view.peekView()?.setBackgroundColor(color)
38
+ }
31
39
  }
@@ -1,5 +1,8 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.bottomtabs
2
2
 
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.AnimatorSet
3
6
  import android.app.Activity
4
7
  import android.graphics.Color
5
8
  import android.util.Log
@@ -33,6 +36,7 @@ import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
33
36
  import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
34
37
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
35
38
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
39
+ import dev.apexnative.navigation.views.animations.FadeThrough
36
40
  import dev.apexnative.navigation.views.bottomtabs.BottomTabItem
37
41
  import dev.apexnative.navigation.views.bottomtabs.BottomTabsLayout
38
42
  import dev.apexnative.navigation.views.bottomtabs.CustomBottomTabItemView
@@ -68,6 +72,8 @@ class BottomTabsController(
68
72
  private var cells: List<CustomBottomTabItemView> = emptyList()
69
73
  // `ANN.BottomTabSelected` of a press that waits for the new tab's screen to appear.
70
74
  private var pendingSelected: (() -> Unit)? = null
75
+ // Ends the fade through of a switch still running, with its events.
76
+ private var endSwitch: (() -> Unit)? = null
71
77
  // The bar goes with a stack's transition: the options of the new top don't move it themselves.
72
78
  private var riding = false
73
79
  private var restAttached = false
@@ -150,8 +156,11 @@ class BottomTabsController(
150
156
  /** The view of [tab] joins, or is there already: [visible], or invisible but laid out. */
151
157
  fun attach(tab: ViewController<*>, visible: Boolean)
152
158
 
153
- /** The view of [shown] becomes visible and that of [hidden] invisible: the switch, at once. */
154
- fun switch(shown: ViewController<*>, hidden: ViewController<*>)
159
+ /**
160
+ * The view of [shown] becomes visible and that of [hidden] invisible: at once, or with Material's fade through
161
+ * when [animated]; then [onEnd]. The function returned ends a switch still running at once, [onEnd] included.
162
+ */
163
+ fun switch(shown: ViewController<*>, hidden: ViewController<*>, animated: Boolean, onEnd: () -> Unit): () -> Unit
155
164
  }
156
165
 
157
166
  /** The views of the tabs; none before the tabs' view is made. */
@@ -166,9 +175,43 @@ class BottomTabsController(
166
175
  tab.view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
167
176
  }
168
177
 
169
- override fun switch(shown: ViewController<*>, hidden: ViewController<*>) {
170
- shown.view.visibility = View.VISIBLE
171
- hidden.peekView()?.visibility = View.INVISIBLE
178
+ override fun switch(
179
+ shown: ViewController<*>,
180
+ hidden: ViewController<*>,
181
+ animated: Boolean,
182
+ onEnd: () -> Unit,
183
+ ): () -> Unit {
184
+ val coming = shown.view
185
+ coming.visibility = View.VISIBLE
186
+ val going = hidden.peekView()
187
+ if (!animated || going == null) {
188
+ going?.visibility = View.INVISIBLE
189
+ onEnd()
190
+ return {}
191
+ }
192
+ val exit = FadeThrough.exit(going)
193
+ // Faded out, the tab that goes takes no more touches and no accessibility focus.
194
+ exit.addListener(object : AnimatorListenerAdapter() {
195
+ override fun onAnimationEnd(animation: Animator) {
196
+ going.visibility = View.INVISIBLE
197
+ }
198
+ })
199
+ val set = AnimatorSet()
200
+ set.playTogether(exit, FadeThrough.enter(coming))
201
+ set.addListener(object : AnimatorListenerAdapter() {
202
+ override fun onAnimationEnd(animation: Animator) {
203
+ set.removeAllListeners()
204
+ // The tab that went keeps its look for its next selection; only its visibility hides it.
205
+ going.visibility = View.INVISIBLE
206
+ going.alpha = 1f
207
+ coming.alpha = 1f
208
+ coming.scaleX = 1f
209
+ coming.scaleY = 1f
210
+ onEnd()
211
+ }
212
+ })
213
+ set.start()
214
+ return set::end
172
215
  }
173
216
  }
174
217
 
@@ -302,10 +345,9 @@ class BottomTabsController(
302
345
  }
303
346
 
304
347
  /**
305
- * Selects the tab at [index]: what it shows appears, the tab it left disappears — at once, one container invisible
306
- * and the other visible —, then for a press `ANN.BottomTabSelected` right after the new screen's DidAppear and the
307
- * left tab's `popToRootOnBlur`, with the state as one update after them; from code and by back, `popToRootOnBlur`
308
- * and the state at once. The history of back follows.
348
+ * Selects the tab at [index]: the bar shows it at once; on screen the tab it left fades through to it (Material's
349
+ * fade through), off screen one container becomes invisible and the other visible at once; then [switched]. The
350
+ * history of back follows.
309
351
  */
310
352
  private fun select(index: Int, how: How) {
311
353
  if (index == selectedIndex || index !in tabs.indices || isDestroyed) return
@@ -328,10 +370,27 @@ class BottomTabsController(
328
370
  // A layer that waits for the tabs waits for the tab they show now.
329
371
  navigator?.shownScreensChanged(this)
330
372
  if (onScreen) AppearanceDispatcher.beginTransition(listOf(shown))
331
- views?.switch(shown, left)
373
+ if (views == null) return switched(index, previous, how, onScreen, hasViews = false)
374
+ endSwitch = views.switch(shown, left, animated = onScreen) {
375
+ endSwitch = null
376
+ if (!isDestroyed) switched(index, previous, how, onScreen, hasViews = true)
377
+ }
378
+ }
379
+
380
+ /**
381
+ * The switch to the tab at [index] from [previous] ended: what went disappears, what came appears — unless the tabs
382
+ * stopped showing during the fade (a modal over them): it appears when they show again; then for a press
383
+ * `ANN.BottomTabSelected` right after the new screen's DidAppear and the left tab's `popToRootOnBlur`, with the state
384
+ * as one update after them; from code and by back, `popToRootOnBlur` and the state at once. [onScreen]: the switch
385
+ * started on screen.
386
+ */
387
+ private fun switched(index: Int, previous: Int, how: How, onScreen: Boolean, hasViews: Boolean) {
388
+ val left = tabs[previous]
389
+ val shown = tabs[index]
332
390
  if (onScreen) {
333
- AppearanceDispatcher.endTransition(listOf(left), listOf(shown), null, null, null, reportsState = false)
334
- } else if (views != null) {
391
+ val showing = if (isOnScreen) listOf(shown) else emptyList()
392
+ AppearanceDispatcher.endTransition(listOf(left), showing, null, null, null, reportsState = false)
393
+ } else if (hasViews) {
335
394
  // Not on screen: the bar takes the new tab's options as they are, without appearance events.
336
395
  val options = resolveShownOptions(DefaultOptions.current)
337
396
  bar.applyBar(options.bottomTabs, directionOf(options))
@@ -358,8 +417,10 @@ class BottomTabsController(
358
417
  if (screen == null) finish() else AppearanceDispatcher.afterDidAppear(screen, finish)
359
418
  }
360
419
 
361
- // A press's `ANN.BottomTabSelected` still waiting goes before anything newer on these tabs.
420
+ // A switch still running ends at once and a press's `ANN.BottomTabSelected` still waiting goes, before anything
421
+ // newer on these tabs.
362
422
  private fun flushPendingSelected() {
423
+ endSwitch?.invoke()
363
424
  pendingSelected?.invoke()
364
425
  }
365
426
 
@@ -621,6 +682,8 @@ class BottomTabsController(
621
682
  if (isDestroyed) return
622
683
  pendingSelected = null
623
684
  super.destroy()
685
+ // A switch still running ends with its views at rest, without events: the tabs are gone.
686
+ endSwitch?.invoke()
624
687
  peekView()?.let { layout ->
625
688
  layout.bar.destroyCustomItemViews()
626
689
  layout.row?.let(CustomTabRowPlacer::forget)
@@ -36,7 +36,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
36
36
  ) : Call()
37
37
 
38
38
  /** The icon's size in dp, width over height. */
39
- data class IconSize(override val index: Int, val width: Int?, val height: Int?) : Call()
39
+ data class IconSize(override val index: Int, val width: Double?, val height: Double?) : Call()
40
40
 
41
41
  data class Font(override val index: Int, val font: FontOptions) : Call()
42
42
 
@@ -86,7 +86,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
86
86
  options.icon.get(null)?.let { Call.Icon(index, it) },
87
87
  options.selectedIcon.get(null)?.let { Call.SelectedIcon(index, it) },
88
88
  colors(index, options),
89
- Call.IconSize(index, options.iconWidth.get(null)?.toInt(), options.iconHeight.get(null)?.toInt()),
89
+ Call.IconSize(index, options.iconWidth.get(null), options.iconHeight.get(null)),
90
90
  Call.Font(index, options.font.copy()),
91
91
  Call.FontSizes(index, options.selectedFontSize.get(null)?.toFloat(), options.fontSize.get(null)?.toFloat()),
92
92
  Call.TestId(index, options.testID.get(null)),
@@ -110,7 +110,7 @@ internal class TabItems(private val color: (ThemeColour) -> Int?) {
110
110
  calls += colors(index, resolved)
111
111
  }
112
112
  if (merge.iconWidth.hasValue() || merge.iconHeight.hasValue()) {
113
- calls += Call.IconSize(index, resolved.iconWidth.get(null)?.toInt(), resolved.iconHeight.get(null)?.toInt())
113
+ calls += Call.IconSize(index, resolved.iconWidth.get(null), resolved.iconHeight.get(null))
114
114
  }
115
115
  if (merge.font.hasValue()) calls += Call.Font(index, resolved.font.copy())
116
116
  if (merge.fontSize.hasValue() || merge.selectedFontSize.hasValue()) {
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
6
6
  import android.graphics.drawable.Drawable
7
7
  import android.graphics.drawable.LayerDrawable
8
8
  import android.view.View
9
+ import com.google.android.material.color.MaterialColors
9
10
  import dev.apexnative.navigation.options.DefaultOptions
10
11
  import dev.apexnative.navigation.options.Options
11
12
  import dev.apexnative.navigation.options.params.ThemeColour
@@ -15,9 +16,9 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
15
16
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
17
 
17
18
  /**
18
- * The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor`, kept
19
- * out of the status bar's height where the status bar shows over it, and the React content takes
20
- * `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
19
+ * The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor` (a
20
+ * React screen without it the theme's background), kept out of the status bar's height where the status bar shows over
21
+ * it, and the React content takes `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
21
22
  * (`mergeOptions` step 4). It applies as the screen appears, on `mergeOptions` of what it covers, and again with new
22
23
  * default options or after a light/dark switch from the resolved options it is given, which it never writes. Each
23
24
  * appearance tells the activity's system UI owner, which follows the screen on top. Main thread only.
@@ -49,8 +50,8 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
49
50
  applyBackground(controller, options)
50
51
 
51
52
  /**
52
- * The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view, none without
53
- * it (the window shows); `layout.componentBackgroundColor` on its React content.
53
+ * The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view, without it the
54
+ * theme's background for a React screen ([defaultColorOf]); `layout.componentBackgroundColor` on its React content.
54
55
  */
55
56
  fun applyBackground(screen: ViewController<*>, options: Options) {
56
57
  val view = screen.peekView() ?: return
@@ -59,11 +60,11 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
59
60
  }
60
61
 
61
62
  private fun screenBackground(screen: ViewController<*>, options: Options): Drawable? {
62
- val color = colorOf(options.layout.backgroundColor) ?: return null
63
+ val color = colorOf(options.layout.backgroundColor) ?: defaultColorOf(screen) ?: return null
63
64
  val frame = frameOf(screen) ?: return ColorDrawable(color)
64
65
  val band = ScreenSafeArea.backgroundBand(
65
66
  statusBarHeight = frame.statusBarHeight,
66
- hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
67
+ hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(screen.isInOverlay),
67
68
  inSideMenu = screen.isInSideMenu,
68
69
  inSheet = screen.isInSheet,
69
70
  inPair = frame.isInPair,
@@ -79,6 +80,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
79
80
  private fun colorOf(colour: ThemeColour): Int? =
80
81
  if (colour.hasValue()) colour.get(activity, Color.TRANSPARENT) ?: Color.TRANSPARENT else null
81
82
 
83
+ // A React screen is opaque without a color of its own, as iOS gives it the system background: the screen under it
84
+ // never shows through, also while a push slides it aside. The app theme's `android:colorBackground`, read now, so a
85
+ // light/dark switch takes the new mode's. A screen of a sheet shows the sheet's surface; a native screen paints its
86
+ // own view.
87
+ private fun defaultColorOf(screen: ViewController<*>): Int? {
88
+ if (screen !is ComponentViewController || screen.isInSheet) return null
89
+ return themeBackgroundOf(activity)
90
+ }
91
+
82
92
  private fun frameOf(screen: ViewController<*>): ScreenFrame? = when (screen) {
83
93
  is ComponentViewController -> screen.frame
84
94
  is ExternalComponentViewController -> screen.frame
@@ -87,4 +97,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
87
97
 
88
98
  // The view under the React content: the React root, or the container of a slot screen. A native screen has none.
89
99
  private fun contentOf(screen: ViewController<*>): View? = (screen as? ComponentViewController)?.content?.asView()
100
+
101
+ internal companion object {
102
+ /**
103
+ * The background of a React screen without `layout.backgroundColor`: `android:colorBackground` of [activity]'s
104
+ * theme now. None for an activity without a theme (the unit tests' fakes).
105
+ */
106
+ fun themeBackgroundOf(activity: Activity): Int? {
107
+ if (activity.theme == null) return null
108
+ return MaterialColors.getColor(activity, android.R.attr.colorBackground, Color.WHITE)
109
+ }
110
+ }
90
111
  }
@@ -143,7 +143,7 @@ internal class ScreenFrame(
143
143
  val parent = node.parentController
144
144
  val statusBar = ScreenSafeArea.statusBarRoom(
145
145
  statusBarHeight = statusBarHeight,
146
- hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
146
+ hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(node.isInOverlay),
147
147
  inSheet = node.isInSheet,
148
148
  inSideMenu = node.isInSideMenu,
149
149
  keptAround = parent?.keepsStatusBarRoomOf(node) == true,