@apexnative/navigation 1.0.0-alpha.1 → 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 +38 -0
- package/README.md +147 -51
- package/android/CLAUDE.md +31 -6
- package/android/CORE.md +7 -2
- package/android/RECIPES.md +4 -3
- 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/react/ArrangementStates.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
- package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
- package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -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/bottomtabs/TabItems.kt +3 -3
- 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 +86 -16
- 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 +41 -23
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +24 -20
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
- 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 +42 -4
- 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 +5 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +16 -2
- 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 +123 -8
- 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/bottomtabs/BottomTabs.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
- package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
- 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/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
- package/cli/app.js +1 -1
- package/cli/project.js +6 -2
- package/cli/setup/index.js +13 -0
- package/cli/setup/steps/appDelegate.js +66 -11
- package/cli/setup/steps/sceneDelegate.js +108 -0
- package/docs-md/architecture/transitions.md +7 -1
- 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/guides/deep-links.md +15 -33
- 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/docs-md/start/installation.md +1 -0
- package/ios/Animations/ANNTransitionAnimator.mm +13 -11
- package/ios/Animations/ANNTransitionWait.mm +6 -7
- package/ios/Animations/Elements/ANNElementFinder.h +57 -0
- package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
- package/ios/Animations/Elements/ANNElementTransition.h +7 -5
- package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
- package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
- package/ios/CLAUDE.md +10 -2
- package/ios/CORE.md +3 -2
- package/ios/Contract/ANNCommandAnswer.h +4 -3
- package/ios/Contract/ANNCommandAnswer.mm +12 -2
- package/ios/Contract/ANNCommandRunner.h +2 -2
- package/ios/Contract/ANNCommandRunner.mm +1 -1
- package/ios/Contract/ANNErrorTexts.h +6 -2
- package/ios/Contract/ANNErrorTexts.mm +18 -10
- package/ios/Core/ANNDeepLinkQueue.h +20 -1
- package/ios/Core/ANNDeepLinkQueue.mm +107 -12
- package/ios/Core/ANNErrorHandler.h +8 -3
- package/ios/Core/ANNErrorHandler.mm +17 -1
- package/ios/Core/ANNEventContract.h +246 -0
- package/ios/Core/ANNEventContract.mm +225 -0
- package/ios/Core/ANNEventEmitter.h +5 -1
- package/ios/Core/ANNEventEmitter.mm +117 -80
- package/ios/Core/ANNHinge.mm +6 -4
- package/ios/Core/ANNSceneDelegate.mm +25 -15
- package/ios/Core/ANNSystemUI.h +3 -2
- package/ios/Core/ANNSystemUI.mm +7 -0
- package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
- package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
- package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
- package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
- package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
- package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
- package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
- package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
- package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
- package/ios/Options/ANNSheetOptions.h +4 -1
- package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
- package/ios/RECIPES.md +2 -1
- package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
- package/ios/TurboModules/ANNTurboModule.mm +5 -2
- package/lib/module/ARCHITECTURE.md +7 -4
- package/lib/module/api/app/createNavigation.js +2 -0
- package/lib/module/api/app/createNavigation.js.map +1 -1
- package/lib/module/api/hooks/useNavigation.js +8 -3
- package/lib/module/api/hooks/useNavigation.js.map +1 -1
- package/lib/module/api/linking/dispatchedLinks.js +23 -0
- package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
- package/lib/module/interfaces/contract/errors.json +275 -0
- package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
- package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
- package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
- package/lib/typescript/interfaces/Events.d.ts +9 -0
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +27 -18
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/modals.d.ts +33 -24
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
- package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +10 -6
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +7 -4
- package/src/api/app/createNavigation.ts +2 -0
- package/src/api/hooks/useNavigation.ts +8 -3
- package/src/api/linking/dispatchedLinks.ts +18 -0
- package/src/interfaces/Events.ts +10 -0
- package/src/interfaces/contract/errors.json +275 -0
- package/src/interfaces/options/animations.ts +27 -18
- package/src/interfaces/options/modals.ts +33 -24
- package/src/interfaces/options/sideMenu.ts +7 -6
- package/src/interfaces/options/system.ts +10 -6
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
|
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt
CHANGED
|
@@ -147,8 +147,11 @@ internal class ModalCoordinator(
|
|
|
147
147
|
*/
|
|
148
148
|
fun carry(record: ModalRecord, oldRoot: ViewController<*>?)
|
|
149
149
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
150
|
+
/**
|
|
151
|
+
* New [options] (resolved) of the record: its `testID`s, its sheet's `update`; [merge] is the `mergeOptions`
|
|
152
|
+
* that brought them, which alone may move the sheet, or null when the shown screen changed.
|
|
153
|
+
*/
|
|
154
|
+
fun update(record: ModalRecord, options: Options, merge: Options?)
|
|
152
155
|
|
|
153
156
|
/** The colors of the record's own views again (a light/dark switch). */
|
|
154
157
|
fun repaint(record: ModalRecord) = Unit
|
|
@@ -258,6 +261,54 @@ internal class ModalCoordinator(
|
|
|
258
261
|
}
|
|
259
262
|
}
|
|
260
263
|
|
|
264
|
+
/**
|
|
265
|
+
* A modal of a `setRoot`'s layout made with the root, before its turn: it joins the modals layer and renders
|
|
266
|
+
* unseen while the root waits and comes in, held — it doesn't show, and commands and back don't count it — until
|
|
267
|
+
* [release]; [animated] as in [showInChain]. A failure is logged, and nothing is made (null).
|
|
268
|
+
*/
|
|
269
|
+
fun hold(id: String, make: () -> ViewController<*>, animated: Boolean): ModalRecord? = try {
|
|
270
|
+
start(make, null, ModalRecord.Chain(animated) {}, held = true)
|
|
271
|
+
} catch (e: Throwable) {
|
|
272
|
+
val reason = ErrorTexts.reasonOf(e)
|
|
273
|
+
host.log("[ApexNavigation] setRoot: ${ErrorTexts.couldNot("show the screen '$id'", reason)}")
|
|
274
|
+
null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The turn of a [hold]'s modal in its `setRoot`'s chain: it shows as soon as it rendered, as [showInChain]'s
|
|
279
|
+
* would, with [done] once it showed or went; at once when it went meanwhile. The function returned stops it while
|
|
280
|
+
* it waits.
|
|
281
|
+
*/
|
|
282
|
+
fun release(record: ModalRecord, done: () -> Unit): () -> Unit {
|
|
283
|
+
if (!record.held || record.state != State.Waiting) {
|
|
284
|
+
done()
|
|
285
|
+
return {}
|
|
286
|
+
}
|
|
287
|
+
record.chain = ModalRecord.Chain(record.chain?.animated ?: true, done)
|
|
288
|
+
record.held = false
|
|
289
|
+
guarded(record) {
|
|
290
|
+
host.backChanged()
|
|
291
|
+
// Rendered while held: the targets of its elements are in the layer under it only now.
|
|
292
|
+
if (record.isReady && !record.targetsAwaited && awaitsTargets(record)) {
|
|
293
|
+
record.isReady = false
|
|
294
|
+
awaitTargets(record)
|
|
295
|
+
} else {
|
|
296
|
+
presentReady()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
record.chain = null
|
|
301
|
+
if (record.state == State.Waiting) cancel(record, null, reports = false)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A [hold]'s modal whose `setRoot` ended before its turn: it goes without a word. */
|
|
306
|
+
fun drop(record: ModalRecord) {
|
|
307
|
+
if (!record.held) return
|
|
308
|
+
record.chain = null
|
|
309
|
+
if (record.state == State.Waiting) cancel(record, null, reports = false)
|
|
310
|
+
}
|
|
311
|
+
|
|
261
312
|
/**
|
|
262
313
|
* `dismissModal` after its shared steps: the record of [componentId], Android's rule of the last modal without a
|
|
263
314
|
* root, [options] merged into the modal's visible screen, the animation of the command's screen with its parents
|
|
@@ -276,6 +327,7 @@ internal class ModalCoordinator(
|
|
|
276
327
|
State.ClosedBelow -> return answer.resolve(record.rootId)
|
|
277
328
|
else -> Unit
|
|
278
329
|
}
|
|
330
|
+
if (record.held) return answer.reject(ErrorTexts.screenNotShown(componentId))
|
|
279
331
|
if (isLastWithoutRoot(record)) return answer.reject(ErrorTexts.modalWithoutRoot(componentId))
|
|
280
332
|
options?.let { mergeInto(record, it) }
|
|
281
333
|
val from = node?.takeUnless { it.isDestroyed } ?: record.root
|
|
@@ -290,7 +342,7 @@ internal class ModalCoordinator(
|
|
|
290
342
|
* before anything changes, with the id of the top modal's visible screen.
|
|
291
343
|
*/
|
|
292
344
|
fun dismissAll(options: Options?, unreadablePath: String?, answer: CommandAnswer) {
|
|
293
|
-
val members = records.filter { it.batch == null && it.state in BATCHED }
|
|
345
|
+
val members = records.filter { it.batch == null && !it.held && it.state in BATCHED }
|
|
294
346
|
if (members.isEmpty()) {
|
|
295
347
|
// Nothing else to take: the second one answers with the running batch (N6), else at once.
|
|
296
348
|
val running = batches.lastOrNull()
|
|
@@ -344,6 +396,12 @@ internal class ModalCoordinator(
|
|
|
344
396
|
return true
|
|
345
397
|
}
|
|
346
398
|
|
|
399
|
+
/**
|
|
400
|
+
* The root of the modal that back goes to first; none without one. A back gesture previews only a pop in it: its
|
|
401
|
+
* dismissal has a preview of its own nowhere yet.
|
|
402
|
+
*/
|
|
403
|
+
fun backTargetRoot(): ViewController<*>? = backTarget()?.root
|
|
404
|
+
|
|
347
405
|
/** The mirror of [handleBack], without acting; null without a modal to ask. */
|
|
348
406
|
fun canHandleBack(): Boolean? {
|
|
349
407
|
val record = backTarget() ?: return null
|
|
@@ -353,13 +411,14 @@ internal class ModalCoordinator(
|
|
|
353
411
|
// Options.
|
|
354
412
|
|
|
355
413
|
/**
|
|
356
|
-
* A `mergeOptions` went into [node] (`mergeOptions` step 5): when it is in a modal, the modal's sheet
|
|
357
|
-
* options again
|
|
414
|
+
* A `mergeOptions` of [merge] went into [node] (`mergeOptions` step 5): when it is in a modal, the modal's sheet
|
|
415
|
+
* takes its resolved options again, moving only to a `selectedDetent` that [merge] names, and its `testID`s apply
|
|
416
|
+
* again.
|
|
358
417
|
*/
|
|
359
|
-
fun optionsMergedInto(node: ViewController
|
|
418
|
+
fun optionsMergedInto(node: ViewController<*>, merge: Options) {
|
|
360
419
|
val record = recordOf(node) ?: return
|
|
361
420
|
if (record.root.isDestroyed) return
|
|
362
|
-
views.update(record, record.root.resolveShownOptions(DefaultOptions.current),
|
|
421
|
+
views.update(record, record.root.resolveShownOptions(DefaultOptions.current), merge)
|
|
363
422
|
}
|
|
364
423
|
|
|
365
424
|
/**
|
|
@@ -369,7 +428,7 @@ internal class ModalCoordinator(
|
|
|
369
428
|
fun visibleScreenChanged(node: ViewController<*>) {
|
|
370
429
|
val record = recordOf(node) ?: return
|
|
371
430
|
if (record.state == State.Waiting || record.root.isDestroyed) return
|
|
372
|
-
views.update(record, record.root.resolveShownOptions(DefaultOptions.current),
|
|
431
|
+
views.update(record, record.root.resolveShownOptions(DefaultOptions.current), merge = null)
|
|
373
432
|
}
|
|
374
433
|
|
|
375
434
|
/** The colors of the modals' own views again, after a light/dark switch. */
|
|
@@ -418,6 +477,8 @@ internal class ModalCoordinator(
|
|
|
418
477
|
}
|
|
419
478
|
batches.clear()
|
|
420
479
|
for (record in records.toList()) {
|
|
480
|
+
// A modal of the new root's layout, made with it, stays: it shows over the new root in its turn.
|
|
481
|
+
if (record.held) continue
|
|
421
482
|
if (record.state == State.Waiting) {
|
|
422
483
|
cancel(record, null, reports = false)
|
|
423
484
|
continue
|
|
@@ -428,7 +489,7 @@ internal class ModalCoordinator(
|
|
|
428
489
|
views.carry(record, old)
|
|
429
490
|
carried += record
|
|
430
491
|
}
|
|
431
|
-
records.
|
|
492
|
+
records.retainAll { it.held }
|
|
432
493
|
host.backChanged()
|
|
433
494
|
}
|
|
434
495
|
|
|
@@ -478,7 +539,12 @@ internal class ModalCoordinator(
|
|
|
478
539
|
|
|
479
540
|
// The show (3, 4).
|
|
480
541
|
|
|
481
|
-
private fun start(
|
|
542
|
+
private fun start(
|
|
543
|
+
make: () -> ViewController<*>,
|
|
544
|
+
answer: CommandAnswer?,
|
|
545
|
+
chain: ModalRecord.Chain?,
|
|
546
|
+
held: Boolean = false,
|
|
547
|
+
): ModalRecord {
|
|
482
548
|
val root = make()
|
|
483
549
|
val record: ModalRecord
|
|
484
550
|
val options: Options
|
|
@@ -488,6 +554,7 @@ internal class ModalCoordinator(
|
|
|
488
554
|
val sheet = ModalRecord.isSheet(options)
|
|
489
555
|
root.shownAsSheet = sheet
|
|
490
556
|
record = ModalRecord(root, sheet)
|
|
557
|
+
record.held = held
|
|
491
558
|
} catch (e: Throwable) {
|
|
492
559
|
root.destroy()
|
|
493
560
|
throw e
|
|
@@ -551,7 +618,7 @@ internal class ModalCoordinator(
|
|
|
551
618
|
private fun ready(record: ModalRecord) {
|
|
552
619
|
if (record.state != State.Waiting) return
|
|
553
620
|
record.wait = null
|
|
554
|
-
if (!record.targetsAwaited && awaitsTargets(record)) return awaitTargets(record)
|
|
621
|
+
if (!record.targetsAwaited && !record.held && awaitsTargets(record)) return awaitTargets(record)
|
|
555
622
|
record.cancelLimit()
|
|
556
623
|
record.isReady = true
|
|
557
624
|
presentReady()
|
|
@@ -603,7 +670,7 @@ internal class ModalCoordinator(
|
|
|
603
670
|
if (taking) return
|
|
604
671
|
for (record in records.toList()) {
|
|
605
672
|
if (record.state != State.Waiting) continue
|
|
606
|
-
if (!record.isReady) return
|
|
673
|
+
if (record.held || !record.isReady) return
|
|
607
674
|
present(record)
|
|
608
675
|
}
|
|
609
676
|
}
|
|
@@ -1152,13 +1219,16 @@ internal class ModalCoordinator(
|
|
|
1152
1219
|
|
|
1153
1220
|
// A record is the top when no record above it waits, comes in or shows; one that leaves doesn't count.
|
|
1154
1221
|
private fun isTop(record: ModalRecord): Boolean =
|
|
1155
|
-
records.drop(records.indexOf(record) + 1).none
|
|
1222
|
+
records.drop(records.indexOf(record) + 1).none(::countsAsTop)
|
|
1156
1223
|
|
|
1157
1224
|
// The top record, as the commands see it; none.
|
|
1158
|
-
private fun topRecord(): ModalRecord? = records.lastOrNull
|
|
1225
|
+
private fun topRecord(): ModalRecord? = records.lastOrNull(::countsAsTop)
|
|
1159
1226
|
|
|
1160
1227
|
// Back goes to the top modal that waits, comes in or shows.
|
|
1161
|
-
private fun backTarget(): ModalRecord? = records.lastOrNull
|
|
1228
|
+
private fun backTarget(): ModalRecord? = records.lastOrNull(::countsAsTop)
|
|
1229
|
+
|
|
1230
|
+
// A record that waits, comes in or shows; a held one doesn't count before its turn.
|
|
1231
|
+
private fun countsAsTop(record: ModalRecord): Boolean = !record.held && record.effective in TOP_STATES
|
|
1162
1232
|
|
|
1163
1233
|
// `hardwareBackButton.dismissModalOnPress: false` of the modal's options now.
|
|
1164
1234
|
private fun holdsBack(record: ModalRecord): Boolean =
|
|
@@ -1230,7 +1300,7 @@ internal class ModalCoordinator(
|
|
|
1230
1300
|
val screen = AppearanceDispatcher.visibleNode(record.root)
|
|
1231
1301
|
if (screen.isDestroyed) return
|
|
1232
1302
|
screen.mergeOptions(options)
|
|
1233
|
-
optionsMergedInto(screen)
|
|
1303
|
+
optionsMergedInto(screen, options)
|
|
1234
1304
|
}
|
|
1235
1305
|
|
|
1236
1306
|
private inline fun whileTaking(work: () -> Unit) {
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt
CHANGED
|
@@ -17,7 +17,10 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
|
17
17
|
internal class ModalRecord(
|
|
18
18
|
/** The top node of the modal's layout. */
|
|
19
19
|
val root: ViewController<*>,
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The modal shows as a Material sheet (`pageSheet`, `formSheet`, `popover`, or `modal.sheet` without a style),
|
|
22
|
+
* decided before its root's view is made.
|
|
23
|
+
*/
|
|
21
24
|
val isSheet: Boolean,
|
|
22
25
|
) : WaitingLayer {
|
|
23
26
|
/** Where the modal is. */
|
|
@@ -78,6 +81,12 @@ internal class ModalRecord(
|
|
|
78
81
|
/** Its wait ended: it shows once every older waiting modal showed or went (N1). */
|
|
79
82
|
var isReady = false
|
|
80
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A modal of a `setRoot`'s layout made with the root, before its turn: it renders unseen while the root waits and
|
|
86
|
+
* comes in, and neither shows nor counts for commands or back until its step of the chain lets it go.
|
|
87
|
+
*/
|
|
88
|
+
var held = false
|
|
89
|
+
|
|
81
90
|
/** The wait for its screens while it waits. */
|
|
82
91
|
var wait: RenderWait? = null
|
|
83
92
|
|
|
@@ -160,14 +169,20 @@ internal class ModalRecord(
|
|
|
160
169
|
/** `modalPresentationStyle` values that keep the layer under in place. */
|
|
161
170
|
private val OVER_STYLES = setOf("overFullScreen", "overCurrentContext")
|
|
162
171
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The styles that always show as a sheet: the sheets of iOS, and the popover, which an iPhone shows as a sheet
|
|
174
|
+
* too.
|
|
175
|
+
*/
|
|
176
|
+
private val SHEET_STYLES = setOf("pageSheet", "formSheet", "popover")
|
|
165
177
|
|
|
166
178
|
/**
|
|
167
|
-
* Whether a modal with [options] (resolved, with the defaults) shows as a sheet:
|
|
168
|
-
*
|
|
179
|
+
* Whether a modal with [options] (resolved, with the defaults) shows as a sheet: its style is `pageSheet`,
|
|
180
|
+
* `formSheet` or `popover`, or it has none and any `modal.sheet` field is set.
|
|
169
181
|
*/
|
|
170
|
-
fun isSheet(options: Options): Boolean
|
|
182
|
+
fun isSheet(options: Options): Boolean {
|
|
183
|
+
val style = styleOf(options)
|
|
184
|
+
return style in SHEET_STYLES || (style == null && options.modal.sheet.hasValue)
|
|
185
|
+
}
|
|
171
186
|
|
|
172
187
|
/**
|
|
173
188
|
* Whether a modal covers the layer under it at the end of its show: full screen, unless it is a sheet or
|
|
@@ -29,11 +29,12 @@ import dev.apexnative.navigation.utils.dp
|
|
|
29
29
|
import kotlin.math.roundToInt
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* A modal shown as a Material bottom sheet (`modal.sheet`
|
|
33
|
-
* [BottomSheetBehavior], in the modals' CoordinatorLayout, over a scrim. It stops at
|
|
34
|
-
* `medium` at half the height, `large` at the full height under the status bar, a number
|
|
35
|
-
* user drags it between them.
|
|
36
|
-
*
|
|
32
|
+
* A modal shown as a Material bottom sheet (`pageSheet`, `formSheet`, `popover`, or `modal.sheet` without a style): the
|
|
33
|
+
* modal's view in a container with a [BottomSheetBehavior], in the modals' CoordinatorLayout, over a scrim. It stops at
|
|
34
|
+
* the detents of its options — `medium` at half the height, `large` at the full height under the status bar, a number
|
|
35
|
+
* at that height in dp — and the user drags it between them. In a window wider than [MAX_WIDTH_DP] it stands in the
|
|
36
|
+
* middle at that width. A drag down past the smallest one closes it, unless `swipeToDismiss` is false: then the sheet
|
|
37
|
+
* stays, and the try is told. The screen under it stays in place.
|
|
37
38
|
*/
|
|
38
39
|
internal class ModalSheet(
|
|
39
40
|
private val layout: CoordinatorLayout,
|
|
@@ -96,11 +97,19 @@ internal class ModalSheet(
|
|
|
96
97
|
outline.setRoundRect(0, 0, view.width, view.height + radius.roundToInt(), radius)
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
// The sheet is under the status bar: its screens leave no room for it at their top.
|
|
100
|
+
// The sheet is under the status bar: its screens leave no room for it at their top. In the middle of a wide
|
|
101
|
+
// window it reaches no side, and keeps room only for the part of a side bar that is over it.
|
|
100
102
|
ViewCompat.setOnApplyWindowInsetsListener(container) { _, insets ->
|
|
101
103
|
val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
|
102
104
|
val bars = insets.getInsets(types)
|
|
103
|
-
|
|
105
|
+
val laidOut = container.width > 0
|
|
106
|
+
val left = if (laidOut) (bars.left - container.left).coerceAtLeast(0) else bars.left
|
|
107
|
+
val right = if (laidOut) (bars.right - (layout.width - container.right)).coerceAtLeast(0) else bars.right
|
|
108
|
+
WindowInsetsCompat.Builder(insets).setInsets(types, Insets.of(left, 0, right, bars.bottom)).build()
|
|
109
|
+
}
|
|
110
|
+
// Its sides move with the window's width (a rotation, a resize): the room for the side bars follows.
|
|
111
|
+
container.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
|
|
112
|
+
if (left != oldLeft || right != oldRight) container.post { ViewCompat.requestApplyInsets(container) }
|
|
104
113
|
}
|
|
105
114
|
container.addView(content, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
|
106
115
|
container.addView(grabber, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.TOP or Gravity.CENTER_HORIZONTAL))
|
|
@@ -120,33 +129,39 @@ internal class ModalSheet(
|
|
|
120
129
|
layout.addView(scrim, CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
|
|
121
130
|
// As tall as its largest detent (maxHeight), not moved down by a margin: the behavior counts its states from
|
|
122
131
|
// where the sheet is laid out.
|
|
123
|
-
|
|
132
|
+
// In a wide window it stands in the middle, as wide as a Material modal sheet at most.
|
|
133
|
+
behavior.maxWidth = MAX_WIDTH_DP.dp
|
|
134
|
+
val params = CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
|
135
|
+
params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
|
136
|
+
params.behavior = behavior
|
|
137
|
+
layout.addView(container, params)
|
|
124
138
|
shown = onShown
|
|
125
|
-
update(sheetOptions, swipeToDismiss,
|
|
139
|
+
update(sheetOptions, swipeToDismiss, merge = null)
|
|
140
|
+
selected = sheetOptions.selectedDetent?.takeIf { it in detents }
|
|
126
141
|
// Laid out hidden first, so it slides up from the bottom.
|
|
127
142
|
container.post { if (closing == null) behavior.state = stateOf(selected ?: detents.first()) }
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
/**
|
|
146
|
+
* New resolved options: the detents and the look. The sheet moves only as [SheetStops] says, after a `mergeOptions`
|
|
147
|
+
* of [merge]; without one (the shown screen changed, the show) it stays where it is.
|
|
148
|
+
*/
|
|
149
|
+
fun update(sheetOptions: SheetOptions, swipeToDismiss: Boolean, merge: SheetOptions?) {
|
|
133
150
|
options = sheetOptions
|
|
134
151
|
this.swipeToDismiss = swipeToDismiss
|
|
135
152
|
detents = sheetOptions.detents?.takeIf { it.isNotEmpty() }?.sortedBy { heightOf(it) } ?: listOf(SheetDetent.Large)
|
|
136
|
-
selected = sheetOptions.selectedDetent?.takeIf { it in detents }
|
|
137
153
|
grabber.visibility = if (sheetOptions.grabber.isTrue) View.VISIBLE else View.GONE
|
|
138
154
|
behavior.dragAnywhere = sheetOptions.expandsOnScroll.get(true)
|
|
139
155
|
container.invalidateOutline()
|
|
140
156
|
configure()
|
|
141
|
-
if (
|
|
157
|
+
if (merge == null || closing != null) return
|
|
142
158
|
val settled = detentOf(behavior.state)
|
|
143
159
|
if (settled != null) behavior.isHideable = swipeToDismiss && settled == detents.first()
|
|
144
|
-
val target =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
val target = SheetStops.target(settled, detents, merge.selectedDetent, sheetOptions.selectedDetent)
|
|
161
|
+
if (target != null) {
|
|
162
|
+
selected = target
|
|
163
|
+
behavior.state = stateOf(target)
|
|
148
164
|
}
|
|
149
|
-
if (target != null) behavior.state = stateOf(target)
|
|
150
165
|
updateScrim()
|
|
151
166
|
}
|
|
152
167
|
|
|
@@ -333,10 +348,13 @@ internal class ModalSheet(
|
|
|
333
348
|
}
|
|
334
349
|
}
|
|
335
350
|
|
|
336
|
-
|
|
351
|
+
companion object {
|
|
352
|
+
/** The widest a sheet is, in dp: the Material modal bottom sheet's `maxWidth`. */
|
|
353
|
+
const val MAX_WIDTH_DP = 640
|
|
354
|
+
|
|
337
355
|
/** The dim of a Material modal bottom sheet's scrim. */
|
|
338
|
-
const val SCRIM_ALPHA = 0.32f
|
|
339
|
-
const val GRAB_AREA_DP = 48
|
|
340
|
-
const val ATTEMPT_DISTANCE_DP = 48
|
|
356
|
+
private const val SCRIM_ALPHA = 0.32f
|
|
357
|
+
private const val GRAB_AREA_DP = 48
|
|
358
|
+
private const val ATTEMPT_DISTANCE_DP = 48
|
|
341
359
|
}
|
|
342
360
|
}
|
|
@@ -4,9 +4,9 @@ import android.animation.Animator
|
|
|
4
4
|
import android.animation.AnimatorListenerAdapter
|
|
5
5
|
import android.animation.AnimatorSet
|
|
6
6
|
import android.animation.ObjectAnimator
|
|
7
|
-
import android.animation.PropertyValuesHolder
|
|
8
7
|
import android.animation.TimeInterpolator
|
|
9
8
|
import android.content.Context
|
|
9
|
+
import android.view.Gravity
|
|
10
10
|
import android.view.View
|
|
11
11
|
import android.view.ViewGroup
|
|
12
12
|
import android.view.animation.LinearInterpolator
|
|
@@ -17,6 +17,7 @@ import androidx.core.view.WindowInsetsCompat
|
|
|
17
17
|
import dev.apexnative.navigation.options.ModalAnimationOptions
|
|
18
18
|
import dev.apexnative.navigation.options.Options
|
|
19
19
|
import dev.apexnative.navigation.utils.TestIds
|
|
20
|
+
import dev.apexnative.navigation.utils.dp
|
|
20
21
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
21
22
|
import dev.apexnative.navigation.views.animations.AnimationRules
|
|
22
23
|
import dev.apexnative.navigation.views.animations.SideAnimator
|
|
@@ -62,9 +63,15 @@ internal class ModalViews(
|
|
|
62
63
|
try {
|
|
63
64
|
val view = record.root.view
|
|
64
65
|
(view.parent as? ViewGroup)?.removeView(view)
|
|
65
|
-
// A sheet's content waits at the sheet's full height, under the status bar.
|
|
66
|
+
// A sheet's content waits at the sheet's full height, under the status bar, and at its width.
|
|
66
67
|
val params = CoordinatorLayout.LayoutParams(MATCH, MATCH)
|
|
67
|
-
if (record.isSheet)
|
|
68
|
+
if (record.isSheet) {
|
|
69
|
+
params.topMargin = statusBarTop()
|
|
70
|
+
params.gravity = Gravity.CENTER_HORIZONTAL
|
|
71
|
+
// The modals layer is hidden while it has no modal: the window's content gives the width.
|
|
72
|
+
val maxWidth = ModalSheet.MAX_WIDTH_DP.dp
|
|
73
|
+
if (((modalsLayer.parent as? View)?.width ?: 0) > maxWidth) params.width = maxWidth
|
|
74
|
+
}
|
|
68
75
|
container.addView(view, params)
|
|
69
76
|
modalsLayer.addView(container, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
70
77
|
} catch (e: Throwable) {
|
|
@@ -100,14 +107,13 @@ internal class ModalViews(
|
|
|
100
107
|
container.translationY = 0f
|
|
101
108
|
return playOwn(record, parts, motion, showing = true, onEnd)
|
|
102
109
|
}
|
|
110
|
+
// The system's slide over the window below: the modal comes up whole, without a fade.
|
|
103
111
|
val distance = distanceOf(container)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
animator.interpolator = TimeInterpolator { ModalAnimations.decelerate(it) }
|
|
110
|
-
return play(parts, animator, onEnd)
|
|
112
|
+
container.alpha = 1f
|
|
113
|
+
container.translationY = distance
|
|
114
|
+
val animator = ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, distance, 0f)
|
|
115
|
+
animator.interpolator = TimeInterpolator { ModalAnimations.linearOutSlowIn(it) }
|
|
116
|
+
return play(parts, animator, ModalAnimations.SHOW_DURATION, onEnd)
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
// The root's view leaves the container and goes into the sheet's, in one turn; the sheet slides to its first
|
|
@@ -163,13 +169,11 @@ internal class ModalViews(
|
|
|
163
169
|
if (!animated || !seen) return none(onEnd)
|
|
164
170
|
if (motion != null) return playOwn(record, parts, motion, showing = false, onEnd)
|
|
165
171
|
val container = parts.container
|
|
166
|
-
val animator = ObjectAnimator.
|
|
167
|
-
container,
|
|
168
|
-
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, container.translationY, distanceOf(container)),
|
|
169
|
-
PropertyValuesHolder.ofFloat(View.ALPHA, container.alpha, 0f),
|
|
172
|
+
val animator = ObjectAnimator.ofFloat(
|
|
173
|
+
container, View.TRANSLATION_Y, container.translationY, distanceOf(container),
|
|
170
174
|
)
|
|
171
|
-
animator.interpolator = TimeInterpolator { ModalAnimations.
|
|
172
|
-
return play(parts, animator, onEnd)
|
|
175
|
+
animator.interpolator = TimeInterpolator { ModalAnimations.accelerate(it) }
|
|
176
|
+
return play(parts, animator, ModalAnimations.DISMISS_DURATION, onEnd)
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
override fun awaitTargets(
|
|
@@ -245,10 +249,10 @@ internal class ModalViews(
|
|
|
245
249
|
hideEmptyLayer()
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
override fun update(record: ModalRecord, options: Options,
|
|
252
|
+
override fun update(record: ModalRecord, options: Options, merge: Options?) {
|
|
249
253
|
val parts = parts[record] ?: return
|
|
250
254
|
applyTestIds(record, parts, options)
|
|
251
|
-
parts.sheet?.update(options.modal.sheet, swipeToDismissOf(options),
|
|
255
|
+
parts.sheet?.update(options.modal.sheet, swipeToDismissOf(options), merge?.modal?.sheet)
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
override fun repaint(record: ModalRecord) {
|
|
@@ -355,8 +359,8 @@ internal class ModalViews(
|
|
|
355
359
|
private fun viewOf(node: ViewController<*>): View? =
|
|
356
360
|
parts.entries.firstOrNull { it.key.root === node }?.value?.container ?: node.peekView()
|
|
357
361
|
|
|
358
|
-
private fun play(parts: Parts, animator: Animator, onEnd: () -> Unit): () -> Unit {
|
|
359
|
-
animator.duration =
|
|
362
|
+
private fun play(parts: Parts, animator: Animator, duration: Long, onEnd: () -> Unit): () -> Unit {
|
|
363
|
+
animator.duration = duration
|
|
360
364
|
animator.addListener(object : AnimatorListenerAdapter() {
|
|
361
365
|
override fun onAnimationEnd(animation: Animator) {
|
|
362
366
|
if (parts.animator === animation) parts.animator = null
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package dev.apexnative.navigation.viewcontrollers.modal
|
|
2
|
+
|
|
3
|
+
import dev.apexnative.navigation.options.SheetDetent
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where a shown sheet goes at new options, as on iOS: the sheet keeps the stop it stands at (the user's last drag
|
|
7
|
+
* included) until a `selectedDetent` arrives in a `mergeOptions`, or until its stop is no longer among the detents.
|
|
8
|
+
*/
|
|
9
|
+
internal object SheetStops {
|
|
10
|
+
/**
|
|
11
|
+
* The stop to move to, or null to stay: [arrived] (the `selectedDetent` the merge names) when it is among
|
|
12
|
+
* [detents]; else, when [settled] (the stop the sheet rests at) is no longer one of them, [selected] (the resolved
|
|
13
|
+
* `selectedDetent`) when it is, or the lowest.
|
|
14
|
+
*/
|
|
15
|
+
fun target(
|
|
16
|
+
settled: SheetDetent?,
|
|
17
|
+
detents: List<SheetDetent>,
|
|
18
|
+
arrived: SheetDetent?,
|
|
19
|
+
selected: SheetDetent?,
|
|
20
|
+
): SheetDetent? = when {
|
|
21
|
+
arrived != null && arrived in detents -> arrived
|
|
22
|
+
settled != null && settled !in detents -> selected?.takeIf { it in detents } ?: detents.first()
|
|
23
|
+
else -> null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -41,6 +41,10 @@ internal object BackCallbackState {
|
|
|
41
41
|
entries.removeAll { it.navigator.get().let { attached -> attached == null || attached === navigator } }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** JavaScript listens to back (`BackHandler`): it hears a press before the navigation does. */
|
|
45
|
+
val hasJsListeners: Boolean
|
|
46
|
+
get() = jsListeners
|
|
47
|
+
|
|
44
48
|
/** `setBackListeners` of JavaScript; false again when its JavaScript goes. */
|
|
45
49
|
fun setJsListeners(active: Boolean) {
|
|
46
50
|
jsListeners = active
|