@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +22 -0
- package/android/CLAUDE.md +26 -9
- package/android/CORE.md +32 -17
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
- 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/AnimationsOptions.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/HardwareBackButtonOptions.kt +9 -1
- package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
- package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
- package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +29 -36
- package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +33 -22
- 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/ReactView.kt +39 -20
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +10 -5
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +47 -46
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -4
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
- 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/ArrangementController.kt +3 -2
- 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 +25 -17
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +8 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -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 +61 -15
- 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 +25 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
- 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/ScreenSearch.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +5 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
- 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 +14 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
- package/cli/doctor/checks/app.js +61 -13
- package/cli/doctor/checks/splash.js +4 -22
- package/cli/migrate/native.js +18 -0
- package/cli/setup/source.js +2 -1
- package/cli/setup/steps/mainApplication.js +134 -19
- package/cli/splash/index.js +3 -1
- package/docs-md/architecture/transitions.md +6 -7
- package/docs-md/containers/modals.md +2 -2
- package/docs-md/guides/cli.md +1 -2
- package/docs-md/help/glossary.md +1 -1
- package/docs-md/help/troubleshooting.md +22 -5
- package/docs-md/index.json +4 -0
- package/docs-md/migration/from-react-native-navigation.md +8 -2
- package/docs-md/screen-ui/back-and-gestures.md +24 -2
- package/docs-md/start/installation.md +4 -2
- package/ios/CLAUDE.md +17 -5
- package/ios/CORE.md +18 -13
- package/ios/Contract/ANNCommandAnswer.h +9 -2
- package/ios/Contract/ANNCommandAnswer.mm +28 -30
- package/ios/Contract/ANNCommandContract.h +15 -0
- package/ios/Contract/ANNCommandContract.mm +78 -0
- package/ios/Core/ANNAppearanceDispatcher.h +5 -4
- package/ios/Core/ANNAppearanceDispatcher.mm +12 -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 -5
- package/ios/Core/ANNNavigationState.mm +8 -7
- 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/Root/ANNSplashViewController.h +5 -3
- package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
- 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/ANNPreviewPush.h +5 -5
- package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
- 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/commands/commands.js +54 -6
- package/lib/module/api/commands/commands.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 +52 -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/commands/commands.d.ts.map +1 -1
- package/lib/typescript/api/commands/presentation.d.ts +4 -3
- package/lib/typescript/api/commands/presentation.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 +7 -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.d.ts +1 -1
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +23 -4
- 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/stack.d.ts +9 -0
- package/lib/typescript/interfaces/options/stack.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/commands/commands.ts +48 -7
- package/src/api/commands/presentation.ts +4 -3
- package/src/api/events/navigationEvents.ts +1 -1
- package/src/api/hooks/useNavigation.ts +1 -1
- 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 +65 -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/Options.ts +1 -0
- package/src/interfaces/contract/commands.json +101 -0
- package/src/interfaces/options/animations.ts +24 -4
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/stack.ts +9 -0
- package/src/interfaces/options/topBar.ts +22 -9
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
- package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
|
@@ -95,8 +95,9 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
|
|
|
95
95
|
val reactContext = context as? ReactContext ?: return
|
|
96
96
|
val dispatcher = UIManagerHelper.getEventDispatcher(reactContext) ?: return
|
|
97
97
|
readySent = true
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
99
|
+
dispatcher.dispatchEvent(SafeAreaReadyEvent(surfaceId, id))
|
|
100
|
+
screenId?.let { ScreenReadiness.laidOut(it, surfaceId) }
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// The navigation tells whether this view's screen waits: a component of a bar (title, button, background, tab)
|
|
@@ -111,12 +112,13 @@ class SafeAreaView(reactContext: ThemedReactContext) : ReactViewGroup(reactConte
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// Once, by the screen's id: the container asks the screen's view for the layout React mounts the content without.
|
|
114
|
-
// On attach too: content added before this view was attached is signalled then.
|
|
115
|
+
// On attach too: content added before this view was attached is signalled then. The signal names this view's
|
|
116
|
+
// surface: only the root that shows it takes it as its own.
|
|
115
117
|
private fun signalContent() {
|
|
116
118
|
if (contentSignalled) return
|
|
117
119
|
val id = screenId ?: return
|
|
118
120
|
contentSignalled = true
|
|
119
|
-
ScreenReadiness.contentRendered(id)
|
|
121
|
+
ScreenReadiness.contentRendered(id, UIManagerHelper.getSurfaceId(this))
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
fun drop() {
|
|
@@ -9,14 +9,15 @@ interface SlotContainer {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* The native containers the slots of the app's React tree show their children in, by slot id
|
|
13
|
-
*
|
|
12
|
+
* The native containers the slots of the app's React tree show their children in, by slot id, and the slot views by
|
|
13
|
+
* the same id: a change of a container goes straight to the views of its id. A destroyed container and a collected
|
|
14
|
+
* view drop out on their own.
|
|
14
15
|
*/
|
|
15
16
|
object SlotContainers {
|
|
16
17
|
private class Entry(val view: WeakReference<ViewGroup>, val passThrough: Boolean)
|
|
17
18
|
|
|
18
19
|
private val containers = HashMap<String, Entry>()
|
|
19
|
-
private val slots =
|
|
20
|
+
private val slots = SlotViews<SlotView>()
|
|
20
21
|
|
|
21
22
|
/** `passThrough`: touches outside the slot's content go to the screens below, as in an overlay for a toast. */
|
|
22
23
|
fun put(slotId: String, container: ViewGroup, passThrough: Boolean = false) {
|
|
@@ -33,12 +34,14 @@ object SlotContainers {
|
|
|
33
34
|
changed(slotId)
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** The container of a slot appeared or changed its size. */
|
|
37
|
+
/** The container of a slot appeared or changed its size: the slot views under its id, and only they, hear it. */
|
|
37
38
|
fun changed(slotId: String) {
|
|
38
|
-
slots.
|
|
39
|
+
slots.views(slotId).forEach { it.containerChanged(slotId) }
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
/** The slot view goes under its slot id, and out from under the id it had. Main thread only. */
|
|
43
|
+
internal fun register(slotId: String, slot: SlotView) = slots.register(slotId, slot)
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
/** The slot view unmounted or lost its slot id: no container change reaches it. Main thread only. */
|
|
46
|
+
internal fun unregister(slot: SlotView) = slots.unregister(slot)
|
|
44
47
|
}
|
|
@@ -31,7 +31,12 @@ class SlotScreenView(
|
|
|
31
31
|
private val whenMounted = mutableListOf<Runnable>()
|
|
32
32
|
private val whenRendered = mutableListOf<Runnable>()
|
|
33
33
|
private val mountedHandlers = LinkedHashMap<String, Runnable>()
|
|
34
|
-
private var
|
|
34
|
+
private var listening = false
|
|
35
|
+
|
|
36
|
+
// The safe area view of a slot renders in the app's one React tree: every signal of the id is the screen's.
|
|
37
|
+
private val readiness = object : ScreenReadiness.Listener {
|
|
38
|
+
override fun onContentRendered() = checkReadiness()
|
|
39
|
+
}
|
|
35
40
|
private var mounted = false
|
|
36
41
|
private var readyTold = false
|
|
37
42
|
private var destroyed = false
|
|
@@ -105,14 +110,9 @@ class SlotScreenView(
|
|
|
105
110
|
if (readyTold) return listener.run()
|
|
106
111
|
whenRendered += listener
|
|
107
112
|
if (isReady()) return checkReadiness()
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
override fun onContentRendered() = checkReadiness()
|
|
113
|
-
}
|
|
114
|
-
readinessListener = readiness
|
|
115
|
-
ScreenReadiness.setListener(componentId, readiness)
|
|
113
|
+
if (listening) return
|
|
114
|
+
listening = true
|
|
115
|
+
ScreenReadiness.addListener(componentId, this, readiness)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
override fun whenSlotContentMounted(listener: Runnable) {
|
|
@@ -151,8 +151,8 @@ class SlotScreenView(
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
private fun stopListening() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ScreenReadiness.removeListener(componentId,
|
|
154
|
+
if (!listening) return
|
|
155
|
+
listening = false
|
|
156
|
+
ScreenReadiness.removeListener(componentId, this)
|
|
157
157
|
}
|
|
158
158
|
}
|
|
@@ -86,7 +86,6 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
|
|
|
86
86
|
init {
|
|
87
87
|
content.visibility = INVISIBLE
|
|
88
88
|
addView(content)
|
|
89
|
-
SlotContainers.listen(this)
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
override fun setId(id: Int) {
|
|
@@ -100,7 +99,9 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
|
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
fun attachContent() {
|
|
103
|
-
|
|
102
|
+
// The view hears the changes of the container under the id it has now, and no other.
|
|
103
|
+
val id = slotId ?: return SlotContainers.unregister(this)
|
|
104
|
+
SlotContainers.register(id, this)
|
|
104
105
|
val container = SlotContainers.get(id)
|
|
105
106
|
val target: ViewGroup = container ?: this
|
|
106
107
|
if (content.parent !== target) {
|
|
@@ -135,7 +136,7 @@ class SlotView(reactContext: ThemedReactContext) : ReactViewGroup(reactContext)
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
fun drop() {
|
|
138
|
-
SlotContainers.
|
|
139
|
+
SlotContainers.unregister(this)
|
|
139
140
|
(content.parent as? ViewGroup)?.removeView(content)
|
|
140
141
|
}
|
|
141
142
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package dev.apexnative.navigation.slots
|
|
2
|
+
|
|
3
|
+
import java.lang.ref.WeakReference
|
|
4
|
+
import java.util.WeakHashMap
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The slot views by their slot id, held weakly, so a change of a container reaches only the views of its id. A view is
|
|
8
|
+
* under one id at a time: registering it under a new one (its `nativeID` changed, or Fabric reused it) takes it out
|
|
9
|
+
* from under the old one. Two live views under one id both stay, in the order they came, and both hear the change, as
|
|
10
|
+
* every view with that id did when the change went to all of them. Main thread only.
|
|
11
|
+
*/
|
|
12
|
+
internal class SlotViews<T : Any> {
|
|
13
|
+
private val byId = HashMap<String, MutableList<WeakReference<T>>>()
|
|
14
|
+
private val idOf = WeakHashMap<T, String>()
|
|
15
|
+
|
|
16
|
+
fun register(slotId: String, view: T) {
|
|
17
|
+
if (idOf[view] == slotId) return
|
|
18
|
+
unregister(view)
|
|
19
|
+
idOf[view] = slotId
|
|
20
|
+
byId.getOrPut(slotId) { ArrayList(1) }.add(WeakReference(view))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun unregister(view: T) {
|
|
24
|
+
val slotId = idOf.remove(view) ?: return
|
|
25
|
+
prune(slotId) { it === view }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The live views under [slotId], a copy: a view may register or leave while it hears the change. */
|
|
29
|
+
fun views(slotId: String): List<T> {
|
|
30
|
+
prune(slotId) { false }
|
|
31
|
+
return byId[slotId]?.mapNotNull { it.get() }.orEmpty()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Drops the collected views under the id and the ones `drop` picks; an id with no views left goes.
|
|
35
|
+
private fun prune(slotId: String, drop: (T) -> Boolean) {
|
|
36
|
+
val views = byId[slotId] ?: return
|
|
37
|
+
views.removeAll { reference -> reference.get()?.let(drop) ?: true }
|
|
38
|
+
if (views.isEmpty()) byId.remove(slotId)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package dev.apexnative.navigation.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.view.View
|
|
6
|
+
import java.io.File
|
|
7
|
+
import java.net.URI
|
|
8
|
+
import java.net.URISyntaxException
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What an image of the options is cached by: its [uri] and what changes the decoded result for the same uri, the
|
|
12
|
+
* screen's density (a drawable of the app picks its density's file, a decoded file is scaled to it), light or dark
|
|
13
|
+
* (`drawable-night`) and the layout direction (`drawable-ldrtl`, auto-mirrored drawables). For a `file:` uri also the
|
|
14
|
+
* file's [modified] time and [size]: a file the app writes again at the same path is another image. The loader bakes
|
|
15
|
+
* no tint and no size into an image, so neither is part of it.
|
|
16
|
+
*/
|
|
17
|
+
internal data class ImageKey(
|
|
18
|
+
val uri: String,
|
|
19
|
+
val densityDpi: Int,
|
|
20
|
+
val night: Boolean,
|
|
21
|
+
val rightToLeft: Boolean,
|
|
22
|
+
/** When the file of a `file:` uri last changed, in milliseconds; 0 for any other uri or a file that isn't there. */
|
|
23
|
+
val modified: Long = 0,
|
|
24
|
+
/** The bytes of the file of a `file:` uri; 0 for any other uri or a file that isn't there. */
|
|
25
|
+
val size: Long = 0,
|
|
26
|
+
) {
|
|
27
|
+
companion object {
|
|
28
|
+
private const val FILE_SCHEME = "file:"
|
|
29
|
+
private const val FILE_PREFIX = "file://"
|
|
30
|
+
|
|
31
|
+
/** The key of [uri] in the current configuration of [context]. Any thread. */
|
|
32
|
+
fun of(context: Context, uri: String): ImageKey {
|
|
33
|
+
val configuration = context.resources.configuration
|
|
34
|
+
return of(uri, configuration.densityDpi, configuration.uiMode, configuration.layoutDirection)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The key of [uri] for a configuration's density, `uiMode` (only its night bits count) and layout direction;
|
|
39
|
+
* a `file:` uri reads its file's modification time and size. Any thread.
|
|
40
|
+
*/
|
|
41
|
+
fun of(uri: String, densityDpi: Int, uiMode: Int, layoutDirection: Int): ImageKey {
|
|
42
|
+
val file = filePath(uri)?.let(::File)
|
|
43
|
+
return ImageKey(
|
|
44
|
+
uri = uri,
|
|
45
|
+
densityDpi = densityDpi,
|
|
46
|
+
night = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES,
|
|
47
|
+
rightToLeft = layoutDirection == View.LAYOUT_DIRECTION_RTL,
|
|
48
|
+
// Both 0 when the file isn't there or can't be read.
|
|
49
|
+
modified = file?.lastModified() ?: 0,
|
|
50
|
+
size = file?.length() ?: 0,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The path of a `file:` uri, decoded as the loader's `Uri.getPath` decodes it; none for any other uri. A uri
|
|
55
|
+
// that `URI` can't parse (an unescaped space) is taken as it is after its scheme.
|
|
56
|
+
private fun filePath(uri: String): String? {
|
|
57
|
+
if (!uri.startsWith(FILE_SCHEME)) return null
|
|
58
|
+
val path = try {
|
|
59
|
+
URI(uri).path
|
|
60
|
+
} catch (e: URISyntaxException) {
|
|
61
|
+
null
|
|
62
|
+
}
|
|
63
|
+
return path ?: uri.removePrefix(FILE_PREFIX).removePrefix(FILE_SCHEME).ifEmpty { null }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
package dev.apexnative.navigation.utils
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.content.res.Resources
|
|
5
|
-
import android.graphics.BitmapFactory
|
|
6
|
-
import android.graphics.drawable.BitmapDrawable
|
|
7
4
|
import android.graphics.drawable.Drawable
|
|
8
|
-
import android.net.Uri
|
|
9
|
-
import android.os.StrictMode
|
|
10
|
-
import androidx.core.content.ContextCompat
|
|
11
|
-
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
|
|
12
|
-
import java.io.FileInputStream
|
|
13
|
-
import java.io.FileNotFoundException
|
|
14
5
|
import java.io.IOException
|
|
15
|
-
import java.io.InputStream
|
|
16
|
-
import java.net.URL
|
|
17
6
|
|
|
18
7
|
/** What an image loads into: [onComplete] with the image, or [onError] when it can't load. Main thread. */
|
|
19
8
|
open class ImageLoadingListenerAdapter {
|
|
@@ -23,90 +12,19 @@ open class ImageLoadingListenerAdapter {
|
|
|
23
12
|
}
|
|
24
13
|
|
|
25
14
|
/**
|
|
26
|
-
* Loads
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* that read only) or through the content resolver. A remote address that is not Metro's asset is never loaded. A
|
|
31
|
-
* failure never ends the app: the listener's [ImageLoadingListenerAdapter.onError], the log, and React Native's red
|
|
32
|
-
* screen in development.
|
|
15
|
+
* Loads an image or an icon of the options at once, through the shared cache of [OptionImages]: each image its own
|
|
16
|
+
* mutable copy. For the places whose first frame needs the image (the top bar's buttons, the back button, menus, search
|
|
17
|
+
* suggestions); a place that can show it when ready asks through [ImageRequests]. A failure never ends the app: the
|
|
18
|
+
* listener's [ImageLoadingListenerAdapter.onError], and one warning per address.
|
|
33
19
|
*/
|
|
34
20
|
class ImageLoader {
|
|
35
21
|
/** Loads [uri] and gives it to [listener] before returning. Main thread. */
|
|
36
22
|
fun loadIcon(context: Context, uri: String, listener: ImageLoadingListenerAdapter) {
|
|
37
|
-
val drawable =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
val reason = if (e is LoadFailure) "" else ": ${e.message ?: e.javaClass.simpleName}"
|
|
41
|
-
DevelopmentLog.error("Could not load image $uri$reason${(e as? LoadFailure)?.why.orEmpty()}", e)
|
|
42
|
-
listener.onError(e)
|
|
23
|
+
val drawable = OptionImages.load(context, uri)
|
|
24
|
+
if (drawable == null) {
|
|
25
|
+
listener.onError(IOException("Could not load image $uri"))
|
|
43
26
|
return
|
|
44
27
|
}
|
|
45
28
|
listener.onComplete(drawable)
|
|
46
29
|
}
|
|
47
|
-
|
|
48
|
-
private fun load(context: Context, uri: String): Drawable {
|
|
49
|
-
if (uri.startsWith(FILE_SCHEME)) return decode(context) { FileInputStream(Uri.parse(uri).path) }
|
|
50
|
-
resourceDrawable(context, uri)?.let { return it }
|
|
51
|
-
if (!DevelopmentLog.isDevelopment) throw LoadFailure("")
|
|
52
|
-
val address = Uri.parse(uri)
|
|
53
|
-
return when (address.scheme) {
|
|
54
|
-
"http", "https" -> {
|
|
55
|
-
if (!isMetroAsset(address)) throw LoadFailure(": remote images are not supported in options")
|
|
56
|
-
withNetworkOnMainThread { decode(context) { URL(uri).openStream() } }
|
|
57
|
-
}
|
|
58
|
-
else -> decode(context) {
|
|
59
|
-
context.contentResolver.openInputStream(address) ?: throw FileNotFoundException(uri)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// A drawable of the app by name, its own copy; none when the app has none by that name.
|
|
65
|
-
private fun resourceDrawable(context: Context, name: String): Drawable? {
|
|
66
|
-
val id = ResourceDrawableIdHelper.getResourceDrawableId(context, name)
|
|
67
|
-
if (id <= 0) return null
|
|
68
|
-
return try {
|
|
69
|
-
ContextCompat.getDrawable(context, id)?.mutate()
|
|
70
|
-
} catch (e: Resources.NotFoundException) {
|
|
71
|
-
null
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// The image of the stream [open] gives, closed after the read, also on an error; one it can't decode fails, and so
|
|
76
|
-
// does one too large for the memory: that is a load error too, never the end of the app.
|
|
77
|
-
private fun decode(context: Context, open: () -> InputStream): Drawable {
|
|
78
|
-
val decoded = try {
|
|
79
|
-
open().use { BitmapFactory.decodeStream(it) }
|
|
80
|
-
} catch (e: OutOfMemoryError) {
|
|
81
|
-
throw LoadFailure(": it is too large to decode")
|
|
82
|
-
}
|
|
83
|
-
val bitmap = decoded ?: throw LoadFailure(": it can't be decoded")
|
|
84
|
-
return BitmapDrawable(context.resources, bitmap)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// A failure the loader names itself: `Could not load image <source>`, then [why].
|
|
88
|
-
private class LoadFailure(val why: String) : IOException("Could not load image$why")
|
|
89
|
-
|
|
90
|
-
// Metro's asset: an address of the server the JavaScript bundle came from, under `/assets/`.
|
|
91
|
-
private fun isMetroAsset(address: Uri): Boolean {
|
|
92
|
-
val bundle = DevelopmentLog.bundleUrl?.let(Uri::parse) ?: return false
|
|
93
|
-
return address.host == bundle.host && address.port == bundle.port &&
|
|
94
|
-
address.path.orEmpty().startsWith(ASSETS_PATH)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Reading from the network on the main thread for [read] only; the thread's policy comes back, also on an error.
|
|
98
|
-
private fun <T> withNetworkOnMainThread(read: () -> T): T {
|
|
99
|
-
val policy = StrictMode.getThreadPolicy()
|
|
100
|
-
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(policy).permitNetwork().build())
|
|
101
|
-
try {
|
|
102
|
-
return read()
|
|
103
|
-
} finally {
|
|
104
|
-
StrictMode.setThreadPolicy(policy)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private companion object {
|
|
109
|
-
const val FILE_SCHEME = "file:"
|
|
110
|
-
const val ASSETS_PATH = "/assets/"
|
|
111
|
-
}
|
|
112
30
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package dev.apexnative.navigation.utils
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.Drawable
|
|
4
|
+
|
|
5
|
+
/** Where the images of the options come from, for one context: [OptionImages.source], or a fake in the tests. */
|
|
6
|
+
internal interface ImageSource {
|
|
7
|
+
/** The key of [uri] in the context's configuration now; for a `file:` uri it reads the file's version. */
|
|
8
|
+
fun key(uri: String): ImageKey
|
|
9
|
+
|
|
10
|
+
/** A copy of the image of [key] when the cache has it, none otherwise; never loads. */
|
|
11
|
+
fun cached(key: ImageKey): Drawable?
|
|
12
|
+
|
|
13
|
+
/** A copy of the image of [key], from the cache or loaded on the caller's thread; none on a failure. */
|
|
14
|
+
fun now(key: ImageKey): Drawable?
|
|
15
|
+
|
|
16
|
+
/** Whether [uri] can load off the main thread: a file or a drawable of the app. */
|
|
17
|
+
fun loadsLater(uri: String): Boolean
|
|
18
|
+
|
|
19
|
+
/** Loads [key] off the main thread and gives a copy of it, or none on a failure, to [done] on the main thread. */
|
|
20
|
+
fun later(key: ImageKey, done: (Drawable?) -> Unit)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The images that the places of a view asked for (an item's icon, a button's icon), by place [P]: what the cache has
|
|
25
|
+
* comes at once; what it doesn't, when the place can show it later, loads off the main thread and comes when ready;
|
|
26
|
+
* everything else loads at once. The last request of a place wins: a load that arrives after the place asked again,
|
|
27
|
+
* or after [forget] or [clear], is dropped. A load that fails gives the place no image, as an option without one:
|
|
28
|
+
* the image it showed before doesn't stay. A request builds the key of its image once (a file's version is read once).
|
|
29
|
+
* Main thread only.
|
|
30
|
+
*/
|
|
31
|
+
internal class ImageRequests<P : Any>(private val source: ImageSource) {
|
|
32
|
+
// The open load of each place: its number, which a later request of the place replaces.
|
|
33
|
+
private val waiting = HashMap<P, Long>()
|
|
34
|
+
private var last = 0L
|
|
35
|
+
|
|
36
|
+
/** The key of [uri] now, for a caller that compares it before it asks ([request] with the key). */
|
|
37
|
+
fun key(uri: String): ImageKey = source.key(uri)
|
|
38
|
+
|
|
39
|
+
/** [request] for the image of [uri], its key built here. */
|
|
40
|
+
fun request(place: P, uri: String, mayWait: Boolean, show: (Drawable?) -> Unit) =
|
|
41
|
+
request(place, source.key(uri), mayWait, show)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gives [show] the image of [key] for [place]: now when the cache has it, or when the place can't wait for it
|
|
45
|
+
* ([mayWait] false: the first frame needs it) or the uri can't load off the main thread (Metro's address, a content
|
|
46
|
+
* uri); otherwise later, unless [place] asks again first. Null when it doesn't load: the place shows none.
|
|
47
|
+
*/
|
|
48
|
+
fun request(place: P, key: ImageKey, mayWait: Boolean, show: (Drawable?) -> Unit) {
|
|
49
|
+
waiting.remove(place)
|
|
50
|
+
source.cached(key)?.let {
|
|
51
|
+
show(it)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (!mayWait || !source.loadsLater(key.uri)) {
|
|
55
|
+
show(source.now(key))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
val number = ++last
|
|
59
|
+
waiting[place] = number
|
|
60
|
+
source.later(key) { image ->
|
|
61
|
+
if (waiting[place] != number) return@later
|
|
62
|
+
waiting.remove(place)
|
|
63
|
+
show(image)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The place shows no image of the options now: a load it asked for earlier is dropped. */
|
|
68
|
+
fun forget(place: P) {
|
|
69
|
+
waiting.remove(place)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The view goes: every open load is dropped. */
|
|
73
|
+
fun clear() {
|
|
74
|
+
waiting.clear()
|
|
75
|
+
}
|
|
76
|
+
}
|