@apexnative/navigation 1.0.0-alpha.5 → 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 +25 -0
- package/README.md +1 -1
- package/android/CLAUDE.md +14 -12
- package/android/CORE.md +9 -6
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +36 -5
- package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +0 -2
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +12 -49
- package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -6
- 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 +9 -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 +13 -10
- 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 +7 -8
- 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/Navigator.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
- 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 +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
- 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/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/docs-md/architecture/transitions.md +4 -2
- package/docs-md/containers/modals.md +2 -0
- 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/help/glossary.md +2 -2
- package/docs-md/help/troubleshooting.md +21 -0
- 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 +14 -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/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 +24 -20
- 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/ANNJavaScriptCalls.h +11 -12
- package/ios/Core/ANNJavaScriptCalls.mm +7 -16
- 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/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 +57 -5
- 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/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/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/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/screens.d.ts +20 -6
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +12 -7
- 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/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/hooks/useSafeAreaInsets.ts +4 -3
- package/src/api/options/OptionsChain.ts +2 -2
- package/src/engine/commands.ts +30 -6
- package/src/engine/screens.tsx +29 -8
- package/src/interfaces/options/animations.ts +12 -7
- package/src/interfaces/options/bottomTabs.ts +18 -5
- package/src/interfaces/options/buttons.ts +8 -8
- 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/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/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
|
+
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt
CHANGED
|
@@ -33,6 +33,7 @@ import dev.apexnative.navigation.viewcontrollers.externalcomponent.ExternalCompo
|
|
|
33
33
|
import dev.apexnative.navigation.viewcontrollers.modal.ModalCoordinator
|
|
34
34
|
import dev.apexnative.navigation.viewcontrollers.modal.ModalRecord
|
|
35
35
|
import dev.apexnative.navigation.viewcontrollers.modal.ModalViews
|
|
36
|
+
import dev.apexnative.navigation.viewcontrollers.modal.SheetBack
|
|
36
37
|
import dev.apexnative.navigation.viewcontrollers.overlay.OverlayCoordinator
|
|
37
38
|
import dev.apexnative.navigation.viewcontrollers.overlay.OverlayRecord
|
|
38
39
|
import dev.apexnative.navigation.viewcontrollers.overlay.OverlayViews
|
|
@@ -177,6 +178,9 @@ class Navigator(
|
|
|
177
178
|
return if (modalRoot != null) modalRoot.stackPoppedByBack() else root?.stackPoppedByBack()
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
/** The sheet [handleBack] would dismiss, as a drag down of the user would; none when back would do anything else. */
|
|
182
|
+
internal fun sheetDismissedByBack(): SheetBack? = modals.backTargetSheet()
|
|
183
|
+
|
|
180
184
|
/**
|
|
181
185
|
* [layer] may wait to show from now on ([WaitingLayer]): the nodes in it ask it through [isInWaitingLayer] and
|
|
182
186
|
* [shownScreensChanged]. The root's layer is there from the start.
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt
CHANGED
|
@@ -129,12 +129,9 @@ abstract class ParentController<V : ViewGroup>(
|
|
|
129
129
|
open fun leavesTopEdgeFree(child: ViewController<*>): Boolean =
|
|
130
130
|
parentController?.leavesTopEdgeFree(this) ?: false
|
|
131
131
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*/
|
|
136
|
-
open fun leavesBottomEdgeFree(child: ViewController<*>, screen: ViewController<*> = child): Boolean =
|
|
137
|
-
parentController?.leavesBottomEdgeFree(this, screen) ?: false
|
|
132
|
+
/** Whether a large title shows over [child]: its stack shows the bar with one. No by default. */
|
|
133
|
+
open fun showsLargeTitleOver(child: ViewController<*>): Boolean =
|
|
134
|
+
parentController?.showsLargeTitleOver(this) ?: false
|
|
138
135
|
|
|
139
136
|
/**
|
|
140
137
|
* The tab bar at rest under [child]: where a stack's FAB keeps clear of it, by the options of [screen], the stack's
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt
CHANGED
|
@@ -2,18 +2,18 @@ package dev.apexnative.navigation.viewcontrollers.stack
|
|
|
2
2
|
|
|
3
3
|
import android.view.View
|
|
4
4
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
5
|
-
import dev.apexnative.navigation.react.
|
|
5
|
+
import dev.apexnative.navigation.react.ScreenEnvironment
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Where a screen prepared for a push (`preparePush`) shows, in dp: the size of the container its slot fills and the
|
|
9
9
|
* padding of its safe area view, as the push lays it out. JavaScript renders the screen with them before the push, and
|
|
10
10
|
* the slot and the safe area view start their states with them.
|
|
11
11
|
*/
|
|
12
|
-
internal data class PreparedFrame(val width: Float, val height: Float, val padding:
|
|
12
|
+
internal data class PreparedFrame(val width: Float, val height: Float, val padding: ScreenEnvironment.Insets) {
|
|
13
13
|
companion object {
|
|
14
14
|
/** The size of [container] as its slot measures it (`SlotView`), so the slot finds it equal. */
|
|
15
15
|
@JvmStatic
|
|
16
|
-
fun of(container: View, padding:
|
|
16
|
+
fun of(container: View, padding: ScreenEnvironment.Insets) =
|
|
17
17
|
PreparedFrame(container.width.toFloat().pxToDp(), container.height.toFloat().pxToDp(), padding)
|
|
18
18
|
}
|
|
19
19
|
}
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt
CHANGED
|
@@ -11,7 +11,7 @@ import dev.apexnative.navigation.options.DefaultOptions
|
|
|
11
11
|
import dev.apexnative.navigation.react.CommandAnswer
|
|
12
12
|
import dev.apexnative.navigation.react.CommandListener
|
|
13
13
|
import dev.apexnative.navigation.react.ErrorTexts
|
|
14
|
-
import dev.apexnative.navigation.react.
|
|
14
|
+
import dev.apexnative.navigation.react.ScreenEnvironment
|
|
15
15
|
import dev.apexnative.navigation.slots.SlotScreenView
|
|
16
16
|
import dev.apexnative.navigation.utils.NavigationLog
|
|
17
17
|
import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
|
|
@@ -318,7 +318,7 @@ internal class StackCommands(
|
|
|
318
318
|
layoutNow(stackView, screenView)
|
|
319
319
|
node.updateSafeArea()
|
|
320
320
|
val content = node.content?.asView()
|
|
321
|
-
val padding =
|
|
321
|
+
val padding = ScreenEnvironment.padding(node.id)
|
|
322
322
|
val noInsets = content == null || ViewCompat.getRootWindowInsets(content) == null
|
|
323
323
|
if (noInsets || padding == null || content.width <= 0 || content.height <= 0) {
|
|
324
324
|
prepared.discard(node.id)
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt
CHANGED
|
@@ -594,6 +594,10 @@ class StackController(
|
|
|
594
594
|
override fun leavesTopEdgeFree(child: ViewController<*>): Boolean =
|
|
595
595
|
TopBarRoom.leavesTopEdgeFree(resolvedOf(child), largeTitleShown(child)) || super.leavesTopEdgeFree(child)
|
|
596
596
|
|
|
597
|
+
/** A large title shows over [child] in this stack's bar. */
|
|
598
|
+
override fun showsLargeTitleOver(child: ViewController<*>): Boolean =
|
|
599
|
+
largeTitleShown(child) || super.showsLargeTitleOver(child)
|
|
600
|
+
|
|
597
601
|
/** A screen under another doesn't compute its safe area again; the prepared and waiting do. */
|
|
598
602
|
override fun isTopOfStack(child: ViewController<*>): Boolean =
|
|
599
603
|
child === queue.currentChild || child === queue.waitingScreen || isPrepared(child)
|
|
@@ -15,6 +15,7 @@ import dev.apexnative.navigation.viewcontrollers.component.ComponentViewControll
|
|
|
15
15
|
import dev.apexnative.navigation.viewcontrollers.externalcomponent.ExternalComponentViewController
|
|
16
16
|
import dev.apexnative.navigation.viewcontrollers.stack.StackDisplay.Motion
|
|
17
17
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
18
|
+
import dev.apexnative.navigation.views.animations.AnimationLayer
|
|
18
19
|
import dev.apexnative.navigation.views.animations.AnimationRules
|
|
19
20
|
import dev.apexnative.navigation.views.animations.SideAnimator
|
|
20
21
|
import dev.apexnative.navigation.views.animations.SystemTransition
|
|
@@ -157,6 +158,8 @@ internal class StackViews(private val stack: StackController) : StackDisplay {
|
|
|
157
158
|
custom.alpha?.let { view.alpha = it }
|
|
158
159
|
val own = custom.side?.let { SideAnimator.build(view, it, SideAnimator.Base(alpha = custom.alpha)) }
|
|
159
160
|
val fade = if (own == null) StackTransitions.fade(view, custom.fade) else null
|
|
161
|
+
// A fade of the whole screen draws it through a hardware layer, as the default transition does ([dress]).
|
|
162
|
+
for (animator in listOfNotNull(own, fade)) AnimationLayer.during(view, animator)
|
|
160
163
|
val elements = custom.elements?.let { elements ->
|
|
161
164
|
val layout = layout ?: return@let null
|
|
162
165
|
val fadeLength = custom.side?.takeIf(AnimationRules::isSingleAlpha)?.let(AnimationRules::lengthOf)
|
|
@@ -13,8 +13,8 @@ import java.lang.ref.WeakReference
|
|
|
13
13
|
* tag) is in the screen, found once per tag —, not the horizontal ones, nor those of a modal, an overlay or another
|
|
14
14
|
* screen; a slot screen's lists are in its container too. While a list is dragged, each follower gets the drag as a
|
|
15
15
|
* translation to follow; when the drag ends, the way to settle; at every scroll event, the drags' and the inertia's,
|
|
16
|
-
* the offset. It listens to React Native's
|
|
17
|
-
*
|
|
16
|
+
* the offset. It listens to React Native's scroll views ([ScrollEvents]) only while it has followers and its screen's
|
|
17
|
+
* view. Released with the screen. Main thread only.
|
|
18
18
|
*/
|
|
19
19
|
class ScrollEventListener() {
|
|
20
20
|
/** Follows the content while it is dragged. */
|
|
@@ -105,15 +105,12 @@ class ScrollEventListener() {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Listens to React Native's scroll
|
|
109
|
-
*
|
|
110
|
-
* window.
|
|
108
|
+
* Listens to React Native's scroll views when followers wait for them and it doesn't yet, once it has the screen's
|
|
109
|
+
* view to tell its own lists by; asked again as the screen joins a window.
|
|
111
110
|
*/
|
|
112
111
|
internal fun attachIfNeeded() {
|
|
113
|
-
if (released || stopListening != null || registrations.isEmpty()) return
|
|
114
|
-
|
|
115
|
-
stopListening = ScrollEvents.listen(context, ::onScrollEvent)
|
|
116
|
-
if (stopListening == null) ScrollEvents.waitForDispatcher(this)
|
|
112
|
+
if (released || stopListening != null || registrations.isEmpty() || scope?.get() == null) return
|
|
113
|
+
stopListening = ScrollEvents.listen(::onScrollEvent)
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
/** The screen is destroyed: no follower hears it any more. */
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package dev.apexnative.navigation.views.animations
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.AnimatorSet
|
|
6
|
+
import android.animation.ObjectAnimator
|
|
7
|
+
import android.view.View
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A hardware layer on a view for the time of an animator that fades it. A view whose `alpha` is below 1 and whose
|
|
11
|
+
* rendering overlaps (any `ViewGroup` by default) is drawn into an offscreen buffer at every frame; through a layer it
|
|
12
|
+
* is drawn once and the GPU only composites it. The layer is taken as the animator starts and only from
|
|
13
|
+
* `LAYER_TYPE_NONE` (a layer someone else set stays theirs), and given back as it ends or is cancelled. A pure
|
|
14
|
+
* translation or scale needs none: RenderThread draws it. The stack's default transition has its own
|
|
15
|
+
* (`StackViews.dress`). Main thread only.
|
|
16
|
+
*/
|
|
17
|
+
internal object AnimationLayer {
|
|
18
|
+
private const val ALPHA = "alpha"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* [view] draws through a hardware layer while [animator] plays, when [animator] changes the `alpha` of [view] and
|
|
22
|
+
* the view's rendering overlaps; [animator] itself, for chaining.
|
|
23
|
+
*/
|
|
24
|
+
fun during(view: View, animator: Animator): Animator {
|
|
25
|
+
if (!changesAlpha(animator, view)) return animator
|
|
26
|
+
val hold = Hold({ view.layerType }, { view.setLayerType(it, null) })
|
|
27
|
+
animator.addListener(object : AnimatorListenerAdapter() {
|
|
28
|
+
override fun onAnimationStart(animation: Animator) {
|
|
29
|
+
hold.take(changesAlpha = true, overlapping = view.hasOverlappingRendering())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun onAnimationCancel(animation: Animator) = hold.release()
|
|
33
|
+
|
|
34
|
+
override fun onAnimationEnd(animation: Animator) = hold.release()
|
|
35
|
+
})
|
|
36
|
+
return animator
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether an animation takes a layer: it fades, its view's rendering overlaps, and the view has no layer. */
|
|
40
|
+
fun takes(changesAlpha: Boolean, overlapping: Boolean, layerType: Int): Boolean =
|
|
41
|
+
changesAlpha && overlapping && layerType == View.LAYER_TYPE_NONE
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The layer one animation took on one view, through [layerType] and [setLayerType]: taken at most once at a time,
|
|
45
|
+
* given back once, and only while it is still the hardware layer it set.
|
|
46
|
+
*/
|
|
47
|
+
class Hold(private val layerType: () -> Int, private val setLayerType: (Int) -> Unit) {
|
|
48
|
+
/** Whether the animation holds the view's layer now. */
|
|
49
|
+
var isTaken = false
|
|
50
|
+
private set
|
|
51
|
+
|
|
52
|
+
/** The animation starts: the layer, when [takes] says so. */
|
|
53
|
+
fun take(changesAlpha: Boolean, overlapping: Boolean) {
|
|
54
|
+
if (isTaken || !takes(changesAlpha, overlapping, layerType())) return
|
|
55
|
+
setLayerType(View.LAYER_TYPE_HARDWARE)
|
|
56
|
+
isTaken = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The animation ended or stopped: the layer goes, unless someone changed it meanwhile. */
|
|
60
|
+
fun release() {
|
|
61
|
+
if (!isTaken) return
|
|
62
|
+
isTaken = false
|
|
63
|
+
if (layerType() == View.LAYER_TYPE_HARDWARE) setLayerType(View.LAYER_TYPE_NONE)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Whether [animator], or an animator in it, writes the alpha of [view].
|
|
68
|
+
private fun changesAlpha(animator: Animator, view: View): Boolean = when (animator) {
|
|
69
|
+
is AnimatorSet -> animator.childAnimations.any { changesAlpha(it, view) }
|
|
70
|
+
is ObjectAnimator -> animator.target === view && animator.values.orEmpty().any { it.propertyName == ALPHA }
|
|
71
|
+
else -> false
|
|
72
|
+
}
|
|
73
|
+
}
|
package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt
CHANGED
|
@@ -18,6 +18,18 @@ internal object ValueInterpolation {
|
|
|
18
18
|
else -> from + fraction * (to - from)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* An angle in degrees at [fraction] of the way along the shorter arc, as iOS turns a shared element: 0 → 270 turns
|
|
23
|
+
* back by 90, 170 → −170 turns on by 20; half a turn goes on. At the end of the time exactly [to].
|
|
24
|
+
*/
|
|
25
|
+
fun angle(from: Float, to: Float, fraction: Float, time: Float = fraction): Float {
|
|
26
|
+
if (time <= 0f) return from
|
|
27
|
+
if (time >= 1f) return to
|
|
28
|
+
val forward = ((to - from) % FULL_TURN + FULL_TURN) % FULL_TURN
|
|
29
|
+
val turn = if (forward > HALF_TURN) forward - FULL_TURN else forward
|
|
30
|
+
return from + fraction * turn
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* A color (an sRGB color int, as React Native keeps a background; none is transparent) at [fraction] of the way
|
|
23
35
|
* from [from] to [to]: L, a and b in Lab (D65), the alpha linear, the result rounded; at the end of the time
|
|
@@ -90,4 +102,6 @@ internal object ValueInterpolation {
|
|
|
90
102
|
private const val WHITE_Z = 108.883
|
|
91
103
|
private const val EPSILON = 0.008856
|
|
92
104
|
private const val KAPPA = 903.3
|
|
105
|
+
private const val FULL_TURN = 360f
|
|
106
|
+
private const val HALF_TURN = 180f
|
|
93
107
|
}
|