@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.
Files changed (235) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +23 -14
  4. package/android/CORE.md +32 -19
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +60 -13
  6. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  7. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  8. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  10. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
  11. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
  12. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  13. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  14. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -6
  15. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +19 -51
  17. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +30 -39
  19. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  21. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +12 -10
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +10 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +22 -21
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +12 -9
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -5
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +13 -5
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +2 -5
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +4 -9
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  53. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  56. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  57. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  63. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  64. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  65. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  66. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  67. package/cli/doctor/checks/splash.js +4 -22
  68. package/cli/splash/index.js +3 -1
  69. package/docs-md/architecture/transitions.md +4 -2
  70. package/docs-md/containers/modals.md +4 -2
  71. package/docs-md/containers/overlays.md +1 -1
  72. package/docs-md/containers/side-menu.md +1 -1
  73. package/docs-md/containers/tabs.md +2 -0
  74. package/docs-md/flows/bottom-sheets.md +1 -1
  75. package/docs-md/fundamentals/starting-the-app.md +1 -1
  76. package/docs-md/guides/cli.md +1 -2
  77. package/docs-md/help/glossary.md +2 -2
  78. package/docs-md/help/troubleshooting.md +23 -2
  79. package/docs-md/index.json +12 -0
  80. package/docs-md/migration/from-expo-router.md +1 -1
  81. package/docs-md/migration/from-react-native-navigation.md +1 -1
  82. package/docs-md/migration/from-react-navigation.md +2 -2
  83. package/docs-md/screen-ui/back-and-gestures.md +36 -0
  84. package/docs-md/screen-ui/bar-buttons.md +4 -4
  85. package/docs-md/screen-ui/search.md +1 -1
  86. package/docs-md/screen-ui/system-ui.md +16 -11
  87. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  88. package/docs-md/screen-ui/top-bar.md +8 -4
  89. package/docs-md/start/installation.md +1 -1
  90. package/ios/Animations/ANNFrameDriver.mm +2 -0
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  92. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  93. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  94. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  95. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  96. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  97. package/ios/CLAUDE.md +17 -15
  98. package/ios/CORE.md +30 -26
  99. package/ios/Contract/ANNCommandAnswer.h +9 -2
  100. package/ios/Contract/ANNCommandAnswer.mm +25 -12
  101. package/ios/Contract/ANNCommandRunner.h +8 -7
  102. package/ios/Contract/ANNCommandRunner.mm +21 -27
  103. package/ios/Contract/ANNLayoutNode.h +3 -3
  104. package/ios/Contract/ANNLayoutReader.h +3 -3
  105. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  106. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  107. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  108. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  109. package/ios/Core/ANNNavigationState.h +4 -6
  110. package/ios/Core/ANNNavigationState.mm +0 -4
  111. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  112. package/ios/Core/ApexNavigation+Internal.h +1 -1
  113. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  114. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  115. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  116. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  117. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  118. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  119. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  120. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  121. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  122. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  123. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  124. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  125. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  126. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  127. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  128. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  129. package/ios/Layouts/Stack/ANNPreviewPush.mm +142 -27
  130. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  131. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  132. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  133. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  134. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  135. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  136. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  137. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  138. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  139. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  140. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  141. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  142. package/ios/Options/ANNOptions.h +18 -0
  143. package/ios/Options/ANNOptions.mm +112 -1
  144. package/ios/Options/ANNOptionsReader.mm +110 -0
  145. package/ios/Options/Params/ANNParam.h +15 -0
  146. package/ios/Options/Params/ANNParam.mm +61 -0
  147. package/ios/Options/Params/Color.h +3 -0
  148. package/ios/Options/Params/Color.mm +21 -0
  149. package/ios/Options/Params/Image.h +4 -0
  150. package/ios/Options/Params/Image.mm +20 -0
  151. package/ios/React/ANNReactView.mm +6 -25
  152. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  153. package/ios/React/ANNSurfaceHostingView.h +6 -0
  154. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  155. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  156. package/ios/TopBar/ANNBarAppearance.h +2 -1
  157. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  158. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  159. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  160. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  161. package/ios/TopBar/ANNBarButtons.h +5 -3
  162. package/ios/TopBar/ANNBarButtons.mm +186 -17
  163. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  164. package/ios/TopBar/ANNMainList.h +3 -0
  165. package/ios/TopBar/ANNMainList.mm +4 -0
  166. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  167. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  168. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  169. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  170. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  171. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  172. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  173. package/lib/module/ARCHITECTURE.md +8 -4
  174. package/lib/module/api/commands/commands.js +54 -6
  175. package/lib/module/api/commands/commands.js.map +1 -1
  176. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  177. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  178. package/lib/module/api/options/OptionsChain.js +2 -2
  179. package/lib/module/engine/commands.js +21 -1
  180. package/lib/module/engine/commands.js.map +1 -1
  181. package/lib/module/engine/places.js +8 -0
  182. package/lib/module/engine/places.js.map +1 -1
  183. package/lib/module/engine/screens.js +29 -4
  184. package/lib/module/engine/screens.js.map +1 -1
  185. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  186. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  187. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  188. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  189. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  190. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  191. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/places.d.ts +2 -0
  194. package/lib/typescript/engine/places.d.ts.map +1 -1
  195. package/lib/typescript/engine/screens.d.ts +20 -6
  196. package/lib/typescript/engine/screens.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/Options.d.ts +1 -1
  198. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/animations.d.ts +30 -9
  200. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  202. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  204. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  205. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  206. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  207. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  208. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  209. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  210. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  211. package/package.json +1 -1
  212. package/src/ARCHITECTURE.md +8 -4
  213. package/src/api/app/createNavigation.ts +3 -3
  214. package/src/api/commands/commands.ts +48 -7
  215. package/src/api/commands/presentation.ts +4 -3
  216. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  217. package/src/api/options/OptionsChain.ts +2 -2
  218. package/src/engine/commands.ts +30 -6
  219. package/src/engine/places.ts +9 -0
  220. package/src/engine/screens.tsx +29 -8
  221. package/src/interfaces/Options.ts +1 -0
  222. package/src/interfaces/options/animations.ts +31 -9
  223. package/src/interfaces/options/bottomTabs.ts +18 -5
  224. package/src/interfaces/options/buttons.ts +8 -8
  225. package/src/interfaces/options/stack.ts +9 -0
  226. package/src/interfaces/options/system.ts +8 -5
  227. package/src/interfaces/options/toolbar.ts +2 -2
  228. package/src/interfaces/options/topBar.ts +32 -29
  229. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  230. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  231. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  232. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
  233. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  234. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  235. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
@@ -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
@@ -40,7 +41,6 @@ import dev.apexnative.navigation.viewcontrollers.parent.ParentController
40
41
  import dev.apexnative.navigation.viewcontrollers.stack.PreparedPushes
41
42
  import dev.apexnative.navigation.viewcontrollers.stack.StackCommands
42
43
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
43
- import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
44
44
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
45
45
  import dev.apexnative.navigation.views.animations.FadeThrough
46
46
  import dev.apexnative.navigation.views.animations.SideAnimator
@@ -105,7 +105,10 @@ class Navigator(
105
105
  readiness = ContentReadiness,
106
106
  scheduler = scheduler,
107
107
  firstRoot = firstRootDraw,
108
- onRootChanged = { root = it },
108
+ onRootChanged = {
109
+ root = it
110
+ BackCallbackState.update(this)
111
+ },
109
112
  onRootShown = {
110
113
  onRootShown?.invoke()
111
114
  BackCallbackState.update(this)
@@ -142,10 +145,21 @@ class Navigator(
142
145
 
143
146
  /**
144
147
  * Whether back has something to do in the navigation: the mirror of [handleBack], without acting — the top modal
145
- * first, then the root. The back callback asks it before each frame (`BackCallbackState`).
148
+ * first, then the root. The back callback asks it whenever what back does can have changed (`BackCallbackState`).
146
149
  */
147
150
  fun canHandleBack(): Boolean = modals.canHandleBack() ?: (root?.canHandleBack() == true)
148
151
 
152
+ /**
153
+ * Whether back stays in the app also when the navigation has nothing to do: the layout back acts on — the top
154
+ * modal's, else the root — shows a screen with `hardwareBackButton.systemAnimation: false`. The system then doesn't
155
+ * play its animation of going home, and the press does the activity's own back ([handleBack] is false). The back
156
+ * callback asks it with [canHandleBack] (`BackCallbackState`).
157
+ */
158
+ fun keepsBackFromSystem(): Boolean {
159
+ val layout = modals.backTargetRoot() ?: root ?: return false
160
+ return layout.resolveShownOptions(DefaultOptions.current).hardwareBackButton.systemAnimation.isFalse
161
+ }
162
+
149
163
  /**
150
164
  * The navigation's step of back, after JavaScript's listeners didn't take it: the top modal first (its content,
151
165
  * else it closes or holds), else the root does the first thing it can (a stack's top leaves or holds, an open side
@@ -164,6 +178,9 @@ class Navigator(
164
178
  return if (modalRoot != null) modalRoot.stackPoppedByBack() else root?.stackPoppedByBack()
165
179
  }
166
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
+
167
184
  /**
168
185
  * [layer] may wait to show from now on ([WaitingLayer]): the nodes in it ask it through [isInWaitingLayer] and
169
186
  * [shownScreensChanged]. The root's layer is there from the start.
@@ -187,12 +204,14 @@ class Navigator(
187
204
  * A layout of [node] (a stack, the tabs, a side menu, a split view, a pair) changed the screens it shows: when it
188
205
  * is in a layer that still waits, the layer waits for the screens it shows now, and no longer for one that went
189
206
  * ([WaitingLayer.shownScreensChanged]); so does the operation of its stack that waits for [node] as its entry (a
190
- * pushed pair); a modal's sheet takes the hold of the screen it shows now.
207
+ * pushed pair); a modal's sheet takes the hold of the screen it shows now; what back does is computed again (the
208
+ * tab, the pane or the menu back goes to, and the options of the screen it shows).
191
209
  */
192
210
  internal fun shownScreensChanged(node: ViewController<*>) {
193
211
  waitingLayerOf(node)?.shownScreensChanged()
194
212
  node.findParentStack()?.queue?.shownScreensChanged(node)
195
213
  modals.visibleScreenChanged(node)
214
+ BackCallbackState.update(this)
196
215
  }
197
216
 
198
217
  // The waiting layer [node] is in, up through the layouts around it; none.
@@ -435,7 +454,8 @@ class Navigator(
435
454
  destroyLayers(answerPending = false)
436
455
  // Nothing the old screens told of their readiness holds for the screens of the new JavaScript.
437
456
  ScreenReadiness.reset()
438
- BarComponents.reset()
457
+ // Without layers back leaves the app until the new JavaScript sets its root.
458
+ BackCallbackState.update(this)
439
459
  }
440
460
 
441
461
  /**
@@ -488,8 +488,8 @@ internal class RootCoordinator(
488
488
  }
489
489
 
490
490
  // The navigation state goes once, when the root is in place and its layers have shown, right before the answer:
491
- // what the old root's destroy, the root's appearance and each layer ask for meanwhile waits (StateHold). Taken once
492
- // per `setRoot`, before the old root goes.
491
+ // what the old root's destroy, the root's appearance and each layer ask for meanwhile waits (the hold of
492
+ // NavigationState). Taken once per `setRoot`, before the old root goes.
493
493
  private fun holdState(pending: Pending) {
494
494
  if (pending.holdsState) return
495
495
  pending.holdsState = true
@@ -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
- * Whether the safe area of [child] leaves its bottom edge free by default: the tabs show with
134
- * `bottomTabs.hideOnScroll` of [screen], the screen that asks.
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
@@ -153,6 +153,9 @@ class SplitViewController(
153
153
 
154
154
  private fun onWidthChanged(layout: SplitViewLayout) {
155
155
  primary.getView().visibility = if (primaryHidden && layout.isWide == true) View.GONE else View.VISIBLE
156
+ // Compact, side by side or one over the other decides what back does from this measure on: the callback only
157
+ // takes its value here, nothing moves; the panes' end below computes it again.
158
+ BackCallbackState.update()
156
159
  // Where a later `compactTopColumn` puts the pane over the other at this switch: true open, false closed.
157
160
  val settle = if (measured && isSliding) laterTopColumn?.let { it == SECONDARY } else null
158
161
  if (!measured) {
@@ -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.ScreenInsets
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: ScreenInsets.Insets) {
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: ScreenInsets.Insets) =
16
+ fun of(container: View, padding: ScreenEnvironment.Insets) =
17
17
  PreparedFrame(container.width.toFloat().pxToDp(), container.height.toFloat().pxToDp(), padding)
18
18
  }
19
19
  }
@@ -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.ScreenInsets
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 = ScreenInsets.padding(node.id)
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)
@@ -270,8 +270,8 @@ class StackController(
270
270
 
271
271
  /**
272
272
  * The default pop of the top is what back does now and it can be previewed: two entries or more, nothing running
273
- * or waiting, no open search, the top doesn't hold back and pops with the default transition, the screen under it
274
- * has its view, and the stack shows.
273
+ * or waiting, no open search, the top doesn't hold back, pops with the default transition and doesn't turn the
274
+ * preview off (`animations.pop.preview: false`), the screen under it has its view, and the stack shows.
275
275
  */
276
276
  override fun stackPoppedByBack(): StackController? {
277
277
  if (topBar.isSearchOpen() || !queue.isAtRest) return null
@@ -282,9 +282,8 @@ class StackController(
282
282
  if (!queue.isRendered(target) || backGesture != null) return null
283
283
  val options = resolvedOf(top)
284
284
  val pop = options.animations.pop
285
- if (options.hardwareBackButton.popStackOnPress.isFalse || pop.enabled.isFalse || AnimationRules.stackIsOwn(pop)) {
286
- return null
287
- }
285
+ if (options.hardwareBackButton.popStackOnPress.isFalse || pop.enabled.isFalse) return null
286
+ if (pop.preview.isFalse || AnimationRules.stackIsOwn(pop)) return null
288
287
  val display = queue.display
289
288
  return this.takeIf { display.isOnScreen && display.canAnimate }
290
289
  }
@@ -595,6 +594,10 @@ class StackController(
595
594
  override fun leavesTopEdgeFree(child: ViewController<*>): Boolean =
596
595
  TopBarRoom.leavesTopEdgeFree(resolvedOf(child), largeTitleShown(child)) || super.leavesTopEdgeFree(child)
597
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
+
598
601
  /** A screen under another doesn't compute its safe area again; the prepared and waiting do. */
599
602
  override fun isTopOfStack(child: ViewController<*>): Boolean =
600
603
  child === queue.currentChild || child === queue.waitingScreen || isPrepared(child)
@@ -694,15 +697,20 @@ class StackController(
694
697
  private fun optionsScreen(node: ViewController<*>): ViewController<*> =
695
698
  (node as? OptionsScreen)?.optionsScreen ?: node
696
699
 
700
+ // What back does is computed again on each change of the queue (BackCallbackState): its entries change as an
701
+ // operation is accepted, the screen whose options count as its current child changes (a push that waited for its
702
+ // screen begins after its command), and an operation ends or rejects.
697
703
  private inner class QueueHost : StackQueue.Host {
698
704
  override fun operationAccepted() {
699
705
  backGesture?.abort()
706
+ BackCallbackState.update()
700
707
  }
701
708
 
702
709
  // The bar takes the options of the new top at once, also without appearance events.
703
710
  override fun currentChildChanged() {
704
711
  updateBar()
705
712
  navigator?.shownScreensChanged(this@StackController)
713
+ BackCallbackState.update()
706
714
  }
707
715
 
708
716
  // Before the bar takes the new top's options: the row's picture or the bar's travel, and how it shows or hides.
@@ -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)
@@ -36,6 +36,8 @@ import dev.apexnative.navigation.views.stack.topbar.search.SuggestionList
36
36
  class ScreenSearch(
37
37
  private val host: Host,
38
38
  private val screenId: String,
39
+ /** The sender of the JavaScript the screen was made for: silent once it went. */
40
+ private val emitter: EventEmitter,
39
41
  private val context: Context,
40
42
  private val loadIcon: (String) -> Drawable?,
41
43
  /** A scope the user picks goes into the screen's options, so it stays when they apply again. */
@@ -45,7 +47,6 @@ class ScreenSearch(
45
47
  /** The search changed what the bar shows (open or closed, its text): shown again if it is the shown one. */
46
48
  fun refresh(search: ScreenSearch)
47
49
  fun isShown(search: ScreenSearch): Boolean
48
- fun emitter(): EventEmitter?
49
50
  fun keyboard(view: View, show: Boolean)
50
51
  /** The stack's view: a search view goes over it. */
51
52
  fun stackView(): ViewGroup?
@@ -402,7 +403,7 @@ class ScreenSearch(
402
403
 
403
404
  // A pick sends searchSuggestionPicked with the suggestion's id; the text stays.
404
405
  private fun pickSuggestion(suggestionId: String) {
405
- host.emitter()?.emitSearchSuggestionPicked(screenId, suggestionId)
406
+ emitter.emitSearchSuggestionPicked(screenId, suggestionId)
406
407
  setSuggestions(emptyList())
407
408
  }
408
409
 
@@ -426,18 +427,15 @@ class ScreenSearch(
426
427
  private fun sendUpdate(
427
428
  focused: Boolean = isActive && (panel?.searchView?.editText ?: searchField.editText).hasFocus(),
428
429
  ) {
429
- val emitter = host.emitter() ?: return
430
430
  eventTargets().forEach { emitter.emitSearchBarUpdated(it, text, focused, scope()) }
431
431
  }
432
432
 
433
433
  private fun sendCancel() {
434
- val emitter = host.emitter() ?: return
435
434
  eventTargets().forEach { emitter.emitSearchBarCancelPressed(it) }
436
435
  sendUpdate()
437
436
  }
438
437
 
439
438
  private fun sendSubmit() {
440
- val emitter = host.emitter() ?: return
441
439
  eventTargets().forEach { emitter.emitSearchBarSubmitted(it, text, scope()) }
442
440
  }
443
441
 
@@ -12,7 +12,6 @@ import androidx.appcompat.content.res.AppCompatResources
12
12
  import androidx.coordinatorlayout.widget.CoordinatorLayout
13
13
  import androidx.core.view.WindowCompat
14
14
  import androidx.core.view.WindowInsetsCompat
15
- import com.facebook.react.ReactApplication
16
15
  import dev.apexnative.navigation.options.SearchBarOptions
17
16
  import dev.apexnative.navigation.options.params.ThemeColour
18
17
  import dev.apexnative.navigation.react.events.EventEmitter
@@ -137,9 +136,6 @@ class StackSearchPresenter(
137
136
  refresh()
138
137
  }
139
138
 
140
- override fun emitter(): EventEmitter? =
141
- (activity.application as? ReactApplication)?.reactHost?.currentReactContext?.let { EventEmitter(it) }
142
-
143
139
  override fun keyboard(view: View, show: Boolean) {
144
140
  val controller = WindowCompat.getInsetsController(activity.window, view)
145
141
  if (show) controller.show(WindowInsetsCompat.Type.ime()) else controller.hide(WindowInsetsCompat.Type.ime())
@@ -150,7 +146,8 @@ class StackSearchPresenter(
150
146
  // The search of a pair of screens is its primary's: its events name that screen, which keeps the scope in
151
147
  // its user layer, where a merge that sends the same options again doesn't take it away.
152
148
  val owner = (screen as? OptionsScreen)?.optionsScreen ?: screen
153
- ScreenSearch(this, owner.id, styleContext(), loadIcon) { scope ->
149
+ // Its events go to the JavaScript the screen was made for, and to no other.
150
+ ScreenSearch(this, owner.id, EventEmitter.of(owner.jsGeneration), styleContext(), loadIcon) { scope ->
154
151
  owner.changeChoices { it.pickScope(scope) }
155
152
  }
156
153
  }
@@ -5,7 +5,6 @@ import dev.apexnative.navigation.options.Options
5
5
  import dev.apexnative.navigation.options.TitleComponentOptions
6
6
  import dev.apexnative.navigation.options.TopBarBackgroundComponentOptions
7
7
  import dev.apexnative.navigation.options.params.Text
8
- import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
9
8
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.IReactView
10
9
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
11
10
 
@@ -15,7 +14,8 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
15
14
  * same root, never made again; another replaces it, and the old one stops at once. A background with the same `name`
16
15
  * and `componentId` on several screens is one root, shared; it stops when no screen of the stack names it any more. The
17
16
  * roots go with their screen ([screenDestroyed]) or the stack ([destroyAll]). Each root renders at once, without
18
- * waiting for its layout: [makeRoot] marks its id so before its surface starts. Main thread only.
17
+ * waiting for its layout: [makeRoot] makes it so (`ReactView`'s `rendersAtOnce`), and each answers by its own render
18
+ * when several live roots show one id (`ScreenReadiness`). Main thread only.
19
19
  */
20
20
  internal class BarComponents(private val makeRoot: (componentId: String, name: String) -> IReactView) {
21
21
  /** A root of the bar: what it renders, and its content. */
@@ -151,18 +151,10 @@ internal class BarComponents(private val makeRoot: (componentId: String, name: S
151
151
  backgrounds.remove(key)?.let(::stop)
152
152
  }
153
153
 
154
- // Made first, counted after: the maker asks whether another root with the id lives.
155
- private fun make(key: Key): Root {
156
- val root = Root(key.name, key.componentId, makeRoot(key.componentId, key.name))
157
- LiveIds.add(key.componentId)
158
- return root
159
- }
154
+ private fun make(key: Key): Root = Root(key.name, key.componentId, makeRoot(key.componentId, key.name))
160
155
 
161
- // Stops a root once; what the readiness store kept of its id goes unless another live root holds the id.
162
- private fun stop(root: Root) {
163
- root.view.destroy()
164
- if (LiveIds.remove(root.componentId)) ScreenReadiness.remove(root.componentId)
165
- }
156
+ // Stops a root once; its content takes its mark of rendering at once with it.
157
+ private fun stop(root: Root) = root.view.destroy()
166
158
 
167
159
  private fun keyOf(name: Text, componentId: Text, id: Text): Key? {
168
160
  val componentName = name.get(null)?.takeIf { it.isNotEmpty() } ?: return null
@@ -170,42 +162,4 @@ internal class BarComponents(private val makeRoot: (componentId: String, name: S
170
162
  ?: return null
171
163
  return Key(componentName, rootId)
172
164
  }
173
-
174
- companion object {
175
- /** Whether a bar root with [id] is alive now, in any stack. */
176
- fun isLive(id: String): Boolean = LiveIds.has(id)
177
-
178
- /**
179
- * Another root that renders at once lives with [id] (a cell of the tabs' custom row): it counts as the bar's
180
- * roots do, so a root that stops clears the id's readiness only when it was the last one with it.
181
- */
182
- internal fun rootMade(id: String) = LiveIds.add(id)
183
-
184
- /** A root of [rootMade] stopped; true when no live root has [id] any more. */
185
- internal fun rootStopped(id: String): Boolean = LiveIds.remove(id)
186
-
187
- /** The JavaScript went (a reload): no id counts as held by a live root any more, as `ScreenReadiness.reset`. */
188
- fun reset() = LiveIds.clear()
189
- }
190
-
191
- // The ids of the bar roots alive now, in every stack: a root that stops clears its id's readiness only when it was
192
- // the last one with it.
193
- private object LiveIds {
194
- private val counts = HashMap<String, Int>()
195
-
196
- fun has(id: String): Boolean = (counts[id] ?: 0) > 0
197
-
198
- fun clear() = counts.clear()
199
-
200
- fun add(id: String) {
201
- counts[id] = (counts[id] ?: 0) + 1
202
- }
203
-
204
- // True when no root with the id is left.
205
- fun remove(id: String): Boolean {
206
- val count = (counts[id] ?: return true) - 1
207
- if (count <= 0) counts.remove(id) else counts[id] = count
208
- return count <= 0
209
- }
210
- }
211
165
  }
@@ -41,7 +41,6 @@ import dev.apexnative.navigation.utils.ImageLoader
41
41
  import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
42
42
  import dev.apexnative.navigation.utils.TestIds
43
43
  import dev.apexnative.navigation.utils.UiUtils
44
- import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
45
44
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
46
45
  import dev.apexnative.navigation.viewcontrollers.stack.StackDisplay
47
46
  import dev.apexnative.navigation.viewcontrollers.stack.StackDisplay.BarMotion
@@ -936,14 +935,10 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
936
935
  return resolved.component
937
936
  }
938
937
 
939
- private fun makeRoot(componentId: String, name: String): IReactView {
940
- // A component of the bar renders at once, without waiting for its layout, from before its surface starts.
941
- ScreenReadiness.setRendersAtOnce(componentId, true)
942
- // Another live root has this id (a component of the stack's options on two screens): what it rendered isn't
943
- // this root's, which answers by its own render.
944
- if (BarComponents.isLive(componentId)) ScreenReadiness.forgetRender(componentId)
945
- return ReactView(activity, componentId, name)
946
- }
938
+ // A component of the bar renders at once, without waiting for its layout. Another live root may show the same id
939
+ // (a component of the stack's options on two screens): each answers by the render of its own surface.
940
+ private fun makeRoot(componentId: String, name: String): IReactView =
941
+ ReactView(activity, componentId, name, rendersAtOnce = true)
947
942
 
948
943
  private fun loadIcon(uri: String): Drawable? {
949
944
  var loaded: Drawable? = null
@@ -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 events only while it has followers; without React Native's event dispatcher
17
- * (around a reload) it waits and listens once there is one. Released with the screen. Main thread only.
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 events when followers wait for them and it doesn't yet: after a reload there may
109
- * be no event dispatcher at first: then it is asked again once React Native has one, and as the screen joins a
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
- val context = scope?.get()?.context ?: return
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
+ }
@@ -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
  }