@apexnative/navigation 1.0.0-alpha.2 → 1.0.0-alpha.4
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.
- package/CHANGELOG.md +31 -0
- package/android/CLAUDE.md +38 -9
- package/android/CORE.md +11 -4
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
- package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
- package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
- package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
- package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +92 -19
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +79 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +45 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +35 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +10 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +72 -16
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +164 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +26 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +136 -40
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +107 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +131 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +75 -21
- package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
- package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
- package/cli/doctor/checks/app.js +61 -13
- package/cli/migrate/native.js +18 -0
- package/cli/setup/source.js +2 -1
- package/cli/setup/steps/mainApplication.js +134 -19
- package/docs-md/architecture/transitions.md +12 -7
- package/docs-md/containers/modals.md +4 -2
- package/docs-md/containers/overlays.md +3 -1
- package/docs-md/containers/side-menu.md +4 -2
- package/docs-md/containers/tabs.md +2 -0
- package/docs-md/flows/forms-in-a-modal.md +1 -1
- package/docs-md/help/glossary.md +2 -2
- package/docs-md/help/troubleshooting.md +20 -3
- package/docs-md/index.json +12 -0
- package/docs-md/migration/from-react-native-navigation.md +8 -2
- package/docs-md/screen-ui/back-and-gestures.md +8 -2
- package/docs-md/screen-ui/system-ui.md +1 -1
- package/docs-md/start/installation.md +3 -1
- package/ios/CLAUDE.md +17 -5
- package/ios/CORE.md +13 -8
- package/ios/Contract/ANNCommandAnswer.mm +5 -20
- package/ios/Contract/ANNCommandContract.h +15 -0
- package/ios/Contract/ANNCommandContract.mm +78 -0
- package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
- package/ios/Core/ANNErrorHandler.h +1 -1
- package/ios/Core/ANNErrorHandler.mm +3 -1
- package/ios/Core/ANNEventEmitter.h +1 -1
- package/ios/Core/ANNEventEmitter.mm +2 -2
- package/ios/Core/ANNJavaScriptCalls.h +34 -0
- package/ios/Core/ANNJavaScriptCalls.mm +61 -0
- package/ios/Core/ANNNavigationState.h +7 -3
- package/ios/Core/ANNNavigationState.mm +10 -5
- package/ios/Core/ANNNodeHelper.h +32 -12
- package/ios/Core/ANNNodeHelper.mm +100 -3
- package/ios/Core/ANNRenderWait.h +15 -1
- package/ios/Core/ANNRenderWait.mm +119 -17
- package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
- package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
- package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
- package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
- package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
- package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
- package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
- package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
- package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
- package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
- package/ios/Layouts/Stack/ANNStackController.mm +26 -3
- package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
- package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
- package/ios/Options/ANNOptionsCache.h +7 -6
- package/ios/Options/ANNSearchBarOptions.h +4 -3
- package/ios/Options/ANNUserChoice.h +57 -0
- package/ios/Options/ANNUserChoice.mm +44 -0
- package/ios/Options/Params/ANNImageSource.h +61 -0
- package/ios/Options/Params/ANNImageSource.mm +101 -0
- package/ios/Options/Params/ANNOptionImages.h +37 -0
- package/ios/Options/Params/ANNOptionImages.mm +284 -0
- package/ios/Options/Params/Image.h +11 -2
- package/ios/Options/Params/Image.mm +15 -2
- package/ios/Options/Params/ImageParser.h +5 -5
- package/ios/Options/Params/ImageParser.mm +28 -46
- package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
- package/ios/RECIPES.md +11 -5
- package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
- package/ios/React/Slots/ANNSlotContainers.h +14 -6
- package/ios/React/Slots/ANNSlotContainers.mm +75 -2
- package/ios/TopBar/ANNBarButtons.h +9 -5
- package/ios/TopBar/ANNBarButtons.mm +211 -63
- package/ios/TopBar/ANNIconButtonImage.h +1 -0
- package/ios/TopBar/ANNIconButtonImage.mm +34 -1
- package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
- package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
- package/ios/TopBar/ANNSearchPresenter.mm +170 -19
- package/ios/TopBar/ANNTopBarPresenter.h +5 -0
- package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
- package/ios/TurboModules/ANNTurboModule.mm +39 -21
- package/lib/module/ARCHITECTURE.md +18 -18
- package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
- package/lib/module/api/hooks/useNavigation.js +8 -3
- package/lib/module/api/hooks/useNavigation.js.map +1 -1
- package/lib/module/api/navigation.js.map +1 -1
- package/lib/module/engine/commands.js +9 -7
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/layout.js +13 -9
- package/lib/module/engine/layout.js.map +1 -1
- package/lib/module/engine/options.js +44 -11
- package/lib/module/engine/options.js.map +1 -1
- package/lib/module/engine/places.js +44 -7
- package/lib/module/engine/places.js.map +1 -1
- package/lib/module/engine/props.js +30 -1
- package/lib/module/engine/props.js.map +1 -1
- package/lib/module/engine/rememberedOptions.js +1 -1
- package/lib/module/engine/rememberedOptions.js.map +1 -1
- package/lib/module/engine/screens.js +22 -12
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/module/interfaces/contract/commands.json +101 -0
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
- package/lib/typescript/api/navigation.d.ts +4 -1
- package/lib/typescript/api/navigation.d.ts.map +1 -1
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/options.d.ts +5 -2
- package/lib/typescript/engine/options.d.ts.map +1 -1
- package/lib/typescript/engine/places.d.ts +5 -2
- package/lib/typescript/engine/places.d.ts.map +1 -1
- package/lib/typescript/engine/props.d.ts +8 -1
- package/lib/typescript/engine/props.d.ts.map +1 -1
- package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
- package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
- package/lib/typescript/interfaces/Events.d.ts +19 -10
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +15 -7
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
- package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/modals.d.ts +18 -13
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +6 -3
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +18 -18
- package/src/adapters/NativeANNTurboModule.ts +5 -2
- package/src/api/events/navigationEvents.ts +1 -1
- package/src/api/hooks/useNavigation.ts +9 -4
- package/src/api/navigation.ts +4 -1
- package/src/engine/commands.ts +19 -7
- package/src/engine/layout.ts +16 -9
- package/src/engine/options.ts +39 -11
- package/src/engine/places.ts +56 -7
- package/src/engine/props.ts +30 -1
- package/src/engine/rememberedOptions.ts +4 -1
- package/src/engine/screens.tsx +23 -13
- package/src/interfaces/ComponentEvents.ts +2 -2
- package/src/interfaces/Events.ts +19 -10
- package/src/interfaces/contract/commands.json +101 -0
- package/src/interfaces/options/animations.ts +15 -7
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/modals.ts +18 -13
- package/src/interfaces/options/system.ts +6 -3
- package/src/interfaces/options/topBar.ts +22 -9
|
@@ -228,13 +228,15 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
|
|
|
228
228
|
applyLongPressesAgain()
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
/** Null: the item has no icon. */
|
|
232
|
+
fun setIcon(index: Int, icon: Drawable?) {
|
|
232
233
|
val item = items.getOrNull(index) ?: return
|
|
233
234
|
if (item.icon == icon) return
|
|
234
235
|
updateIcon(index, item.copy(icon = icon))
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
/** Null: the item shows its icon when selected too. */
|
|
239
|
+
fun setSelectedIcon(index: Int, icon: Drawable?) {
|
|
238
240
|
val item = items.getOrNull(index) ?: return
|
|
239
241
|
if (item.selectedIcon == icon) return
|
|
240
242
|
updateIcon(index, item.copy(selectedIcon = icon))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package dev.apexnative.navigation.views.stack
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.Drawable
|
|
4
|
+
import android.view.View
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What fills the room beside a moving screen in a transition (`StackLayout.extendEdge`), where the system's
|
|
8
|
+
* transitions between activities stretch a window's edge (`<extend>` of `activity_open_*` and `activity_close_*`): the
|
|
9
|
+
* screen's own background, which is what its edge shows unless its content covers it. Taken without drawing the
|
|
10
|
+
* screen, at the start of every push and pop. Main thread only.
|
|
11
|
+
*/
|
|
12
|
+
internal object ScreenEdge {
|
|
13
|
+
/** A copy of [view]'s background, stretched across the room; none without one. */
|
|
14
|
+
fun pictureOf(view: View): Drawable? = view.background?.constantState?.newDrawable(view.resources)?.mutate()
|
|
15
|
+
}
|
|
@@ -2,6 +2,8 @@ package dev.apexnative.navigation.views.stack
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.drawable.Drawable
|
|
5
7
|
import android.view.Gravity
|
|
6
8
|
import android.view.View
|
|
7
9
|
import android.view.ViewGroup
|
|
@@ -18,7 +20,8 @@ import java.util.WeakHashMap
|
|
|
18
20
|
* moves between its places without leaving it, so it doesn't leave and join the window again; a covered screen that
|
|
19
21
|
* comes back gets the last window insets the stack got before it joins, and nothing asks the window to send them again
|
|
20
22
|
* (CoordinatorLayout asks for insets only as it joins the window itself and only with `fitsSystemWindows`, which this
|
|
21
|
-
* view doesn't set).
|
|
23
|
+
* view doesn't set). In a transition of the stack's own the bar is drawn with the screen it goes with
|
|
24
|
+
* ([barTravels]). Main thread only.
|
|
22
25
|
*/
|
|
23
26
|
@SuppressLint("ViewConstructor")
|
|
24
27
|
class StackLayout(context: Context) : CoordinatorLayout(context) {
|
|
@@ -134,4 +137,137 @@ class StackLayout(context: Context) : CoordinatorLayout(context) {
|
|
|
134
137
|
requestLayout()
|
|
135
138
|
invalidate()
|
|
136
139
|
}
|
|
140
|
+
|
|
141
|
+
// The edges of the screens in a transition: drawn beside each, under it, where it moves and as transparent as it.
|
|
142
|
+
private val edges = HashMap<View, Edge>()
|
|
143
|
+
|
|
144
|
+
private class Edge(val picture: Drawable, val width: Int, val atStart: Boolean)
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* [view] extends its start edge ([atStart]) or its end edge by [width] pixels while it moves, as the system's
|
|
148
|
+
* transitions between activities extend a window: its background fills the room it moves away from, so the screen
|
|
149
|
+
* under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls [edgesMoved].
|
|
150
|
+
*/
|
|
151
|
+
fun extendEdge(view: View, atStart: Boolean, width: Int) {
|
|
152
|
+
val left = atStart != (layoutDirection == LAYOUT_DIRECTION_RTL)
|
|
153
|
+
val picture = ScreenEdge.pictureOf(view) ?: return
|
|
154
|
+
edges[view] = Edge(picture, width, left)
|
|
155
|
+
invalidate()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** [view] moves no more: its edge goes. */
|
|
159
|
+
fun dropEdge(view: View) {
|
|
160
|
+
if (edges.remove(view) != null) invalidate()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A screen with an extended edge moved: the edges are drawn again where the screens are now. */
|
|
164
|
+
fun edgesMoved() {
|
|
165
|
+
if (edges.isNotEmpty()) invalidate()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The bar's travel in a transition of the stack's own: the screen the bar goes with, the screen that leaves with the
|
|
169
|
+
// picture of the bar as it was, and whether that one lies over the bar (a pop).
|
|
170
|
+
private var barScreen: View? = null
|
|
171
|
+
private var barLeaving: View? = null
|
|
172
|
+
private var barPicture: Drawable? = null
|
|
173
|
+
private var leavingOver = false
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The bar goes with [screen] in a transition of the stack's own: it is drawn as [screen] moves (its translation,
|
|
177
|
+
* scale and rotation around its pivot, its alpha). [picture], laid where the bar is, goes the same way with
|
|
178
|
+
* [leaving]: it is drawn right over that screen, outside its bounds too (a screen laid out under the bar). With
|
|
179
|
+
* [over] the leaving screen and its picture lie over the bar (a pop), else under it. Only the drawing moves: the
|
|
180
|
+
* bar's views and touches stay in place. [endBarTravel] ends it.
|
|
181
|
+
*/
|
|
182
|
+
fun barTravels(screen: View, leaving: View?, picture: Drawable?, over: Boolean) {
|
|
183
|
+
barScreen = screen
|
|
184
|
+
barLeaving = leaving
|
|
185
|
+
barPicture = picture
|
|
186
|
+
leavingOver = over && leaving != null
|
|
187
|
+
invalidate()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The bar is drawn in its place again, over the screens; the picture goes. */
|
|
191
|
+
fun endBarTravel() {
|
|
192
|
+
if (barScreen == null) return
|
|
193
|
+
barScreen = null
|
|
194
|
+
barLeaving = null
|
|
195
|
+
barPicture = null
|
|
196
|
+
leavingOver = false
|
|
197
|
+
invalidate()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
201
|
+
super.dispatchDraw(canvas)
|
|
202
|
+
// The screens' animators change only their own properties, which don't draw this view again: each frame of
|
|
203
|
+
// the bar's travel asks for the next.
|
|
204
|
+
if (barScreen != null) postInvalidateOnAnimation()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
|
|
208
|
+
edges[child]?.let { drawEdge(canvas, child, it) }
|
|
209
|
+
val screen = barScreen ?: return super.drawChild(canvas, child, drawingTime)
|
|
210
|
+
// The leaving screen, while it is still here, and its picture; on a pop the bar right under them.
|
|
211
|
+
val leaving = barLeaving?.takeIf(::holds)
|
|
212
|
+
val under = leaving.takeIf { leavingOver }
|
|
213
|
+
if (child === topBar) {
|
|
214
|
+
if (under != null) return false
|
|
215
|
+
return drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
|
|
216
|
+
topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
|
|
217
|
+
super.drawChild(canvas, topBar, drawingTime)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (child === under && topBar.visibility == VISIBLE) {
|
|
221
|
+
drawWith(canvas, screen, topBar.left.toFloat(), topBar.top + topBar.translationY,
|
|
222
|
+
topBar.right.toFloat(), topBar.bottom + topBar.translationY) {
|
|
223
|
+
super.drawChild(canvas, topBar, drawingTime)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
val drawn = super.drawChild(canvas, child, drawingTime)
|
|
227
|
+
val picture = barPicture
|
|
228
|
+
if (child === leaving && picture != null) {
|
|
229
|
+
val bounds = picture.bounds
|
|
230
|
+
drawWith(canvas, child, bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat(),
|
|
231
|
+
bounds.bottom.toFloat()) {
|
|
232
|
+
picture.draw(canvas)
|
|
233
|
+
false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return drawn
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Draws with [screen]'s transform and alpha what [draw] draws within the bounds given, in this view's coordinates.
|
|
240
|
+
private fun drawWith(
|
|
241
|
+
canvas: Canvas,
|
|
242
|
+
screen: View,
|
|
243
|
+
left: Float,
|
|
244
|
+
top: Float,
|
|
245
|
+
right: Float,
|
|
246
|
+
bottom: Float,
|
|
247
|
+
draw: () -> Boolean,
|
|
248
|
+
): Boolean {
|
|
249
|
+
val alpha = if (screen.visibility == VISIBLE) screen.alpha else 0f
|
|
250
|
+
if (alpha <= 0f) return false
|
|
251
|
+
val saved = canvas.save()
|
|
252
|
+
canvas.translate(screen.left.toFloat(), screen.top.toFloat())
|
|
253
|
+
canvas.concat(screen.matrix)
|
|
254
|
+
canvas.translate(-screen.left.toFloat(), -screen.top.toFloat())
|
|
255
|
+
if (alpha < 1f) canvas.saveLayerAlpha(left, top, right, bottom, (alpha * 255).toInt())
|
|
256
|
+
val drawn = draw()
|
|
257
|
+
canvas.restoreToCount(saved)
|
|
258
|
+
return drawn
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// At the child's place to the fraction of a pixel, as its own translation draws it, and one pixel under it: no seam
|
|
262
|
+
// of the screen below shows between them.
|
|
263
|
+
private fun drawEdge(canvas: Canvas, child: View, edge: Edge) {
|
|
264
|
+
if (child.visibility != VISIBLE || child.alpha <= 0f) return
|
|
265
|
+
val x = if (edge.atStart) child.left + child.translationX - edge.width else child.right + child.translationX - 1f
|
|
266
|
+
val saved = canvas.save()
|
|
267
|
+
canvas.translate(x, child.top + child.translationY)
|
|
268
|
+
edge.picture.setBounds(0, 0, edge.width + 1, child.height)
|
|
269
|
+
edge.picture.alpha = (child.alpha * 255).toInt()
|
|
270
|
+
edge.picture.draw(canvas)
|
|
271
|
+
canvas.restoreToCount(saved)
|
|
272
|
+
}
|
|
137
273
|
}
|
|
@@ -4,12 +4,11 @@ import android.content.Context
|
|
|
4
4
|
import android.content.res.ColorStateList
|
|
5
5
|
import android.content.res.Configuration
|
|
6
6
|
import android.graphics.Color
|
|
7
|
-
import android.graphics.drawable.Drawable
|
|
8
7
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
9
8
|
import dev.apexnative.navigation.options.params.ThemeColour
|
|
10
|
-
import dev.apexnative.navigation.utils.
|
|
11
|
-
import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
|
|
9
|
+
import dev.apexnative.navigation.utils.ImageRequests
|
|
12
10
|
import dev.apexnative.navigation.utils.MaterialTheme
|
|
11
|
+
import dev.apexnative.navigation.utils.OptionImages
|
|
13
12
|
import dev.apexnative.navigation.utils.UiUtils
|
|
14
13
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListener
|
|
15
14
|
import androidx.appcompat.R as AppCompatR
|
|
@@ -29,6 +28,7 @@ class Fab(context: Context, val fabId: String) : FloatingActionButton(MaterialTh
|
|
|
29
28
|
// The icon's color of the options: a container color merged later kept the theme's icon color instead.
|
|
30
29
|
private var iconColor: Int? = null
|
|
31
30
|
private var optionRippleColor: Int? = null
|
|
31
|
+
private val icons = ImageRequests<Unit>(OptionImages.source(this.context))
|
|
32
32
|
|
|
33
33
|
init {
|
|
34
34
|
// Material's theme overlay of the button is a copy of the theme, made once: it follows a light/dark switch too.
|
|
@@ -36,14 +36,18 @@ class Fab(context: Context, val fabId: String) : FloatingActionButton(MaterialTh
|
|
|
36
36
|
MaterialTheme.followOverlay(this.context)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The icon of the options, with its color. [mayWait]: the button can show it when it is ready (it shows an icon
|
|
41
|
+
* already, it is hidden with `visible: false`, or it is an action of a closed menu), so an icon the cache doesn't
|
|
42
|
+
* have loads off the main thread; the last icon asked for wins. Otherwise it loads at once. An icon that doesn't
|
|
43
|
+
* load leaves the button without an icon.
|
|
44
|
+
*/
|
|
45
|
+
fun applyIcon(icon: String, color: ThemeColour, mayWait: Boolean) {
|
|
40
46
|
if (color.hasValue()) iconColor = color.get()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
+
icons.request(Unit, icon, mayWait) { drawable ->
|
|
48
|
+
setImageDrawable(drawable)
|
|
49
|
+
iconColor?.let { imageTintList = ColorStateList.valueOf(it) }
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** iconColor alone, without a new icon: it did nothing in a merge. */
|
|
@@ -118,6 +118,8 @@ class TitleBar(context: Context) : ViewGroup(context) {
|
|
|
118
118
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
|
119
119
|
val inner = max(width - sideInsetsBox[0] - sideInsetsBox[1], 0)
|
|
120
120
|
val exactHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
121
|
+
alignButtons(leftButtonBar, height)
|
|
122
|
+
alignButtons(rightButtonBar, height)
|
|
121
123
|
leftButtonBar.measure(MeasureSpec.makeMeasureSpec(inner, MeasureSpec.AT_MOST), exactHeight)
|
|
122
124
|
val rightRoom = max(inner - leftButtonBar.measuredWidth, 0)
|
|
123
125
|
rightButtonBar.measure(MeasureSpec.makeMeasureSpec(rightRoom, MeasureSpec.AT_MOST), exactHeight)
|
|
@@ -192,6 +194,12 @@ class TitleBar(context: Context) : ViewGroup(context) {
|
|
|
192
194
|
return min(max(middle, startBar), max(inner - endBar - width, startBar))
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
// A toolbar stands its back button and its menu at the top of its minimum height, centered in it (`buttonGravity`
|
|
198
|
+
// is top): with the row's height as that minimum, they stand in the middle of the row, as the title does.
|
|
199
|
+
private fun alignButtons(toolbar: MaterialToolbar, height: Int) {
|
|
200
|
+
if (toolbar.minimumHeight != height) toolbar.minimumHeight = height
|
|
201
|
+
}
|
|
202
|
+
|
|
195
203
|
// A toolbar that holds only buttons: no insets of its own around them (the row places them).
|
|
196
204
|
private fun plain(toolbar: MaterialToolbar) {
|
|
197
205
|
// Before the toolbar makes its menu, which takes the popup theme once: "⋮" in the current mode.
|
|
@@ -200,7 +208,6 @@ class TitleBar(context: Context) : ViewGroup(context) {
|
|
|
200
208
|
toolbar.contentInsetStartWithNavigation = 0
|
|
201
209
|
toolbar.contentInsetEndWithActions = 0
|
|
202
210
|
toolbar.setPadding(0, 0, 0, 0)
|
|
203
|
-
toolbar.minimumHeight = 0
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
private companion object {
|
|
@@ -55,6 +55,8 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
|
|
|
55
55
|
private var heightChanged = false
|
|
56
56
|
// How far the large title is collapsed, in pixels.
|
|
57
57
|
private var largeTitleShift = 0
|
|
58
|
+
// `topBar.elevation`, in pixels: the bar's elevation unless it is flat.
|
|
59
|
+
private var shadow = 0f
|
|
58
60
|
|
|
59
61
|
/** The toolbar of the right buttons: its menu holds them, the overflow items and the search's magnifier. */
|
|
60
62
|
val rightButtonBar: MaterialToolbar
|
|
@@ -187,14 +189,27 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
|
|
|
187
189
|
* The shadow of `topBar.elevation`, in pixels: its colors are black (Android 9 and later read them), none at 0.
|
|
188
190
|
*/
|
|
189
191
|
fun setShadow(elevation: Float) {
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
+
if (shadow == elevation) return
|
|
193
|
+
shadow = elevation
|
|
194
|
+
if (!isFlat) this.elevation = elevation
|
|
192
195
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
193
196
|
outlineAmbientShadowColor = Color.BLACK
|
|
194
197
|
outlineSpotShadowColor = Color.BLACK
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The bar draws without its shadow, at the level of the screens, in a transition that draws it with a screen: a
|
|
203
|
+
* screen drawn after it covers it, and an alpha layer around it fades it (with an elevation the renderer would draw
|
|
204
|
+
* the bar over every screen and out of the layer).
|
|
205
|
+
*/
|
|
206
|
+
var isFlat = false
|
|
207
|
+
set(value) {
|
|
208
|
+
if (field == value) return
|
|
209
|
+
field = value
|
|
210
|
+
elevation = if (value) 0f else shadow
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
/** The line under the bar: [height] pixels of [color]. */
|
|
199
214
|
fun setBorder(color: Int, height: Int) {
|
|
200
215
|
border.setBackgroundColor(color)
|
|
@@ -335,8 +350,15 @@ class TopBar(context: Context) : FrameLayout(themed(context)) {
|
|
|
335
350
|
* A picture of the bar under its row — the background, the large title, the search, the line — and where it starts
|
|
336
351
|
* in the bar, for a push to a screen without a large title; none when the bar can't be drawn in software.
|
|
337
352
|
*/
|
|
338
|
-
fun pictureUnderRow(): Pair<Bitmap, Int>?
|
|
339
|
-
|
|
353
|
+
fun pictureUnderRow(): Pair<Bitmap, Int>? = pictureFrom(content.top + rowArea.bottom)
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A picture of the whole bar, for the screen it leaves with in a transition of the stack's own; none when the bar
|
|
357
|
+
* can't be drawn in software.
|
|
358
|
+
*/
|
|
359
|
+
fun picture(): Bitmap? = pictureFrom(0)?.first
|
|
360
|
+
|
|
361
|
+
private fun pictureFrom(top: Int): Pair<Bitmap, Int>? {
|
|
340
362
|
val width = width
|
|
341
363
|
val height = height - top
|
|
342
364
|
if (width <= 0 || height <= 0) return null
|
package/cli/doctor/checks/app.js
CHANGED
|
@@ -11,8 +11,10 @@ const {
|
|
|
11
11
|
reactNativeVersion,
|
|
12
12
|
usesExpo,
|
|
13
13
|
} = require('../../app');
|
|
14
|
+
const { mainActivity } = require('../../links/native');
|
|
14
15
|
const { findProject } = require('../../project');
|
|
15
16
|
const { codeOf } = require('../../setup/source');
|
|
17
|
+
const { loadsReactNativeItself } = require('../../setup/steps/mainApplication');
|
|
16
18
|
const { planCodegen } = require('../../codegen');
|
|
17
19
|
const { readConfig } = require('../../config');
|
|
18
20
|
const { planProject } = require('../../setup');
|
|
@@ -27,6 +29,16 @@ const { splashChecks } = require('./splash');
|
|
|
27
29
|
const SETUP = { label: 'npx apex-navigation setup', run: /** @type {const} */ ('setup') };
|
|
28
30
|
const GENERATED = ['ApexNavigation.generated.swift', 'ApexNavigationGenerated.kt'];
|
|
29
31
|
const CODEGEN = { label: 'npx apex-navigation codegen', run: /** @type {const} */ ('codegen') };
|
|
32
|
+
// The changes the main activity must handle itself; the template's value has them all.
|
|
33
|
+
const CONFIG_CHANGES = [
|
|
34
|
+
'orientation',
|
|
35
|
+
'screenSize',
|
|
36
|
+
'smallestScreenSize',
|
|
37
|
+
'screenLayout',
|
|
38
|
+
'uiMode',
|
|
39
|
+
];
|
|
40
|
+
const TEMPLATE_CONFIG_CHANGES =
|
|
41
|
+
'keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode';
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
44
|
* React Native, the navigation, and the files setup sets up.
|
|
@@ -122,28 +134,64 @@ function appChecks({ root, run }) {
|
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
const configChanges = configChangesCheck(project.manifest);
|
|
138
|
+
if (configChanges) checks.push(configChanges);
|
|
139
|
+
|
|
125
140
|
checks.push(...splashChecks(root, project), ...iconChecks(root, project));
|
|
126
141
|
|
|
127
|
-
//
|
|
142
|
+
// NavigationApplication leaves the load of React Native to the template's call: without it the app closes on start,
|
|
143
|
+
// and so it does with a second load of the app's own next to it.
|
|
128
144
|
const application = project.mainApplication && fs.readFileSync(project.mainApplication, 'utf8');
|
|
129
|
-
if (
|
|
130
|
-
application
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
if (application && /:\s*NavigationApplication\(/.test(application)) {
|
|
146
|
+
const loads = /\bloadReactNative\s*\(/.test(codeOf(application));
|
|
147
|
+
if (!loads) {
|
|
148
|
+
checks.push({
|
|
149
|
+
group: 'app',
|
|
150
|
+
title: 'React Native not loaded',
|
|
151
|
+
status: 'error',
|
|
152
|
+
detail: "MainApplication.kt doesn't call loadReactNative(this) in onCreate",
|
|
153
|
+
fix: SETUP,
|
|
154
|
+
});
|
|
155
|
+
} else if (loadsReactNativeItself(application)) {
|
|
156
|
+
checks.push({
|
|
157
|
+
group: 'app',
|
|
158
|
+
title: 'React Native loads twice',
|
|
159
|
+
status: 'error',
|
|
160
|
+
detail:
|
|
161
|
+
'MainApplication.kt loads React Native itself (SoLoader.init or the New Architecture entry point) next to loadReactNative(this)',
|
|
162
|
+
fix: SETUP,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
166
|
const generated = generatedCheck(root, run);
|
|
143
167
|
if (generated) checks.push(generated);
|
|
144
168
|
return checks;
|
|
145
169
|
}
|
|
146
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Whether the main activity handles the changes of its window itself, as the React Native template has it. Without
|
|
173
|
+
* one of them Android recreates the activity on a rotation, a resize, a fold or a light/dark switch: every screen goes
|
|
174
|
+
* with it and the navigation starts again from its root. Nothing without a manifest or a main activity.
|
|
175
|
+
* @param {string | undefined} manifest
|
|
176
|
+
* @returns {Check | undefined}
|
|
177
|
+
*/
|
|
178
|
+
function configChangesCheck(manifest) {
|
|
179
|
+
if (!manifest || !fs.existsSync(manifest)) return undefined;
|
|
180
|
+
const activity = mainActivity(fs.readFileSync(manifest, 'utf8'));
|
|
181
|
+
if (!activity) return undefined;
|
|
182
|
+
const value = /android:configChanges\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(activity.text);
|
|
183
|
+
const declared = (value?.[1] ?? value?.[2] ?? '').split('|').map((change) => change.trim());
|
|
184
|
+
const missing = CONFIG_CHANGES.filter((change) => !declared.includes(change));
|
|
185
|
+
if (missing.length === 0) return undefined;
|
|
186
|
+
return {
|
|
187
|
+
group: 'app',
|
|
188
|
+
title: 'Config changes',
|
|
189
|
+
status: 'warn',
|
|
190
|
+
detail: `the main activity doesn't handle ${missing.join(', ')}: Android recreates it, and every screen with it`,
|
|
191
|
+
fix: { label: `Set android:configChanges="${TEMPLATE_CONFIG_CHANGES}" on the main activity` },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
147
195
|
/**
|
|
148
196
|
* One line for a few files: set up, or which need setup or a change by hand.
|
|
149
197
|
* @param {string} title
|
package/cli/migrate/native.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { withReactNativeLoaded } = require('../setup/steps/mainApplication');
|
|
6
7
|
|
|
7
8
|
/** @typedef {import('../changes').FileChange} FileChange */
|
|
8
9
|
/** @typedef {import('./report').ReportItem} ReportItem */
|
|
@@ -158,6 +159,23 @@ function planNative(project) {
|
|
|
158
159
|
const before = fs.readFileSync(file, 'utf8');
|
|
159
160
|
let next = before;
|
|
160
161
|
for (const [pattern, replacement] of rewrite.rules) next = next.replace(pattern, replacement);
|
|
162
|
+
// react-native-navigation loaded React Native itself; the navigation's application leaves it to the template's
|
|
163
|
+
// loadReactNative(this), which such an app doesn't have.
|
|
164
|
+
if (file === project.mainApplication && /:\s*NavigationApplication\(/.test(next)) {
|
|
165
|
+
const loaded = withReactNativeLoaded(next);
|
|
166
|
+
if (loaded !== undefined) next = loaded;
|
|
167
|
+
else {
|
|
168
|
+
report.push({
|
|
169
|
+
file: path.relative(root, file),
|
|
170
|
+
line: 1,
|
|
171
|
+
step: 'native',
|
|
172
|
+
before: 'onCreate',
|
|
173
|
+
after: 'super.onCreate() then loadReactNative(this), and no load of its own',
|
|
174
|
+
what: 'React Native loads through loadReactNative(this) in onCreate: add it by hand',
|
|
175
|
+
docs: `${DOCS}#native`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
161
179
|
if (next !== before) {
|
|
162
180
|
changes.push({
|
|
163
181
|
name: path.basename(file),
|
package/cli/setup/source.js
CHANGED
|
@@ -35,7 +35,8 @@ function removeUnusedImports(text, names) {
|
|
|
35
35
|
for (const name of names) {
|
|
36
36
|
const line = new RegExp(`^import\\s+${escape(name)}[ \\t]*\\r?\\n`, 'm');
|
|
37
37
|
const simple = name.slice(name.lastIndexOf('.') + 1);
|
|
38
|
-
|
|
38
|
+
// A name in a comment is no use of the import.
|
|
39
|
+
const code = codeOf(result).replace(/^import\s.*$/gm, '');
|
|
39
40
|
if (line.test(result) && !new RegExp(`\\b${escape(simple)}\\b`).test(code))
|
|
40
41
|
result = result.replace(line, '');
|
|
41
42
|
}
|
|
@@ -5,24 +5,51 @@ const { addImport, codeOf, removeUnusedImports } = require('../source');
|
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('./types').StepResult} StepResult */
|
|
7
7
|
|
|
8
|
-
const HOW_TO = `Extend NavigationApplication
|
|
9
|
-
|
|
8
|
+
const HOW_TO = `Extend NavigationApplication and keep the template's loadReactNative(this) in onCreate: it loads
|
|
9
|
+
React Native as the app's Gradle files say, and the navigation makes its React host after it.
|
|
10
10
|
|
|
11
|
+
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
|
11
12
|
import dev.apexnative.navigation.NavigationApplication
|
|
12
13
|
|
|
13
14
|
class MainApplication : NavigationApplication() {
|
|
14
15
|
override val reactHost: ReactHost by lazy { ... } // as it is
|
|
16
|
+
|
|
17
|
+
override fun onCreate() {
|
|
18
|
+
super.onCreate()
|
|
19
|
+
loadReactNative(this)
|
|
20
|
+
}
|
|
15
21
|
}`;
|
|
16
22
|
|
|
23
|
+
const LOAD_IMPORT = 'import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative';
|
|
24
|
+
|
|
17
25
|
// Imports of the template that the change leaves unused.
|
|
18
|
-
const TEMPLATE_IMPORTS = [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
const TEMPLATE_IMPORTS = ['android.app.Application', 'com.facebook.react.ReactApplication'];
|
|
27
|
+
|
|
28
|
+
// A load of React Native of the app's own, next to loadReactNative(this) a second one that crashes the app at start:
|
|
29
|
+
// the New Architecture's entry point (react-native-navigation's apps call it in onCreate) and SoLoader.
|
|
30
|
+
// A bare `load()` is the entry point's only where the file imports it. A form these don't take out (`load` with
|
|
31
|
+
// arguments, an `if` without braces) is left to the app's author.
|
|
32
|
+
const OWN_LOAD = [
|
|
33
|
+
/^[ \t]*if\s*\(\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED\s*\)\s*\{(?:\s*\/\/[^\n]*)*\s*DefaultNewArchitectureEntryPoint\.load\(\)\s*\}[ \t]*\r?\n/gm,
|
|
34
|
+
/^[ \t]*DefaultNewArchitectureEntryPoint\.load\(\)[ \t]*\r?\n/gm,
|
|
35
|
+
/^[ \t]*SoLoader\.init\([^\n]*\)[ \t]*\r?\n/gm,
|
|
36
|
+
];
|
|
37
|
+
const BARE_LOAD = [
|
|
38
|
+
/^[ \t]*if\s*\(\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED\s*\)\s*\{(?:\s*\/\/[^\n]*)*\s*load\(\)\s*\}[ \t]*\r?\n/gm,
|
|
39
|
+
/^[ \t]*load\(\)[ \t]*\r?\n/gm,
|
|
40
|
+
];
|
|
41
|
+
const BARE_LOAD_IMPORT =
|
|
42
|
+
/^import\s+com\.facebook\.react\.defaults\.DefaultNewArchitectureEntryPoint\.load\b/m;
|
|
43
|
+
const OWN_LOAD_IMPORTS = [
|
|
44
|
+
'com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load',
|
|
45
|
+
'com.facebook.react.defaults.DefaultNewArchitectureEntryPoint',
|
|
46
|
+
'com.facebook.soloader.SoLoader',
|
|
47
|
+
'com.facebook.react.soloader.OpenSourceMergedSoMapping',
|
|
22
48
|
];
|
|
23
49
|
|
|
24
50
|
/**
|
|
25
|
-
* MainApplication.kt: the application becomes a NavigationApplication
|
|
51
|
+
* MainApplication.kt: the application becomes a NavigationApplication, and loads React Native in its onCreate as the
|
|
52
|
+
* template does.
|
|
26
53
|
* @param {string} text
|
|
27
54
|
* @returns {StepResult}
|
|
28
55
|
*/
|
|
@@ -32,21 +59,20 @@ function planMainApplication(text) {
|
|
|
32
59
|
note: 'changed by hand, see below',
|
|
33
60
|
howTo: HOW_TO,
|
|
34
61
|
});
|
|
35
|
-
if (/:\s*NavigationApplication\(
|
|
36
|
-
|
|
62
|
+
if (/:\s*NavigationApplication\(/.test(text)) {
|
|
63
|
+
if (loadsReactNative(text) && !loadsReactNativeItself(text)) {
|
|
64
|
+
return { status: 'done', note: 'already set up' };
|
|
65
|
+
}
|
|
66
|
+
const next = withReactNativeLoaded(text);
|
|
67
|
+
return next === undefined
|
|
37
68
|
? manual
|
|
38
|
-
: { status: '
|
|
69
|
+
: { status: 'change', next, note: 'loadReactNative(this) in onCreate' };
|
|
39
70
|
}
|
|
40
71
|
const header = /class\s+(\w+)\s*:\s*Application\(\)\s*,\s*ReactApplication\b/;
|
|
41
72
|
if (!header.test(text)) return manual;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
next
|
|
45
|
-
/\n\n?[ \t]*override\s+fun\s+onCreate\(\)\s*\{\s*super\.onCreate\(\)\s*loadReactNative\(this\)\s*\}/,
|
|
46
|
-
''
|
|
47
|
-
);
|
|
48
|
-
next = next.replace(/^[ \t]*loadReactNative\(this\)[ \t]*\r?\n/m, '');
|
|
49
|
-
if (/\bloadReactNative\s*\(/.test(codeOf(next))) return manual;
|
|
73
|
+
// The template's onCreate stays: its loadReactNative(this) loads React Native for the navigation too.
|
|
74
|
+
let next = withReactNativeLoaded(text.replace(header, 'class $1 : NavigationApplication()'));
|
|
75
|
+
if (next === undefined) return manual;
|
|
50
76
|
// Added first: with the template's imports gone the file would have none to sort it among.
|
|
51
77
|
next = addImport(next, 'import dev.apexnative.navigation.NavigationApplication', {
|
|
52
78
|
sorted: true,
|
|
@@ -55,4 +81,93 @@ function planMainApplication(text) {
|
|
|
55
81
|
return { status: 'change', next, note: 'NavigationApplication' };
|
|
56
82
|
}
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Whether the code of the file, not a comment, calls loadReactNative.
|
|
86
|
+
* @param {string} text
|
|
87
|
+
*/
|
|
88
|
+
function loadsReactNative(text) {
|
|
89
|
+
return /\bloadReactNative\s*\(/.test(codeOf(text));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether the code of the file loads React Native on its own as well: the New Architecture's entry point or SoLoader.
|
|
94
|
+
* @param {string} text
|
|
95
|
+
*/
|
|
96
|
+
function loadsReactNativeItself(text) {
|
|
97
|
+
const code = codeOf(text);
|
|
98
|
+
return (
|
|
99
|
+
/\bSoLoader\.init\s*\(/.test(code) ||
|
|
100
|
+
/\bDefaultNewArchitectureEntryPoint\.load\s*\(/.test(code) ||
|
|
101
|
+
(BARE_LOAD_IMPORT.test(text) && /(?<![\w.])load\s*\(/.test(code))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The application with `loadReactNative(this)` in its onCreate, after `super.onCreate()`: as it is when it has the
|
|
107
|
+
* call, with an onCreate of the template's when it has none. Undefined for an onCreate that doesn't call
|
|
108
|
+
* `super.onCreate()`, whose order only its author knows.
|
|
109
|
+
* @param {string} text
|
|
110
|
+
* @returns {string | undefined}
|
|
111
|
+
*/
|
|
112
|
+
function withReactNativeLoaded(text) {
|
|
113
|
+
let without = text;
|
|
114
|
+
const patterns = BARE_LOAD_IMPORT.test(text) ? [...OWN_LOAD, ...BARE_LOAD] : OWN_LOAD;
|
|
115
|
+
for (const pattern of patterns) without = without.replace(pattern, '');
|
|
116
|
+
without = removeUnusedImports(without, OWN_LOAD_IMPORTS);
|
|
117
|
+
if (loadsReactNativeItself(without)) return undefined;
|
|
118
|
+
if (loadsReactNative(without)) return without;
|
|
119
|
+
return withLoadCall(without);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} text an application without a load of React Native
|
|
124
|
+
* @returns {string | undefined}
|
|
125
|
+
*/
|
|
126
|
+
function withLoadCall(text) {
|
|
127
|
+
let next;
|
|
128
|
+
const superCall = /^([ \t]*)super\.onCreate\(\)[ \t]*\r?\n/m;
|
|
129
|
+
if (/\boverride\s+fun\s+onCreate\s*\(/.test(text)) {
|
|
130
|
+
if (!superCall.test(text)) return undefined;
|
|
131
|
+
next = text.replace(superCall, (line, indent) => `${line}${indent}loadReactNative(this)\n`);
|
|
132
|
+
} else {
|
|
133
|
+
const close = classEnd(text);
|
|
134
|
+
if (close === undefined) return undefined;
|
|
135
|
+
const indent = /^([ \t]+)override\b/m.exec(text)?.[1] ?? ' ';
|
|
136
|
+
const body = text.slice(0, close).replace(/\s*$/, '');
|
|
137
|
+
const onCreate = [
|
|
138
|
+
'',
|
|
139
|
+
`${indent}override fun onCreate() {`,
|
|
140
|
+
`${indent}${indent}super.onCreate()`,
|
|
141
|
+
`${indent}${indent}loadReactNative(this)`,
|
|
142
|
+
`${indent}}`,
|
|
143
|
+
].join('\n');
|
|
144
|
+
next = `${body}\n${onCreate}\n${text.slice(close)}`;
|
|
145
|
+
}
|
|
146
|
+
return addImport(next, LOAD_IMPORT, { sorted: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The index of the closing brace of the file's application class: its braces counted from the class's own, past any
|
|
151
|
+
* declaration or comment after it. Undefined when the braces don't close.
|
|
152
|
+
* @param {string} text
|
|
153
|
+
* @returns {number | undefined}
|
|
154
|
+
*/
|
|
155
|
+
function classEnd(text) {
|
|
156
|
+
// The comments and the string and character literals blanked out, so that an index in it is one in the text and a
|
|
157
|
+
// brace in a literal doesn't count.
|
|
158
|
+
const blank = (/** @type {string} */ part) => part.replace(/[^\n]/g, ' ');
|
|
159
|
+
const code = text
|
|
160
|
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
161
|
+
.replace(/\/\/.*$/gm, blank)
|
|
162
|
+
.replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g, blank);
|
|
163
|
+
const header = /\bclass\s+\w+\s*:[^{]*\{/.exec(code);
|
|
164
|
+
if (!header) return undefined;
|
|
165
|
+
let depth = 0;
|
|
166
|
+
for (let index = header.index + header[0].length - 1; index < code.length; index++) {
|
|
167
|
+
if (code[index] === '{') depth++;
|
|
168
|
+
else if (code[index] === '}' && --depth === 0) return index;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { planMainApplication, withReactNativeLoaded, loadsReactNativeItself };
|