@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-alpha.5](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* android back options, a launch screen copy from UILaunchScreen, modal and back fixes ([#43](https://github.com/apexnative/navigation/issues/43)) ([29f311e](https://github.com/apexnative/navigation/commit/29f311e318134f284a0c6279e6c86e15612c9343))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **android:** send the state of a tab switch from code at its start ([#44](https://github.com/apexnative/navigation/issues/44)) ([34d03d0](https://github.com/apexnative/navigation/commit/34d03d0214cf09e14329d0bae51e47d46063f054))
|
|
14
|
+
* **hooks:** measure agent folder scopes from the worktree a file is in ([#42](https://github.com/apexnative/navigation/issues/42)) ([d306137](https://github.com/apexnative/navigation/commit/d306137534d3ba551fab8a7346c7ad7300c0faf7))
|
|
15
|
+
|
|
3
16
|
## [1.0.0-alpha.4](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2026-10-23)
|
|
4
17
|
|
|
5
18
|
|
package/android/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
25
25
|
`ReactView` (a React root), the stores (`ScreenInsets`, `ScreenRegions`, `ArrangementStates`, `Hinge`, `StartInfo`),
|
|
26
26
|
`LaunchLink`, `ScrollEvents`.
|
|
27
27
|
- `react/events/`: `AppearanceDispatcher`, `ScreenAppearance`, `EventEmitter`, `EventContract` (made by
|
|
28
|
-
`scripts/contract.mjs`), `
|
|
28
|
+
`scripts/contract.mjs`), `NavigationState`, `ComponentType`.
|
|
29
29
|
- `viewcontrollers/viewcontroller/`: `ViewController` (the base of every node), `Presenter`, `IReactView` (what a
|
|
30
30
|
screen shows), `ScrollEventListener`.
|
|
31
31
|
- `viewcontrollers/child/`, `parent/`: `ChildController` and the registry; `ParentController` (the base of layouts),
|
|
@@ -105,7 +105,7 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
105
105
|
- System bars: `system/SystemUiOwner.kt`, `SystemUiLayers.kt`, `SystemUiRules.kt`, `SystemUiWindow.kt`,
|
|
106
106
|
`AppDirection.kt`; `viewcontrollers/parent/ContainerBackground.kt`.
|
|
107
107
|
- Events and state: `react/events/AppearanceDispatcher.kt`, `ScreenAppearance.kt`, `EventEmitter.kt`,
|
|
108
|
-
`
|
|
108
|
+
`NavigationState.kt`; `react/CommandAnswer.kt`.
|
|
109
109
|
- Options (parsing, merging): `options/Options.kt`, `OptionsReader.kt`, `OptionsCache.kt`, `DefaultOptions.kt`,
|
|
110
110
|
`params/`, `parsers/`; `viewcontrollers/viewcontroller/ViewController.kt` (resolution).
|
|
111
111
|
- Error texts and answers: `react/ErrorTexts.kt` and `react/CommandContract.kt`, made by `scripts/contract.mjs` from
|
|
@@ -172,6 +172,9 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
172
172
|
`cancel` it on every exit.
|
|
173
173
|
- A layer that waits to show must be a `WaitingLayer` and leave on every exit (show, supersede, destroy, root gone).
|
|
174
174
|
- Never resolve a promise yourself: only `CommandAnswer` knows whether its JavaScript went and sends the state first.
|
|
175
|
+
- An event of a screen goes to the JavaScript the screen was made for: `EventEmitter.of(node.jsGeneration)`, or an
|
|
176
|
+
emitter kept from then (it goes silent after a reload). React Native keeps one context across a reload, so a sender
|
|
177
|
+
taken from the context at event time tells the new JavaScript about a screen it never had.
|
|
175
178
|
- A signal that comes inside a layout pass or a view's attach must not move views there; the stack posts readiness
|
|
176
179
|
(`deliver`).
|
|
177
180
|
- A step that runs later (a timer, a readiness signal, an animator's end, a user callback) runs under no runner: it
|
|
@@ -206,6 +209,10 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
206
209
|
- The back gesture's preview shows the screen under the top outside any operation of the queue: it may only reveal
|
|
207
210
|
it and put it back, and its release goes through `StackQueue.cut(gesture = true)`. A command that comes meanwhile
|
|
208
211
|
stops it through `StackDisplay.stop`, like any transition it takes over.
|
|
212
|
+
- What back does (`Navigator.canHandleBack`, `keepsBackFromSystem`) is computed on events, not before each frame: a
|
|
213
|
+
new path that changes it without a command (a gesture, an animator's end, a layout's switch) calls
|
|
214
|
+
`BackCallbackState.update`, or the system's back leaves the app where it should go back. Debug builds log a missed
|
|
215
|
+
one (`BackCallbackCheck`).
|
|
209
216
|
- Without edge to edge the window resizes for the keyboard by the manifest's soft input mode, so
|
|
210
217
|
`layout.adjustResize: false` goes through the system UI owner's keyboard group (`SystemUiWindow.setKeyboardCovers`),
|
|
211
218
|
not through the screen's padding.
|
package/android/CORE.md
CHANGED
|
@@ -31,7 +31,7 @@ coordinators: RootCoordinator · StackQueue (one per stack) · ModalCoordinator
|
|
|
31
31
|
presenters: ComponentPresenter · StackPresenter + TopBarPresenter · BottomTabsPresenter · SideMenuPresenter ·
|
|
32
32
|
SplitViewPresenter · system/SystemUiOwner (status bar, navigation bar, orientation, keyboard)
|
|
33
33
|
▼
|
|
34
|
-
react/events/AppearanceDispatcher ─►
|
|
34
|
+
react/events/AppearanceDispatcher ─► NavigationState ─► EventEmitter ─► ANN.* events ─► JS
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
- **Way in.** `NavigationTurboModule` only turns a method into a `CommandRunner` call; it holds no navigation logic. It
|
|
@@ -84,11 +84,12 @@ react/events/AppearanceDispatcher ─► NavigationStateReporter ─► EventEmi
|
|
|
84
84
|
`AppearanceDispatcher.beginTransition`: **`ComponentWillAppear`** of Profile. The transition runs
|
|
85
85
|
(`StackTransitions`, `SystemTransition`, or the screens' own animation, `AnimationRules.stackIsOwn`).
|
|
86
86
|
8. **End.** `finishPush` calls `AppearanceDispatcher.endTransition`: **`ComponentDidDisappear`** of Home, then
|
|
87
|
-
**`ComponentDidAppear`** of Profile, then
|
|
88
|
-
transition's own events (none for a push), then
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
**`ComponentDidAppear`** of Profile, then the answer's one state step (`CommandAnswer.reportStateNow`:
|
|
88
|
+
**`NavigationStateChanged`**, only if it changed), then the transition's own events (none for a push), then
|
|
89
|
+
`CommandAnswer.resolve` with Profile's id.
|
|
90
|
+
9. **Answer.** `resolve` sends the state only when no transition sent it (`reportStateNow` runs once per answer: a
|
|
91
|
+
`setRoot` with layout modals sends it here), resolves the promise, then sends **`CommandCompleted`**
|
|
92
|
+
(`CommandContract.COMPLETED`). After the task, `BackCallbackState.update` recomputes what back does.
|
|
92
93
|
|
|
93
94
|
The events go from the main thread, synchronously, through `EventEmitter.current()`; the promise is answered once.
|
|
94
95
|
|
|
@@ -127,13 +128,16 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
|
|
|
127
128
|
`beginTransition` starts the appearances; `endTransition` runs what left, then what came, then the state, then the
|
|
128
129
|
transition's events, then the answer. Events asked before a screen's content is ready are held in its
|
|
129
130
|
`ScreenAppearance` (until the content mounts or `HOLD_LIMIT_MILLIS`).
|
|
130
|
-
- **Navigation state.** `
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
131
|
+
- **Navigation state.** `NavigationState` owns it: the last state sent, the walk (`stateOf`), the posted update and its
|
|
132
|
+
holds (`NavigationState.Updates`). `AppearanceDispatcher.state` (`StateUpdates`) is the one way to ask for it:
|
|
133
|
+
`setNeedsUpdate` coalesces the asks to one posted update per pass of the main loop and keeps it while held; the
|
|
134
|
+
update calls `updateNow`. A command's answer sends it once, at once and never held (`CommandAnswer.reportStateNow`):
|
|
135
|
+
at the end of its transition, else right before its promise for the commands that change what shows; nothing for a
|
|
136
|
+
JavaScript that went. It reads the navigator through `Navigators.current()`, and `stateOf` finds the
|
|
134
137
|
`tab` as the nearest tabs of the visible screen (the last tabs met walking down from the visible layout). A `setRoot`
|
|
135
138
|
with layout modals or overlays takes `AppearanceDispatcher.StateUpdates.hold()` and releases it right before its
|
|
136
|
-
answer, so JavaScript hears one state; every hold has one `release()`.
|
|
139
|
+
answer, so JavaScript hears one state; every hold has one `release()`. The holds go when the JavaScript goes
|
|
140
|
+
(`NavigationState.javaScriptWent`), so one left by its `setRoot` never holds the next JavaScript's updates.
|
|
137
141
|
- **Answer.** `CommandAnswer`: resolves or rejects once, and nothing once the JavaScript went. The id a stack entry
|
|
138
142
|
answers with is one rule, `StackController.answerIdOf` (the queue asks it through `StackQueue.Host.answerIdOf` with
|
|
139
143
|
an `AnswerKind`): `push` answers the entry's own id (a pair's), every other command the screen it stands for (a
|
|
@@ -141,7 +145,11 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
|
|
|
141
145
|
- **Stack queue.** `StackQueue`: every command, back and return of a container goes through it; it keeps `entries`
|
|
142
146
|
(what JavaScript sees) and `shown` (what the container shows) and answers once.
|
|
143
147
|
- **Screen readiness.** `ScreenReadiness` (by id; written by `SafeAreaView`) and `ContentReadiness` (the `Readiness`
|
|
144
|
-
the waits ask, through `IReactView.isReady`).
|
|
148
|
+
the waits ask, through `IReactView.isReady`). As on iOS, the signals are kept (a listener that comes later hears them
|
|
149
|
+
at once), an id has a listener per key (several roots of one bar component), and each signal carries the React
|
|
150
|
+
surface it came from: a `ReactView` takes only its own surface's, so an old root with the same id never makes a new
|
|
151
|
+
one ready. An id renders at once while a root marks it (`ReactView`'s `rendersAtOnce`); the last one takes the id's
|
|
152
|
+
signals with it. A screen's id is cleared as the screen is made and destroyed, everything on reload.
|
|
145
153
|
- **Render wait.** `RenderWait`: the one wait for screens to render, used by `RootCoordinator`, `ModalCoordinator`,
|
|
146
154
|
`OverlayCoordinator` and `StackQueue`; `RenderWait.waitedScreens` walks `ParentController.waitedChildren()` at every
|
|
147
155
|
level. What differs between users are values (limit, bar phase, unknown-content rule), never steps.
|
|
@@ -171,7 +179,8 @@ Each rule has one owner. Change the owner, not a caller; if a new feature needs
|
|
|
171
179
|
- **Back.** `NavigationActivity.passBack` → `ReactGateway.onBackPressed` (JavaScript's `BackHandler`) →
|
|
172
180
|
`invokeDefaultOnBackPressed` → `Navigator.handleBack` → `ModalCoordinator.handleBack` → the root's
|
|
173
181
|
`ParentController.handleBack` → `StackController.handleBack` → `StackQueue.cut`. `BackCallbackState` keeps the
|
|
174
|
-
system callback enabled only while back has something to do
|
|
182
|
+
system callback enabled only while back has something to do, computed again on the events that change it (its KDoc
|
|
183
|
+
lists them), never before each frame; debug builds check it (`BackCallbackCheck`).
|
|
175
184
|
- **Error texts.** `ErrorTexts`: one function per text, the same words as iOS, made with iOS's from
|
|
176
185
|
`src/interfaces/contract/errors.json` (`scripts/contract.mjs`). JavaScript adds `[navigation] <command>:`.
|
|
177
186
|
- **Event names and keys.** `EventContract`, made from the event types of `src/interfaces/`; `EventEmitter` and the
|
|
@@ -201,7 +210,7 @@ Modules are the feature classes that plug into the core through its contract (th
|
|
|
201
210
|
(`splash/`), animations (`FadeThrough`, `SystemTransition`, `TransitionTargets`, three element animators), utils
|
|
202
211
|
(`MaterialTheme`, `NavigationLog`, `awaitBarComponents`, `whenDrawn`).
|
|
203
212
|
- **Stores and singletons:** regions (`Hinge`, `ScreenInsets`, `ScreenRegions`, `WindowFolds`), pair state
|
|
204
|
-
(`ArrangementStates`), links (`LaunchLink`, `StartInfo`), events (`
|
|
213
|
+
(`ArrangementStates`), links (`LaunchLink`, `StartInfo`), events (`NavigationState`), commands
|
|
205
214
|
(`BackCallbackState`), stack (`PreparedFrame`).
|
|
206
215
|
|
|
207
216
|
How they plug in:
|
|
@@ -226,4 +235,5 @@ How they plug in:
|
|
|
226
235
|
- **Core code in a module's folder:** `viewcontrollers/splitview/SplitViewPresenter.kt`, `slots/SlotScreenView.kt`,
|
|
227
236
|
and in `views/animations/` and `views/element/` everything except the files named above.
|
|
228
237
|
- **Stores** are written on the main thread by the core and read from any thread; they are cleared with their nodes and on
|
|
229
|
-
reload.
|
|
238
|
+
reload. Their events, and the search's, go through the emitter of the JavaScript the screen was made for
|
|
239
|
+
(`EventEmitter.of(node.jsGeneration)`); the hinge keeps the `EventEmitter.current()` of the call that turned it on.
|
|
@@ -35,6 +35,7 @@ import dev.apexnative.navigation.splash.SplashTheme
|
|
|
35
35
|
import dev.apexnative.navigation.splash.SplashView
|
|
36
36
|
import dev.apexnative.navigation.system.SystemUiWindow
|
|
37
37
|
import dev.apexnative.navigation.utils.MaterialTheme
|
|
38
|
+
import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackCheck
|
|
38
39
|
import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
|
|
39
40
|
import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
|
|
40
41
|
import dev.apexnative.navigation.viewcontrollers.navigator.Navigators
|
|
@@ -75,6 +76,7 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
75
76
|
private val backCallback = object : OnBackPressedCallback(false) {
|
|
76
77
|
override fun handleOnBackStarted(backEvent: BackEventCompat) {
|
|
77
78
|
backGesture = null
|
|
79
|
+
navigator?.let { backCheck?.atBack(it) }
|
|
78
80
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || BackCallbackState.hasJsListeners) return
|
|
79
81
|
val stack = navigator?.stackPoppedByBack() ?: return
|
|
80
82
|
backGesture = stack.startBackGesture(backEvent.swipeEdge == BackEventCompat.EDGE_LEFT, backEvent.touchY)
|
|
@@ -87,9 +89,11 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
87
89
|
override fun handleOnBackCancelled() {
|
|
88
90
|
backGesture?.cancel()
|
|
89
91
|
backGesture = null
|
|
92
|
+
navigator?.let(BackCallbackState::update)
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
override fun handleOnBackPressed() {
|
|
96
|
+
navigator?.let { backCheck?.atBack(it) }
|
|
93
97
|
val gesture = backGesture
|
|
94
98
|
backGesture = null
|
|
95
99
|
if (gesture?.commit() == true) BackCallbackState.update() else passBack()
|
|
@@ -99,10 +103,12 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
99
103
|
// The back gesture that previews a pop now; none.
|
|
100
104
|
private var backGesture: StackController.BackGesture? = null
|
|
101
105
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
private val
|
|
105
|
-
|
|
106
|
+
// Debug builds only: the check that what back does never changes without its trigger (BackCallbackState), sampled
|
|
107
|
+
// before frames and at back events. It only logs; release builds have neither the check nor the listener.
|
|
108
|
+
private val backCheck = if (BuildConfig.DEBUG) BackCallbackCheck() else null
|
|
109
|
+
private val backCheckBeforeFrame = ViewTreeObserver.OnPreDrawListener {
|
|
110
|
+
val navigator = navigator
|
|
111
|
+
if (navigator != null) backCheck?.sample(navigator, SystemClock.uptimeMillis())
|
|
106
112
|
true
|
|
107
113
|
}
|
|
108
114
|
private var frameObserver: ViewTreeObserver? = null
|
|
@@ -143,7 +149,9 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
143
149
|
SlotHost.attachIfStarted(this)
|
|
144
150
|
reactGateway.onActivityCreated(this)
|
|
145
151
|
onBackPressedDispatcher.addCallback(this, backCallback)
|
|
146
|
-
|
|
152
|
+
if (backCheck != null) {
|
|
153
|
+
frameObserver = window.decorView.viewTreeObserver.also { it.addOnPreDrawListener(backCheckBeforeFrame) }
|
|
154
|
+
}
|
|
147
155
|
// The activity's system UI: its owner remembers the theme's colors and the manifest's orientation now, before
|
|
148
156
|
// anything changes them.
|
|
149
157
|
navigator.systemUi.start(SystemUiWindow.of(this))
|
|
@@ -218,6 +226,8 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
218
226
|
isNavigationResumed = true
|
|
219
227
|
// The root, the modals and the overlays lay out again; nothing is sent.
|
|
220
228
|
navigator?.onResume()
|
|
229
|
+
// A safety net: what back does changes on its own triggers, and the system asks the callback from here on.
|
|
230
|
+
navigator?.let(BackCallbackState::update)
|
|
221
231
|
if (developerSupport() != null) reloadBroadcast.register()
|
|
222
232
|
ReloadCoordinator.launchIfDue(this)
|
|
223
233
|
}
|
|
@@ -239,6 +249,8 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
239
249
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
240
250
|
super.onWindowFocusChanged(hasFocus)
|
|
241
251
|
if (reactGateway.currentReactContext != null) reactGateway.host.onWindowFocusChange(hasFocus)
|
|
252
|
+
// A safety net, as at onResume: back after a dialog or another window over the activity.
|
|
253
|
+
if (hasFocus) navigator?.let(BackCallbackState::update)
|
|
242
254
|
}
|
|
243
255
|
|
|
244
256
|
override fun onUserLeaveHint() {
|
|
@@ -302,9 +314,11 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
302
314
|
splashHold = null
|
|
303
315
|
reloadBroadcast.release()
|
|
304
316
|
navigator?.let(BackCallbackState::detach)
|
|
305
|
-
// The window's observer now, and the one
|
|
306
|
-
|
|
307
|
-
|
|
317
|
+
// The debug check's listener: from the window's observer now, and the one it was added to, if they differ.
|
|
318
|
+
if (backCheck != null) {
|
|
319
|
+
val observers = listOfNotNull(frameObserver, window?.peekDecorView()?.viewTreeObserver).distinct()
|
|
320
|
+
observers.forEach { if (it.isAlive) it.removeOnPreDrawListener(backCheckBeforeFrame) }
|
|
321
|
+
}
|
|
308
322
|
frameObserver = null
|
|
309
323
|
SlotHost.detach(this)
|
|
310
324
|
navigator?.destroyWithActivity()
|
|
@@ -317,7 +331,9 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
317
331
|
/**
|
|
318
332
|
* Back that JavaScript's listeners didn't take: the navigation does its step first — a stack's top leaves or holds,
|
|
319
333
|
* a pane closes —; with nothing to do there, the activity's own back, as without the navigation. An app may
|
|
320
|
-
* override it (e.g. `moveTaskToBack(true)`).
|
|
334
|
+
* override it (e.g. `moveTaskToBack(true)`). The callback is off for that back only: the dispatcher's own back is
|
|
335
|
+
* the activity's, not [onBackPressed]; then it is computed again, and `hardwareBackButton.systemAnimation: false`
|
|
336
|
+
* turns it on again.
|
|
321
337
|
*/
|
|
322
338
|
override fun invokeDefaultOnBackPressed() {
|
|
323
339
|
val handled = navigator?.handleBack(CommandListenerAdapter()) == true
|
|
@@ -8,7 +8,7 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
8
8
|
*/
|
|
9
9
|
class AnimationsOptions {
|
|
10
10
|
@JvmField var push = StackPushAnimationOptions()
|
|
11
|
-
@JvmField var pop =
|
|
11
|
+
@JvmField var pop = StackPopAnimationOptions()
|
|
12
12
|
@JvmField var setStackRoot = SetStackRootAnimationOptions()
|
|
13
13
|
@JvmField var setRoot = EnterExitAnimationOptions()
|
|
14
14
|
@JvmField var showModal = ModalAnimationOptions()
|
|
@@ -43,7 +43,7 @@ class AnimationsOptions {
|
|
|
43
43
|
val options = AnimationsOptions()
|
|
44
44
|
if (map == null) return options
|
|
45
45
|
options.push = map.group("push") { StackPushAnimationOptions.parse(it) }
|
|
46
|
-
options.pop = map.group("pop") {
|
|
46
|
+
options.pop = map.group("pop") { StackPopAnimationOptions.parse(it) }
|
|
47
47
|
options.setStackRoot = map.group("setStackRoot") { SetStackRootAnimationOptions.parse(it) }
|
|
48
48
|
options.setRoot = map.group("setRoot") { EnterExitAnimationOptions.parse(it) }
|
|
49
49
|
options.showModal = map.group("showModal") { ModalAnimationOptions.parse(it) }
|
package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt
CHANGED
|
@@ -15,20 +15,27 @@ class HardwareBackButtonOptions {
|
|
|
15
15
|
@JvmField var popStackOnPress: Bool = NullBool()
|
|
16
16
|
/** Internal. */
|
|
17
17
|
@JvmField var dismissModalOnPress: Bool = NullBool()
|
|
18
|
+
/**
|
|
19
|
+
* `systemAnimation`: `false` keeps back in the app on the first screen too, so the system doesn't play its
|
|
20
|
+
* animation of going home; the press then does the activity's own back.
|
|
21
|
+
*/
|
|
22
|
+
@JvmField var systemAnimation: Bool = NullBool()
|
|
18
23
|
|
|
19
24
|
fun hasValue(): Boolean = bottomTabsOnPress.hasValue() || popStackOnPress.hasValue() ||
|
|
20
|
-
dismissModalOnPress.hasValue()
|
|
25
|
+
dismissModalOnPress.hasValue() || systemAnimation.hasValue()
|
|
21
26
|
|
|
22
27
|
fun mergeWith(other: HardwareBackButtonOptions) {
|
|
23
28
|
if (other.bottomTabsOnPress.hasValue()) bottomTabsOnPress = other.bottomTabsOnPress
|
|
24
29
|
if (other.popStackOnPress.hasValue()) popStackOnPress = other.popStackOnPress
|
|
25
30
|
if (other.dismissModalOnPress.hasValue()) dismissModalOnPress = other.dismissModalOnPress
|
|
31
|
+
if (other.systemAnimation.hasValue()) systemAnimation = other.systemAnimation
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
fun mergeWithDefault(defaults: HardwareBackButtonOptions) {
|
|
29
35
|
if (!bottomTabsOnPress.hasValue()) bottomTabsOnPress = defaults.bottomTabsOnPress
|
|
30
36
|
if (!popStackOnPress.hasValue()) popStackOnPress = defaults.popStackOnPress
|
|
31
37
|
if (!dismissModalOnPress.hasValue()) dismissModalOnPress = defaults.dismissModalOnPress
|
|
38
|
+
if (!systemAnimation.hasValue()) systemAnimation = defaults.systemAnimation
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
fun copy() = HardwareBackButtonOptions().also { it.mergeWith(this) }
|
|
@@ -43,6 +50,7 @@ class HardwareBackButtonOptions {
|
|
|
43
50
|
options.bottomTabsOnPress = TextParser.parse(map, "bottomTabsOnPress", BOTTOM_TABS_ON_PRESS)
|
|
44
51
|
options.popStackOnPress = BoolParser.parse(map, "popStackOnPress")
|
|
45
52
|
options.dismissModalOnPress = BoolParser.parse(map, "dismissModalOnPress")
|
|
53
|
+
options.systemAnimation = BoolParser.parse(map, "systemAnimation")
|
|
46
54
|
return options
|
|
47
55
|
}
|
|
48
56
|
}
|
|
@@ -99,8 +99,8 @@ class BottomTabsEnterExitAnimationOptions {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* `StackAnimationOptions`: the
|
|
103
|
-
*
|
|
102
|
+
* `StackAnimationOptions`: the base of the animations of `push`, `pop` and `setStackRoot`; over the shared elements and
|
|
103
|
+
* element transitions of [LayoutAnimation].
|
|
104
104
|
*/
|
|
105
105
|
open class StackAnimationOptions : LayoutAnimation() {
|
|
106
106
|
@JvmField var enabled: Bool = NullBool()
|
|
@@ -188,6 +188,44 @@ class StackPushAnimationOptions : StackAnimationOptions() {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* `StackPopAnimationOptions`: the animation of `pop`, and `preview`: whether a back gesture over the stack previews the
|
|
193
|
+
* pop of its top (Android 14+); `false` pops it when the gesture is released, as on Android 13.
|
|
194
|
+
*/
|
|
195
|
+
class StackPopAnimationOptions : StackAnimationOptions() {
|
|
196
|
+
@JvmField var preview: Bool = NullBool()
|
|
197
|
+
|
|
198
|
+
override fun hasValue(): Boolean = super.hasValue() || preview.hasValue()
|
|
199
|
+
|
|
200
|
+
fun mergeWith(other: StackPopAnimationOptions) {
|
|
201
|
+
super.mergeWith(other)
|
|
202
|
+
if (other.preview.hasValue()) preview = other.preview
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fun mergeWithDefault(defaults: StackPopAnimationOptions) {
|
|
206
|
+
super.mergeWithDefault(defaults)
|
|
207
|
+
if (!preview.hasValue()) preview = defaults.preview
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
override fun copy(): StackPopAnimationOptions = StackPopAnimationOptions().also { it.mergeWith(this) }
|
|
211
|
+
|
|
212
|
+
// Reads the fields of this class from [map], after those of its parent.
|
|
213
|
+
private fun readStackPopAnimationOptions(map: ReadableMap) {
|
|
214
|
+
readStackAnimationOptions(map)
|
|
215
|
+
preview = BoolParser.parse(map, "preview")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
companion object {
|
|
219
|
+
@JvmStatic
|
|
220
|
+
fun parse(map: ReadableMap?): StackPopAnimationOptions {
|
|
221
|
+
val options = StackPopAnimationOptions()
|
|
222
|
+
if (map == null) return options
|
|
223
|
+
options.readStackPopAnimationOptions(map)
|
|
224
|
+
return options
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
/** `SetStackRootAnimationOptions`: the animation of `setStackRoot`. */
|
|
192
230
|
class SetStackRootAnimationOptions : StackAnimationOptions() {
|
|
193
231
|
@JvmField var asPush: Bool = NullBool()
|
|
@@ -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
|
|
@@ -19,11 +17,13 @@ internal object ArrangementStates {
|
|
|
19
17
|
|
|
20
18
|
private val states = ConcurrentHashMap<String, State>()
|
|
21
19
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Keeps the screen's state, and tells JavaScript when it changed and [notify] holds, through [emitter], the one of
|
|
22
|
+
* the JavaScript the screen was made for: silent once it went. Main thread only.
|
|
23
|
+
*/
|
|
24
|
+
fun set(screenId: String, state: State, notify: Boolean, emitter: EventEmitter) {
|
|
24
25
|
if (states.put(screenId, state) == state || !notify) return
|
|
25
|
-
|
|
26
|
-
EventEmitter(reactContext).emitArrangementChanged(screenId, toMap(state))
|
|
26
|
+
emitter.emitArrangementChanged(screenId, toMap(state))
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** `{ placement, layout, front, hidden }`, empty for a screen outside a pair. Any thread. */
|
|
@@ -3,13 +3,14 @@ package dev.apexnative.navigation.react
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import com.facebook.react.bridge.Promise
|
|
5
5
|
import dev.apexnative.navigation.react.events.EventEmitter
|
|
6
|
-
import dev.apexnative.navigation.react.events.
|
|
6
|
+
import dev.apexnative.navigation.react.events.NavigationState
|
|
7
7
|
import dev.apexnative.navigation.utils.NavigationLog
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The one answer of a command with a promise: [resolve] or [reject], once; after that, and once the JavaScript it came
|
|
11
|
-
* from went ([generation]), nothing.
|
|
12
|
-
* a [CommandListener] answers through [asListener]. Main
|
|
11
|
+
* from went ([generation]), nothing. It owns the command's one state step ([reportStateNow]). [CommandRunner] makes it
|
|
12
|
+
* on the main thread for each command; area code that takes a [CommandListener] answers through [asListener]. Main
|
|
13
|
+
* thread only.
|
|
13
14
|
*/
|
|
14
15
|
internal class CommandAnswer(
|
|
15
16
|
/** The method of the command: `push`, `setRoot`, … */
|
|
@@ -27,19 +28,36 @@ internal class CommandAnswer(
|
|
|
27
28
|
var isAnswered = false
|
|
28
29
|
private set
|
|
29
30
|
|
|
31
|
+
// The state step of this answer ran ([reportStateNow]).
|
|
32
|
+
private var stateReported = false
|
|
33
|
+
|
|
30
34
|
/** Whether an answer now would reach the JavaScript the command came from. */
|
|
31
35
|
val canAnswer: Boolean
|
|
32
36
|
get() = !isAnswered && ReloadCoordinator.isCurrent(generation)
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* The navigation state now, sent when it changed, once per answer: the end of the command's transition asks for it
|
|
40
|
+
* after the DidAppear it sends and before the transition's own events (`AppearanceDispatcher.endTransition`);
|
|
41
|
+
* otherwise [resolve] asks for it before the promise. A later call sends nothing, and nothing goes for a JavaScript
|
|
42
|
+
* that went. A failure of the step is logged and costs only the state.
|
|
43
|
+
*/
|
|
44
|
+
fun reportStateNow() {
|
|
45
|
+
if (stateReported) return
|
|
46
|
+
stateReported = true
|
|
47
|
+
if (!ReloadCoordinator.isCurrent(generation)) return
|
|
48
|
+
guarded("report the navigation state") { events.stateNow() }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the promise with [value]. For the commands that change what shows (`CommandContract.STATE`), the
|
|
53
|
+
* navigation state goes first, when it changed and the command's transition didn't send it already
|
|
54
|
+
* ([reportStateNow]), so JavaScript has it when its `await` goes on; `ANN.CommandCompleted` follows the promise,
|
|
55
|
+
* for the commands that show or dismiss. A failure of either is logged; the promise goes all the same.
|
|
38
56
|
*/
|
|
39
57
|
fun resolve(value: Any?) {
|
|
40
58
|
if (!canAnswer) return
|
|
41
59
|
isAnswered = true
|
|
42
|
-
if (commandName in CommandContract.STATE)
|
|
60
|
+
if (commandName in CommandContract.STATE) reportStateNow()
|
|
43
61
|
promise.resolve(value)
|
|
44
62
|
val commandId = commandId
|
|
45
63
|
if (commandName in CommandContract.COMPLETED && commandId != null) {
|
|
@@ -69,18 +87,15 @@ internal class CommandAnswer(
|
|
|
69
87
|
|
|
70
88
|
/** What goes to JavaScript around the answer of a command. Main thread only. */
|
|
71
89
|
internal interface AnswerEvents {
|
|
72
|
-
/**
|
|
73
|
-
fun
|
|
90
|
+
/** The answer's one state step ([CommandAnswer.reportStateNow]): the navigation state now, when it changed. */
|
|
91
|
+
fun stateNow()
|
|
74
92
|
|
|
75
93
|
/** After the promise resolved: `ANN.CommandCompleted`. */
|
|
76
94
|
fun commandCompleted(commandName: String, commandId: String, completionTime: Long)
|
|
77
95
|
|
|
78
96
|
/** Through the sender of the JavaScript that runs now. */
|
|
79
97
|
object Current : AnswerEvents {
|
|
80
|
-
override fun
|
|
81
|
-
val emitter = EventEmitter.current()
|
|
82
|
-
if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
|
|
83
|
-
}
|
|
98
|
+
override fun stateNow() = NavigationState.updateNow()
|
|
84
99
|
|
|
85
100
|
override fun commandCompleted(commandName: String, commandId: String, completionTime: Long) {
|
|
86
101
|
EventEmitter.current().emitCommandCompleted(commandName, commandId, completionTime)
|
|
@@ -9,7 +9,6 @@ import android.os.Build
|
|
|
9
9
|
import androidx.lifecycle.DefaultLifecycleObserver
|
|
10
10
|
import androidx.lifecycle.Lifecycle
|
|
11
11
|
import androidx.lifecycle.LifecycleOwner
|
|
12
|
-
import com.facebook.react.ReactApplication
|
|
13
12
|
import com.facebook.react.bridge.Arguments
|
|
14
13
|
import com.facebook.react.bridge.WritableMap
|
|
15
14
|
import dev.apexnative.navigation.react.events.EventContract
|
|
@@ -22,7 +21,8 @@ import java.lang.ref.WeakReference
|
|
|
22
21
|
* sensor (Android 11+), in degrees to a tenth as on iOS. Android has no public state of a closed device: closed is a
|
|
23
22
|
* window without a fold, or an angle under 90° ([valueOf]). No sensor and no fold: no hinge, `{}`. It runs while
|
|
24
23
|
* JavaScript has a hook mounted (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value
|
|
25
|
-
* is not sent twice.
|
|
24
|
+
* is not sent twice. The values go to the JavaScript that turned the updates on (`EventEmitter.current` then): after a
|
|
25
|
+
* reload, until the updates stop, they go nowhere. Main thread only, but [get].
|
|
26
26
|
*/
|
|
27
27
|
internal object Hinge {
|
|
28
28
|
/** A value of the hinge: its state, and its angle in degrees to a tenth; none while the state is unknown. */
|
|
@@ -33,6 +33,8 @@ internal object Hinge {
|
|
|
33
33
|
private const val CLOSED_BELOW = 90f
|
|
34
34
|
|
|
35
35
|
private var enabled = false
|
|
36
|
+
// The sender of the JavaScript that turned the updates on; none while they are off.
|
|
37
|
+
private var emitter: EventEmitter? = null
|
|
36
38
|
private var activity: WeakReference<Activity>? = null
|
|
37
39
|
private var folds: WindowFolds? = null
|
|
38
40
|
private var sensorManager: SensorManager? = null
|
|
@@ -63,9 +65,11 @@ internal object Hinge {
|
|
|
63
65
|
fun setEnabled(activity: Activity?, enabled: Boolean) {
|
|
64
66
|
this.enabled = enabled
|
|
65
67
|
if (!enabled) {
|
|
68
|
+
emitter = null
|
|
66
69
|
detach()
|
|
67
70
|
return
|
|
68
71
|
}
|
|
72
|
+
emitter = EventEmitter.current()
|
|
69
73
|
activity?.let(::attachTo)
|
|
70
74
|
update()
|
|
71
75
|
}
|
|
@@ -134,8 +138,7 @@ internal object Hinge {
|
|
|
134
138
|
val value = valueOf(folds.folds.firstOrNull(), sensor != null, angle)
|
|
135
139
|
if (value == last) return
|
|
136
140
|
last = value
|
|
137
|
-
|
|
138
|
-
EventEmitter(reactContext).emitHingeChanged(toMap(value))
|
|
141
|
+
emitter?.emitHingeChanged(toMap(value))
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
/**
|