@apexnative/navigation 1.0.0-alpha.8 → 1.0.0-alpha.9
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/src/main/java/dev/apexnative/navigation/options/SideMenuOptions.kt +6 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +36 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +35 -3
- package/android/src/main/java/dev/apexnative/navigation/views/sidemenu/SideMenuView.kt +12 -3
- package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +34 -4
- package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +6 -3
- package/docs-md/containers/side-menu.md +15 -0
- package/docs-md/containers/tabs.md +1 -1
- package/docs-md/index.json +4 -0
- package/ios/CLAUDE.md +4 -1
- package/ios/CORE.md +2 -2
- package/ios/Core/ANNLayoutProtocol.h +5 -5
- package/ios/Core/ANNParking.h +5 -4
- package/ios/Core/ANNReloadCoordinator.mm +1 -4
- package/ios/Core/ANNRenderWait.h +5 -5
- package/ios/Core/ANNRenderWait.mm +24 -39
- package/ios/Core/ApexNavigation.mm +1 -4
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
- package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +1 -1
- package/ios/Layouts/Root/ANNRootCoordinator.mm +1 -1
- package/ios/Layouts/Screen/ANNComponentViewController.h +9 -15
- package/ios/Layouts/Screen/ANNComponentViewController.mm +6 -42
- package/ios/Layouts/Screen/ANNScreenPlace.h +12 -0
- package/ios/Layouts/Screen/ANNScreenPlace.mm +46 -16
- package/ios/Layouts/SideMenu/ANNSideMenuTypes.h +8 -0
- package/ios/Layouts/SideMenu/ANNSideMenuViewController.mm +83 -14
- package/ios/Layouts/Stack/ANNStackController.mm +5 -2
- package/ios/Layouts/Stack/ANNStackQueue.h +2 -1
- package/ios/Layouts/Tabs/ANNBottomTabsController.h +3 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +222 -51
- package/ios/Options/ANNSideMenuOptions.h +2 -0
- package/ios/Options/ANNSideMenuOptions.mm +2 -0
- package/ios/React/ANNSafeAreaViewComponentView.mm +2 -2
- package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -1
- package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/sideMenu.d.ts +16 -0
- package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/interfaces/options/bottomTabs.ts +2 -1
- package/src/interfaces/options/sideMenu.ts +17 -0
- package/ios/Layouts/Tabs/ANNTabsPreloadWindow.h +0 -27
- package/ios/Layouts/Tabs/ANNTabsPreloadWindow.mm +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-alpha.9](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2026-10-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* side menus can be created after the center or at their first open ([#67](https://github.com/apexnative/navigation/issues/67)) ([a4a0ecf](https://github.com/apexnative/navigation/commit/a4a0ecfdc04ccd13d2c96b5799b45e510113a7e8))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **android:** the room beside a moving screen goes on with what its edge shows ([#64](https://github.com/apexnative/navigation/issues/64)) ([eb0936f](https://github.com/apexnative/navigation/commit/eb0936f15d8d157d31621917920814df2c725ff1))
|
|
14
|
+
* **ios:** tabs not shown yet render their screens before they show ([#68](https://github.com/apexnative/navigation/issues/68)) ([cf34a09](https://github.com/apexnative/navigation/commit/cf34a09a772865a8e3ff377b053c968e306f4972))
|
|
15
|
+
|
|
3
16
|
## [1.0.0-alpha.8](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2026-10-29)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -20,16 +20,18 @@ class SideMenuOptions {
|
|
|
20
20
|
@JvmField var right = SideMenuSideOptions()
|
|
21
21
|
@JvmField var openGestureMode: Text = NullText()
|
|
22
22
|
@JvmField var animationType: Text = NullText()
|
|
23
|
+
@JvmField var menusAttachMode: Text = NullText()
|
|
23
24
|
@JvmField var testID: Text = NullText()
|
|
24
25
|
|
|
25
26
|
fun hasValue(): Boolean = left.hasValue() || right.hasValue() || openGestureMode.hasValue() ||
|
|
26
|
-
animationType.hasValue() || testID.hasValue()
|
|
27
|
+
animationType.hasValue() || menusAttachMode.hasValue() || testID.hasValue()
|
|
27
28
|
|
|
28
29
|
fun mergeWith(other: SideMenuOptions) {
|
|
29
30
|
left.mergeWith(other.left)
|
|
30
31
|
right.mergeWith(other.right)
|
|
31
32
|
if (other.openGestureMode.hasValue()) openGestureMode = other.openGestureMode
|
|
32
33
|
if (other.animationType.hasValue()) animationType = other.animationType
|
|
34
|
+
if (other.menusAttachMode.hasValue()) menusAttachMode = other.menusAttachMode
|
|
33
35
|
if (other.testID.hasValue()) testID = other.testID
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -38,6 +40,7 @@ class SideMenuOptions {
|
|
|
38
40
|
right.mergeWithDefault(defaults.right)
|
|
39
41
|
if (!openGestureMode.hasValue()) openGestureMode = defaults.openGestureMode
|
|
40
42
|
if (!animationType.hasValue()) animationType = defaults.animationType
|
|
43
|
+
if (!menusAttachMode.hasValue()) menusAttachMode = defaults.menusAttachMode
|
|
41
44
|
if (!testID.hasValue()) testID = defaults.testID
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -46,6 +49,7 @@ class SideMenuOptions {
|
|
|
46
49
|
companion object {
|
|
47
50
|
private val OPEN_GESTURE_MODES = setOf("bezel", "entireScreen")
|
|
48
51
|
private val ANIMATION_TYPES = setOf("parallax", "slide", "slide-and-scale", "door")
|
|
52
|
+
private val MENUS_ATTACH_MODES = setOf("together", "afterCenter", "onOpen")
|
|
49
53
|
|
|
50
54
|
@JvmStatic
|
|
51
55
|
fun parse(map: ReadableMap?): SideMenuOptions {
|
|
@@ -55,6 +59,7 @@ class SideMenuOptions {
|
|
|
55
59
|
options.right = map.group("right") { SideMenuSideOptions.parse(it) }
|
|
56
60
|
options.openGestureMode = TextParser.parse(map, "openGestureMode", OPEN_GESTURE_MODES)
|
|
57
61
|
options.animationType = TextParser.parse(map, "animationType", ANIMATION_TYPES)
|
|
62
|
+
options.menusAttachMode = TextParser.parse(map, "menusAttachMode", MENUS_ATTACH_MODES)
|
|
58
63
|
options.testID = TextParser.parse(map, "testID")
|
|
59
64
|
return options
|
|
60
65
|
}
|
|
@@ -29,7 +29,8 @@ import dev.apexnative.navigation.views.sidemenu.SideMenuView
|
|
|
29
29
|
* - The current child is the open menu's node, else the center ([SideMenuMotion.openSide]: from the end of the
|
|
30
30
|
* opening to the end of the closing); the state, the options and back follow it.
|
|
31
31
|
* - The center and the open menu show with the side menu ([visibleChildren]); a wait for the side menu to render
|
|
32
|
-
* waits for the center ([waitedChildren]); the menus' views
|
|
32
|
+
* waits for the center ([waitedChildren]); the menus' views join the drawer as `sideMenu.menusAttachMode` says
|
|
33
|
+
* ([attached]): with the side menu's view, once it first appeared, or each as it first starts to show.
|
|
33
34
|
* - Its options ([SideMenuPresenter]): from the resolution of [resolveShownOptions] at creation, at every appearance
|
|
34
35
|
* of a screen inside, and for the fields a merge names; `sideMenu.<side>.visible` acts once, from a merge or from the
|
|
35
36
|
* layout at the first show. What goes on up goes without `sideMenu`: the nearest side menu acts.
|
|
@@ -64,6 +65,12 @@ internal class SideMenuController(
|
|
|
64
65
|
// From the end of its appearance's start until it disappears: the drawer's motion tells the menu screens then.
|
|
65
66
|
private var appeared = false
|
|
66
67
|
private var firstShown = false
|
|
68
|
+
private var firstAppeared = false
|
|
69
|
+
|
|
70
|
+
private val attachMode = SideMenuRules.attachModeOf(initialOptions.sideMenu, DefaultOptions.current.sideMenu)
|
|
71
|
+
|
|
72
|
+
/** The menus whose nodes' views are in the drawer, or join it as it is made. */
|
|
73
|
+
internal val attached = HashSet<Side>(2)
|
|
67
74
|
// Its children go first as it is destroyed: nothing reacts to them, nor to the drawer, from the start.
|
|
68
75
|
private var destroying = false
|
|
69
76
|
|
|
@@ -72,6 +79,7 @@ internal class SideMenuController(
|
|
|
72
79
|
|
|
73
80
|
init {
|
|
74
81
|
getChildControllers().forEach { it.parentController = this }
|
|
82
|
+
SideMenuRules.attachedAtCreation(attachMode).filter { childOf(it) != null }.forEach { attached += it }
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
override fun getChildControllers(): List<ViewController<*>> = listOfNotNull(center, left, right)
|
|
@@ -88,13 +96,16 @@ internal class SideMenuController(
|
|
|
88
96
|
|
|
89
97
|
private fun childOf(side: Side): ViewController<*>? = if (side == Side.Left) left else right
|
|
90
98
|
|
|
91
|
-
// The drawer and its parts, each node
|
|
92
|
-
// view existed is open in it from the start; the drawer is silent before its first layout.
|
|
99
|
+
// The drawer and its parts, the view of each node attached so far in its part; the creation's options. An open menu
|
|
100
|
+
// recorded before the view existed is open in it from the start; the drawer is silent before its first layout.
|
|
93
101
|
override fun createView(): SideMenuView {
|
|
94
102
|
val view = SideMenuView(activity)
|
|
95
103
|
view.setCenter(center.view)
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
for (side in Side.entries) {
|
|
105
|
+
val child = childOf(side) ?: continue
|
|
106
|
+
view.addPart(side)
|
|
107
|
+
if (side in attached) view.fillPart(side, child.view)
|
|
108
|
+
}
|
|
98
109
|
menus.applyOnCreation(view, resolveShownOptions(DefaultOptions.current))
|
|
99
110
|
motion.openSide?.let { view.open(it, false) }
|
|
100
111
|
view.follow(motion)
|
|
@@ -172,6 +183,14 @@ internal class SideMenuController(
|
|
|
172
183
|
motion.containerAppeared()
|
|
173
184
|
}
|
|
174
185
|
|
|
186
|
+
// `afterCenter`: the menus join once the side menu first appeared with its center.
|
|
187
|
+
override fun onViewDidAppear() {
|
|
188
|
+
super.onViewDidAppear()
|
|
189
|
+
if (firstAppeared || destroying || isDestroyed) return
|
|
190
|
+
firstAppeared = true
|
|
191
|
+
SideMenuRules.attachedAfterAppearance(attachMode).forEach(::attach)
|
|
192
|
+
}
|
|
193
|
+
|
|
175
194
|
override fun onViewDisappear() {
|
|
176
195
|
appeared = false
|
|
177
196
|
motion.containerDisappeared()
|
|
@@ -222,6 +241,16 @@ internal class SideMenuController(
|
|
|
222
241
|
if (child === currentChild) super.tabBarSettled(child)
|
|
223
242
|
}
|
|
224
243
|
|
|
244
|
+
// The view of the menu on [side] joins its part, once, before its screens hear that they appear: a stack lays out
|
|
245
|
+
// its bar as it appears in the window. The drawer's first movement may come while it draws (a peek settles in
|
|
246
|
+
// `computeScroll`, before the drawer draws its children): the view joins the part then, measured and laid out at the
|
|
247
|
+
// next frame.
|
|
248
|
+
private fun attach(side: Side) {
|
|
249
|
+
val child = childOf(side) ?: return
|
|
250
|
+
if (destroying || isDestroyed || !attached.add(side)) return
|
|
251
|
+
peekView()?.fillPart(side, child.view)
|
|
252
|
+
}
|
|
253
|
+
|
|
225
254
|
/** Destroys the side menu once: the drawer's callbacks stop first, then the children go. */
|
|
226
255
|
override fun destroy() {
|
|
227
256
|
if (isDestroyed) return
|
|
@@ -238,6 +267,8 @@ internal class SideMenuController(
|
|
|
238
267
|
|
|
239
268
|
override fun child(side: Side): ViewController<*>? = childOf(side)
|
|
240
269
|
|
|
270
|
+
override fun starts(side: Side) = attach(side)
|
|
271
|
+
|
|
241
272
|
override fun isEnabled(side: Side): Boolean =
|
|
242
273
|
SideMenuRules.isEnabled(resolveShownOptions(DefaultOptions.current).sideMenu, side)
|
|
243
274
|
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt
CHANGED
|
@@ -82,6 +82,12 @@ internal class SideMenuMotion(
|
|
|
82
82
|
/** `sideMenu.<side>.enabled` of the side menu's options now. */
|
|
83
83
|
fun isEnabled(side: Side): Boolean
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The menu on [side] starts to show: the drawer's first movement, or a command that opens it. Before its
|
|
87
|
+
* screen hears that it appears.
|
|
88
|
+
*/
|
|
89
|
+
fun starts(side: Side)
|
|
90
|
+
|
|
85
91
|
/** `visible: true` for a side the layout doesn't have: a warning, nothing else. */
|
|
86
92
|
fun missingSide(side: Side)
|
|
87
93
|
|
|
@@ -122,6 +128,7 @@ internal class SideMenuMotion(
|
|
|
122
128
|
fun slid(side: Side, offset: Float) {
|
|
123
129
|
// Not more than 0 also takes a NaN: `DrawerLayout` divides by the drawer's width, which may be 0.
|
|
124
130
|
if (destroyed || !(offset > 0f)) return
|
|
131
|
+
if (!isOnScreen(side)) host.starts(side)
|
|
125
132
|
onScreen += side
|
|
126
133
|
if (side in appearing || !host.appeared) return
|
|
127
134
|
appearing += side
|
|
@@ -207,6 +214,7 @@ internal class SideMenuMotion(
|
|
|
207
214
|
}
|
|
208
215
|
// Never decided by where the other menu is: a side asked to open in the same merge hasn't moved yet.
|
|
209
216
|
if (drawer.has(side.other)) move(side.other, open = false, animate = animated)
|
|
217
|
+
if (!isOnScreen(side)) host.starts(side)
|
|
210
218
|
onScreen += side
|
|
211
219
|
// A locked menu opened from code can be dragged closed while it is open.
|
|
212
220
|
drawer.lock(side, locked = false)
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt
CHANGED
|
@@ -11,11 +11,23 @@ import kotlin.math.min
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* What the options of a side menu do on Android (`sideMenu.*`), without views: the values of each moment, `visible` as
|
|
14
|
-
* a command, the sizes, the `testID`s. Android reads `enabled`, `width`, `height`,
|
|
15
|
-
* `testID`s of `sideMenu`; `openGestureMode`, `animationType`,
|
|
16
|
-
* and `glass` are iOS's.
|
|
14
|
+
* a command, the sizes, the `testID`s, when the menus join the drawer. Android reads `enabled`, `width`, `height`,
|
|
15
|
+
* `visible`, `animate`, `menusAttachMode` and the `testID`s of `sideMenu`; `openGestureMode`, `animationType`,
|
|
16
|
+
* `openMode`, `shouldStretchDrawer`, `animationVelocity` and `glass` are iOS's.
|
|
17
17
|
*/
|
|
18
18
|
internal object SideMenuRules {
|
|
19
|
+
/** When the views of the menus' nodes join the drawer (`sideMenu.menusAttachMode`): their content renders then. */
|
|
20
|
+
enum class AttachMode {
|
|
21
|
+
/** With the side menu's view. */
|
|
22
|
+
Together,
|
|
23
|
+
|
|
24
|
+
/** Once the side menu first appeared, with its center. */
|
|
25
|
+
AfterCenter,
|
|
26
|
+
|
|
27
|
+
/** Each as it first starts to show: the drawer's first movement, or a command that opens it. */
|
|
28
|
+
OnOpen,
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/** A side's `visible` of one merge: open or close it, with its `animate`. */
|
|
20
32
|
data class Command(val side: Side, val visible: Boolean, val animate: Boolean)
|
|
21
33
|
|
|
@@ -43,6 +55,26 @@ internal object SideMenuRules {
|
|
|
43
55
|
*/
|
|
44
56
|
fun firstShow(layout: SideMenuOptions): List<Side> = Side.entries.filter { sideOf(layout, it).visible.isTrue }
|
|
45
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The mode of `sideMenu.menusAttachMode`: the side menu's [own] options, else the [defaults]; `together` without
|
|
60
|
+
* either. Read once, as the side menu is made.
|
|
61
|
+
*/
|
|
62
|
+
fun attachModeOf(own: SideMenuOptions, defaults: SideMenuOptions): AttachMode {
|
|
63
|
+
val value = own.menusAttachMode.takeIf { it.hasValue() } ?: defaults.menusAttachMode
|
|
64
|
+
return when (value.get(null)) {
|
|
65
|
+
"afterCenter" -> AttachMode.AfterCenter
|
|
66
|
+
"onOpen" -> AttachMode.OnOpen
|
|
67
|
+
else -> AttachMode.Together
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The menus that join as the side menu is made: both with `together`. */
|
|
72
|
+
fun attachedAtCreation(mode: AttachMode): List<Side> = if (mode == AttachMode.Together) Side.entries else emptyList()
|
|
73
|
+
|
|
74
|
+
/** The menus that join once the side menu first appeared: both with `afterCenter`. */
|
|
75
|
+
fun attachedAfterAppearance(mode: AttachMode): List<Side> =
|
|
76
|
+
if (mode == AttachMode.AfterCenter) Side.entries else emptyList()
|
|
77
|
+
|
|
46
78
|
/** `enabled` of [side]: true unless `false`, from the side menu's resolved options. */
|
|
47
79
|
fun isEnabled(options: SideMenuOptions, side: Side): Boolean = !sideOf(options, side).enabled.isFalse
|
|
48
80
|
|
|
@@ -48,15 +48,24 @@ internal class SideMenuView(context: Context) : DrawerLayout(context), SideMenuM
|
|
|
48
48
|
center.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The part of [side], empty until [fillPart]: a drawer at that edge, the whole height of the side menu. It opens,
|
|
53
|
+
* closes and takes its width and lock without its content.
|
|
54
|
+
*/
|
|
55
|
+
fun addPart(side: Side): FrameLayout {
|
|
53
56
|
val part = FrameLayout(context)
|
|
54
|
-
part.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
|
55
57
|
addView(part, LayoutParams(MATCH_PARENT, MATCH_PARENT, gravityOf(side)))
|
|
56
58
|
parts[side.ordinal] = part
|
|
57
59
|
return part
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/** [content] fills the part of [side], once. */
|
|
63
|
+
fun fillPart(side: Side, content: View) {
|
|
64
|
+
val part = partOf(side) ?: return
|
|
65
|
+
if (content.parent === part) return
|
|
66
|
+
part.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/** The part of [side]; none when the layout has no menu there. */
|
|
61
70
|
fun partOf(side: Side): FrameLayout? = parts[side.ordinal]
|
|
62
71
|
|
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
package dev.apexnative.navigation.views.stack
|
|
2
2
|
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.drawable.BitmapDrawable
|
|
3
6
|
import android.graphics.drawable.Drawable
|
|
4
7
|
import android.view.View
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* What fills the room beside a moving screen in a transition (`StackLayout.extendEdge`), where the system's
|
|
8
11
|
* transitions between activities stretch a window's edge (`<extend>` of `activity_open_*` and `activity_close_*`): the
|
|
9
|
-
* screen's
|
|
10
|
-
* screen
|
|
12
|
+
* column of pixels at the screen's edge, stretched across the room, as the system stretches the window's. What the
|
|
13
|
+
* screen shows there goes on beside it — a gradient its content draws, not only its own background. Taken at the start
|
|
14
|
+
* of every push and pop. Main thread only.
|
|
11
15
|
*/
|
|
12
16
|
internal object ScreenEdge {
|
|
13
|
-
/**
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The column of pixels at [view]'s left edge ([left]) or right edge, made to be stretched; its background when the
|
|
19
|
+
* screen isn't laid out as it shows ([laidOut]: not after a rotation while it was out of the stack) or can't be drawn
|
|
20
|
+
* in software (a `HARDWARE` bitmap in it, a view that fails to draw), none without one either.
|
|
21
|
+
*/
|
|
22
|
+
fun pictureOf(view: View, left: Boolean, laidOut: Boolean): Drawable? =
|
|
23
|
+
(if (laidOut) columnOf(view, left) else null) ?: backgroundOf(view)
|
|
24
|
+
|
|
25
|
+
// One pixel wide and as tall as the screen: the view draws itself into a canvas that keeps that column only. Any
|
|
26
|
+
// view of the screen may fail to draw in software (a hardware bitmap, a view that wants a hardware canvas): the
|
|
27
|
+
// screen's background stands in then, as the transition must go on.
|
|
28
|
+
private fun columnOf(view: View, left: Boolean): Drawable? {
|
|
29
|
+
val width = view.width
|
|
30
|
+
val height = view.height
|
|
31
|
+
if (width <= 0 || height <= 0) return null
|
|
32
|
+
return try {
|
|
33
|
+
val column = Bitmap.createBitmap(1, height, Bitmap.Config.ARGB_8888)
|
|
34
|
+
val canvas = Canvas(column)
|
|
35
|
+
if (!left) canvas.translate(-(width - 1).toFloat(), 0f)
|
|
36
|
+
view.draw(canvas)
|
|
37
|
+
BitmapDrawable(view.resources, column)
|
|
38
|
+
} catch (e: RuntimeException) {
|
|
39
|
+
null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun backgroundOf(view: View): Drawable? =
|
|
44
|
+
view.background?.constantState?.newDrawable(view.resources)?.mutate()
|
|
15
45
|
}
|
|
@@ -145,12 +145,15 @@ class StackLayout(context: Context) : CoordinatorLayout(context) {
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* [view] extends its start edge ([atStart]) or its end edge by [width] pixels while it moves, as the system's
|
|
148
|
-
* transitions between activities extend a window: its
|
|
149
|
-
* under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls
|
|
148
|
+
* transitions between activities extend a window: the pixels at its edge, stretched, fill the room it moves away
|
|
149
|
+
* from, so the screen under it never shows at its edge. [dropEdge] ends it. Each frame of the move calls
|
|
150
|
+
* [edgesMoved].
|
|
150
151
|
*/
|
|
151
152
|
fun extendEdge(view: View, atStart: Boolean, width: Int) {
|
|
152
153
|
val left = atStart != (layoutDirection == LAYOUT_DIRECTION_RTL)
|
|
153
|
-
|
|
154
|
+
// A screen put back for a pop keeps the size of its last layout until the next one: after a rotation while it was
|
|
155
|
+
// out of the stack, its pixels are of another width.
|
|
156
|
+
val picture = ScreenEdge.pictureOf(view, left, laidOut = view.width == this.width) ?: return
|
|
154
157
|
edges[view] = Edge(picture, width, left)
|
|
155
158
|
invalidate()
|
|
156
159
|
}
|
|
@@ -124,6 +124,21 @@ On iOS the content moves aside, and the menu comes in under it with a parallax;
|
|
|
124
124
|
|
|
125
125
|
(iOS) 26: `left({ glass: true })` puts the menu on Liquid Glass. The glass shows where the menu's screen has no background of its own.
|
|
126
126
|
|
|
127
|
+
## When the menus are created
|
|
128
|
+
|
|
129
|
+
The screens of the menus are created with the side menu by default, with their React content. A heavy menu competes
|
|
130
|
+
with the first render of the center. `options({ menusAttachMode })` moves the creation:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
SideMenu({ left: 'Menu', center: 'Home' }).options({ menusAttachMode: 'onOpen' });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `'together'` (the default): with the center.
|
|
137
|
+
- `'afterCenter'`: right after the side menu first appears. The menus don't compete with the first render of the center and are ready at the first open.
|
|
138
|
+
- `'onOpen'`: each menu the first time it starts to open, from a swipe, `openMenu`, `visible: true` or `visible` in the layout. Until React draws the menu, it shows its background (`backgroundColor`). A second open reuses it.
|
|
139
|
+
|
|
140
|
+
The trade-off of `'onOpen'`: the first open of a menu doesn't wait for its render and shows its background for a moment, while a menu not opened yet runs no code. A push into the stack of such a menu and `mergeOptions` on its screen wait for the menu, like a tab not yet selected. The screens of the menus get appear events only when the menu opens, in every mode. The mode is read once, when the side menu is created, from its own options, else the default options; `mergeOptions` doesn't change it. With `appProvider` the screens render in the app's React tree at once in every mode, and the mode delays only their native containers. It means the same on iOS and Android.
|
|
141
|
+
|
|
127
142
|
## Where the swipe is off
|
|
128
143
|
|
|
129
144
|
A swipe from the edge can clash with the content, such as a map or a carousel. A screen turns it off **while it is on top of the center**, in its options:
|
|
@@ -148,7 +148,7 @@ From a screen of the tab, `setTabBadge(count)` needs no id: it sets the badge of
|
|
|
148
148
|
|
|
149
149
|
## When tabs are created
|
|
150
150
|
|
|
151
|
-
By default every tab is created with the tabs, so a switch shows it at once. `Tabs().lazy()` creates each tab the first time it shows. The app starts faster when its tabs are heavy, and a tab the user never opens costs nothing.
|
|
151
|
+
By default every tab is created with the tabs, so a switch shows it at once. On (iOS) a text input's `autoFocus` in a tab not selected yet does nothing then: focus the field in `useScreenAppear`. `Tabs().lazy()` creates each tab the first time it shows. The app starts faster when its tabs are heavy, and a tab the user never opens costs nothing.
|
|
152
152
|
|
|
153
153
|
```ts
|
|
154
154
|
const main = Tabs(/* … */).lazy();
|
package/docs-md/index.json
CHANGED
package/ios/CLAUDE.md
CHANGED
|
@@ -20,7 +20,7 @@ Swift is not used here: apps reach the core through the public headers (`Public/
|
|
|
20
20
|
- `Layouts/Screen/`: The screen (`ANNComponentViewController`), its presenter, readiness, its environment for JS (`ANNScreenEnvironment`), regions, safe-area math, the main list rule, previews.
|
|
21
21
|
- `Layouts/External/`: Native screens: `ANNExternalViewController`, `ANNNativeScreenCalls`.
|
|
22
22
|
- `Layouts/Stack/`: `ANNStackController`, `ANNStackQueue`, `ANNStackCommands`, `ANNStackPresenter`, `ANNPreviewPush`, the swipe-back delegate.
|
|
23
|
-
- `Layouts/Tabs/`: `ANNBottomTabsController`, its presenter, item builder, custom row,
|
|
23
|
+
- `Layouts/Tabs/`: `ANNBottomTabsController`, its presenter, item builder, custom row, More delegate, long press, dots.
|
|
24
24
|
- `Layouts/Modal/`: `ANNModalCoordinator`, `ANNModal`, `ANNPresentationGate`, `ANNModalPresentation`, `ANNModalTransitions`, sheets and popovers.
|
|
25
25
|
- `Layouts/Overlay/`: `ANNOverlayCoordinator`, `ANNOverlayWindow`.
|
|
26
26
|
- `Layouts/SideMenu/`: The side menu module and its presenter and wrapper.
|
|
@@ -106,6 +106,9 @@ Top-level files: `Package.swift` (SwiftPM; `headerFolders` lists every folder wh
|
|
|
106
106
|
list would move as the push starts and UIKit would show the large title only after the transition. On iOS 26 its
|
|
107
107
|
large title also keeps a place of the core's for the push (`arrivesByPushOn:`). Check Showcase 1.4 (with a font
|
|
108
108
|
size), every section panel and 12.6 on push, pop and swipe after any change to waits, parking or the large title.
|
|
109
|
+
A tab rendered before it shows keeps its parent, yet UIKit gives its views no safe area either: it renders parked
|
|
110
|
+
in the tabs' view with places measured in the shown tab's view, then leaves the parking, since the core takes a view
|
|
111
|
+
in a window for one on screen (`isInUnseenTab:`); its screens keep that place until they appear.
|
|
109
112
|
- Every change of a stack's children goes through its `ANNStackQueue`. The core's own calls to UIKit's navigation
|
|
110
113
|
come only from there; UIKit's own pops are followed (`uikitShows:`), not fought.
|
|
111
114
|
- Every UIKit present and dismiss in the main window is a step in `ANNPresentationGate`. UIKit drops a transition
|
package/ios/CORE.md
CHANGED
|
@@ -128,8 +128,8 @@ Each of these owns one rule. Call it; do not copy it.
|
|
|
128
128
|
- `ANNNavigationState`: The visible screen and the nearest tabs it is in (the last tabs met walking down); coalesced updates, holds, the state now. Called by: The dispatcher, `ANNCommandAnswer`, `ANNRootCoordinator`, `ANNModalCoordinator`, `ANNBottomTabsController`.
|
|
129
129
|
- `ANNScreenReadiness`: The signals of a screen's safe area view (has one, laid out, content rendered), sticky, by screen id. Called by: `ANNSafeAreaViewComponentView`, `ANNReactView`.
|
|
130
130
|
- `ANNRenderWait`: The one wait for the render of what a command shows: content, bar components, limits, the unknown-content rule. Called by: Root, stack queue, modals, overlays, `showInColumn`.
|
|
131
|
-
- `ANNParking`: Where a view waits for its content unseen: the window, a stack's view, a preview's host view. Called by: Root, stack, modals, overlays, `ANNPreviewPush
|
|
132
|
-
- `ANNScreenPlace`: The safe area a parked screen lays out with, measured where it will show on top of a stack. Called by: `ANNStackController` (`stackPark:`), `ANNSplitViewCommands` (`showInColumn`), through `placeWhileParked` of the screen.
|
|
131
|
+
- `ANNParking`: Where a view waits for its content unseen: the window, a stack's view, a preview's host view, the tabs' view. Called by: Root, stack, modals, overlays, `ANNPreviewPush`, tabs (a tab rendered before it shows).
|
|
132
|
+
- `ANNScreenPlace`: The safe area a parked screen lays out with, measured where it will show on top of a stack. Called by: `ANNStackController` (`stackPark:`), `ANNSplitViewCommands` (`showInColumn`), the tabs (a tab rendered before it shows, measured in the shown tab's view), through `placeWhileParked` of the screen.
|
|
133
133
|
- `ANNPendingLayers`: Layers that wait to show (a pending root, a modal, an overlay); a node inside counts as off screen. Called by: The layer's owner; nodes ask.
|
|
134
134
|
- `ANNShownScreens`: "What a node shows changed": tells the pending layer, the modal hold and the system UI, in that order. Called by: Every container that changes what it shows.
|
|
135
135
|
- `ANNModalCoordinator`, `ANNModal`: One record per modal, its states and waiting commands. Called by: Commands, root, state, repaint, reload.
|
|
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
11
11
|
typedef NS_ENUM(NSInteger, ANNWaitedReadiness) {
|
|
12
12
|
/// The screen's content rendered: what the user sees.
|
|
13
13
|
ANNWaitedReadinessContent,
|
|
14
|
-
/// The screen's
|
|
15
|
-
/// renders
|
|
16
|
-
///
|
|
17
|
-
|
|
14
|
+
/// The screen's content rendered, on a screen the user doesn't see yet (an unselected tab of `together`, which
|
|
15
|
+
/// renders unseen in the tabs' view). Its part of a wait ends at the command's 0.5 s at the latest, and its bar
|
|
16
|
+
/// components are not waited for.
|
|
17
|
+
ANNWaitedReadinessUnseen,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/// A transition of the navigation is done: what it moved is in place.
|
|
@@ -97,7 +97,7 @@ typedef void (^ANNTransitionRejectionBlock)(NSString *message, NSError *_Nullabl
|
|
|
97
97
|
- (NSArray<UIViewController *> *)waitedComponentViewControllers;
|
|
98
98
|
|
|
99
99
|
/// The readiness a wait needs of `screen`, a waited screen inside this node; a node without an answer leaves it to its
|
|
100
|
-
/// parents (`ANNWaitedReadinessOf`). The tabs answer "
|
|
100
|
+
/// parents (`ANNWaitedReadinessOf`). The tabs answer "unseen" for the screens of their unselected tabs.
|
|
101
101
|
- (ANNWaitedReadiness)waitedReadinessOfScreen:(UIViewController *)screen;
|
|
102
102
|
|
|
103
103
|
#pragma mark - The children
|
package/ios/Core/ANNParking.h
CHANGED
|
@@ -4,10 +4,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
4
4
|
|
|
5
5
|
/// A place where the views of nodes wait for their content unseen (their safe area views lay out only in a window): the
|
|
6
6
|
/// window under the current root while a root waits, a stack's view under UIKit's container while a screen of the stack
|
|
7
|
-
/// waits or renders
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
7
|
+
/// waits or renders, the tabs' view while a tab rendered before it shows renders its content. A parked view sits at the
|
|
8
|
+
/// back of its host, at the host's size and following it (a rotation, a resize), gets no touches and is hidden from
|
|
9
|
+
/// accessibility; it isn't a child of anything there (a tab stays the tabs' child), and its node's appearance is driven
|
|
10
|
+
/// meanwhile (`ANNAppearanceDispatcher driveAppearanceOf:`), so UIKit's appearance callbacks report nothing for it.
|
|
11
|
+
/// Each node leaves it as it was. Main queue.
|
|
11
12
|
@interface ANNParking : NSObject
|
|
12
13
|
|
|
13
14
|
/// `node`'s view waits in `host` from now on, with what it had kept. NO when it waits here already.
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
#import "ANNScreenReadiness.h"
|
|
16
16
|
#import "ANNSplashViewController.h"
|
|
17
17
|
#import "ANNSplitViewCommands.h"
|
|
18
|
-
#import "ANNTabsPreloadWindow.h"
|
|
19
18
|
#import "ApexNavigation+Internal.h"
|
|
20
19
|
#import "UIViewController+LayoutProtocol.h"
|
|
21
20
|
#import <React/RCTConstants.h>
|
|
@@ -143,9 +142,7 @@ static BOOL ANNWatchingBundles;
|
|
|
143
142
|
[oldScreens addObjectsFromArray:[ANNOverlayCoordinator takeAllForReload]];
|
|
144
143
|
// 4. The bar components and the tabs' cells and accessories have no store to empty: each screen and each tabs node
|
|
145
144
|
// keeps its own roots in its registry, so a new node never gets an old root by its id, and the old roots stop with
|
|
146
|
-
// their old nodes after the hold. The
|
|
147
|
-
// updates are off already.
|
|
148
|
-
[ANNTabsPreloadWindow hideAllAfterReload];
|
|
145
|
+
// their old nodes after the hold. The loop updates are off already.
|
|
149
146
|
// 5. The native screens of the old screens are silent by their emitter. The modals and overlays waiting to show
|
|
150
147
|
// left what waits for them in steps 2 and 3; the screens that wait to show in a split view's column leave it here,
|
|
151
148
|
// without an answer.
|
package/ios/Core/ANNRenderWait.h
CHANGED
|
@@ -36,7 +36,7 @@ typedef NS_ENUM(NSInteger, ANNRenderWaitBar) {
|
|
|
36
36
|
|
|
37
37
|
/// `screens` gives the screens the wait is about: their bar components, the screens the limit marks ready, the screens
|
|
38
38
|
/// of the unknown-content rule, and with `ANNRenderWaitContentEachScreen` the screens whose content it waits for, each
|
|
39
|
-
/// by the readiness it needs (`ANNWaitedReadinessOf`: content, or "
|
|
39
|
+
/// by the readiness it needs (`ANNWaitedReadinessOf`: content, or "unseen" content without its bar components). It is
|
|
40
40
|
/// asked as the wait starts, as it takes its screens again (`retakeScreens:`) and as its bar phase starts. `node` is
|
|
41
41
|
/// the node of `ANNRenderWaitContentNode`; nil otherwise. `limit` 0: no limit.
|
|
42
42
|
- (instancetype)initWithContent:(ANNRenderWaitContent)content
|
|
@@ -49,10 +49,10 @@ typedef NS_ENUM(NSInteger, ANNRenderWaitBar) {
|
|
|
49
49
|
- (instancetype)init NS_UNAVAILABLE;
|
|
50
50
|
+ (instancetype)new NS_UNAVAILABLE;
|
|
51
51
|
|
|
52
|
-
/// The longest a waited screen of the "
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
@property(nonatomic) NSTimeInterval
|
|
52
|
+
/// The longest a waited screen of the "unseen" kind (`ANNWaitedReadinessOf`, an unselected tab's screen) is waited
|
|
53
|
+
/// for, from the start, also when the wait has no limit: a screen the user doesn't see never holds a command for long.
|
|
54
|
+
/// 0: only the wait's own limit.
|
|
55
|
+
@property(nonatomic) NSTimeInterval unseenLimit;
|
|
56
56
|
|
|
57
57
|
/// The rule for content of unknown readiness: 1 s after the start, a screen whose rendered state stays unknown (no safe
|
|
58
58
|
/// area view says it) counts as ready; a screen whose safe area view spoke waits for its signal. Off by default (a root
|