@apexnative/navigation 1.0.0-alpha.2 → 1.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/android/CLAUDE.md +21 -2
- package/android/CORE.md +3 -1
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
- package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +76 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +74 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +41 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +29 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +2 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +14 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +148 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +24 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +51 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +105 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +121 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +19 -0
- package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
- package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
- package/docs-md/architecture/transitions.md +6 -0
- package/docs-md/containers/modals.md +4 -2
- package/docs-md/containers/overlays.md +3 -1
- package/docs-md/containers/side-menu.md +4 -2
- package/docs-md/containers/tabs.md +2 -0
- package/docs-md/flows/forms-in-a-modal.md +1 -1
- package/docs-md/help/glossary.md +1 -1
- package/docs-md/index.json +8 -0
- package/docs-md/screen-ui/back-and-gestures.md +6 -0
- package/docs-md/screen-ui/system-ui.md +1 -1
- package/lib/module/api/hooks/useNavigation.js +8 -3
- package/lib/module/api/hooks/useNavigation.js.map +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +10 -5
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/modals.d.ts +18 -13
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +6 -3
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/hooks/useNavigation.ts +8 -3
- package/src/interfaces/options/animations.ts +10 -5
- package/src/interfaces/options/modals.ts +18 -13
- package/src/interfaces/options/system.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-alpha.3](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2026-10-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **android:** system stack transitions, an opaque default screen background and predictive back ([#26](https://github.com/apexnative/navigation/issues/26)) ([9f28969](https://github.com/apexnative/navigation/commit/9f28969a30a8902c1512cd2a24c3ddf5439f4642))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **android:** center the back button and the bar buttons in the row ([#35](https://github.com/apexnative/navigation/issues/35)) ([c908971](https://github.com/apexnative/navigation/commit/c9089716ea4e3b3f4bcdc42b16e57134018d76c0))
|
|
14
|
+
* **android:** fade through on a tab switch, a new tab's first frame in place ([#29](https://github.com/apexnative/navigation/issues/29)) ([60d69b6](https://github.com/apexnative/navigation/commit/60d69b618a97c8f7da974fbdf6f427461960d672))
|
|
15
|
+
* **android:** let an overlay cover the status bar ([#27](https://github.com/apexnative/navigation/issues/27)) ([2632194](https://github.com/apexnative/navigation/commit/26321945cf0c457780b3eb7bba1f3d988dad4d96))
|
|
16
|
+
* **android:** match each modal presentation style to its Android equivalent ([#31](https://github.com/apexnative/navigation/issues/31)) ([54ab265](https://github.com/apexnative/navigation/commit/54ab2651f3139e869a2d99a43465b33bfb171414))
|
|
17
|
+
* **android:** move the top bar with its screen in transitions of their own ([#34](https://github.com/apexnative/navigation/issues/34)) ([4d12ee6](https://github.com/apexnative/navigation/commit/4d12ee6d6ec26a04e529bdef8b8111d20e5116aa))
|
|
18
|
+
* close the side menu as a pick in it changes the center ([#28](https://github.com/apexnative/navigation/issues/28)) ([c4b3e81](https://github.com/apexnative/navigation/commit/c4b3e81ba44d2edb6d93d3ddc906eca165d46e29))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Performance Improvements
|
|
22
|
+
|
|
23
|
+
* **android:** render the modals of a setRoot's layout together with its root ([#32](https://github.com/apexnative/navigation/issues/32)) ([cf3e1d9](https://github.com/apexnative/navigation/commit/cf3e1d98ff7c5d2a05aba6485203e6fe35a3423d))
|
|
24
|
+
|
|
3
25
|
## [1.0.0-alpha.2](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2026-10-22)
|
|
4
26
|
|
|
5
27
|
|
package/android/CLAUDE.md
CHANGED
|
@@ -73,8 +73,10 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
73
73
|
`ScreenFrame.kt` (bars' room, window insets, keyboard), `ScreenSafeArea.kt`, `ComponentPresenter.kt` (background),
|
|
74
74
|
`ScreenContentFactory.kt`, `ScreenFocus.kt`; `react/ReactView.kt`; `views/component/ComponentLayout.kt`.
|
|
75
75
|
- Stack (queue, commands, back): `viewcontrollers/stack/StackQueue.kt`, `StackCommands.kt`, `StackController.kt`,
|
|
76
|
-
`StackViews.kt`, `StackTransitions.kt`, `PreparedPushes.kt`; `views/stack/StackLayout.kt
|
|
77
|
-
`NavigationActivity.kt`, `viewcontrollers/navigator/BackCallbackState.kt
|
|
76
|
+
`StackViews.kt`, `StackTransitions.kt`, `PreparedPushes.kt`; `views/stack/StackLayout.kt`, `ScreenEdge.kt` (the edge
|
|
77
|
+
a moving screen extends); back: `NavigationActivity.kt`, `viewcontrollers/navigator/BackCallbackState.kt`; the
|
|
78
|
+
preview of a pop during the back gesture (Android 14+): `StackController.BackGesture`, `BackPreview.kt`,
|
|
79
|
+
`stackPoppedByBack` of the layouts.
|
|
78
80
|
- Stack FAB: `viewcontrollers/stack/fab/FabPresenter.kt`, `FabPlan.kt`, `FabPlacement.kt`; `views/stack/fab/`.
|
|
79
81
|
- Top bar (title, buttons, menus, large title): `viewcontrollers/stack/topbar/TopBarPresenter.kt`, `TopBarRoom.kt`,
|
|
80
82
|
`TopBarVisibility.kt`, `BarComponents.kt`, `ButtonPlaces.kt`, `LargeTitleCollapse.kt`, `BarRowTransition.kt`;
|
|
@@ -167,6 +169,9 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
167
169
|
`OverlayCoordinator.guarded`), which rejects the open answer and ends the record, or an exception ends the app and
|
|
168
170
|
the command never answers.
|
|
169
171
|
- A node makes its view in `createView`, never in its constructor.
|
|
172
|
+
- Whether a node keeps the status bar's room (its margin, its background band) goes through
|
|
173
|
+
`StatusBarOptions.isHiddenOrDrawBehind(node.isInOverlay)`: an overlay covers the whole window, and a rule that reads
|
|
174
|
+
`drawBehind` alone puts a light band over an overlay's dimmed backdrop.
|
|
170
175
|
- Only `SystemUiWindow` writes to the window. A presenter that changes a bar option calls `SystemUiOwner.invalidate`.
|
|
171
176
|
- The core names a module container only in `LayoutFactory`; elsewhere use a capability or `visibleChildren()`.
|
|
172
177
|
- A hidden stack keeps a `mergeOptions` per entry; applying it for the wrong entry paints the wrong screen's bar.
|
|
@@ -177,6 +182,20 @@ top-level folder. Main thread only, unless a line says otherwise.
|
|
|
177
182
|
offset that `ScrollEventListener.lastOffset` keeps, never to 0, or it comes back expanded over a scrolled list.
|
|
178
183
|
- A view's place in the window while a modal slides or a sheet is dragged comes from layout (`RestPlace`), never from
|
|
179
184
|
`getLocationInWindow`, which counts the translations of the animation.
|
|
185
|
+
- The bar measures to its children: a picture laid into it for a transition that is taller than the bar the incoming
|
|
186
|
+
screen gives holds the bar at that height to the end (the row's picture of a pop from a large title). What doesn't
|
|
187
|
+
stay in the bar rides with the leaving screen (`TopBarPresenter.pictureUnderRow`).
|
|
188
|
+
- A `Toolbar` centers its navigation button and its menu in `min(minimumHeight, height)` (`buttonGravity` is top):
|
|
189
|
+
the row's toolbars take the row's height as their minimum (`TitleBar`); 0 pins the buttons to the row's top.
|
|
190
|
+
- A screen's overlay is clipped to the screen, and a screen without a large title is laid out under the bar (its `top`
|
|
191
|
+
is the bar's height): a picture of the bar in its overlay doesn't show. The bar's picture in a transition of the
|
|
192
|
+
stack's own is drawn by the stack's view with the screen's transform (`StackLayout.barTravels`).
|
|
193
|
+
- A React screen without `layout.backgroundColor` is opaque with the theme's `android:colorBackground`
|
|
194
|
+
(`ComponentPresenter`): a push keeps the covered screen under the new one until it ends, and a transparent screen
|
|
195
|
+
shows it. Only an explicit `'transparent'` / `null` (overlays, see-through modals) lets it through.
|
|
196
|
+
- The back gesture's preview shows the screen under the top outside any operation of the queue: it may only reveal
|
|
197
|
+
it and put it back, and its release goes through `StackQueue.cut(gesture = true)`. A command that comes meanwhile
|
|
198
|
+
stops it through `StackDisplay.stop`, like any transition it takes over.
|
|
180
199
|
- Without edge to edge the window resizes for the keyboard by the manifest's soft input mode, so
|
|
181
200
|
`layout.adjustResize: false` goes through the system UI owner's keyboard group (`SystemUiWindow.setKeyboardCovers`),
|
|
182
201
|
not through the screen's padding.
|
package/android/CORE.md
CHANGED
|
@@ -91,7 +91,9 @@ The events go from the main thread, synchronously, through `EventEmitter.current
|
|
|
91
91
|
current root in the root layer, transparent (`RootLayerViews.place`), so its React roots start while the old root keeps
|
|
92
92
|
the touches. It waits (`RenderWait`, by `animations.setRoot.enter.waitForRender`), then fades through or plays its own
|
|
93
93
|
animation, the old root is destroyed with its `ComponentDidDisappear`, `NativeScreenReleased` and `SlotDestroyed`, and
|
|
94
|
-
the end steps and answer are the same. The layout's modals and overlays show after it, one by one, before the answer
|
|
94
|
+
the end steps and answer are the same. The layout's modals and overlays show after it, one by one, before the answer;
|
|
95
|
+
its modals are made with the root and render unseen in the modals layer, held (`ModalCoordinator.hold`), so each
|
|
96
|
+
shows in its turn without a wait of its own.
|
|
95
97
|
The last `setRoot` wins: an older one is rejected (`ErrorTexts.setRootReplaced`) or finished at once.
|
|
96
98
|
- **`showModal`** (`Navigator.showModal`, `ModalCoordinator.show`). One `ModalRecord` per modal; it joins the modals
|
|
97
99
|
layer unseen (`ModalViews`), waits with its own `RenderWait`, then slides in (or opens as a `ModalSheet`). It shows
|
|
@@ -11,6 +11,7 @@ import android.view.View
|
|
|
11
11
|
import android.view.ViewGroup
|
|
12
12
|
import android.view.ViewTreeObserver
|
|
13
13
|
import android.widget.EditText
|
|
14
|
+
import androidx.activity.BackEventCompat
|
|
14
15
|
import androidx.activity.OnBackPressedCallback
|
|
15
16
|
import androidx.activity.SystemBarStyle
|
|
16
17
|
import androidx.activity.enableEdgeToEdge as enableActivityEdgeToEdge
|
|
@@ -37,6 +38,7 @@ import dev.apexnative.navigation.utils.MaterialTheme
|
|
|
37
38
|
import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
|
|
38
39
|
import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
|
|
39
40
|
import dev.apexnative.navigation.viewcontrollers.navigator.Navigators
|
|
41
|
+
import dev.apexnative.navigation.viewcontrollers.stack.StackController
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* The activity of an app with the navigation: its content holds, bottom to top, the copy of the splash and the layers
|
|
@@ -67,10 +69,36 @@ open class NavigationActivity : AppCompatActivity(), DefaultHardwareBackBtnHandl
|
|
|
67
69
|
private val reloadBroadcast = ReloadBroadcast(this) { reloadFromDeveloperTools() }
|
|
68
70
|
|
|
69
71
|
// Enabled while back has something to do in the app (BackCallbackState); otherwise the system does its own back.
|
|
72
|
+
// From Android 14, where the app hears the progress of the back gesture, a gesture that would pop a stack with the
|
|
73
|
+
// default transition previews the pop as the system previews back between activities (`StackController
|
|
74
|
+
// .BackGesture`); JavaScript's listeners hear back first, so with them there is no preview.
|
|
70
75
|
private val backCallback = object : OnBackPressedCallback(false) {
|
|
71
|
-
override fun
|
|
76
|
+
override fun handleOnBackStarted(backEvent: BackEventCompat) {
|
|
77
|
+
backGesture = null
|
|
78
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || BackCallbackState.hasJsListeners) return
|
|
79
|
+
val stack = navigator?.stackPoppedByBack() ?: return
|
|
80
|
+
backGesture = stack.startBackGesture(backEvent.swipeEdge == BackEventCompat.EDGE_LEFT, backEvent.touchY)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
|
|
84
|
+
backGesture?.progress(backEvent.progress, backEvent.touchY)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun handleOnBackCancelled() {
|
|
88
|
+
backGesture?.cancel()
|
|
89
|
+
backGesture = null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun handleOnBackPressed() {
|
|
93
|
+
val gesture = backGesture
|
|
94
|
+
backGesture = null
|
|
95
|
+
if (gesture?.commit() == true) BackCallbackState.update() else passBack()
|
|
96
|
+
}
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
// The back gesture that previews a pop now; none.
|
|
100
|
+
private var backGesture: StackController.BackGesture? = null
|
|
101
|
+
|
|
74
102
|
// Before each frame of the window: what back does may have changed, and the system decides at the start of a
|
|
75
103
|
// gesture whether the app takes it.
|
|
76
104
|
private val backBeforeFrame = ViewTreeObserver.OnPreDrawListener {
|
|
@@ -22,9 +22,13 @@ class StatusBarOptions {
|
|
|
22
22
|
@JvmField var hideWithTopBar: Bool = NullBool()
|
|
23
23
|
@JvmField var blur: Bool = NullBool()
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The status bar is hidden, or the screen is drawn under it: the screen keeps no room for it. Without `drawBehind`, a
|
|
27
|
+
* screen or a layout in an overlay ([inOverlay]) is drawn under it: an overlay covers the whole window, its dimmed
|
|
28
|
+
* backdrop the status bar too.
|
|
29
|
+
*/
|
|
30
|
+
fun isHiddenOrDrawBehind(inOverlay: Boolean): Boolean =
|
|
31
|
+
visible.isFalse || if (drawBehind.hasValue()) drawBehind.isTrue else inOverlay
|
|
28
32
|
|
|
29
33
|
fun hasValue(): Boolean = visible.hasValue() || style.hasValue() || backgroundColor.hasValue() ||
|
|
30
34
|
drawBehind.hasValue() || translucent.hasValue() || animate.hasValue() || hideWithTopBar.hasValue() ||
|
|
@@ -2,24 +2,25 @@ package dev.apexnative.navigation.viewcontrollers.arrangement
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import dev.apexnative.navigation.options.Options
|
|
5
|
+
import dev.apexnative.navigation.viewcontrollers.component.ComponentPresenter
|
|
5
6
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
6
7
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The presenter of the secondary screen of a pair ([ArrangementController]). The status bar, the navigation bar and
|
|
10
|
-
* the orientation are the primary's, as the top bar is: the secondary applies only its background
|
|
11
|
-
* front it gives them back to the primary.
|
|
11
|
+
* the orientation are the primary's, as the top bar is: the secondary applies only its background (the theme's
|
|
12
|
+
* without `layout.backgroundColor`, as any React screen), and brought to the front it gives them back to the primary.
|
|
12
13
|
*/
|
|
13
14
|
internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Options) :
|
|
14
15
|
Presenter(activity, defaultOptions) {
|
|
15
16
|
|
|
16
17
|
override fun applyOptions(view: ViewController<*>, options: Options) =
|
|
17
|
-
|
|
18
|
+
applyBackground(view, options.copy().withDefaultOptions(defaultOptions))
|
|
18
19
|
|
|
19
20
|
override fun mergeOptions(viewController: ViewController<*>, options: Options) {
|
|
20
21
|
if (!options.layout.backgroundColor.hasValue()) return
|
|
21
22
|
val merged = viewController.resolveCurrentOptions().copy().mergeWith(options)
|
|
22
|
-
|
|
23
|
+
applyBackground(viewController, merged.withDefaultOptions(defaultOptions))
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
override fun onViewBroughtToFront(viewController: ViewController<*>, options: Options) {
|
|
@@ -27,5 +28,12 @@ internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Opti
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
override fun onConfigurationChanged(controller: ViewController<*>, options: Options) =
|
|
30
|
-
|
|
31
|
+
applyBackground(controller, options.copy().withDefaultOptions(defaultOptions))
|
|
32
|
+
|
|
33
|
+
// `layout.backgroundColor`, else the theme's background (`ComponentPresenter.themeBackgroundOf`).
|
|
34
|
+
private fun applyBackground(view: ViewController<*>, options: Options) {
|
|
35
|
+
if (options.layout.backgroundColor.hasValue()) return applyBackgroundColor(view, options)
|
|
36
|
+
val color = ComponentPresenter.themeBackgroundOf(activity) ?: return
|
|
37
|
+
view.peekView()?.setBackgroundColor(color)
|
|
38
|
+
}
|
|
31
39
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
package dev.apexnative.navigation.viewcontrollers.bottomtabs
|
|
2
2
|
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.AnimatorSet
|
|
3
6
|
import android.app.Activity
|
|
4
7
|
import android.graphics.Color
|
|
5
8
|
import android.util.Log
|
|
@@ -33,6 +36,7 @@ import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
|
|
|
33
36
|
import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
|
|
34
37
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
35
38
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
39
|
+
import dev.apexnative.navigation.views.animations.FadeThrough
|
|
36
40
|
import dev.apexnative.navigation.views.bottomtabs.BottomTabItem
|
|
37
41
|
import dev.apexnative.navigation.views.bottomtabs.BottomTabsLayout
|
|
38
42
|
import dev.apexnative.navigation.views.bottomtabs.CustomBottomTabItemView
|
|
@@ -68,6 +72,8 @@ class BottomTabsController(
|
|
|
68
72
|
private var cells: List<CustomBottomTabItemView> = emptyList()
|
|
69
73
|
// `ANN.BottomTabSelected` of a press that waits for the new tab's screen to appear.
|
|
70
74
|
private var pendingSelected: (() -> Unit)? = null
|
|
75
|
+
// Ends the fade through of a switch still running, with its events.
|
|
76
|
+
private var endSwitch: (() -> Unit)? = null
|
|
71
77
|
// The bar goes with a stack's transition: the options of the new top don't move it themselves.
|
|
72
78
|
private var riding = false
|
|
73
79
|
private var restAttached = false
|
|
@@ -150,8 +156,11 @@ class BottomTabsController(
|
|
|
150
156
|
/** The view of [tab] joins, or is there already: [visible], or invisible but laid out. */
|
|
151
157
|
fun attach(tab: ViewController<*>, visible: Boolean)
|
|
152
158
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The view of [shown] becomes visible and that of [hidden] invisible: at once, or with Material's fade through
|
|
161
|
+
* when [animated]; then [onEnd]. The function returned ends a switch still running at once, [onEnd] included.
|
|
162
|
+
*/
|
|
163
|
+
fun switch(shown: ViewController<*>, hidden: ViewController<*>, animated: Boolean, onEnd: () -> Unit): () -> Unit
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
/** The views of the tabs; none before the tabs' view is made. */
|
|
@@ -166,9 +175,43 @@ class BottomTabsController(
|
|
|
166
175
|
tab.view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
override fun switch(
|
|
170
|
-
shown
|
|
171
|
-
hidden
|
|
178
|
+
override fun switch(
|
|
179
|
+
shown: ViewController<*>,
|
|
180
|
+
hidden: ViewController<*>,
|
|
181
|
+
animated: Boolean,
|
|
182
|
+
onEnd: () -> Unit,
|
|
183
|
+
): () -> Unit {
|
|
184
|
+
val coming = shown.view
|
|
185
|
+
coming.visibility = View.VISIBLE
|
|
186
|
+
val going = hidden.peekView()
|
|
187
|
+
if (!animated || going == null) {
|
|
188
|
+
going?.visibility = View.INVISIBLE
|
|
189
|
+
onEnd()
|
|
190
|
+
return {}
|
|
191
|
+
}
|
|
192
|
+
val exit = FadeThrough.exit(going)
|
|
193
|
+
// Faded out, the tab that goes takes no more touches and no accessibility focus.
|
|
194
|
+
exit.addListener(object : AnimatorListenerAdapter() {
|
|
195
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
196
|
+
going.visibility = View.INVISIBLE
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
val set = AnimatorSet()
|
|
200
|
+
set.playTogether(exit, FadeThrough.enter(coming))
|
|
201
|
+
set.addListener(object : AnimatorListenerAdapter() {
|
|
202
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
203
|
+
set.removeAllListeners()
|
|
204
|
+
// The tab that went keeps its look for its next selection; only its visibility hides it.
|
|
205
|
+
going.visibility = View.INVISIBLE
|
|
206
|
+
going.alpha = 1f
|
|
207
|
+
coming.alpha = 1f
|
|
208
|
+
coming.scaleX = 1f
|
|
209
|
+
coming.scaleY = 1f
|
|
210
|
+
onEnd()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
set.start()
|
|
214
|
+
return set::end
|
|
172
215
|
}
|
|
173
216
|
}
|
|
174
217
|
|
|
@@ -302,10 +345,9 @@ class BottomTabsController(
|
|
|
302
345
|
}
|
|
303
346
|
|
|
304
347
|
/**
|
|
305
|
-
* Selects the tab at [index]:
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* and the state at once. The history of back follows.
|
|
348
|
+
* Selects the tab at [index]: the bar shows it at once; on screen the tab it left fades through to it (Material's
|
|
349
|
+
* fade through), off screen one container becomes invisible and the other visible at once; then [switched]. The
|
|
350
|
+
* history of back follows.
|
|
309
351
|
*/
|
|
310
352
|
private fun select(index: Int, how: How) {
|
|
311
353
|
if (index == selectedIndex || index !in tabs.indices || isDestroyed) return
|
|
@@ -328,10 +370,27 @@ class BottomTabsController(
|
|
|
328
370
|
// A layer that waits for the tabs waits for the tab they show now.
|
|
329
371
|
navigator?.shownScreensChanged(this)
|
|
330
372
|
if (onScreen) AppearanceDispatcher.beginTransition(listOf(shown))
|
|
331
|
-
views
|
|
373
|
+
if (views == null) return switched(index, previous, how, onScreen, hasViews = false)
|
|
374
|
+
endSwitch = views.switch(shown, left, animated = onScreen) {
|
|
375
|
+
endSwitch = null
|
|
376
|
+
if (!isDestroyed) switched(index, previous, how, onScreen, hasViews = true)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The switch to the tab at [index] from [previous] ended: what went disappears, what came appears — unless the tabs
|
|
382
|
+
* stopped showing during the fade (a modal over them): it appears when they show again; then for a press
|
|
383
|
+
* `ANN.BottomTabSelected` right after the new screen's DidAppear and the left tab's `popToRootOnBlur`, with the state
|
|
384
|
+
* as one update after them; from code and by back, `popToRootOnBlur` and the state at once. [onScreen]: the switch
|
|
385
|
+
* started on screen.
|
|
386
|
+
*/
|
|
387
|
+
private fun switched(index: Int, previous: Int, how: How, onScreen: Boolean, hasViews: Boolean) {
|
|
388
|
+
val left = tabs[previous]
|
|
389
|
+
val shown = tabs[index]
|
|
332
390
|
if (onScreen) {
|
|
333
|
-
|
|
334
|
-
|
|
391
|
+
val showing = if (isOnScreen) listOf(shown) else emptyList()
|
|
392
|
+
AppearanceDispatcher.endTransition(listOf(left), showing, null, null, null, reportsState = false)
|
|
393
|
+
} else if (hasViews) {
|
|
335
394
|
// Not on screen: the bar takes the new tab's options as they are, without appearance events.
|
|
336
395
|
val options = resolveShownOptions(DefaultOptions.current)
|
|
337
396
|
bar.applyBar(options.bottomTabs, directionOf(options))
|
|
@@ -358,8 +417,10 @@ class BottomTabsController(
|
|
|
358
417
|
if (screen == null) finish() else AppearanceDispatcher.afterDidAppear(screen, finish)
|
|
359
418
|
}
|
|
360
419
|
|
|
361
|
-
// A press's `ANN.BottomTabSelected` still waiting goes before anything
|
|
420
|
+
// A switch still running ends at once and a press's `ANN.BottomTabSelected` still waiting goes, before anything
|
|
421
|
+
// newer on these tabs.
|
|
362
422
|
private fun flushPendingSelected() {
|
|
423
|
+
endSwitch?.invoke()
|
|
363
424
|
pendingSelected?.invoke()
|
|
364
425
|
}
|
|
365
426
|
|
|
@@ -621,6 +682,8 @@ class BottomTabsController(
|
|
|
621
682
|
if (isDestroyed) return
|
|
622
683
|
pendingSelected = null
|
|
623
684
|
super.destroy()
|
|
685
|
+
// A switch still running ends with its views at rest, without events: the tabs are gone.
|
|
686
|
+
endSwitch?.invoke()
|
|
624
687
|
peekView()?.let { layout ->
|
|
625
688
|
layout.bar.destroyCustomItemViews()
|
|
626
689
|
layout.row?.let(CustomTabRowPlacer::forget)
|
|
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
|
|
|
6
6
|
import android.graphics.drawable.Drawable
|
|
7
7
|
import android.graphics.drawable.LayerDrawable
|
|
8
8
|
import android.view.View
|
|
9
|
+
import com.google.android.material.color.MaterialColors
|
|
9
10
|
import dev.apexnative.navigation.options.DefaultOptions
|
|
10
11
|
import dev.apexnative.navigation.options.Options
|
|
11
12
|
import dev.apexnative.navigation.options.params.ThemeColour
|
|
@@ -15,9 +16,9 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
|
15
16
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor
|
|
19
|
-
* out of the status bar's height where the status bar shows over
|
|
20
|
-
* `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
|
|
19
|
+
* The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor` (a
|
|
20
|
+
* React screen without it the theme's background), kept out of the status bar's height where the status bar shows over
|
|
21
|
+
* it, and the React content takes `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
|
|
21
22
|
* (`mergeOptions` step 4). It applies as the screen appears, on `mergeOptions` of what it covers, and again with new
|
|
22
23
|
* default options or after a light/dark switch from the resolved options it is given, which it never writes. Each
|
|
23
24
|
* appearance tells the activity's system UI owner, which follows the screen on top. Main thread only.
|
|
@@ -49,8 +50,8 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
|
|
|
49
50
|
applyBackground(controller, options)
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view,
|
|
53
|
-
*
|
|
53
|
+
* The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view, without it the
|
|
54
|
+
* theme's background for a React screen ([defaultColorOf]); `layout.componentBackgroundColor` on its React content.
|
|
54
55
|
*/
|
|
55
56
|
fun applyBackground(screen: ViewController<*>, options: Options) {
|
|
56
57
|
val view = screen.peekView() ?: return
|
|
@@ -59,11 +60,11 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
private fun screenBackground(screen: ViewController<*>, options: Options): Drawable? {
|
|
62
|
-
val color = colorOf(options.layout.backgroundColor) ?: return null
|
|
63
|
+
val color = colorOf(options.layout.backgroundColor) ?: defaultColorOf(screen) ?: return null
|
|
63
64
|
val frame = frameOf(screen) ?: return ColorDrawable(color)
|
|
64
65
|
val band = ScreenSafeArea.backgroundBand(
|
|
65
66
|
statusBarHeight = frame.statusBarHeight,
|
|
66
|
-
hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
|
|
67
|
+
hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(screen.isInOverlay),
|
|
67
68
|
inSideMenu = screen.isInSideMenu,
|
|
68
69
|
inSheet = screen.isInSheet,
|
|
69
70
|
inPair = frame.isInPair,
|
|
@@ -79,6 +80,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
|
|
|
79
80
|
private fun colorOf(colour: ThemeColour): Int? =
|
|
80
81
|
if (colour.hasValue()) colour.get(activity, Color.TRANSPARENT) ?: Color.TRANSPARENT else null
|
|
81
82
|
|
|
83
|
+
// A React screen is opaque without a color of its own, as iOS gives it the system background: the screen under it
|
|
84
|
+
// never shows through, also while a push slides it aside. The app theme's `android:colorBackground`, read now, so a
|
|
85
|
+
// light/dark switch takes the new mode's. A screen of a sheet shows the sheet's surface; a native screen paints its
|
|
86
|
+
// own view.
|
|
87
|
+
private fun defaultColorOf(screen: ViewController<*>): Int? {
|
|
88
|
+
if (screen !is ComponentViewController || screen.isInSheet) return null
|
|
89
|
+
return themeBackgroundOf(activity)
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
private fun frameOf(screen: ViewController<*>): ScreenFrame? = when (screen) {
|
|
83
93
|
is ComponentViewController -> screen.frame
|
|
84
94
|
is ExternalComponentViewController -> screen.frame
|
|
@@ -87,4 +97,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
|
|
|
87
97
|
|
|
88
98
|
// The view under the React content: the React root, or the container of a slot screen. A native screen has none.
|
|
89
99
|
private fun contentOf(screen: ViewController<*>): View? = (screen as? ComponentViewController)?.content?.asView()
|
|
100
|
+
|
|
101
|
+
internal companion object {
|
|
102
|
+
/**
|
|
103
|
+
* The background of a React screen without `layout.backgroundColor`: `android:colorBackground` of [activity]'s
|
|
104
|
+
* theme now. None for an activity without a theme (the unit tests' fakes).
|
|
105
|
+
*/
|
|
106
|
+
fun themeBackgroundOf(activity: Activity): Int? {
|
|
107
|
+
if (activity.theme == null) return null
|
|
108
|
+
return MaterialColors.getColor(activity, android.R.attr.colorBackground, Color.WHITE)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt
CHANGED
|
@@ -143,7 +143,7 @@ internal class ScreenFrame(
|
|
|
143
143
|
val parent = node.parentController
|
|
144
144
|
val statusBar = ScreenSafeArea.statusBarRoom(
|
|
145
145
|
statusBarHeight = statusBarHeight,
|
|
146
|
-
hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
|
|
146
|
+
hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(node.isInOverlay),
|
|
147
147
|
inSheet = node.isInSheet,
|
|
148
148
|
inSideMenu = node.isInSideMenu,
|
|
149
149
|
keptAround = parent?.keepsStatusBarRoomOf(node) == true,
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
package dev.apexnative.navigation.viewcontrollers.modal
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import kotlin.math.cos
|
|
3
|
+
import dev.apexnative.navigation.views.animations.Curves
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
* The default animations of a modal on Android, as values:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The default animations of a modal over the whole screen on Android, as values: the system's motion of a window that
|
|
7
|
+
* opens over the one below it (`activity_translucent_open_enter` and `activity_translucent_close_exit` of the
|
|
8
|
+
* platform, which Android 16 plays for a translucent activity). The modal comes up from the bottom of the window by the
|
|
9
|
+
* window's height (in a split screen or a window, the window's, not the display's) in [SHOW_DURATION], slowing down at
|
|
10
|
+
* the end; it goes down in [DISMISS_DURATION], speeding up. It doesn't fade: the screen below is seen only around it,
|
|
11
|
+
* as under a translucent activity. A sheet moves only by the Material sheet's own motion. Pure arithmetic.
|
|
11
12
|
*/
|
|
12
13
|
internal object ModalAnimations {
|
|
13
|
-
/** How long the default show
|
|
14
|
-
const val
|
|
14
|
+
/** How long the default show lasts (ms): `activity_translucent_open_enter`. */
|
|
15
|
+
const val SHOW_DURATION = 300L
|
|
16
|
+
|
|
17
|
+
/** How long the default dismissal lasts (ms): `activity_translucent_close_exit`. */
|
|
18
|
+
const val DISMISS_DURATION = 250L
|
|
19
|
+
|
|
20
|
+
// `linear_out_slow_in` of the platform: the path (0, 0) (0, 0) (0.2, 1) (1, 1).
|
|
21
|
+
private val linearOutSlowIn = Curves.CubicBezier(0.0, 0.0, 0.2, 1.0)
|
|
15
22
|
|
|
16
23
|
/** How far a modal comes up and goes down: the window's height ([windowHeight]), or the display's while it is 0. */
|
|
17
24
|
fun distance(windowHeight: Int, displayHeight: Int): Float =
|
|
18
25
|
(if (windowHeight > 0) windowHeight else displayHeight).toFloat()
|
|
19
26
|
|
|
20
|
-
/** The curve of the show:
|
|
21
|
-
fun
|
|
27
|
+
/** The curve of the show: the platform's `linear_out_slow_in`, fast at first and slow at the end. */
|
|
28
|
+
fun linearOutSlowIn(progress: Float): Float = linearOutSlowIn.getInterpolation(progress)
|
|
22
29
|
|
|
23
|
-
/** The curve of the dismissal:
|
|
24
|
-
fun
|
|
30
|
+
/** The curve of the dismissal: the platform's `accelerate_quad`, p². */
|
|
31
|
+
fun accelerate(progress: Float): Float = progress.coerceIn(0f, 1f).let { it * it }
|
|
25
32
|
|
|
26
|
-
/** Where a modal is at [progress] (0 … 1, in time) of its show over [distance]: its translation
|
|
27
|
-
fun show(progress: Float, distance: Float):
|
|
28
|
-
val value = decelerate(progress)
|
|
29
|
-
return Frame(translationY = distance * (1f - value), alpha = value)
|
|
30
|
-
}
|
|
33
|
+
/** Where a modal is at [progress] (0 … 1, in time) of its show over [distance]: its translation. */
|
|
34
|
+
fun show(progress: Float, distance: Float): Float = distance * (1f - linearOutSlowIn(progress))
|
|
31
35
|
|
|
32
36
|
/** Where a modal is at [progress] of its dismissal over [distance], from where it rests. */
|
|
33
|
-
fun dismiss(progress: Float, distance: Float):
|
|
34
|
-
val value = accelerateDecelerate(progress)
|
|
35
|
-
return Frame(translationY = distance * value, alpha = 1f - value)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A modal's place in its animation. */
|
|
39
|
-
data class Frame(val translationY: Float, val alpha: Float)
|
|
37
|
+
fun dismiss(progress: Float, distance: Float): Float = distance * accelerate(progress)
|
|
40
38
|
}
|