@apexnative/navigation 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +1 -1
- package/android/CLAUDE.md +23 -14
- package/android/CORE.md +32 -19
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +60 -13
- package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
- package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
- package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -6
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +19 -51
- package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +30 -39
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +12 -10
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
- package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +22 -21
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +12 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +13 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +2 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +4 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
- package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
- package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
- package/cli/doctor/checks/splash.js +4 -22
- package/cli/splash/index.js +3 -1
- package/docs-md/architecture/transitions.md +4 -2
- package/docs-md/containers/modals.md +4 -2
- package/docs-md/containers/overlays.md +1 -1
- package/docs-md/containers/side-menu.md +1 -1
- package/docs-md/containers/tabs.md +2 -0
- package/docs-md/flows/bottom-sheets.md +1 -1
- package/docs-md/fundamentals/starting-the-app.md +1 -1
- package/docs-md/guides/cli.md +1 -2
- package/docs-md/help/glossary.md +2 -2
- package/docs-md/help/troubleshooting.md +23 -2
- package/docs-md/index.json +12 -0
- package/docs-md/migration/from-expo-router.md +1 -1
- package/docs-md/migration/from-react-native-navigation.md +1 -1
- package/docs-md/migration/from-react-navigation.md +2 -2
- package/docs-md/screen-ui/back-and-gestures.md +36 -0
- package/docs-md/screen-ui/bar-buttons.md +4 -4
- package/docs-md/screen-ui/search.md +1 -1
- package/docs-md/screen-ui/system-ui.md +16 -11
- package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
- package/docs-md/screen-ui/top-bar.md +8 -4
- package/docs-md/start/installation.md +1 -1
- package/ios/Animations/ANNFrameDriver.mm +2 -0
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
- package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
- package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
- package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
- package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
- package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
- package/ios/CLAUDE.md +17 -15
- package/ios/CORE.md +30 -26
- package/ios/Contract/ANNCommandAnswer.h +9 -2
- package/ios/Contract/ANNCommandAnswer.mm +25 -12
- package/ios/Contract/ANNCommandRunner.h +8 -7
- package/ios/Contract/ANNCommandRunner.mm +21 -27
- package/ios/Contract/ANNLayoutNode.h +3 -3
- package/ios/Contract/ANNLayoutReader.h +3 -3
- package/ios/Core/ANNAppearanceDispatcher.h +5 -4
- package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
- package/ios/Core/ANNJavaScriptCalls.h +11 -12
- package/ios/Core/ANNJavaScriptCalls.mm +7 -16
- package/ios/Core/ANNNavigationState.h +4 -6
- package/ios/Core/ANNNavigationState.mm +0 -4
- package/ios/Core/ANNReloadCoordinator.mm +4 -8
- package/ios/Core/ApexNavigation+Internal.h +1 -1
- package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
- package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
- package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
- package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
- package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
- package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
- package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
- package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
- package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
- package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
- package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
- package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
- package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
- package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
- package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
- package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
- package/ios/Layouts/Stack/ANNPreviewPush.mm +142 -27
- package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
- package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
- package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
- package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
- package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
- package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
- package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
- package/ios/Options/ANNBottomTabsOptions.h +2 -0
- package/ios/Options/ANNBottomTabsOptions.mm +1 -0
- package/ios/Options/ANNOptions.h +18 -0
- package/ios/Options/ANNOptions.mm +112 -1
- package/ios/Options/ANNOptionsReader.mm +110 -0
- package/ios/Options/Params/ANNParam.h +15 -0
- package/ios/Options/Params/ANNParam.mm +61 -0
- package/ios/Options/Params/Color.h +3 -0
- package/ios/Options/Params/Color.mm +21 -0
- package/ios/Options/Params/Image.h +4 -0
- package/ios/Options/Params/Image.mm +20 -0
- package/ios/React/ANNReactView.mm +6 -25
- package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
- package/ios/React/ANNSurfaceHostingView.h +6 -0
- package/ios/React/ANNSurfaceHostingView.mm +116 -0
- package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
- package/ios/TopBar/ANNBarAppearance.h +2 -1
- package/ios/TopBar/ANNBarAppearance.mm +111 -30
- package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
- package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
- package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
- package/ios/TopBar/ANNBarButtons.h +5 -3
- package/ios/TopBar/ANNBarButtons.mm +186 -17
- package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
- package/ios/TopBar/ANNMainList.h +3 -0
- package/ios/TopBar/ANNMainList.mm +4 -0
- package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
- package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
- package/ios/TopBar/ANNSearchPresenter.mm +2 -2
- package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
- package/ios/TopBar/ANNTopBarPresenter.h +3 -2
- package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
- package/ios/TurboModules/ANNTurboModule.mm +20 -16
- package/lib/module/ARCHITECTURE.md +8 -4
- package/lib/module/api/commands/commands.js +54 -6
- package/lib/module/api/commands/commands.js.map +1 -1
- package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
- package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
- package/lib/module/api/options/OptionsChain.js +2 -2
- package/lib/module/engine/commands.js +21 -1
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/places.js +8 -0
- package/lib/module/engine/places.js.map +1 -1
- package/lib/module/engine/screens.js +29 -4
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/typescript/api/app/createNavigation.d.ts +3 -3
- package/lib/typescript/api/commands/commands.d.ts.map +1 -1
- package/lib/typescript/api/commands/presentation.d.ts +4 -3
- package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
- package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
- package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
- package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/places.d.ts +2 -0
- package/lib/typescript/engine/places.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts +20 -6
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/Options.d.ts +1 -1
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +30 -9
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
- package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
- package/lib/typescript/interfaces/options/stack.d.ts +9 -0
- package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +8 -5
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
- package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +8 -4
- package/src/api/app/createNavigation.ts +3 -3
- package/src/api/commands/commands.ts +48 -7
- package/src/api/commands/presentation.ts +4 -3
- package/src/api/hooks/useSafeAreaInsets.ts +4 -3
- package/src/api/options/OptionsChain.ts +2 -2
- package/src/engine/commands.ts +30 -6
- package/src/engine/places.ts +9 -0
- package/src/engine/screens.tsx +29 -8
- package/src/interfaces/Options.ts +1 -0
- package/src/interfaces/options/animations.ts +31 -9
- package/src/interfaces/options/bottomTabs.ts +18 -5
- package/src/interfaces/options/buttons.ts +8 -8
- package/src/interfaces/options/stack.ts +9 -0
- package/src/interfaces/options/system.ts +8 -5
- package/src/interfaces/options/toolbar.ts +2 -2
- package/src/interfaces/options/topBar.ts +32 -29
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
- package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
- package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
- package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package dev.apexnative.navigation.viewcontrollers.component
|
|
2
2
|
|
|
3
|
-
import dev.apexnative.navigation.react.
|
|
3
|
+
import dev.apexnative.navigation.react.ScreenEnvironment.Insets
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The arithmetic of a screen's room, safe area and background on Android, without views: the screen nodes read the
|
|
@@ -19,6 +19,7 @@ internal object ScreenSafeArea {
|
|
|
19
19
|
const val LEFT = "left"
|
|
20
20
|
|
|
21
21
|
private val ALL_EDGES = setOf(TOP, RIGHT, BOTTOM, LEFT)
|
|
22
|
+
private val DEFAULT_EDGES = setOf(TOP, RIGHT, LEFT)
|
|
22
23
|
private const val OPAQUE = 0xFF
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -57,10 +58,10 @@ internal object ScreenSafeArea {
|
|
|
57
58
|
headerBottom: Int?,
|
|
58
59
|
tabsTop: Int?,
|
|
59
60
|
density: Float,
|
|
60
|
-
):
|
|
61
|
+
): Insets {
|
|
61
62
|
val coveredAbove = maxOf(system.top, headerBottom ?: 0)
|
|
62
63
|
val coveredBelow = minOf(windowHeight - system.bottom, tabsTop ?: windowHeight)
|
|
63
|
-
return
|
|
64
|
+
return Insets(
|
|
64
65
|
top = dp((coveredAbove - root.top).coerceAtLeast(0), density),
|
|
65
66
|
right = dp((root.right - (windowWidth - system.right)).coerceAtLeast(0), density),
|
|
66
67
|
bottom = dp((root.bottom - coveredBelow).coerceAtLeast(0), density),
|
|
@@ -69,26 +70,43 @@ internal object ScreenSafeArea {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* The edges the screen's safe area view keeps its content off: exactly [safeAreaEdges] when the options give them
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* The edges the screen's safe area view keeps its content off: exactly [safeAreaEdges] when the options give them,
|
|
74
|
+
* but the top under a large title ([largeTitle]); else the top and the sides, but the top when the stack's header
|
|
75
|
+
* hides on scroll or a large title shows ([topFree]). The bottom is free by default: the content goes down to the
|
|
76
|
+
* bottom, and a list keeps its last rows clear of the navigation bar with `useSafeAreaInsets()`.
|
|
75
77
|
*/
|
|
76
|
-
fun paddedEdges(safeAreaEdges: List<String>?, topFree: Boolean,
|
|
77
|
-
if (safeAreaEdges != null)
|
|
78
|
-
|
|
78
|
+
fun paddedEdges(safeAreaEdges: List<String>?, topFree: Boolean, largeTitle: Boolean): Set<String> {
|
|
79
|
+
if (safeAreaEdges != null) {
|
|
80
|
+
val edges = safeAreaEdges.filterTo(LinkedHashSet()) { it in ALL_EDGES }
|
|
81
|
+
// Under a large title the list keeps its first rows clear of the bar itself; a padded top would follow the
|
|
82
|
+
// header as it collapses and move the list with it.
|
|
83
|
+
if (largeTitle) edges -= TOP
|
|
84
|
+
return edges
|
|
85
|
+
}
|
|
86
|
+
val edges = LinkedHashSet(DEFAULT_EDGES)
|
|
79
87
|
if (topFree) edges -= TOP
|
|
80
|
-
if (bottomFree) edges -= BOTTOM
|
|
81
88
|
return edges
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/** The padding of the safe area view: the [insets] on the [edges] it keeps, 0 on the others. */
|
|
85
|
-
fun padding(insets:
|
|
92
|
+
fun padding(insets: Insets, edges: Set<String>): Insets = Insets(
|
|
86
93
|
top = if (TOP in edges) insets.top else 0.0,
|
|
87
94
|
right = if (RIGHT in edges) insets.right else 0.0,
|
|
88
95
|
bottom = if (BOTTOM in edges) insets.bottom else 0.0,
|
|
89
96
|
left = if (LEFT in edges) insets.left else 0.0,
|
|
90
97
|
)
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The insets JavaScript gets (`useSafeAreaInsets`): what of the [insets] still covers the content once the safe
|
|
101
|
+
* area view keeps it off its [padding], 0 on a padded edge.
|
|
102
|
+
*/
|
|
103
|
+
fun uncovered(insets: Insets, padding: Insets) = Insets(
|
|
104
|
+
top = (insets.top - padding.top).coerceAtLeast(0.0),
|
|
105
|
+
right = (insets.right - padding.right).coerceAtLeast(0.0),
|
|
106
|
+
bottom = (insets.bottom - padding.bottom).coerceAtLeast(0.0),
|
|
107
|
+
left = (insets.left - padding.left).coerceAtLeast(0.0),
|
|
108
|
+
)
|
|
109
|
+
|
|
92
110
|
/**
|
|
93
111
|
* The inner padding of a screen's view: the side system bars ([systemBars]) at the sides; at the bottom the larger
|
|
94
112
|
* of the navigation bar (none when the screen draws under it) and the keyboard ([imeBottom]; none with
|
|
@@ -10,11 +10,9 @@ import dev.apexnative.navigation.layout.LayoutException
|
|
|
10
10
|
import dev.apexnative.navigation.nativescreen.NativeScreenContext
|
|
11
11
|
import dev.apexnative.navigation.options.DefaultOptions
|
|
12
12
|
import dev.apexnative.navigation.options.Options
|
|
13
|
-
import dev.apexnative.navigation.react.ArrangementStates
|
|
14
13
|
import dev.apexnative.navigation.react.CommandScope
|
|
15
14
|
import dev.apexnative.navigation.react.ErrorTexts
|
|
16
|
-
import dev.apexnative.navigation.react.
|
|
17
|
-
import dev.apexnative.navigation.react.ScreenRegions
|
|
15
|
+
import dev.apexnative.navigation.react.ScreenEnvironment
|
|
18
16
|
import dev.apexnative.navigation.react.events.AppearanceDispatcher
|
|
19
17
|
import dev.apexnative.navigation.react.events.ComponentType
|
|
20
18
|
import dev.apexnative.navigation.react.events.ScreenAppearance
|
|
@@ -170,9 +168,7 @@ class ExternalComponentViewController internal constructor(
|
|
|
170
168
|
peekView()?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
171
169
|
setParentController(null)
|
|
172
170
|
if (isIdTakenByAnother) return
|
|
173
|
-
|
|
174
|
-
ScreenRegions.remove(id)
|
|
175
|
-
ArrangementStates.remove(id)
|
|
171
|
+
ScreenEnvironment.remove(id)
|
|
176
172
|
ScreenReadiness.remove(id)
|
|
177
173
|
if (!notMade) AppearanceDispatcher.nativeScreenReleased(this)
|
|
178
174
|
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt
CHANGED
|
@@ -158,6 +158,9 @@ internal class ModalCoordinator(
|
|
|
158
158
|
|
|
159
159
|
/** The record's views lay out again (the activity resumed). */
|
|
160
160
|
fun requestLayout(record: ModalRecord) = Unit
|
|
161
|
+
|
|
162
|
+
/** The record's sheet as back sees it; none before its show and for a modal that isn't a sheet. */
|
|
163
|
+
fun sheetBack(record: ModalRecord): SheetBack? = null
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
/**
|
|
@@ -397,11 +400,26 @@ internal class ModalCoordinator(
|
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
/**
|
|
400
|
-
* The root of the modal that back goes to first; none without one. A back gesture previews
|
|
401
|
-
* dismissal
|
|
403
|
+
* The root of the modal that back goes to first; none without one. A back gesture previews a pop in it, or the
|
|
404
|
+
* modal's dismissal when it is a sheet ([backTargetSheet]).
|
|
402
405
|
*/
|
|
403
406
|
fun backTargetRoot(): ViewController<*>? = backTarget()?.root
|
|
404
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The sheet back would dismiss now, as [handleBack] would, and as a drag down of the user would: the top modal is
|
|
410
|
+
* a sheet that shows, its content can't go back (else back goes there, and a pop in it has its own preview),
|
|
411
|
+
* `modal.swipeToDismiss` isn't false, `hardwareBackButton.dismissModalOnPress` doesn't hold back, it isn't the only
|
|
412
|
+
* modal over no root, and its sheet would hide, not collapse, and isn't closing already ([SheetBack.hidesOnBack]).
|
|
413
|
+
* None otherwise. The back gesture previews its hide on Android 14+, and its release closes it as the user does.
|
|
414
|
+
*/
|
|
415
|
+
fun backTargetSheet(): SheetBack? {
|
|
416
|
+
val record = backTarget()?.takeIf { it.isSheet && it.state == State.Shown } ?: return null
|
|
417
|
+
if (record.root.canHandleBack() || holdsBack(record) || isLastWithoutRoot(record)) return null
|
|
418
|
+
val options = record.root.resolveShownOptions(DefaultOptions.current)
|
|
419
|
+
if (options.modal.swipeToDismiss.isFalse) return null
|
|
420
|
+
return views.sheetBack(record)?.takeIf { it.hidesOnBack }
|
|
421
|
+
}
|
|
422
|
+
|
|
405
423
|
/** The mirror of [handleBack], without acting; null without a modal to ask. */
|
|
406
424
|
fun canHandleBack(): Boolean? {
|
|
407
425
|
val record = backTarget() ?: return null
|
|
@@ -15,6 +15,7 @@ import android.view.ViewOutlineProvider
|
|
|
15
15
|
import android.view.ViewTreeObserver
|
|
16
16
|
import android.widget.FrameLayout
|
|
17
17
|
import android.widget.ScrollView
|
|
18
|
+
import androidx.activity.BackEventCompat
|
|
18
19
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
19
20
|
import androidx.core.graphics.Insets
|
|
20
21
|
import androidx.core.view.ViewCompat
|
|
@@ -34,13 +35,14 @@ import kotlin.math.roundToInt
|
|
|
34
35
|
* the detents of its options — `medium` at half the height, `large` at the full height under the status bar, a number
|
|
35
36
|
* 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
37
|
* 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.
|
|
38
|
+
* stays, and the try is told. The screen under it stays in place. On Android 14+ the back gesture previews its hide
|
|
39
|
+
* ([SheetBack]): the Material behavior shrinks it with the progress and slides it down at the release.
|
|
38
40
|
*/
|
|
39
41
|
internal class ModalSheet(
|
|
40
42
|
private val layout: CoordinatorLayout,
|
|
41
43
|
private val content: View,
|
|
42
44
|
private val events: Events,
|
|
43
|
-
) {
|
|
45
|
+
) : SheetBack {
|
|
44
46
|
interface Events {
|
|
45
47
|
/** The user dragged the sheet to another detent. */
|
|
46
48
|
fun onDetentChanged(detent: SheetDetent)
|
|
@@ -66,6 +68,28 @@ internal class ModalSheet(
|
|
|
66
68
|
private var dragged = false
|
|
67
69
|
private var shown: (() -> Unit)? = null
|
|
68
70
|
private var closing: (() -> Unit)? = null
|
|
71
|
+
// The back gesture whose preview runs now; none.
|
|
72
|
+
private var backPreview: BackPreview? = null
|
|
73
|
+
// Released: the behavior slides the sheet down from where the preview left it, and hides it at the end.
|
|
74
|
+
private var backFinishing = false
|
|
75
|
+
// The window's observer the scrim follows the preview's frames through, from its start until the sheet is back
|
|
76
|
+
// in place or gone; none otherwise.
|
|
77
|
+
private var backFramesObserver: ViewTreeObserver? = null
|
|
78
|
+
|
|
79
|
+
// The behavior tells no slide while back's preview scales the sheet and its release slides it down (they move it by
|
|
80
|
+
// its transform, not its offset): the scrim follows what is drawn, frame by frame, until the sheet stands again.
|
|
81
|
+
private val backFrames = ViewTreeObserver.OnPreDrawListener {
|
|
82
|
+
updateScrim()
|
|
83
|
+
if (backPreview == null && !backFinishing && container.scaleY == 1f && container.translationY == 0f) {
|
|
84
|
+
stopBackFrames()
|
|
85
|
+
}
|
|
86
|
+
true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private val sheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
|
90
|
+
override fun onStateChanged(sheet: View, newState: Int) = onState(newState)
|
|
91
|
+
override fun onSlide(sheet: View, slideOffset: Float) = updateScrim()
|
|
92
|
+
}
|
|
69
93
|
|
|
70
94
|
// A list inside the sheet scrolls with it, as the Material behavior expects: up at half height the sheet goes up
|
|
71
95
|
// first, down at the top of the list the sheet comes down. The behavior follows views that scroll nested.
|
|
@@ -89,6 +113,13 @@ internal class ModalSheet(
|
|
|
89
113
|
}
|
|
90
114
|
})
|
|
91
115
|
scrim.setOnTouchListener { _, event -> taps.onTouchEvent(event) }
|
|
116
|
+
// The sheet keeps a touch that lands on it and no view in it takes (its bar, an empty part), as Material's
|
|
117
|
+
// BottomSheetDialog does: else the touch falls to the scrim under it, and a tap there closes a dimmed sheet;
|
|
118
|
+
// past an undimmed scrim it reaches the behavior, which takes the sheet into its drag state at the touch's
|
|
119
|
+
// down, before any move. The back gesture starts with such a touch, and a sheet in the drag state has no
|
|
120
|
+
// preview. A drag that starts there still moves the sheet: the behavior takes it past the touch slop, as from
|
|
121
|
+
// the views in it.
|
|
122
|
+
container.setOnTouchListener { _, _ -> true }
|
|
92
123
|
container.clipToOutline = true
|
|
93
124
|
container.outlineProvider = object : ViewOutlineProvider() {
|
|
94
125
|
// The bottom corners are under the screen's bottom: only the top ones round.
|
|
@@ -116,10 +147,7 @@ internal class ModalSheet(
|
|
|
116
147
|
behavior.isHideable = true
|
|
117
148
|
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
118
149
|
behavior.halfExpandedRatio = 0.5f
|
|
119
|
-
behavior.addBottomSheetCallback(
|
|
120
|
-
override fun onStateChanged(sheet: View, newState: Int) = onState(newState)
|
|
121
|
-
override fun onSlide(sheet: View, slideOffset: Float) = updateScrim()
|
|
122
|
-
})
|
|
150
|
+
behavior.addBottomSheetCallback(sheetCallback)
|
|
123
151
|
container.viewTreeObserver.addOnGlobalLayoutListener(scrollViews)
|
|
124
152
|
}
|
|
125
153
|
|
|
@@ -154,10 +182,12 @@ internal class ModalSheet(
|
|
|
154
182
|
behavior.dragAnywhere = sheetOptions.expandsOnScroll.get(true)
|
|
155
183
|
container.invalidateOutline()
|
|
156
184
|
configure()
|
|
157
|
-
if (merge == null || closing != null) return
|
|
185
|
+
if (merge == null || closing != null || backFinishing) return
|
|
158
186
|
val settled = detentOf(behavior.state)
|
|
159
187
|
if (settled != null) behavior.isHideable = swipeToDismiss && settled == detents.first()
|
|
160
188
|
val target = SheetStops.target(settled, detents, merge.selectedDetent, sheetOptions.selectedDetent)
|
|
189
|
+
// A merge that moves the sheet or keeps it from going down ends a preview of back on it.
|
|
190
|
+
if (target != null || !behavior.isHideable) stopBackPreview()
|
|
161
191
|
if (target != null) {
|
|
162
192
|
selected = target
|
|
163
193
|
behavior.state = stateOf(target)
|
|
@@ -172,12 +202,19 @@ internal class ModalSheet(
|
|
|
172
202
|
finishClosing()
|
|
173
203
|
return
|
|
174
204
|
}
|
|
205
|
+
// Released by back, it goes down already: it is closed at the end of that slide.
|
|
206
|
+
if (backFinishing) return
|
|
207
|
+
stopBackPreview()
|
|
175
208
|
behavior.isHideable = true
|
|
176
209
|
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
/** Takes the sheet's views away; the modal's view is the caller's. */
|
|
180
213
|
fun remove() {
|
|
214
|
+
// A slide down of back's release that ends after this tells nothing: the modal went already.
|
|
215
|
+
behavior.removeBottomSheetCallback(sheetCallback)
|
|
216
|
+
stopBackPreview()
|
|
217
|
+
stopBackFrames()
|
|
181
218
|
container.viewTreeObserver.removeOnGlobalLayoutListener(scrollViews)
|
|
182
219
|
container.removeView(content)
|
|
183
220
|
layout.removeView(container)
|
|
@@ -191,6 +228,69 @@ internal class ModalSheet(
|
|
|
191
228
|
TestIds.apply(scrim, testId?.let { "$it.scrim" })
|
|
192
229
|
}
|
|
193
230
|
|
|
231
|
+
override val hidesOnBack: Boolean
|
|
232
|
+
get() = closing == null && !backFinishing && behavior.isHideable && container.isLaidOut &&
|
|
233
|
+
detentOf(behavior.state) != null
|
|
234
|
+
|
|
235
|
+
override fun startBackPreview(event: BackEventCompat): SheetBack.Gesture? {
|
|
236
|
+
if (!hidesOnBack || backPreview != null) return null
|
|
237
|
+
val preview = BackPreview()
|
|
238
|
+
backPreview = preview
|
|
239
|
+
startBackFrames()
|
|
240
|
+
behavior.startBackProgress(event)
|
|
241
|
+
return preview
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// A command closed or moved the sheet, or it goes, while back's preview runs: the sheet grows back where it stands
|
|
245
|
+
// (the behavior's cancel), and the gesture's release does the usual back.
|
|
246
|
+
private fun stopBackPreview() {
|
|
247
|
+
if (backPreview == null) return
|
|
248
|
+
backPreview = null
|
|
249
|
+
behavior.cancelBackProgress()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun startBackFrames() {
|
|
253
|
+
if (backFramesObserver != null) return
|
|
254
|
+
backFramesObserver = container.viewTreeObserver.also { it.addOnPreDrawListener(backFrames) }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The listener leaves the observer it joined: a container out of the window has another one.
|
|
258
|
+
private fun stopBackFrames() {
|
|
259
|
+
val observer = backFramesObserver ?: return
|
|
260
|
+
backFramesObserver = null
|
|
261
|
+
if (observer.isAlive) observer.removeOnPreDrawListener(backFrames)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The back gesture over this sheet: the Material behavior previews its hide (`MaterialBackHandler`). */
|
|
265
|
+
private inner class BackPreview : SheetBack.Gesture {
|
|
266
|
+
override val sheet: SheetBack
|
|
267
|
+
get() = this@ModalSheet
|
|
268
|
+
|
|
269
|
+
override fun progress(event: BackEventCompat) {
|
|
270
|
+
if (backPreview !== this) return
|
|
271
|
+
behavior.updateBackProgress(event)
|
|
272
|
+
updateScrim()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
override fun cancel() {
|
|
276
|
+
if (backPreview === this) stopBackPreview()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The behavior slides the sheet down from the preview and sets its hidden state at the end: `onState` closes
|
|
280
|
+
// it there as a drag down does (`onClosedByUser`), so the modal goes through the user's path once.
|
|
281
|
+
override fun commit(): Boolean {
|
|
282
|
+
if (backPreview !== this) return false
|
|
283
|
+
backPreview = null
|
|
284
|
+
if (!hidesOnBack) {
|
|
285
|
+
behavior.cancelBackProgress()
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
backFinishing = true
|
|
289
|
+
behavior.handleBackInvoked()
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
194
294
|
private fun closeByUser() {
|
|
195
295
|
if (!swipeToDismiss) {
|
|
196
296
|
events.onCloseAttempted()
|
|
@@ -243,6 +343,7 @@ internal class ModalSheet(
|
|
|
243
343
|
when (state) {
|
|
244
344
|
BottomSheetBehavior.STATE_DRAGGING -> dragged = true
|
|
245
345
|
BottomSheetBehavior.STATE_HIDDEN -> {
|
|
346
|
+
backFinishing = false
|
|
246
347
|
when {
|
|
247
348
|
closing != null -> finishClosing()
|
|
248
349
|
else -> {
|
|
@@ -280,13 +381,23 @@ internal class ModalSheet(
|
|
|
280
381
|
// The screen under the sheet dims as the sheet goes up past `largestUndimmedDetent` (from the bottom without
|
|
281
382
|
// one), to the dim of a Material modal sheet at the next detent; while it isn't dimmed, it takes touches.
|
|
282
383
|
private fun updateScrim() {
|
|
283
|
-
|
|
384
|
+
// Hidden at the end of back's slide, it still stands at its detent until the next layout.
|
|
385
|
+
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) return setDim(0f)
|
|
386
|
+
val height = layout.height - sheetTop()
|
|
284
387
|
val undimmed = options.largestUndimmedDetent?.takeIf { it in detents }
|
|
285
388
|
val from = undimmed?.let { heightOf(it) } ?: 0
|
|
286
389
|
val to = detents.firstOrNull { heightOf(it) > from }?.let { heightOf(it) } ?: return run { setDim(0f) }
|
|
287
390
|
setDim(((height - from).toFloat() / (to - from)).coerceIn(0f, 1f))
|
|
288
391
|
}
|
|
289
392
|
|
|
393
|
+
// The top of the sheet as drawn: its offset, and while back's preview moves it, its slide and its scale toward
|
|
394
|
+
// the bottom (the behavior's pivot).
|
|
395
|
+
private fun sheetTop(): Int {
|
|
396
|
+
if (backFramesObserver == null) return container.top
|
|
397
|
+
val scaled = container.pivotY * (1f - container.scaleY)
|
|
398
|
+
return (container.top + container.translationY + scaled).roundToInt()
|
|
399
|
+
}
|
|
400
|
+
|
|
290
401
|
private fun setDim(fraction: Float) {
|
|
291
402
|
scrim.alpha = fraction * SCRIM_ALPHA
|
|
292
403
|
}
|
|
@@ -19,6 +19,7 @@ import dev.apexnative.navigation.options.Options
|
|
|
19
19
|
import dev.apexnative.navigation.utils.TestIds
|
|
20
20
|
import dev.apexnative.navigation.utils.dp
|
|
21
21
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
22
|
+
import dev.apexnative.navigation.views.animations.AnimationLayer
|
|
22
23
|
import dev.apexnative.navigation.views.animations.AnimationRules
|
|
23
24
|
import dev.apexnative.navigation.views.animations.SideAnimator
|
|
24
25
|
import dev.apexnative.navigation.views.element.ElementTransition
|
|
@@ -263,6 +264,8 @@ internal class ModalViews(
|
|
|
263
264
|
parts[record]?.container?.requestLayout()
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
override fun sheetBack(record: ModalRecord): SheetBack? = parts[record]?.sheet
|
|
268
|
+
|
|
266
269
|
// `modal.testID` on the modal's view; a sheet's on the sheet's container, `modal.sheet.testID` first, and its
|
|
267
270
|
// grabber and scrim after it.
|
|
268
271
|
private fun applyTestIds(record: ModalRecord, parts: Parts, options: Options) {
|
|
@@ -293,7 +296,11 @@ internal class ModalViews(
|
|
|
293
296
|
val under = motion.underSide?.let { side -> underView?.let { SideAnimator.build(it, side, underBase) } }
|
|
294
297
|
if (under != null && underView != null) {
|
|
295
298
|
if (showing) moved[underView] = record else moved.remove(underView)
|
|
299
|
+
AnimationLayer.during(underView, under)
|
|
296
300
|
}
|
|
301
|
+
// A fade of the modal or of the layer under it draws it through a hardware layer, given back at its end and at
|
|
302
|
+
// any stop (the set's cancel cancels it).
|
|
303
|
+
for (animator in listOfNotNull(side, fade)) AnimationLayer.during(container, animator)
|
|
297
304
|
val fadeLength = motion.side?.takeIf(AnimationRules::isSingleAlpha)?.let(AnimationRules::lengthOf)
|
|
298
305
|
val elements = motion.elements?.let { animation ->
|
|
299
306
|
val content = modalsLayer.parent as? ViewGroup ?: return@let null
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package dev.apexnative.navigation.viewcontrollers.modal
|
|
2
|
+
|
|
3
|
+
import androidx.activity.BackEventCompat
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A modal's sheet as back sees it ([ModalSheet]): whether a release of back would hide it, and the preview of that
|
|
7
|
+
* hide during the back gesture (Android 14+). [ModalCoordinator.backTargetSheet] gives the one back would dismiss now.
|
|
8
|
+
* Main thread only.
|
|
9
|
+
*/
|
|
10
|
+
internal interface SheetBack {
|
|
11
|
+
/**
|
|
12
|
+
* Back's release would hide the sheet, as a drag down does: it stands at a detent from which it goes down (its
|
|
13
|
+
* smallest, while it lets the user close it), laid out, and it isn't closing already. False at a larger detent:
|
|
14
|
+
* the Material behavior would only collapse it there.
|
|
15
|
+
*/
|
|
16
|
+
val hidesOnBack: Boolean
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Starts the preview of back on the sheet with the gesture's first [event]: the sheet shrinks a little toward the
|
|
20
|
+
* bottom with the progress, the scrim following it. None when [hidesOnBack] doesn't hold or a preview runs.
|
|
21
|
+
*/
|
|
22
|
+
fun startBackPreview(event: BackEventCompat): Gesture?
|
|
23
|
+
|
|
24
|
+
/** A back gesture over the sheet ([startBackPreview]). */
|
|
25
|
+
interface Gesture {
|
|
26
|
+
/** The sheet the gesture previews. */
|
|
27
|
+
val sheet: SheetBack
|
|
28
|
+
|
|
29
|
+
/** The gesture moved. */
|
|
30
|
+
fun progress(event: BackEventCompat)
|
|
31
|
+
|
|
32
|
+
/** Cancelled, or back went elsewhere: the sheet grows back to where it stood, the scrim with it. */
|
|
33
|
+
fun cancel()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Released: the sheet slides down from where the preview left it and goes as one the user dragged down (its
|
|
37
|
+
* events, its dismissal). False when the preview stopped since it started (a command closed or moved the
|
|
38
|
+
* sheet): back goes the usual way.
|
|
39
|
+
*/
|
|
40
|
+
fun commit(): Boolean
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package dev.apexnative.navigation.viewcontrollers.navigator
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import dev.apexnative.navigation.utils.NavigationLog
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The check of [BackCallbackState]'s triggers in debug builds (`NavigationActivity` makes it only when
|
|
8
|
+
* `BuildConfig.DEBUG`): it compares an activity's back callback with what back does now
|
|
9
|
+
* ([BackCallbackState.mismatchOf]) and writes a mismatch to the log once per state, with the screen on top. It never
|
|
10
|
+
* changes the callback, so a missing trigger behaves as in a release build. A [sample] before a frame runs at most
|
|
11
|
+
* every [SAMPLE_MILLIS] and reports a mismatch only when the sample before saw the same one: a change whose trigger
|
|
12
|
+
* comes at the end of its motion (a pane that slides, a menu dragged open) isn't reported while it moves. A back event
|
|
13
|
+
* ([atBack]) reports at once. Main thread only.
|
|
14
|
+
*/
|
|
15
|
+
internal class BackCallbackCheck(
|
|
16
|
+
private val log: (String) -> Unit = { Log.w(NavigationLog.TAG, it) },
|
|
17
|
+
) {
|
|
18
|
+
private var lastSample: Long? = null
|
|
19
|
+
private var previous: String? = null
|
|
20
|
+
private val logged = HashSet<String>()
|
|
21
|
+
|
|
22
|
+
/** Before a frame of the activity of [navigator], at [now] (`SystemClock.uptimeMillis`). */
|
|
23
|
+
fun sample(navigator: Navigator, now: Long) {
|
|
24
|
+
val last = lastSample
|
|
25
|
+
if (last != null && now - last < SAMPLE_MILLIS) return
|
|
26
|
+
lastSample = now
|
|
27
|
+
val mismatch = BackCallbackState.mismatchOf(navigator)
|
|
28
|
+
if (mismatch != null && mismatch == previous) report(mismatch)
|
|
29
|
+
previous = mismatch
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A back event reached the callback of the activity of [navigator], before it acts. */
|
|
33
|
+
fun atBack(navigator: Navigator) {
|
|
34
|
+
BackCallbackState.mismatchOf(navigator)?.let(::report)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Once per state, and not without end.
|
|
38
|
+
private fun report(mismatch: String) {
|
|
39
|
+
if (logged.size >= MAX_LOGGED || !logged.add(mismatch)) return
|
|
40
|
+
log("[ApexNavigation] $mismatch: a change of what back does came without its trigger")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
companion object {
|
|
44
|
+
/** The least time between two samples. */
|
|
45
|
+
const val SAMPLE_MILLIS = 500L
|
|
46
|
+
|
|
47
|
+
// The states logged at most: a debug check, not a second log.
|
|
48
|
+
private const val MAX_LOGGED = 32
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -2,21 +2,38 @@ package dev.apexnative.navigation.viewcontrollers.navigator
|
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import androidx.activity.OnBackPressedCallback
|
|
5
|
+
import dev.apexnative.navigation.react.events.AppearanceDispatcher
|
|
5
6
|
import dev.apexnative.navigation.utils.NavigationLog
|
|
6
7
|
import java.lang.ref.WeakReference
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Keeps the back callback of each navigation activity enabled only while back has something to do in the app: its
|
|
10
|
-
* navigation can go back ([Navigator.canHandleBack]),
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* navigation can go back ([Navigator.canHandleBack]), the screen back acts on keeps it from the system
|
|
12
|
+
* (`hardwareBackButton.systemAnimation: false`, [Navigator.keepsBackFromSystem]), or JavaScript listens to it
|
|
13
|
+
* (`BackHandler`). Otherwise the callback is disabled and the system does its own back. The system plays its back
|
|
14
|
+
* animation (to the home screen, to another task) only when no callback of the app is enabled as the gesture starts,
|
|
15
|
+
* and it decides that before the app hears of the gesture.
|
|
14
16
|
*
|
|
15
17
|
* A stale "enabled" only loses that animation; a stale "disabled" would leave the app where back has to go back. So
|
|
16
|
-
* the value is computed again
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* the value is computed again on every event that can change what back does, never by walking the layouts before
|
|
19
|
+
* each frame:
|
|
20
|
+
* - the end of each command's main-thread task, `setDefaultOptions` and `mergeOptions` too (`CommandRunner`);
|
|
21
|
+
* - a stack's operation accepted, its current child changed, and the operation ended or rejected
|
|
22
|
+
* (`StackController`'s queue host), which covers a push or a `setStackRoot` that waited for its screen, a prepared
|
|
23
|
+
* push, back of the user and the back arrow;
|
|
24
|
+
* - a layout that changed the screens it shows (`Navigator.shownScreensChanged`: the tabs, a side menu, a split view,
|
|
25
|
+
* a pair) and the end of a tab's switch;
|
|
26
|
+
* - every change of the modals (`ModalCoordinator.Host.backChanged`): a show, a dismissal by a command, by back or by
|
|
27
|
+
* the user (a sheet dragged down, its scrim), a failure, a `setRoot` that takes them;
|
|
28
|
+
* - a root that changed or showed, and the layers that went (a reload: `Navigator.destroy`);
|
|
29
|
+
* - a side menu, the panes of a split view or a search that opened or closed (the first two after their last frame),
|
|
30
|
+
* and a split view's width that changed;
|
|
31
|
+
* - the release and the cancel of a back gesture, and each back press;
|
|
32
|
+
* - JavaScript's listeners ([setJsListeners]), and the activity resumed or focused, as a safety net.
|
|
33
|
+
*
|
|
34
|
+
* Overlays don't count: back never goes to them. Each activity has its own navigator and callback; the listeners of
|
|
35
|
+
* JavaScript count for all of them. Debug builds check the triggers ([mismatchOf], `BackCallbackCheck`). Main thread
|
|
36
|
+
* only.
|
|
20
37
|
*/
|
|
21
38
|
internal object BackCallbackState {
|
|
22
39
|
private class Entry(navigator: Navigator, callback: OnBackPressedCallback) {
|
|
@@ -56,22 +73,36 @@ internal object BackCallbackState {
|
|
|
56
73
|
for (entry in entries.toList()) update(entry)
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
/** The callback of the activity of [navigator]:
|
|
76
|
+
/** The callback of the activity of [navigator]: after a change in its navigation. */
|
|
60
77
|
fun update(navigator: Navigator) {
|
|
61
78
|
entries.firstOrNull { it.navigator.get() === navigator }?.let(::update)
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Debug builds only: a callback of [navigator]'s activity that differs from what back does now — a change without
|
|
83
|
+
* its trigger —, described with the screen on top; null when it is right or not attached. It changes nothing.
|
|
84
|
+
*/
|
|
85
|
+
fun mismatchOf(navigator: Navigator): String? {
|
|
86
|
+
val callback = entries.firstOrNull { it.navigator.get() === navigator }?.callback?.get() ?: return null
|
|
87
|
+
val expected = jsListeners || takesBack(navigator)
|
|
88
|
+
if (callback.isEnabled == expected) return null
|
|
89
|
+
val top = navigator.visibleLayout?.let { AppearanceDispatcher.visibleNode(it).id } ?: "none"
|
|
90
|
+
val state = if (callback.isEnabled) "enabled" else "disabled"
|
|
91
|
+
val back = if (expected) "back stays in the app" else "back leaves the app"
|
|
92
|
+
return "the back callback is $state where $back (on top: '$top')"
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
// Enables or disables the callback when what back does changed.
|
|
65
96
|
private fun update(entry: Entry) {
|
|
66
97
|
val callback = entry.callback.get() ?: return
|
|
67
|
-
val enabled = jsListeners ||
|
|
98
|
+
val enabled = jsListeners || takesBack(entry.navigator.get())
|
|
68
99
|
if (callback.isEnabled != enabled) callback.isEnabled = enabled
|
|
69
100
|
}
|
|
70
101
|
|
|
71
|
-
// A layout that fails to answer leaves back to the navigation, as before: this runs
|
|
72
|
-
// exception here would end the app.
|
|
73
|
-
private fun
|
|
74
|
-
navigator
|
|
102
|
+
// A layout that fails to answer leaves back to the navigation, as before: this runs from the triggers of layouts
|
|
103
|
+
// and animators, and an exception here would end the app.
|
|
104
|
+
private fun takesBack(navigator: Navigator?): Boolean = try {
|
|
105
|
+
navigator != null && (navigator.canHandleBack() || navigator.keepsBackFromSystem())
|
|
75
106
|
} catch (e: RuntimeException) {
|
|
76
107
|
if (!failureLogged) {
|
|
77
108
|
failureLogged = true
|