@apexnative/navigation 1.0.0-alpha.4 → 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 +13 -0
- package/android/CLAUDE.md +9 -2
- package/android/CORE.md +25 -15
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
- package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.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/StackAnimationOptions.kt +40 -2
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
- 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 +30 -39
- 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/SlotScreenView.kt +12 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +9 -11
- 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 +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
- 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 +21 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +9 -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 +2 -5
- 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/TopBarPresenter.kt +4 -9
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
- package/cli/doctor/checks/splash.js +4 -22
- package/cli/splash/index.js +3 -1
- package/docs-md/containers/modals.md +2 -2
- package/docs-md/guides/cli.md +1 -2
- package/docs-md/help/troubleshooting.md +2 -2
- package/docs-md/screen-ui/back-and-gestures.md +22 -0
- package/docs-md/start/installation.md +1 -1
- package/ios/CORE.md +6 -6
- package/ios/Contract/ANNCommandAnswer.h +9 -2
- package/ios/Contract/ANNCommandAnswer.mm +25 -12
- package/ios/Core/ANNAppearanceDispatcher.h +5 -4
- package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
- package/ios/Core/ANNNavigationState.h +4 -6
- package/ios/Core/ANNNavigationState.mm +0 -4
- package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
- package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
- package/ios/Layouts/Stack/ANNPreviewPush.h +5 -5
- package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
- package/lib/module/api/commands/commands.js +54 -6
- package/lib/module/api/commands/commands.js.map +1 -1
- package/lib/module/engine/places.js +8 -0
- package/lib/module/engine/places.js.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/engine/places.d.ts +2 -0
- package/lib/typescript/engine/places.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 +18 -2
- package/lib/typescript/interfaces/options/animations.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/package.json +1 -1
- package/src/api/commands/commands.ts +48 -7
- package/src/api/commands/presentation.ts +4 -3
- package/src/engine/places.ts +9 -0
- package/src/interfaces/Options.ts +1 -0
- package/src/interfaces/options/animations.ts +19 -2
- package/src/interfaces/options/stack.ts +9 -0
- 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
|
@@ -23,11 +23,15 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListe
|
|
|
23
23
|
* is made with the view, from the app's React host, and fills it; it starts once, as the view joins a window, and stops
|
|
24
24
|
* once, on the main thread, as the view is destroyed.
|
|
25
25
|
*
|
|
26
|
-
* Ready once the safe area view of the screen told [ScreenReadiness] that the content
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Ready once the safe area view of the screen, in this view's own surface, told [ScreenReadiness] that the content
|
|
27
|
+
* rendered into it: the signal of another root with the same id (an old one, another live root of a bar component)
|
|
28
|
+
* never makes this one ready. A screen without a safe area view (no view ever told anything of its id) is ready once
|
|
29
|
+
* the root view of its surface has children, read on this view's own layout passes. A root that renders at once
|
|
30
|
+
* ([rendersAtOnce]: a component of a bar, a tab's cell) marks its id so from before its surface starts until it stops.
|
|
31
|
+
* Ready once, ready for good; what waits for it ([whenReady]) runs then, once.
|
|
32
|
+
* Without a signal or children, the limits of what waits decide: the view has no timer of its own. It owns the
|
|
33
|
+
* record of its appearance, which the dispatcher of the appearance events holds until the content is ready
|
|
34
|
+
* (`AppearanceDispatcher`).
|
|
31
35
|
*/
|
|
32
36
|
@SuppressLint("ViewConstructor")
|
|
33
37
|
class ReactView @JvmOverloads constructor(
|
|
@@ -36,6 +40,8 @@ class ReactView @JvmOverloads constructor(
|
|
|
36
40
|
override val componentName: String,
|
|
37
41
|
/** Props given by the native side, over `componentId`: the state of a tab's cell. */
|
|
38
42
|
initialProps: Bundle? = null,
|
|
43
|
+
/** The root renders at once, without waiting for its layout: a component of a bar or a tab's cell. */
|
|
44
|
+
private val rendersAtOnce: Boolean = false,
|
|
39
45
|
) : FrameLayout(context), IReactView {
|
|
40
46
|
private val scrollEventListener = ScrollEventListener()
|
|
41
47
|
private var destroyed = false
|
|
@@ -44,7 +50,14 @@ class ReactView @JvmOverloads constructor(
|
|
|
44
50
|
// What waited for the readiness was told; a question of isReady alone tells nothing, so nothing runs inside it.
|
|
45
51
|
private var readyTold = false
|
|
46
52
|
private val readyListeners = mutableListOf<Runnable>()
|
|
47
|
-
private var
|
|
53
|
+
private var listening = false
|
|
54
|
+
|
|
55
|
+
// What this root hears of its id: only the signals of its own surface.
|
|
56
|
+
private val readiness = object : ScreenReadiness.Listener {
|
|
57
|
+
override fun accepts(source: Int): Boolean = ownSource()?.let { it == source } ?: true
|
|
58
|
+
|
|
59
|
+
override fun onContentRendered() = checkReadiness()
|
|
60
|
+
}
|
|
48
61
|
|
|
49
62
|
// None without the app's host (the unit tests).
|
|
50
63
|
private val surface: ReactSurface?
|
|
@@ -59,8 +72,17 @@ class ReactView @JvmOverloads constructor(
|
|
|
59
72
|
*/
|
|
60
73
|
internal var contentChildCount: () -> Int = { (surface?.view as? ViewGroup)?.childCount ?: 0 }
|
|
61
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The id of this root's React surface, which the signals of its safe area view carry: React Native gives it as
|
|
77
|
+
* the surface starts (none matches before) and never gives it again. Null without a surface: every signal of the
|
|
78
|
+
* id is its own. The unit tests give their own.
|
|
79
|
+
*/
|
|
80
|
+
internal var ownSource: () -> Int? = { surface?.surfaceID }
|
|
81
|
+
|
|
62
82
|
init {
|
|
63
83
|
require(componentId.isNotEmpty()) { "a React root needs the id of its screen" }
|
|
84
|
+
// Before the surface exists, so its safe area view never asks before the mark is there.
|
|
85
|
+
if (rendersAtOnce) ScreenReadiness.setRendersAtOnce(componentId, this, true)
|
|
64
86
|
val props = Bundle().apply {
|
|
65
87
|
initialProps?.let(::putAll)
|
|
66
88
|
putString(COMPONENT_ID, componentId)
|
|
@@ -103,14 +125,9 @@ class ReactView @JvmOverloads constructor(
|
|
|
103
125
|
if (readyTold) return listener.run()
|
|
104
126
|
readyListeners += listener
|
|
105
127
|
if (isReady()) return checkReadiness()
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
override fun onContentRendered() = checkReadiness()
|
|
111
|
-
}
|
|
112
|
-
readinessListener = readiness
|
|
113
|
-
ScreenReadiness.setListener(componentId, readiness)
|
|
128
|
+
if (listening) return
|
|
129
|
+
listening = true
|
|
130
|
+
ScreenReadiness.addListener(componentId, this, readiness)
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
/**
|
|
@@ -126,23 +143,24 @@ class ReactView @JvmOverloads constructor(
|
|
|
126
143
|
waiting.forEach(Runnable::run)
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
// The safe area view said so; without one, the root view of the surface has children.
|
|
130
|
-
private fun hasRendered(): Boolean = when (ScreenReadiness.isContentRendered(componentId)) {
|
|
146
|
+
// The safe area view of this root's surface said so; without one, the root view of the surface has children.
|
|
147
|
+
private fun hasRendered(): Boolean = when (ScreenReadiness.isContentRendered(componentId, readiness)) {
|
|
131
148
|
true -> true
|
|
132
149
|
false -> false
|
|
133
150
|
null -> contentChildCount() > 0
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
private fun stopListening() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
ScreenReadiness.removeListener(componentId,
|
|
154
|
+
if (!listening) return
|
|
155
|
+
listening = false
|
|
156
|
+
ScreenReadiness.removeListener(componentId, this)
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
/**
|
|
143
160
|
* Stops the content, once, on the main thread: its appearance ends first (DidDisappear if its WillAppear went,
|
|
144
161
|
* else what is held goes with it), then the surface stops and leaves the host, so a reload doesn't start it again
|
|
145
|
-
* in the new JavaScript.
|
|
162
|
+
* in the new JavaScript. A root that renders at once takes its mark away: the last one of its id takes what was
|
|
163
|
+
* said of the id with it.
|
|
146
164
|
*/
|
|
147
165
|
override fun destroy() {
|
|
148
166
|
if (destroyed) return
|
|
@@ -150,6 +168,7 @@ class ReactView @JvmOverloads constructor(
|
|
|
150
168
|
destroyed = true
|
|
151
169
|
readyListeners.clear()
|
|
152
170
|
stopListening()
|
|
171
|
+
if (rendersAtOnce) ScreenReadiness.setRendersAtOnce(componentId, this, false)
|
|
153
172
|
val surface = surface ?: return
|
|
154
173
|
if (started) surface.stop()
|
|
155
174
|
surface.detach()
|
|
@@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReactContext
|
|
|
4
4
|
import dev.apexnative.navigation.NavigationActivity
|
|
5
5
|
import dev.apexnative.navigation.nativescreen.NativeScreenCalls
|
|
6
6
|
import dev.apexnative.navigation.react.events.EventEmitter
|
|
7
|
-
import dev.apexnative.navigation.react.events.
|
|
7
|
+
import dev.apexnative.navigation.react.events.NavigationState
|
|
8
8
|
import dev.apexnative.navigation.utils.UiThread
|
|
9
9
|
import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
|
|
10
10
|
import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
|
|
@@ -39,7 +39,7 @@ internal object ReloadCoordinator {
|
|
|
39
39
|
/**
|
|
40
40
|
* The JavaScript goes (a reload, or the host is destroyed). Any thread: the number grows at once; the layouts of
|
|
41
41
|
* every navigation activity go on the main thread, with the "reloading" mark, and so do the listeners of the
|
|
42
|
-
* JavaScript that went (back, hinge).
|
|
42
|
+
* JavaScript that went (back, hinge) and the holds of the navigation state its `setRoot` left.
|
|
43
43
|
*/
|
|
44
44
|
fun onJavaScriptGoes() {
|
|
45
45
|
advance()
|
|
@@ -48,6 +48,7 @@ internal object ReloadCoordinator {
|
|
|
48
48
|
launches.contextGone()
|
|
49
49
|
try {
|
|
50
50
|
Navigators.all().forEach { it.onReload() }
|
|
51
|
+
NavigationState.javaScriptWent()
|
|
51
52
|
BackCallbackState.setJsListeners(false)
|
|
52
53
|
Hinge.setEnabled(Navigators.current(), false)
|
|
53
54
|
} finally {
|
|
@@ -59,13 +60,15 @@ internal object ReloadCoordinator {
|
|
|
59
60
|
/**
|
|
60
61
|
* The developer tools of [activity] reload the JavaScript ("R R", the reload broadcast): the number grows first, so
|
|
61
62
|
* the navigation of the activity goes without a word to the JavaScript that goes, then its layers go
|
|
62
|
-
* ([NavigationActivity.onReload]). The host's own reset follows as the
|
|
63
|
+
* ([NavigationActivity.onReload]) with the holds of the navigation state. The host's own reset follows as the
|
|
64
|
+
* reload destroys it. Main thread.
|
|
63
65
|
*/
|
|
64
66
|
fun resetForDeveloperReload(activity: NavigationActivity) {
|
|
65
67
|
advance()
|
|
66
68
|
Navigator.setReloading(true)
|
|
67
69
|
try {
|
|
68
70
|
activity.onReload()
|
|
71
|
+
NavigationState.javaScriptWent()
|
|
69
72
|
} finally {
|
|
70
73
|
Navigator.setReloading(false)
|
|
71
74
|
}
|
|
@@ -92,7 +95,7 @@ internal object ReloadCoordinator {
|
|
|
92
95
|
*/
|
|
93
96
|
fun launchIfDue(activity: NavigationActivity) {
|
|
94
97
|
val context = launches.takeDue(activity, activity.isNavigationResumed) ?: return
|
|
95
|
-
|
|
98
|
+
NavigationState.reset()
|
|
96
99
|
EventEmitter(context).emitAppLaunched()
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
package dev.apexnative.navigation.react
|
|
2
2
|
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import com.facebook.react.ReactApplication
|
|
5
3
|
import com.facebook.react.bridge.Arguments
|
|
6
4
|
import com.facebook.react.bridge.WritableMap
|
|
7
5
|
import dev.apexnative.navigation.react.events.EventContract
|
|
@@ -36,8 +34,11 @@ object ScreenInsets {
|
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Keeps the screen's insets and padding, and tells when they changed through [emitter], the one of the JavaScript
|
|
39
|
+
* the screen was made for: silent once it went.
|
|
40
|
+
*/
|
|
41
|
+
fun update(screenId: String, value: Insets, paddingValue: Insets, emitter: EventEmitter) {
|
|
41
42
|
if (padding.put(screenId, paddingValue) != paddingValue) {
|
|
42
43
|
paddingListeners[screenId]?.toList()?.forEach { it() }
|
|
43
44
|
}
|
|
@@ -49,8 +50,7 @@ object ScreenInsets {
|
|
|
49
50
|
left = (value.left - paddingValue.left).coerceAtLeast(0.0),
|
|
50
51
|
)
|
|
51
52
|
if (insets.put(screenId, uncovered) == uncovered) return
|
|
52
|
-
|
|
53
|
-
EventEmitter(reactContext).emitScreenInsetsChanged(screenId, toMap(uncovered))
|
|
53
|
+
emitter.emitScreenInsetsChanged(screenId, toMap(uncovered))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** `{ top, right, bottom, left }`, empty before the screen was laid out. Any thread. */
|
|
@@ -3,7 +3,6 @@ package dev.apexnative.navigation.react
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.graphics.Rect
|
|
5
5
|
import androidx.core.view.WindowInsetsCompat
|
|
6
|
-
import com.facebook.react.ReactApplication
|
|
7
6
|
import com.facebook.react.bridge.Arguments
|
|
8
7
|
import com.facebook.react.bridge.WritableArray
|
|
9
8
|
import com.facebook.react.bridge.WritableMap
|
|
@@ -32,9 +31,16 @@ internal object ScreenRegions {
|
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* Measures the regions of the screen whose content the layout puts at [content] in the window (pixels), with the
|
|
35
|
-
* window's [insets] (the camera cutout), and tells JavaScript when they changed
|
|
34
|
+
* window's [insets] (the camera cutout), and tells JavaScript when they changed, through [emitter], the one of the
|
|
35
|
+
* JavaScript the screen was made for: silent once it went. Main thread only.
|
|
36
36
|
*/
|
|
37
|
-
fun update(
|
|
37
|
+
fun update(
|
|
38
|
+
activity: Activity,
|
|
39
|
+
screenId: String,
|
|
40
|
+
content: Rect,
|
|
41
|
+
insets: WindowInsetsCompat?,
|
|
42
|
+
emitter: EventEmitter,
|
|
43
|
+
) {
|
|
38
44
|
val folds = WindowFolds.of(activity)?.folds.orEmpty()
|
|
39
45
|
val cutouts = insets?.displayCutout?.boundingRects.orEmpty()
|
|
40
46
|
val density = activity.resources.displayMetrics.density
|
|
@@ -46,8 +52,7 @@ internal object ScreenRegions {
|
|
|
46
52
|
val previous = regions.put(screenId, value)
|
|
47
53
|
// None at first is what JavaScript reads before the first layout.
|
|
48
54
|
if (previous == value || (previous == null && value.isEmpty)) return
|
|
49
|
-
|
|
50
|
-
EventEmitter(reactContext).emitScreenRegionsChanged(screenId, toMap(value))
|
|
55
|
+
emitter.emitScreenRegionsChanged(screenId, toMap(value))
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/** `{ division, occlusion }`, empty lists for an unknown screen and before its first layout. Any thread. */
|
package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt
CHANGED
|
@@ -90,7 +90,7 @@ internal object AppearanceDispatcher {
|
|
|
90
90
|
val appearanceRecord: ScreenAppearance?
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** The navigation state (
|
|
93
|
+
/** The navigation state ([NavigationState]): one update in this pass of the main loop, or one now. */
|
|
94
94
|
interface StateUpdates {
|
|
95
95
|
/**
|
|
96
96
|
* Asks for one update in this pass of the main loop: the calls of one pass make one, after a DidAppear or a
|
|
@@ -98,7 +98,10 @@ internal object AppearanceDispatcher {
|
|
|
98
98
|
*/
|
|
99
99
|
fun setNeedsUpdate()
|
|
100
100
|
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Computes the state now and sends it when it changed: a command's answer, once
|
|
103
|
+
* (`CommandAnswer.reportStateNow`), the end of a user's transition, a tab chosen from code.
|
|
104
|
+
*/
|
|
102
105
|
fun updateNow()
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -157,33 +160,6 @@ internal object AppearanceDispatcher {
|
|
|
157
160
|
EventEmitter.current().emitSheetDetentChanged(componentId, detent)
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
/** The state of `NavigationStateReporter`, sent through the JavaScript that runs now, when it can be. */
|
|
161
|
-
object ReporterState : StateUpdates {
|
|
162
|
-
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
163
|
-
|
|
164
|
-
// The one posted update of a pass of the main loop, which checks the hold when it runs.
|
|
165
|
-
private val held = StateHold(
|
|
166
|
-
post = { task -> handler.post(task) },
|
|
167
|
-
generation = { ReloadCoordinator.generation },
|
|
168
|
-
) {
|
|
169
|
-
val emitter = EventEmitter.current()
|
|
170
|
-
if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
override fun setNeedsUpdate() {
|
|
174
|
-
if (EventEmitter.current().canSend) held.setNeedsUpdate()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
override fun hold() = held.hold()
|
|
178
|
-
|
|
179
|
-
override fun release() = held.release()
|
|
180
|
-
|
|
181
|
-
override fun updateNow() {
|
|
182
|
-
val emitter = EventEmitter.current()
|
|
183
|
-
if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
163
|
/** The main thread's loop. */
|
|
188
164
|
object MainScheduler : Scheduler {
|
|
189
165
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
@@ -198,8 +174,8 @@ internal object AppearanceDispatcher {
|
|
|
198
174
|
/** Where the events go: [EmitterSender]; the tests put a journal here. */
|
|
199
175
|
var sender: Sender = EmitterSender
|
|
200
176
|
|
|
201
|
-
/** The navigation state the
|
|
202
|
-
var state: StateUpdates =
|
|
177
|
+
/** The navigation state the appearance events and transitions update: [NavigationState]; a journal in tests. */
|
|
178
|
+
var state: StateUpdates = NavigationState
|
|
203
179
|
|
|
204
180
|
/** The clock of the hold limit: [MainScheduler]; the tests put a clock that moves when they say. */
|
|
205
181
|
var scheduler: Scheduler = MainScheduler
|
|
@@ -337,12 +313,15 @@ internal object AppearanceDispatcher {
|
|
|
337
313
|
}
|
|
338
314
|
|
|
339
315
|
/**
|
|
340
|
-
* A transition ended
|
|
316
|
+
* A transition ended: what went disappears ([hiding]), what stands appears ([showing], with its WillAppear first
|
|
341
317
|
* where it wasn't asked), the navigation state goes when it changed, then [events] (the transition's own:
|
|
342
318
|
* `ANN.ScreenPopped`, `ANN.ModalDismissed`, and what must follow them, like stopping the content of what went),
|
|
343
|
-
* then [answer] resolves with [value] (the promise, then `ANN.CommandCompleted
|
|
344
|
-
*
|
|
345
|
-
*
|
|
319
|
+
* then [answer] resolves with [value] (the promise, then `ANN.CommandCompleted`; no second state). The state is the
|
|
320
|
+
* answer's one state step (`CommandAnswer.reportStateNow`), which sends nothing for a JavaScript that went; a
|
|
321
|
+
* user's action has no answer, and a command answered before its transition ended had its state then: the state
|
|
322
|
+
* of the end goes all the same, unless its JavaScript went. A state that fails is logged and the transition goes
|
|
323
|
+
* on: [events] and the answer always come. Without [reportsState] the caller sends the state itself, later (a tab
|
|
324
|
+
* switch: after `ANN.BottomTabSelected`; a `setRoot` with layers: from its answer).
|
|
346
325
|
*/
|
|
347
326
|
fun endTransition(
|
|
348
327
|
hiding: List<ViewController<*>>,
|
|
@@ -360,18 +339,30 @@ internal object AppearanceDispatcher {
|
|
|
360
339
|
holdsState = false
|
|
361
340
|
}
|
|
362
341
|
showing.forEach { it.onViewDidAppear() }
|
|
363
|
-
if (reportsState)
|
|
342
|
+
if (reportsState) {
|
|
343
|
+
// The state before the transition's events, once per answer: the answer's own step before its promise
|
|
344
|
+
// finds it sent. Without an answer to wait for, the dispatcher sends it; nothing for a JavaScript that
|
|
345
|
+
// went.
|
|
346
|
+
when {
|
|
347
|
+
answer == null -> reportState()
|
|
348
|
+
answer.isAnswered -> if (ReloadCoordinator.isCurrent(answer.generation)) reportState()
|
|
349
|
+
else -> answer.reportStateNow()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
364
352
|
events?.invoke()
|
|
365
353
|
answer?.resolve(value)
|
|
366
354
|
}
|
|
367
355
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
356
|
+
/**
|
|
357
|
+
* The navigation state now, when it changed, without a command's answer (a command's goes through its answer,
|
|
358
|
+
* `CommandAnswer.reportStateNow`); a failure is logged ([log]), nothing else stops for it.
|
|
359
|
+
*/
|
|
360
|
+
fun reportState() {
|
|
370
361
|
try {
|
|
371
362
|
state.updateNow()
|
|
372
363
|
} catch (e: Throwable) {
|
|
373
364
|
val reason = ErrorTexts.couldNot("report the navigation state", ErrorTexts.reasonOf(e))
|
|
374
|
-
log("[ApexNavigation]
|
|
365
|
+
log("[ApexNavigation] transition: $reason")
|
|
375
366
|
}
|
|
376
367
|
}
|
|
377
368
|
|
|
@@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactContext
|
|
|
6
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
7
|
import dev.apexnative.navigation.NavigationApplication
|
|
8
8
|
import dev.apexnative.navigation.nativescreen.NativeScreenCalls
|
|
9
|
+
import dev.apexnative.navigation.react.ReloadCoordinator
|
|
9
10
|
import dev.apexnative.navigation.utils.NavigationLog
|
|
10
11
|
import org.json.JSONArray
|
|
11
12
|
|
|
@@ -13,12 +14,26 @@ import org.json.JSONArray
|
|
|
13
14
|
* Sends the events `ANN.<Name>` to the JavaScript of [reactContext] through React Native's device event emitter, in
|
|
14
15
|
* the order they are sent: sent from the main thread only. Without a context, or when its React instance is gone, an
|
|
15
16
|
* 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/`.
|
|
17
|
+
* `src/interfaces/`.
|
|
18
|
+
*
|
|
19
|
+
* An emitter belongs to one JavaScript, as on iOS (`ANNEventEmitter`): it takes the number of the JavaScript
|
|
20
|
+
* ([generation], `ReloadCoordinator`) when it is made and drops every event silently once that number moved. React
|
|
21
|
+
* Native keeps one context across a reload (bridgeless), so without it an event of a screen of the JavaScript that
|
|
22
|
+
* went would reach the new one, which may have a screen with the same id. The core and its modules send through
|
|
23
|
+
* [current], or [of] with the number of the JavaScript a node was made for.
|
|
17
24
|
*/
|
|
18
|
-
class EventEmitter
|
|
19
|
-
|
|
25
|
+
class EventEmitter @JvmOverloads constructor(
|
|
26
|
+
private val reactContext: ReactContext?,
|
|
27
|
+
/** The number of the JavaScript this emitter sends to: by default the one that runs as it is made. */
|
|
28
|
+
val generation: Int = ReloadCoordinator.generation,
|
|
29
|
+
) {
|
|
30
|
+
/** Whether the JavaScript of this emitter still runs: its events go only then. Any thread. */
|
|
31
|
+
val isCurrent: Boolean
|
|
32
|
+
get() = ReloadCoordinator.isCurrent(generation)
|
|
33
|
+
|
|
34
|
+
/** Whether an event sent now reaches JavaScript: its JavaScript still runs, in a live React instance. */
|
|
20
35
|
val canSend: Boolean
|
|
21
|
-
get() = reactContext?.hasActiveReactInstance() == true
|
|
36
|
+
get() = isCurrent && reactContext?.hasActiveReactInstance() == true
|
|
22
37
|
|
|
23
38
|
fun emitAppLaunched() = emit(EventContract.AppLaunched.EVENT, Arguments.createMap())
|
|
24
39
|
|
|
@@ -167,6 +182,8 @@ class EventEmitter(private val reactContext: ReactContext?) {
|
|
|
167
182
|
Arguments.createMap().apply { putString(EventContract.ScreenPopped.COMPONENT_ID, componentId) }
|
|
168
183
|
|
|
169
184
|
private fun emit(name: String, data: WritableMap) {
|
|
185
|
+
// The JavaScript of this emitter went: the new one never had its screens.
|
|
186
|
+
if (!isCurrent) return
|
|
170
187
|
val context = reactContext
|
|
171
188
|
if (context == null || !context.hasActiveReactInstance()) {
|
|
172
189
|
Log.w(NavigationLog.TAG, "Could not send event $name. React context is null!")
|
|
@@ -176,10 +193,20 @@ class EventEmitter(private val reactContext: ReactContext?) {
|
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
companion object {
|
|
179
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* The sender to the JavaScript that runs now: the React context of the app's `ReactGateway`. It goes silent
|
|
198
|
+
* once that JavaScript goes (a reload): one kept for later sends nothing to the next.
|
|
199
|
+
*/
|
|
180
200
|
@JvmStatic
|
|
181
|
-
fun current(): EventEmitter =
|
|
182
|
-
|
|
201
|
+
fun current(): EventEmitter = of(ReloadCoordinator.generation)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The sender to the JavaScript [generation] (a node's `jsGeneration`): the React context of the app's
|
|
205
|
+
* `ReactGateway` while that JavaScript runs, silent once it went, so an event of a screen made for the
|
|
206
|
+
* JavaScript that went never reaches the new one.
|
|
207
|
+
*/
|
|
208
|
+
internal fun of(generation: Int): EventEmitter =
|
|
209
|
+
EventEmitter(NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext, generation)
|
|
183
210
|
|
|
184
211
|
// The names of the events, for modules outside the library; inside it, [EventContract] has them with their
|
|
185
212
|
// keys.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package dev.apexnative.navigation.react.events
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import dev.apexnative.navigation.react.ReloadCoordinator
|
|
6
|
+
import dev.apexnative.navigation.viewcontrollers.bottomtabs.BottomTabsController
|
|
7
|
+
import dev.apexnative.navigation.viewcontrollers.navigator.Navigators
|
|
8
|
+
import dev.apexnative.navigation.viewcontrollers.parent.ParentController
|
|
9
|
+
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The one place of the navigation state JavaScript hears (`ANN.NavigationStateChanged`): the screen the user sees and
|
|
13
|
+
* the selected tab, the last state sent, the update posted for a pass of the main loop and its holds. Every change of
|
|
14
|
+
* the screen the user sees comes with a screen appearing: a push, a pop, a modal, a tab, a menu.
|
|
15
|
+
*
|
|
16
|
+
* Two ways ask for it. The dispatcher asks for one update after the events of a pass ([setNeedsUpdate], coalesced and
|
|
17
|
+
* held while a `setRoot` shows the modals and the overlays of its layout). A command's answer sends it at once
|
|
18
|
+
* ([updateNow], never held), once per answer (`CommandAnswer.reportStateNow`): at the end of its transition, else
|
|
19
|
+
* right before its promise. Each goes only when the state differs from the last one sent, through the JavaScript that
|
|
20
|
+
* runs now. Main thread only.
|
|
21
|
+
*/
|
|
22
|
+
internal object NavigationState : AppearanceDispatcher.StateUpdates {
|
|
23
|
+
/** What JavaScript hears: the screen the user sees (`null` for none) and the selected tab (`null` out of tabs). */
|
|
24
|
+
data class State(val screenId: String?, val screenName: String?, val tab: Int?)
|
|
25
|
+
|
|
26
|
+
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
27
|
+
|
|
28
|
+
// The last state sent to the JavaScript that runs now; none before its first.
|
|
29
|
+
private var sent: State? = null
|
|
30
|
+
|
|
31
|
+
// The one posted update of a pass of the main loop, which checks the holds when it runs.
|
|
32
|
+
private val updates = Updates(
|
|
33
|
+
post = { task -> handler.post(task) },
|
|
34
|
+
generation = { ReloadCoordinator.generation },
|
|
35
|
+
update = ::updateNow,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
override fun setNeedsUpdate() {
|
|
39
|
+
if (EventEmitter.current().canSend) updates.setNeedsUpdate()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun hold() = updates.hold()
|
|
43
|
+
|
|
44
|
+
override fun release() = updates.release()
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sends the state at once when it differs from the last one sent. A command's answer calls it once, at the end of
|
|
48
|
+
* its transition or right before its promise: the event and the resolution reach JavaScript in that order, so the
|
|
49
|
+
* state is new when the `await` of the command goes on. The update posted for the pass then finds nothing new.
|
|
50
|
+
*/
|
|
51
|
+
override fun updateNow() {
|
|
52
|
+
val emitter = EventEmitter.current()
|
|
53
|
+
if (!emitter.canSend) return
|
|
54
|
+
// The navigation of the activity the commands run in (`Navigators.current`).
|
|
55
|
+
val navigator = Navigators.current()?.navigator ?: return
|
|
56
|
+
val next = stateOf(navigator.visibleLayout)
|
|
57
|
+
if (next == sent) return
|
|
58
|
+
sent = next
|
|
59
|
+
emitter.emitNavigationStateChanged(next.screenId, next.screenName, next.tab)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The new JavaScript has no state yet (`ANN.AppLaunched`): the next one goes to it whatever it is. */
|
|
63
|
+
fun reset() {
|
|
64
|
+
sent = null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The JavaScript went with its layouts: a hold its `setRoot` left, and an update wanted under it, go with it, so
|
|
69
|
+
* the new JavaScript's updates are never held by the old one's.
|
|
70
|
+
*/
|
|
71
|
+
fun javaScriptWent() = updates.dropHolds()
|
|
72
|
+
|
|
73
|
+
// The top modal, or the root ([layout]), down to the screen the user sees; overlays don't count. The tab is the
|
|
74
|
+
// selected one of the nearest tabs the screen is in, the last met on the way down, in a modal too: the tabs
|
|
75
|
+
// `selectTab` acts on from it. None for a screen out of tabs.
|
|
76
|
+
internal fun stateOf(layout: ViewController<*>?): State {
|
|
77
|
+
var tab: Int? = null
|
|
78
|
+
var current = layout ?: return State(null, null, null)
|
|
79
|
+
while (current is ParentController<*>) {
|
|
80
|
+
if (current is BottomTabsController) tab = current.selectedIndex
|
|
81
|
+
current = current.currentChild ?: return State(null, null, null)
|
|
82
|
+
}
|
|
83
|
+
return State(current.id, current.currentComponentName, tab)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The posted update and its holds. [setNeedsUpdate] posts one update ([post]), coalesced: the asks of one pass of
|
|
88
|
+
* the main loop make one, which runs [update] in a later pass. While held, an ask, or a posted update that runs
|
|
89
|
+
* while held (asked before the hold was taken), only marks one as wanted, and the last release posts it. Released
|
|
90
|
+
* right before the answer, that update finds nothing new: the state before the answer is sent at once, never
|
|
91
|
+
* held, and an identical state is never sent twice. Every [hold] has one [release]; a release without a hold does
|
|
92
|
+
* nothing, so the count never goes below zero. An update keeps the JavaScript it was asked for ([generation]): one
|
|
93
|
+
* asked by a JavaScript that went before it runs is dropped; a newer ask of the new JavaScript still goes.
|
|
94
|
+
*/
|
|
95
|
+
internal class Updates(
|
|
96
|
+
private val post: (() -> Unit) -> Unit,
|
|
97
|
+
private val generation: () -> Int = { 0 },
|
|
98
|
+
private val update: () -> Unit,
|
|
99
|
+
) {
|
|
100
|
+
/** The holds not released yet. */
|
|
101
|
+
var count = 0
|
|
102
|
+
private set
|
|
103
|
+
|
|
104
|
+
// An update was asked while held.
|
|
105
|
+
private var wanted = false
|
|
106
|
+
// An update is posted and hasn't run yet.
|
|
107
|
+
private var scheduled = false
|
|
108
|
+
// The JavaScript the last ask was for.
|
|
109
|
+
private var askedIn = 0
|
|
110
|
+
|
|
111
|
+
/** Asks for one update in a later pass of the main loop; while held, once the last hold is released. */
|
|
112
|
+
fun setNeedsUpdate() {
|
|
113
|
+
askedIn = generation()
|
|
114
|
+
if (count > 0) wanted = true else schedule()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The updates asked from now on, and those posted that run from now on, wait for as many [release]s. */
|
|
118
|
+
fun hold() {
|
|
119
|
+
count++
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One hold ends; with the last, the update asked while held is posted now. */
|
|
123
|
+
fun release() {
|
|
124
|
+
if (count == 0) return
|
|
125
|
+
count--
|
|
126
|
+
if (count > 0 || !wanted) return
|
|
127
|
+
wanted = false
|
|
128
|
+
schedule()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The holds and the update wanted under them go, unreleased: the JavaScript that took them went. An update
|
|
133
|
+
* already posted stays; it runs only for the JavaScript it was asked for.
|
|
134
|
+
*/
|
|
135
|
+
fun dropHolds() {
|
|
136
|
+
count = 0
|
|
137
|
+
wanted = false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun schedule() {
|
|
141
|
+
if (scheduled) return
|
|
142
|
+
scheduled = true
|
|
143
|
+
post {
|
|
144
|
+
scheduled = false
|
|
145
|
+
// A hold taken since it was posted: the update waits for the release. The JavaScript it was asked for
|
|
146
|
+
// went: nothing goes to the new one, which hears its first state after its own changes.
|
|
147
|
+
when {
|
|
148
|
+
count > 0 -> wanted = true
|
|
149
|
+
askedIn == generation() -> update()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -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() {
|