@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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package dev.apexnative.navigation.viewcontrollers.stack
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.ValueAnimator
|
|
6
|
+
import android.content.res.Configuration
|
|
7
|
+
import android.graphics.Bitmap
|
|
8
|
+
import android.graphics.Canvas
|
|
9
|
+
import android.graphics.Color
|
|
10
|
+
import android.graphics.Outline
|
|
11
|
+
import android.graphics.Rect
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.Handler
|
|
14
|
+
import android.os.Looper
|
|
15
|
+
import android.view.PixelCopy
|
|
16
|
+
import android.view.View
|
|
17
|
+
import android.view.ViewGroup
|
|
18
|
+
import android.view.ViewOutlineProvider
|
|
19
|
+
import android.view.Window
|
|
20
|
+
import android.view.animation.DecelerateInterpolator
|
|
21
|
+
import android.view.animation.LinearInterpolator
|
|
22
|
+
import android.view.animation.PathInterpolator
|
|
23
|
+
import android.widget.ImageView
|
|
24
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
25
|
+
import dev.apexnative.navigation.views.animations.SystemTransition
|
|
26
|
+
import dev.apexnative.navigation.views.stack.StackLayout
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The preview of a pop while the user's back gesture runs (predictive back), as the system shows the activity under
|
|
30
|
+
* the one that closes: the screen on top with its bar, a picture taken as the gesture starts, shrinks toward the
|
|
31
|
+
* gesture's side with rounded corners and follows the finger up and down; the screen under it shows behind it with
|
|
32
|
+
* its own bar, smaller and dimmed. Released, the pop plays its end ([commit]); cancelled, everything goes back
|
|
33
|
+
* ([cancel]). The values are those of the system's cross-activity back animation (WMShell
|
|
34
|
+
* `DefaultCrossActivityBackAnimation` of Android 15 and 16, the smallest scale as Android 17 shows it), also on Android
|
|
35
|
+
* 14, whose own preview differs. Main thread only.
|
|
36
|
+
*/
|
|
37
|
+
internal class BackPreview(
|
|
38
|
+
private val layout: StackLayout,
|
|
39
|
+
private val top: View,
|
|
40
|
+
private val target: View,
|
|
41
|
+
/** The gesture started at the left edge: the screen moves right; from the right edge it stays centered. */
|
|
42
|
+
private val fromLeft: Boolean,
|
|
43
|
+
private val startY: Float,
|
|
44
|
+
/**
|
|
45
|
+
* The room the tabs take at the bottom of the top screen: the card is the stack above them, rounded on all four
|
|
46
|
+
* corners, while the tab bar stays, as Gmail shows it.
|
|
47
|
+
*/
|
|
48
|
+
private val bottomRoom: Int,
|
|
49
|
+
/** The picture is there: the stack shows the screen under the top, with its bar, before the preview draws. */
|
|
50
|
+
private val onReveal: () -> Unit,
|
|
51
|
+
) {
|
|
52
|
+
private val density = layout.resources.displayMetrics.density
|
|
53
|
+
private val bar = layout.topBar
|
|
54
|
+
private val scrim = View(layout.context)
|
|
55
|
+
private var card: ImageView? = null
|
|
56
|
+
private var progress = 0f
|
|
57
|
+
private var touchY = startY
|
|
58
|
+
private var animator: Animator? = null
|
|
59
|
+
private var ended = false
|
|
60
|
+
private var topAlpha = 1f
|
|
61
|
+
// What puts the stack as it was after a cancel: once, as the preview ends.
|
|
62
|
+
private var onCancelled: (() -> Unit)? = null
|
|
63
|
+
// Forward is to the end of the line: right to left, the screens move the other way, as the stack's pop does.
|
|
64
|
+
private val forward = if (layout.layoutDirection == View.LAYOUT_DIRECTION_RTL) -1f else 1f
|
|
65
|
+
|
|
66
|
+
/** The picture of the top is there and the screen under it shows: the preview draws. */
|
|
67
|
+
val isReady: Boolean
|
|
68
|
+
get() = card != null && !ended
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Takes the picture of the stack as it shows now, drawn from its views (what lies over the stack, an overlay or a
|
|
72
|
+
* tab bar drawn over it, stays out of it); with a hardware bitmap in it, copied from the window a frame later. The
|
|
73
|
+
* preview draws once it is there.
|
|
74
|
+
*/
|
|
75
|
+
fun start(window: Window) {
|
|
76
|
+
val width = layout.width
|
|
77
|
+
val height = areaHeight
|
|
78
|
+
if (width <= 0 || height <= 0) return
|
|
79
|
+
val picture = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
80
|
+
try {
|
|
81
|
+
layout.draw(Canvas(picture))
|
|
82
|
+
show(picture)
|
|
83
|
+
return
|
|
84
|
+
} catch (e: IllegalArgumentException) {
|
|
85
|
+
picture.eraseColor(Color.TRANSPARENT)
|
|
86
|
+
}
|
|
87
|
+
val place = IntArray(2)
|
|
88
|
+
layout.getLocationInWindow(place)
|
|
89
|
+
val area = Rect(place[0], place[1], place[0] + width, place[1] + height)
|
|
90
|
+
PixelCopy.request(window, area, picture, { result ->
|
|
91
|
+
if (result == PixelCopy.SUCCESS && !ended) show(picture)
|
|
92
|
+
}, Handler(Looper.getMainLooper()))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The gesture moved: [value] is its progress, 0 … 1, and [y] the finger's height in the window. */
|
|
96
|
+
fun progress(value: Float, y: Float) {
|
|
97
|
+
progress = value
|
|
98
|
+
touchY = y
|
|
99
|
+
if (isReady) drawGesture(GESTURE_CURVE.getInterpolation(value))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The gesture was cancelled: the preview springs back, then [restore] puts the stack as it was; at once when the
|
|
104
|
+
* preview ends earlier (a new gesture, a command).
|
|
105
|
+
*/
|
|
106
|
+
fun cancel(restore: () -> Unit) {
|
|
107
|
+
onCancelled = restore
|
|
108
|
+
if (!isReady) return endAtOnce()
|
|
109
|
+
val from = progress
|
|
110
|
+
animator?.cancel()
|
|
111
|
+
animator = ValueAnimator.ofFloat(from, 0f).apply {
|
|
112
|
+
duration = CANCEL_MILLIS
|
|
113
|
+
interpolator = DecelerateInterpolator()
|
|
114
|
+
addUpdateListener { drawGesture(GESTURE_CURVE.getInterpolation(it.animatedValue as Float)) }
|
|
115
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
116
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
117
|
+
if (animator === animation) endAtOnce()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
start()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The gesture was released and the pop runs: the top goes back to its size while it moves away by 96 dp and fades
|
|
126
|
+
* in the first fifth, the screen under it grows to its place, the dim fades; [done] once at the end, after the
|
|
127
|
+
* preview took its views away.
|
|
128
|
+
*/
|
|
129
|
+
fun commit(done: () -> Unit) {
|
|
130
|
+
val card = card ?: return done()
|
|
131
|
+
val distance = SystemTransition.DISTANCE_DP * density
|
|
132
|
+
val q = GESTURE_CURVE.getInterpolation(progress)
|
|
133
|
+
val scale = scaleAt(q)
|
|
134
|
+
val cardX = card.translationX
|
|
135
|
+
val cardY = card.translationY
|
|
136
|
+
val dim = scrim.alpha
|
|
137
|
+
animator?.cancel()
|
|
138
|
+
animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
139
|
+
duration = SystemTransition.DURATION
|
|
140
|
+
interpolator = LinearInterpolator()
|
|
141
|
+
addUpdateListener {
|
|
142
|
+
val t = it.animatedValue as Float
|
|
143
|
+
val e = SystemTransition.CURVE.getInterpolation(t)
|
|
144
|
+
card.scaleX = lerp(scale, 1f, e)
|
|
145
|
+
card.scaleY = card.scaleX
|
|
146
|
+
card.translationX = lerp(cardX, distance * forward, e)
|
|
147
|
+
card.translationY = lerp(cardY, 0f, e)
|
|
148
|
+
card.alpha = (1f - 5f * t).coerceAtLeast(0f)
|
|
149
|
+
entering(lerp(scale, 1f, e), lerp(-distance * forward, 0f, e))
|
|
150
|
+
scrim.alpha = lerp(dim, 0f, t)
|
|
151
|
+
}
|
|
152
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
153
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
154
|
+
if (animator !== animation) return
|
|
155
|
+
endAtOnce()
|
|
156
|
+
done()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
start()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The preview ends where it is: its views go, the screens and the bar take their own values again, and a cancel's
|
|
165
|
+
* restore runs.
|
|
166
|
+
*/
|
|
167
|
+
fun endAtOnce() {
|
|
168
|
+
if (ended) return
|
|
169
|
+
ended = true
|
|
170
|
+
animator?.let {
|
|
171
|
+
animator = null
|
|
172
|
+
it.cancel()
|
|
173
|
+
}
|
|
174
|
+
card?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
175
|
+
card = null
|
|
176
|
+
(scrim.parent as? ViewGroup)?.removeView(scrim)
|
|
177
|
+
top.alpha = topAlpha
|
|
178
|
+
entering(1f, 0f)
|
|
179
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
180
|
+
target.resetPivot()
|
|
181
|
+
bar.resetPivot()
|
|
182
|
+
}
|
|
183
|
+
onCancelled?.let {
|
|
184
|
+
onCancelled = null
|
|
185
|
+
it()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The picture came: the screen under the top shows with its bar, dimmed, and the picture lies over everything in
|
|
190
|
+
// the top's place, cut to rounded corners.
|
|
191
|
+
private fun show(picture: Bitmap) {
|
|
192
|
+
onReveal()
|
|
193
|
+
val dark = layout.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
|
194
|
+
Configuration.UI_MODE_NIGHT_YES
|
|
195
|
+
scrim.setBackgroundColor(Color.BLACK)
|
|
196
|
+
scrim.alpha = if (dark) SCRIM_DARK else SCRIM_LIGHT
|
|
197
|
+
scrim.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
198
|
+
layout.addOverAll(scrim, fill())
|
|
199
|
+
val radius = CORNER_DP * density
|
|
200
|
+
val area = CoordinatorLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, areaHeight)
|
|
201
|
+
card = ImageView(layout.context).apply {
|
|
202
|
+
setImageBitmap(picture)
|
|
203
|
+
scaleType = ImageView.ScaleType.FIT_XY
|
|
204
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
205
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
206
|
+
override fun getOutline(view: View, outline: Outline) =
|
|
207
|
+
outline.setRoundRect(0, 0, view.width, view.height, radius)
|
|
208
|
+
}
|
|
209
|
+
clipToOutline = true
|
|
210
|
+
layout.addOverAll(this, area)
|
|
211
|
+
}
|
|
212
|
+
// Transparent, not invisible: a surface in it (a video, a map) stays, and so does the focus.
|
|
213
|
+
topAlpha = top.alpha
|
|
214
|
+
top.alpha = 0f
|
|
215
|
+
drawGesture(GESTURE_CURVE.getInterpolation(progress))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The values of the gesture at [q], its progress on the system's curve.
|
|
219
|
+
private fun drawGesture(q: Float) {
|
|
220
|
+
val card = card ?: return
|
|
221
|
+
val width = layout.width.toFloat()
|
|
222
|
+
val height = areaHeight.toFloat()
|
|
223
|
+
val margin = MARGIN_DP * density
|
|
224
|
+
val scale = scaleAt(q)
|
|
225
|
+
card.scaleX = scale
|
|
226
|
+
card.scaleY = scale
|
|
227
|
+
// From the left edge the screen's right side ends 8 dp from the window's right edge; from the right edge it stays
|
|
228
|
+
// centered.
|
|
229
|
+
card.translationX = if (fromLeft) (width - width * scale) / 2f - margin * q else 0f
|
|
230
|
+
// Up and down with the finger, never closer than 8 dp to the window's edges.
|
|
231
|
+
val moved = touchY - startY
|
|
232
|
+
val ratio = FOLLOW_CURVE.getInterpolation((kotlin.math.abs(moved) / (height / 2f)).coerceAtMost(1f))
|
|
233
|
+
card.translationY = ((height - height * scale) / 2f - margin).coerceAtLeast(0f) * ratio *
|
|
234
|
+
Math.signum(moved)
|
|
235
|
+
entering(scale, -SystemTransition.DISTANCE_DP * density * forward)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The screen under the top and its bar at [scale] around the stack's center, [x] along the line.
|
|
239
|
+
private fun entering(scale: Float, x: Float) {
|
|
240
|
+
for (view in listOf(target, bar)) {
|
|
241
|
+
view.pivotX = layout.width / 2f - view.left
|
|
242
|
+
view.pivotY = areaHeight / 2f - view.top
|
|
243
|
+
view.scaleX = scale
|
|
244
|
+
view.scaleY = scale
|
|
245
|
+
view.translationX = x
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The stack above the tabs' room: what the card shows and what the screens shrink around.
|
|
250
|
+
private val areaHeight: Int
|
|
251
|
+
get() = (layout.height - bottomRoom).coerceAtLeast(0)
|
|
252
|
+
|
|
253
|
+
private fun scaleAt(q: Float): Float = 1f - (1f - minScale()) * q
|
|
254
|
+
|
|
255
|
+
private fun fill() = CoordinatorLayout.LayoutParams(
|
|
256
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
257
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
private fun lerp(from: Float, to: Float, fraction: Float): Float = from + (to - from) * fraction
|
|
261
|
+
|
|
262
|
+
private companion object {
|
|
263
|
+
/** The system's curve of the back gesture's progress (`BackGestureInterpolator`). */
|
|
264
|
+
val GESTURE_CURVE = PathInterpolator(0.1f, 0.1f, 0f, 1f)
|
|
265
|
+
|
|
266
|
+
/** How the screen follows the finger up and down. */
|
|
267
|
+
val FOLLOW_CURVE = DecelerateInterpolator()
|
|
268
|
+
|
|
269
|
+
/** The room the shrunk screen keeps from the window's edges. */
|
|
270
|
+
const val MARGIN_DP = 8f
|
|
271
|
+
|
|
272
|
+
/** The corners of the shrunk screen, as Android 17 draws them on a Pixel (6.5 dp at its smallest). */
|
|
273
|
+
const val CORNER_DP = 7.5f
|
|
274
|
+
|
|
275
|
+
const val SCRIM_LIGHT = 0.2f
|
|
276
|
+
const val SCRIM_DARK = 0.8f
|
|
277
|
+
|
|
278
|
+
/** The cancel springs back about as fast as the system's (`BackProgressAnimator`'s spring, no bounce). */
|
|
279
|
+
const val CANCEL_MILLIS = 350L
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The smallest scale of the screens: 0.9 in WMShell of Android 15 and 16 (and here on Android 14); Android 17
|
|
283
|
+
* shrinks the window further (0.86, measured on a Pixel 6).
|
|
284
|
+
*/
|
|
285
|
+
fun minScale(): Float = if (android.os.Build.VERSION.SDK_INT >= 37) 0.86f else 0.9f
|
|
286
|
+
}
|
|
287
|
+
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt
CHANGED
|
@@ -2,6 +2,7 @@ package dev.apexnative.navigation.viewcontrollers.stack
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.util.Log
|
|
5
|
+
import android.view.View
|
|
5
6
|
import dev.apexnative.navigation.NavigationActivity
|
|
6
7
|
import dev.apexnative.navigation.options.AnimationsOptions
|
|
7
8
|
import dev.apexnative.navigation.options.BottomTabsEnterExitAnimationOptions
|
|
@@ -35,6 +36,7 @@ import dev.apexnative.navigation.viewcontrollers.stack.topbar.TopBarPresenter
|
|
|
35
36
|
import dev.apexnative.navigation.viewcontrollers.stack.topbar.TopBarRoom
|
|
36
37
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
37
38
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
39
|
+
import dev.apexnative.navigation.views.animations.AnimationRules
|
|
38
40
|
import dev.apexnative.navigation.views.component.ComponentLayout
|
|
39
41
|
import dev.apexnative.navigation.views.stack.StackLayout
|
|
40
42
|
import dev.apexnative.navigation.views.stack.topbar.LargeTitleView
|
|
@@ -257,6 +259,136 @@ class StackController(
|
|
|
257
259
|
*/
|
|
258
260
|
override fun canHandleBack(): Boolean = topBar.isSearchOpen() || queue.entries.size >= 2
|
|
259
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The default pop of the top is what back does now and it can be previewed: two entries or more, nothing running
|
|
264
|
+
* or waiting, no open search, the top doesn't hold back and pops with the default transition, the screen under it
|
|
265
|
+
* has its view, and the stack shows.
|
|
266
|
+
*/
|
|
267
|
+
override fun stackPoppedByBack(): StackController? {
|
|
268
|
+
if (topBar.isSearchOpen() || !queue.isAtRest) return null
|
|
269
|
+
val entries = queue.entries
|
|
270
|
+
val top = queue.currentChild ?: return null
|
|
271
|
+
val target = entries.getOrNull(entries.size - 2) ?: return null
|
|
272
|
+
if (top !== entries.last() || target !in queue.shown || target.peekView() == null) return null
|
|
273
|
+
if (!queue.isRendered(target) || backGesture != null) return null
|
|
274
|
+
val options = resolvedOf(top)
|
|
275
|
+
val pop = options.animations.pop
|
|
276
|
+
if (options.hardwareBackButton.popStackOnPress.isFalse || pop.enabled.isFalse || AnimationRules.stackIsOwn(pop)) {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
val display = queue.display
|
|
280
|
+
return this.takeIf { display.isOnScreen && display.canAnimate }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Starts the preview of the pop a back gesture would make ([stackPoppedByBack] said yes): the top shrinks toward
|
|
285
|
+
* the gesture's side ([fromLeft]) and follows the finger from [touchY]; the screen under it shows with its bar.
|
|
286
|
+
* None when it can't be shown.
|
|
287
|
+
*/
|
|
288
|
+
internal fun startBackGesture(fromLeft: Boolean, touchY: Float): BackGesture? {
|
|
289
|
+
val views = queue.display as? StackViews ?: return null
|
|
290
|
+
val top = queue.currentChild ?: return null
|
|
291
|
+
val target = queue.entries.getOrNull(queue.entries.size - 2) ?: return null
|
|
292
|
+
val gesture = BackGesture(views, top, target)
|
|
293
|
+
val preview = views.startBackPreview(top, target, fromLeft, touchY, gesture::reveal) ?: return null
|
|
294
|
+
gesture.preview = preview
|
|
295
|
+
backGesture = gesture
|
|
296
|
+
return gesture
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The back gesture over this stack whose preview hasn't ended yet; none.
|
|
300
|
+
private var backGesture: BackGesture? = null
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A back gesture over the top of this stack ([startBackGesture]): its progress, its cancel, and its release, which
|
|
304
|
+
* pops the top through the queue with the preview's end. A command accepted meanwhile, or the stack's destroy,
|
|
305
|
+
* ends the preview at once ([abort]).
|
|
306
|
+
*/
|
|
307
|
+
internal inner class BackGesture(
|
|
308
|
+
private val views: StackViews,
|
|
309
|
+
private val top: ViewController<*>,
|
|
310
|
+
private val target: ViewController<*>,
|
|
311
|
+
) {
|
|
312
|
+
lateinit var preview: BackPreview
|
|
313
|
+
// How the screen under the top was before the preview showed it: in the container and seen, its alpha and
|
|
314
|
+
// accessibility.
|
|
315
|
+
private var targetWasPlaced = false
|
|
316
|
+
private var targetAlpha = 1f
|
|
317
|
+
private var targetAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
318
|
+
private var revealed = false
|
|
319
|
+
private var aborted = false
|
|
320
|
+
|
|
321
|
+
/** The gesture moved: [progress] 0 … 1, the finger at [touchY]. */
|
|
322
|
+
fun progress(progress: Float, touchY: Float) {
|
|
323
|
+
if (!aborted) preview.progress(progress, touchY)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Cancelled: the preview springs back, then the screen under the top and the bar are as they were. */
|
|
327
|
+
fun cancel() {
|
|
328
|
+
if (!aborted) preview.cancel(::restore)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Released: the top pops, the preview playing its end; the default pop when its picture isn't there yet. False
|
|
333
|
+
* when the stack changed since the gesture started (a command ran, the stack went): back goes the usual way.
|
|
334
|
+
*/
|
|
335
|
+
fun commit(): Boolean {
|
|
336
|
+
if (aborted || isDestroyed) return false
|
|
337
|
+
if (!preview.isReady) {
|
|
338
|
+
abort()
|
|
339
|
+
queue.cut(target, null, id, "go back in the stack '$id'", null)
|
|
340
|
+
return true
|
|
341
|
+
}
|
|
342
|
+
if (backGesture === this) backGesture = null
|
|
343
|
+
queue.cut(target, null, id, "go back in the stack '$id'", null, gesture = true)
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The stack changed or goes: the preview ends at once and the stack is as it was. */
|
|
348
|
+
fun abort() {
|
|
349
|
+
if (aborted) return
|
|
350
|
+
aborted = true
|
|
351
|
+
preview.endAtOnce()
|
|
352
|
+
restore()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// The preview's picture is there: the screen under the top shows under it, and the bar shows that screen.
|
|
356
|
+
internal fun reveal() {
|
|
357
|
+
val view = target.peekView() ?: return
|
|
358
|
+
targetWasPlaced = view.parent === peekView() && view.visibility == View.VISIBLE
|
|
359
|
+
targetAlpha = view.alpha
|
|
360
|
+
targetAccessibility = view.importantForAccessibility
|
|
361
|
+
revealed = true
|
|
362
|
+
views.reveal(target, top)
|
|
363
|
+
view.alpha = 1f
|
|
364
|
+
topBar.apply(target)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private fun restore() {
|
|
368
|
+
views.previewEnded(preview)
|
|
369
|
+
if (backGesture === this) backGesture = null
|
|
370
|
+
if (!revealed || isDestroyed) return
|
|
371
|
+
revealed = false
|
|
372
|
+
if (target in queue.shown && target !== queue.currentChild) {
|
|
373
|
+
val view = target.peekView()
|
|
374
|
+
if (targetWasPlaced && view != null) {
|
|
375
|
+
view.alpha = targetAlpha
|
|
376
|
+
view.importantForAccessibility = targetAccessibility
|
|
377
|
+
} else {
|
|
378
|
+
views.detach(target)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// The bar shows the top again, its components appeared again.
|
|
382
|
+
val shown = queue.currentChild
|
|
383
|
+
topBar.apply(shown)
|
|
384
|
+
shown?.let {
|
|
385
|
+
topBar.screenWillAppear(optionsScreen(it))
|
|
386
|
+
topBar.screenDidAppear(optionsScreen(it))
|
|
387
|
+
}
|
|
388
|
+
BackCallbackState.update()
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
260
392
|
/**
|
|
261
393
|
* The back arrow of the bar was pressed: the screen the user sees holds it (`topBar.backButton.popStackOnPress:
|
|
262
394
|
* false`: `ANN.NavigationButtonPressed` with `topBar.backButton.id` or `ApexNavigation.back`); on the first screen
|
|
@@ -460,8 +592,9 @@ class StackController(
|
|
|
460
592
|
|
|
461
593
|
/**
|
|
462
594
|
* The bar's bottom in the window while [child] is under it and it shows, from the stack's place in the window: for
|
|
463
|
-
* the screen on top where the bar is laid out (a move of the bar doesn't count); for another
|
|
464
|
-
*
|
|
595
|
+
* the screen on top that the bar shows, where the bar is laid out (a move of the bar doesn't count); for another
|
|
596
|
+
* child (a prepared one) and for a top the bar hasn't shown yet (in a tab not selected yet, which measures though
|
|
597
|
+
* invisible) as its own options set the bar, so its content doesn't move as the bar takes them.
|
|
465
598
|
*/
|
|
466
599
|
override fun headerBottomOf(child: ViewController<*>): Int? {
|
|
467
600
|
if (!isUnderBar(child)) return super.headerBottomOf(child)
|
|
@@ -471,7 +604,7 @@ class StackController(
|
|
|
471
604
|
val layout = peekView() ?: return null
|
|
472
605
|
// The stack's place at rest, as the screen's own: a modal sliding in or a sheet dragged doesn't move it.
|
|
473
606
|
val top = RestPlace.topInWindow(layout)
|
|
474
|
-
if (entry === queue.currentChild) return topBar.barBottomInStack()?.let { top + it }
|
|
607
|
+
if (entry === queue.currentChild && topBar.shows(entry)) return topBar.barBottomInStack()?.let { top + it }
|
|
475
608
|
val bar = topBar.barOf(entry, options)
|
|
476
609
|
val large = if (largeTitleShown(child)) UiUtils.dpToPx(activity, LargeTitleView.HEIGHT).toInt() else 0
|
|
477
610
|
return top + bar.margin + bar.strip + bar.height + large
|
|
@@ -506,6 +639,7 @@ class StackController(
|
|
|
506
639
|
|
|
507
640
|
override fun destroy() {
|
|
508
641
|
if (isDestroyed) return
|
|
642
|
+
backGesture?.abort()
|
|
509
643
|
queue.destroy()
|
|
510
644
|
preparedPushes()?.discardFor(this)
|
|
511
645
|
fab.destroy()
|
|
@@ -552,15 +686,23 @@ class StackController(
|
|
|
552
686
|
(node as? OptionsScreen)?.optionsScreen ?: node
|
|
553
687
|
|
|
554
688
|
private inner class QueueHost : StackQueue.Host {
|
|
689
|
+
override fun operationAccepted() {
|
|
690
|
+
backGesture?.abort()
|
|
691
|
+
}
|
|
692
|
+
|
|
555
693
|
// The bar takes the options of the new top at once, also without appearance events.
|
|
556
694
|
override fun currentChildChanged() {
|
|
557
695
|
updateBar()
|
|
558
696
|
navigator?.shownScreensChanged(this@StackController)
|
|
559
697
|
}
|
|
560
698
|
|
|
561
|
-
// Before the bar takes the new top's options: the row's picture, and how it shows or hides.
|
|
562
|
-
override fun barTransition(
|
|
563
|
-
|
|
699
|
+
// Before the bar takes the new top's options: the row's picture or the bar's travel, and how it shows or hides.
|
|
700
|
+
override fun barTransition(
|
|
701
|
+
motion: StackDisplay.BarMotion?,
|
|
702
|
+
visibility: StackDisplay.BarVisibility,
|
|
703
|
+
travel: StackDisplay.BarTravel?,
|
|
704
|
+
) {
|
|
705
|
+
if (peekView() != null) topBar.beginRowTransition(motion, queue.currentChild, visibility, travel)
|
|
564
706
|
}
|
|
565
707
|
|
|
566
708
|
// The tab bar of the tabs around goes with the transition; a takeover doesn't bring it to rest.
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt
CHANGED
|
@@ -38,6 +38,12 @@ internal interface StackDisplay {
|
|
|
38
38
|
*/
|
|
39
39
|
enum class BarMotion { Push, Pop, FadeThrough }
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* How the bar goes with a transition of the stack's own (`content`): with its screens. [Forward]: the screen that
|
|
43
|
+
* comes lies over the one that goes (a push, `setStackRoot`); [Back]: the one that goes lies over it (a pop).
|
|
44
|
+
*/
|
|
45
|
+
enum class BarTravel { Forward, Back }
|
|
46
|
+
|
|
41
47
|
/**
|
|
42
48
|
* How the bar shows or hides when the new top's `topBar.visible` differs: at once (no animation, or a transition
|
|
43
49
|
* with elements), or [animated] by the transition's [side] (`animations.push.topBar`, the leaving screen's
|
|
@@ -156,6 +162,24 @@ internal interface StackDisplay {
|
|
|
156
162
|
done: () -> Unit,
|
|
157
163
|
): () -> Unit
|
|
158
164
|
|
|
165
|
+
/** The preview of a back gesture over [top], [target] under it, shows ([finishBackGesture] can end it). */
|
|
166
|
+
fun hasBackPreview(top: ViewController<*>, target: ViewController<*>): Boolean = false
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The pop of a back gesture whose preview shows ([hasBackPreview]), released: the preview plays the end of [top]
|
|
170
|
+
* going and [target] coming. [topDone] and [targetDone] once each, as [animate]'s; [stop] of one screen ends the
|
|
171
|
+
* preview where it is and its other screen's part.
|
|
172
|
+
*/
|
|
173
|
+
fun finishBackGesture(
|
|
174
|
+
top: ViewController<*>,
|
|
175
|
+
target: ViewController<*>,
|
|
176
|
+
topDone: () -> Unit,
|
|
177
|
+
targetDone: () -> Unit,
|
|
178
|
+
) {
|
|
179
|
+
topDone()
|
|
180
|
+
targetDone()
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
/** Stops the animator of [screen] where it is, without its end: a later transition takes the screen over. */
|
|
160
184
|
fun stop(screen: ViewController<*>)
|
|
161
185
|
|