@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
@@ -10,13 +10,15 @@ import androidx.annotation.RestrictTo
10
10
  import androidx.core.view.ViewCompat
11
11
  import androidx.core.view.WindowInsetsCompat
12
12
  import dev.apexnative.navigation.utils.MaterialTheme
13
+ import dev.apexnative.navigation.viewcontrollers.bottomtabs.CustomTabRowGeometry
13
14
 
14
15
  /**
15
16
  * The view of bottom tabs: the view of each tab made so far fills it, the selected one visible and the others invisible
16
17
  * but laid out, so a switch shows a tab at once and its screens render; the bar's [container] lies over them at the
17
18
  * bottom, the module's bar ([bar], `R.id.bottomTabs`) in it. The container's distance from the bottom ([barOffset]:
18
19
  * `bottomMargin`, and the navigation bar under a compact bar) is this view's own inset, not a layout parameter a root
19
- * may ignore. The window insets go on to every child as they came. Main thread only.
20
+ * may ignore. The custom [row], when the tabs have one, lies right after the container and over the bar. The window
21
+ * insets go on to every child as they came. Main thread only.
20
22
  */
21
23
  @SuppressLint("ViewConstructor")
22
24
  @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -34,8 +36,25 @@ class BottomTabsLayout(context: Context) : FrameLayout(context) {
34
36
  /** The window insets reached this view: the bar's room and the screens' room may have changed. */
35
37
  internal var onInsets: (() -> Unit)? = null
36
38
 
37
- /** The custom row of the tabs, when every tab draws its cell (`bottomTab.component`); the placer puts it. */
39
+ /**
40
+ * The custom row of the tabs, when every tab draws its cell (`bottomTab.component`): a child right after the
41
+ * container, laid out over the bar, going with the container.
42
+ */
38
43
  internal var row: CustomTabRow? = null
44
+ set(value) {
45
+ if (field === value) return
46
+ field?.let {
47
+ removeView(it)
48
+ it.container = null
49
+ }
50
+ field = value
51
+ if (value == null) return
52
+ (value.parent as? ViewGroup)?.removeView(value)
53
+ value.container = container
54
+ value.place = null
55
+ // Its frame is absolute: the layout below puts it in its parameters.
56
+ addView(value, indexOfChild(container) + 1, LayoutParams(0, 0, Gravity.TOP or Gravity.LEFT))
57
+ }
39
58
 
40
59
  /** The container's distance from the bottom, in pixels. */
41
60
  internal var barOffset = 0
@@ -59,6 +78,16 @@ class BottomTabsLayout(context: Context) : FrameLayout(context) {
59
78
  }
60
79
  }
61
80
 
81
+ /**
82
+ * How far the container goes down to leave the screen whole: from the top of what the bar shows — the container, or
83
+ * the custom row that rises above it — to this view's bottom, in pixels.
84
+ */
85
+ internal val hideDistance: Int
86
+ get() {
87
+ val rowTop = row?.takeIf { it.visibility != View.GONE }?.top ?: container.top
88
+ return (height - minOf(container.top, rowTop)).coerceAtLeast(0)
89
+ }
90
+
62
91
  /** [view] of a tab joins under the bar, invisible until its tab shows. */
63
92
  internal fun addTab(view: View) {
64
93
  if (view.parent === this) return
@@ -85,5 +114,35 @@ class BottomTabsLayout(context: Context) : FrameLayout(context) {
85
114
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
86
115
  super.onLayout(changed, left, top, right, bottom)
87
116
  if (barOffset != 0) container.offsetTopAndBottom(-barOffset)
117
+ row?.let(::placeRow)
118
+ }
119
+
120
+ // The row over the bar where the bar rests (its container's motion the row takes before each frame), when its place
121
+ // changed: laid out there at once, and kept in its parameters, so the next passes measure it and put it there the
122
+ // same way. A hidden bar keeps the row where it was.
123
+ private fun placeRow(row: CustomTabRow) {
124
+ if (container.visibility == View.GONE || bar.width == 0) return
125
+ val barLeft = container.left + bar.left
126
+ val barTop = container.top + bar.top
127
+ val place = row.placeOver(
128
+ barLeft = barLeft,
129
+ barRight = barLeft + bar.width,
130
+ barBottom = barTop + bar.height,
131
+ barHeight = bar.height,
132
+ tabsHeight = height,
133
+ navigationBar = navigationBarInset,
134
+ )
135
+ if (!CustomTabRowGeometry.needsPlacing(row.place, place)) return
136
+ row.place = place
137
+ val params = row.layoutParams as LayoutParams
138
+ params.width = place.right - place.left
139
+ params.height = place.bottom - place.top
140
+ params.leftMargin = place.left
141
+ params.topMargin = place.top
142
+ row.measure(
143
+ MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY),
144
+ MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY),
145
+ )
146
+ row.layout(place.left, place.top, place.right, place.bottom)
88
147
  }
89
148
  }
@@ -22,9 +22,6 @@ open class CustomBottomTabItemView(context: Context) : FrameLayout(context) {
22
22
  private var state: TabCellState? = null
23
23
  private var destroyed = false
24
24
 
25
- /** Runs once as the cell stops: what was kept for its content's id goes. */
26
- internal var onDestroyed: (() -> Unit)? = null
27
-
28
25
  /** The content the cell shows, filling it, made with [state] as its first props. */
29
26
  internal fun show(view: IReactView, state: TabCellState) {
30
27
  content = view
@@ -68,8 +65,6 @@ open class CustomBottomTabItemView(context: Context) : FrameLayout(context) {
68
65
  didDisappear()
69
66
  destroyed = true
70
67
  content?.destroy()
71
- onDestroyed?.invoke()
72
- onDestroyed = null
73
68
  }
74
69
 
75
70
  /** The props of [state] as React Native takes them. */
@@ -11,18 +11,21 @@ import android.view.MotionEvent
11
11
  import android.view.View
12
12
  import android.view.ViewGroup
13
13
  import android.view.ViewOutlineProvider
14
+ import android.view.ViewTreeObserver
14
15
  import android.widget.FrameLayout
15
16
  import android.widget.LinearLayout
16
17
  import dev.apexnative.navigation.customrow.BottomTabsCustomRowOptions
17
18
  import dev.apexnative.navigation.utils.TestIds
19
+ import dev.apexnative.navigation.utils.UiUtils
18
20
  import dev.apexnative.navigation.viewcontrollers.bottomtabs.CustomTabRowGeometry
19
21
 
20
22
  /**
21
- * The custom row of bottom tabs on Android (`bottomTabs.customRow`): a container over the bar, in the window's content,
22
- * that holds the tabs' cells in one row of equal widths in tab order. It owns its place, its look, its visibility and
23
- * the touches: each cell's place takes them, the React elements in the cell don't, and a tap or a long press of a place
24
- * goes to [onPress] or [onLongPress] with the tab's index. Where it lies is the placer's to say (`CustomTabRowPlacer`);
25
- * what it looks like its tabs'. Main thread only.
23
+ * The custom row of bottom tabs on Android (`bottomTabs.customRow`): a container over the bar, a child of the tabs'
24
+ * view right after the bar's container, that holds the tabs' cells in one row of equal widths in tab order. It owns its
25
+ * look, its visibility and the touches: each cell's place takes them, the React elements in the cell don't, and a tap
26
+ * or a long press of a place goes to [onPress] or [onLongPress] with the tab's index. The tabs' view lays it out over
27
+ * the bar ([placeOver]); before each frame it goes with the bar's [container]. What it looks like is its tabs'. Main
28
+ * thread only.
26
29
  */
27
30
  @SuppressLint("ViewConstructor")
28
31
  internal class CustomTabRow(context: Context) : FrameLayout(context) {
@@ -42,11 +45,23 @@ internal class CustomTabRow(context: Context) : FrameLayout(context) {
42
45
  /** A cell's place was long-pressed. */
43
46
  var onLongPress: ((Int) -> Unit)? = null
44
47
 
45
- /** `bottomTabs.customRow` of the tabs, resolved: the placer reads its height and margins. */
48
+ /** `bottomTabs.customRow` of the tabs, resolved: its height and margins say where the row lies. */
46
49
  var options = BottomTabsCustomRowOptions()
47
50
 
48
51
  /**
49
- * Where the placer put the row last, in its parent; none before it did. The frame holds `bottomMargin` below the
52
+ * The bar's container the row goes with: its motion (a ride, a slide, the hide on scroll), its alpha, its height
53
+ * over the screens and whether it shows. The tabs' view sets it as the row joins it.
54
+ */
55
+ var container: View? = null
56
+
57
+ // Before each frame of the window the row is in: it goes with the container.
58
+ private val following = ViewTreeObserver.OnPreDrawListener {
59
+ container?.let(::follow)
60
+ true
61
+ }
62
+
63
+ /**
64
+ * Where the tabs' view put the row last, in it; none before it did. The frame holds `bottomMargin` below the
50
65
  * content: the row draws, clips, casts its shadow, lays its cells and takes touches only over the content, and the
51
66
  * margin below lets the touches through to what is under it.
52
67
  */
@@ -117,10 +132,57 @@ internal class CustomTabRow(context: Context) : FrameLayout(context) {
117
132
  return super.dispatchTouchEvent(event)
118
133
  }
119
134
 
120
- /** Whether the row shows its cells: it hides with the bar. */
121
- fun showWith(bar: View) {
122
- val wanted = if (bar.visibility != View.GONE) View.VISIBLE else View.GONE
135
+ /**
136
+ * Where the row lies over the bar whose edges are [barLeft], [barRight] and [barBottom] and whose height is
137
+ * [barHeight], in the tabs' view of [tabsHeight] with the navigation bar's [navigationBar] at its bottom: its
138
+ * height and margins from [options].
139
+ */
140
+ fun placeOver(barLeft: Int, barRight: Int, barBottom: Int, barHeight: Int, tabsHeight: Int, navigationBar: Int) =
141
+ CustomTabRowGeometry.inTabs(
142
+ barLeft = barLeft,
143
+ barRight = barRight,
144
+ barBottom = barBottom,
145
+ tabsHeight = tabsHeight,
146
+ navigationBar = navigationBar,
147
+ margin = UiUtils.dpToPixels(
148
+ context,
149
+ options.horizontalMargin.get(CustomTabRowGeometry.HORIZONTAL_MARGIN_DP.toDouble()),
150
+ ),
151
+ content = CustomTabRowGeometry.content(
152
+ barHeight = barHeight,
153
+ height = options.height.get(null)?.let { UiUtils.dpToPixels(context, it) },
154
+ extra = UiUtils.dpToPixels(context, CustomTabRowGeometry.EXTRA_HEIGHT_DP.toDouble()),
155
+ ),
156
+ bottomMargin = UiUtils.dpToPixels(context, options.bottomMargin.get(0.0)),
157
+ )
158
+
159
+ override fun onAttachedToWindow() {
160
+ super.onAttachedToWindow()
161
+ viewTreeObserver.addOnPreDrawListener(following)
162
+ }
163
+
164
+ override fun onDetachedFromWindow() {
165
+ viewTreeObserver.removeOnPreDrawListener(following)
166
+ super.onDetachedFromWindow()
167
+ }
168
+
169
+ // The row hides with the bar and takes its motion and its alpha (the tabs' own alpha and motion, and their
170
+ // parents', the row has as their child); it stands as high as the container, and leaves accessibility while its
171
+ // tabs wait to show.
172
+ private fun follow(container: View) {
173
+ val wanted = if (container.visibility != View.GONE) View.VISIBLE else View.GONE
123
174
  if (visibility != wanted) visibility = wanted
175
+ if (alpha != container.alpha) alpha = container.alpha
176
+ if (translationX != container.translationX) translationX = container.translationX
177
+ if (translationY != container.translationY) translationY = container.translationY
178
+ val lift = CustomTabRowGeometry.lift(container.z, elevation)
179
+ if (translationZ != lift) translationZ = lift
180
+ val mode = if (hiddenFromAccessibility()) {
181
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
182
+ } else {
183
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
184
+ }
185
+ if (importantForAccessibility != mode) importantForAccessibility = mode
124
186
  }
125
187
 
126
188
  /** Draws [look]; nothing changes when it is the one drawn. */
@@ -15,6 +15,7 @@ import dev.apexnative.navigation.react.events.AppearanceDispatcher
15
15
  import dev.apexnative.navigation.utils.DevelopmentLog
16
16
  import dev.apexnative.navigation.utils.NavigationLog
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.SideAnimator
19
20
  import dev.apexnative.navigation.views.element.animators.SharedAnimatorFactory
20
21
 
@@ -161,9 +162,15 @@ internal class ElementTransition private constructor(private val layer: Transiti
161
162
  }
162
163
  }
163
164
 
165
+ // An element with views in it fades through a hardware layer only where its alpha would draw it into an offscreen
166
+ // buffer at every frame (AnimationLayer: its rendering overlaps). React's views overlap only with
167
+ // `needsOffscreenAlphaCompositing`, so most elements fade without one and nothing they draw outside their bounds (a
168
+ // box shadow) is clipped. A view without children fades its own drawing at no extra cost.
164
169
  private fun addElement(options: ElementTransitionOptions, item: Moved) {
165
170
  val base = SideAnimator.Base(xOffset = item.originX.toFloat(), yOffset = item.originY.toFloat())
166
- SideAnimator.build(item.view, options, base)?.let(animators::add)
171
+ val animator = SideAnimator.build(item.view, options, base) ?: return
172
+ if (item.view is ViewGroup) AnimationLayer.during(item.view, animator)
173
+ animators += animator
167
174
  }
168
175
 
169
176
  // The pair's flight: `to` hidden until it starts, `from` hidden from its start; false when it doesn't fly.
@@ -176,7 +183,7 @@ internal class ElementTransition private constructor(private val layer: Transiti
176
183
  ): Boolean {
177
184
  val to = item.view
178
185
  val flight = SharedAnimatorFactory.create(
179
- from, to, toParent, layer, options, fallbackDuration, DevelopmentLog::warn,
186
+ from, to, toParent, item.stub, layer, options, fallbackDuration, DevelopmentLog::warn,
180
187
  ) ?: return false
181
188
  val alpha = item.values[ALPHA]
182
189
  val fromAlpha = from.alpha
@@ -45,6 +45,13 @@ internal class MoveAnimator : ValueAnimator() {
45
45
  private val frames = ArrayList<(Float) -> Unit>()
46
46
  private val ends = ArrayList<() -> Unit>()
47
47
  private var failed = false
48
+ private var cancelled = false
49
+
50
+ /**
51
+ * Whether the flight ran to its end: it wasn't cancelled and nothing in it failed. Read at its end; a flight that
52
+ * landed may keep showing its last frame until the transition gives its views back.
53
+ */
54
+ val landed: Boolean get() = !cancelled
48
55
 
49
56
  init {
50
57
  setFloatValues(0f, 1f)
@@ -52,6 +59,10 @@ internal class MoveAnimator : ValueAnimator() {
52
59
  addListener(object : AnimatorListenerAdapter() {
53
60
  override fun onAnimationStart(animation: Animator) = guarded { for (start in starts) start() }
54
61
 
62
+ override fun onAnimationCancel(animation: Animator) {
63
+ cancelled = true
64
+ }
65
+
55
66
  override fun onAnimationEnd(animation: Animator) {
56
67
  for (end in ends) elementStep(element) { end() }
57
68
  }
@@ -1,46 +1,339 @@
1
1
  package dev.apexnative.navigation.views.element.animators
2
2
 
3
- import android.graphics.RectF
4
3
  import android.view.View
5
4
  import android.view.ViewGroup
6
- import dev.apexnative.navigation.utils.ViewUtils
7
- import dev.apexnative.navigation.utils.computeInheritedScale
8
- import dev.apexnative.navigation.views.element.TransitionLayer
5
+ import dev.apexnative.navigation.views.animations.ValueInterpolation
6
+ import kotlin.math.abs
7
+ import kotlin.math.cos
8
+ import kotlin.math.min
9
+ import kotlin.math.roundToInt
10
+ import kotlin.math.sin
9
11
 
10
12
  /**
11
- * Where a shared element flies in its transition's layer: from where `from` is on the screen, at the scale of its
12
- * parents, to `to`'s place in the layer (its margins and size). Pixels of the layer.
13
+ * How a view lies in its parent and is drawn there: its frame's corner, its translation, scale and rotation about its
14
+ * pivot, and its parent's scroll. What `View.getMatrix` does, the scale first, without the tilts (`rotationX`,
15
+ * `rotationY`) but for a half turn about x or y: React Native draws a mirror that way ([of]). Pixels and degrees,
16
+ * clockwise.
17
+ */
18
+ internal class ViewPlacement(
19
+ val left: Float,
20
+ val top: Float,
21
+ val translationX: Float = 0f,
22
+ val translationY: Float = 0f,
23
+ val scaleX: Float = 1f,
24
+ val scaleY: Float = 1f,
25
+ val rotation: Float = 0f,
26
+ val pivotX: Float = 0f,
27
+ val pivotY: Float = 0f,
28
+ val parentScrollX: Float = 0f,
29
+ val parentScrollY: Float = 0f,
30
+ ) {
31
+ /** The point [x], [y] of the view in its parent's coordinates. */
32
+ fun toParent(x: Float, y: Float): FloatArray {
33
+ val dx = (x - pivotX) * scaleX
34
+ val dy = (y - pivotY) * scaleY
35
+ val radians = Math.toRadians(rotation.toDouble())
36
+ val cos = cos(radians).toFloat()
37
+ val sin = sin(radians).toFloat()
38
+ return floatArrayOf(
39
+ dx * cos - dy * sin + pivotX + translationX + left - parentScrollX,
40
+ dx * sin + dy * cos + pivotY + translationY + top - parentScrollY,
41
+ )
42
+ }
43
+
44
+ /** Whether the view shows mirrored: one of its scales is negative. */
45
+ val mirrors: Boolean get() = scaleX * scaleY < 0
46
+
47
+ companion object {
48
+ // How close to half a turn a tilt counts as a flip, in degrees.
49
+ private const val FLIP_TOLERANCE = 0.5f
50
+ private const val HALF_TURN = 180f
51
+
52
+ /** [view] as it lies now; [frameOf] the view whose frame and parent it takes (a stand-in where it was). */
53
+ fun of(view: View, frameOf: View = view): ViewPlacement {
54
+ val parent = frameOf.parent as? View
55
+ return withTilts(
56
+ left = frameOf.left.toFloat(),
57
+ top = frameOf.top.toFloat(),
58
+ translationX = view.translationX,
59
+ translationY = view.translationY,
60
+ scaleX = view.scaleX,
61
+ scaleY = view.scaleY,
62
+ rotation = view.rotation,
63
+ rotationX = view.rotationX,
64
+ rotationY = view.rotationY,
65
+ pivotX = view.pivotX,
66
+ pivotY = view.pivotY,
67
+ parentScrollX = parent?.scrollX?.toFloat() ?: 0f,
68
+ parentScrollY = parent?.scrollY?.toFloat() ?: 0f,
69
+ )
70
+ }
71
+
72
+ /**
73
+ * The placement of a view with these properties. React Native splits a mirror (`scaleX: -1`) into both scales
74
+ * negated and half a turn about x: a view turns about z after its scale and tilts about y, then x, last, and
75
+ * half a turn about x flips y after the turn, which is the same as flipping y first and turning the other way.
76
+ * Other tilts are left out.
77
+ */
78
+ fun withTilts(
79
+ left: Float,
80
+ top: Float,
81
+ translationX: Float,
82
+ translationY: Float,
83
+ scaleX: Float,
84
+ scaleY: Float,
85
+ rotation: Float,
86
+ rotationX: Float,
87
+ rotationY: Float,
88
+ pivotX: Float,
89
+ pivotY: Float,
90
+ parentScrollX: Float,
91
+ parentScrollY: Float,
92
+ ): ViewPlacement {
93
+ val flipsY = abs(abs(rotationX) - HALF_TURN) < FLIP_TOLERANCE
94
+ val flipsX = abs(abs(rotationY) - HALF_TURN) < FLIP_TOLERANCE
95
+ return ViewPlacement(
96
+ left = left,
97
+ top = top,
98
+ translationX = translationX,
99
+ translationY = translationY,
100
+ scaleX = if (flipsX) -scaleX else scaleX,
101
+ scaleY = if (flipsY) -scaleY else scaleY,
102
+ rotation = if (flipsX != flipsY) -rotation else rotation,
103
+ pivotX = pivotX,
104
+ pivotY = pivotY,
105
+ parentScrollX = parentScrollX,
106
+ parentScrollY = parentScrollY,
107
+ )
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Where the points of a view are drawn in a transition's layer: through its own placement and each parent's ([chain],
114
+ * the view first, up to the top view of the window but not it), then moved by [offsetX], [offsetY] (where the top view
115
+ * is from the layer's corner). The angle and the scale are those of the whole chain: a turned or scaled parent turns
116
+ * or scales what is in it.
117
+ */
118
+ internal class LayerPlace(
119
+ private val chain: List<ViewPlacement>,
120
+ private val offsetX: Float,
121
+ private val offsetY: Float,
122
+ ) {
123
+ /** The angle the view is drawn at, degrees, clockwise: a mirrored parent turns what is in it the other way. */
124
+ val rotation: Float = chain.fold(0f) { inner, placement ->
125
+ placement.rotation + if (placement.mirrors) -inner else inner
126
+ }
127
+
128
+ /** The scale it is drawn at along its own x (a mirror is negative). */
129
+ val scaleX: Float = chain.fold(1f) { product, placement -> product * placement.scaleX }
130
+
131
+ /** The scale it is drawn at along its own y. */
132
+ val scaleY: Float = chain.fold(1f) { product, placement -> product * placement.scaleY }
133
+
134
+ /** The point [x], [y] of the view in the layer. */
135
+ fun point(x: Float, y: Float): FloatArray {
136
+ var point = floatArrayOf(x, y)
137
+ for (placement in chain) point = placement.toParent(point[0], point[1])
138
+ return floatArrayOf(point[0] + offsetX, point[1] + offsetY)
139
+ }
140
+
141
+ /** The flight's end of a view of [width] × [height] drawn here. */
142
+ fun end(width: Int, height: Int): FlightEnd {
143
+ val center = point(width / 2f, height / 2f)
144
+ return FlightEnd(
145
+ centerX = center[0],
146
+ centerY = center[1],
147
+ width = width * abs(scaleX),
148
+ height = height * abs(scaleY),
149
+ rotation = rotation,
150
+ mirrorX = if (scaleX < 0) -1f else 1f,
151
+ mirrorY = if (scaleY < 0) -1f else 1f,
152
+ scale = min(abs(scaleX), abs(scaleY)),
153
+ )
154
+ }
155
+
156
+ companion object {
157
+ /**
158
+ * Where [view] is drawn in [layer]. With [standIn], [view] counts as lying where that one lies (its frame and
159
+ * parents) with its own properties: an element that moved into the layer, at the place it goes back to.
160
+ */
161
+ fun of(view: View, layer: View, standIn: View? = null): LayerPlace {
162
+ val placed = standIn ?: view
163
+ val chain = arrayListOf(ViewPlacement.of(view, placed))
164
+ var parent = placed.parent as? View
165
+ while (parent != null) {
166
+ val above = parent.parent as? View ?: break
167
+ chain += ViewPlacement.of(parent)
168
+ parent = above
169
+ }
170
+ // The window's top view (no parent view) lies where it is on the screen, with no properties of its own.
171
+ val base = parent ?: placed
172
+ val screen = IntArray(2)
173
+ base.getLocationOnScreen(screen)
174
+ val origin = IntArray(2)
175
+ layer.getLocationOnScreen(origin)
176
+ return LayerPlace(chain, (screen[0] - origin[0]).toFloat(), (screen[1] - origin[1]).toFloat())
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * One end of a shared element's flight in its layer: where its center is drawn, the size it shows (its own at the
183
+ * scale of its chain), its angle, whether it shows mirrored, and its scale (what its corners shrink by). Pixels.
184
+ */
185
+ internal class FlightEnd(
186
+ val centerX: Float,
187
+ val centerY: Float,
188
+ val width: Float,
189
+ val height: Float,
190
+ val rotation: Float,
191
+ val mirrorX: Float = 1f,
192
+ val mirrorY: Float = 1f,
193
+ val scale: Float = 1f,
194
+ ) {
195
+ /** Whether [other] is drawn at the same place, size, angle and mirror: within half a pixel and 0.1°. */
196
+ fun sameAs(other: FlightEnd): Boolean =
197
+ abs(centerX - other.centerX) < SAME_PLACE && abs(centerY - other.centerY) < SAME_PLACE &&
198
+ abs(width - other.width) < SAME_PLACE && abs(height - other.height) < SAME_PLACE &&
199
+ abs(rotation - other.rotation) < SAME_ANGLE && mirrorX == other.mirrorX && mirrorY == other.mirrorY
200
+
201
+ companion object {
202
+ private const val SAME_PLACE = 0.5f
203
+ private const val SAME_ANGLE = 0.1f
204
+
205
+ /**
206
+ * Whether `to` shows elsewhere in its screen than in the layer with its own properties alone ([standIn] is
207
+ * where it was): a parent of its scales or turns it. A flight that landed then keeps it where it lands until
208
+ * the transition gives it back, or it would jump to its place in the layer meanwhile.
209
+ */
210
+ fun holdsAtEnd(to: View, standIn: View?, layer: View): Boolean {
211
+ if (standIn == null) return false
212
+ val inScreen = LayerPlace.of(to, layer, standIn).end(to.width, to.height)
213
+ return !inScreen.sameAs(LayerPlace.of(to, layer).end(to.width, to.height))
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Where a shared element flies in its transition's layer: from where `from` is drawn to where `to` is drawn once it
220
+ * is back in its screen, both through their own and their parents' translation, scale and rotation. The center, the
221
+ * size and the mirror go evenly, the angle along the shorter arc (as on iOS). `to` lies in the layer at its margins
222
+ * ([toLeft], [toTop]) meanwhile.
13
223
  */
14
224
  internal class FlightGeometry(
15
- /** `from`'s place and size in the layer. */
16
- val from: RectF,
17
- /** `to`'s place and size in the layer. */
18
- val to: RectF,
19
- /** The scale `from`'s parents give it along x: its corners shrink with it. */
20
- val fromScale: Float,
225
+ val from: FlightEnd,
226
+ val to: FlightEnd,
227
+ val toLeft: Int,
228
+ val toTop: Int,
229
+ /** Whether `to` keeps the flight's last frame at the end ([FlightEnd.holdsAtEnd]). */
230
+ val holdsAtEnd: Boolean = false,
21
231
  ) {
22
232
  /** The width and height `to` is laid out at meanwhile: the larger of the two sizes. */
23
233
  val largest: Pair<Int, Int>
24
- get() = maxOf(from.width(), to.width()).toInt() to maxOf(from.height(), to.height()).toInt()
234
+ get() = maxOf(from.width, to.width).roundToInt() to maxOf(from.height, to.height).roundToInt()
235
+
236
+ /** The flight at [fraction] of the way (the curve's value), [time] the part of the time. */
237
+ fun at(fraction: Float, time: Float): FlightEnd = FlightEnd(
238
+ centerX = ValueInterpolation.number(from.centerX, to.centerX, fraction, time),
239
+ centerY = ValueInterpolation.number(from.centerY, to.centerY, fraction, time),
240
+ width = ValueInterpolation.number(from.width, to.width, fraction, time),
241
+ height = ValueInterpolation.number(from.height, to.height, fraction, time),
242
+ rotation = ValueInterpolation.angle(from.rotation, to.rotation, fraction, time),
243
+ mirrorX = ValueInterpolation.number(from.mirrorX, to.mirrorX, fraction, time),
244
+ mirrorY = ValueInterpolation.number(from.mirrorY, to.mirrorY, fraction, time),
245
+ scale = ValueInterpolation.number(from.scale, to.scale, fraction, time),
246
+ )
247
+
248
+ /**
249
+ * The translation and pivot that draw a view laid out at its margins with [width] × [height] (or showing that part
250
+ * of a larger layout from its corner) at [frame]'s center and angle, unscaled: x, y of the translation, then of
251
+ * the pivot.
252
+ */
253
+ fun moveOf(frame: FlightEnd, width: Int, height: Int): FloatArray = floatArrayOf(
254
+ frame.centerX - (toLeft + width / 2f),
255
+ frame.centerY - (toTop + height / 2f),
256
+ width / 2f,
257
+ height / 2f,
258
+ )
25
259
 
26
260
  companion object {
27
- /** The flight of [from] to [to], which is in [layer] already; none when [to] has no place there. */
28
- fun of(from: View, to: View, layer: TransitionLayer): FlightGeometry? {
261
+ /**
262
+ * The flight of [from] to [to], which is in [layer] already in place of [toStandIn] (the stub where it was);
263
+ * none when [to] has no place there.
264
+ */
265
+ fun of(from: View, to: View, toStandIn: View?, layer: View): FlightGeometry? {
29
266
  val place = to.layoutParams as? ViewGroup.MarginLayoutParams ?: return null
30
- val origin = layer.originOnScreen
31
- val onScreen = ViewUtils.getLocationOnScreen(from)
32
- val (scaleX, scaleY) = computeInheritedScale(from)
33
- val left = (onScreen.x - origin.x).toFloat()
34
- val top = (onScreen.y - origin.y).toFloat()
35
- val fromRect = RectF(left, top, left + from.width * scaleX, top + from.height * scaleY)
36
- val toLeft = place.leftMargin.toFloat()
37
- val toTop = place.topMargin.toFloat()
38
- val toRect = RectF(toLeft, toTop, toLeft + to.width, toTop + to.height)
39
- return FlightGeometry(fromRect, toRect, scaleX)
267
+ val fromEnd = LayerPlace.of(from, layer).end(from.width, from.height)
268
+ val toEnd = LayerPlace.of(to, layer, toStandIn).end(to.width, to.height)
269
+ val holds = FlightEnd.holdsAtEnd(to, toStandIn, layer)
270
+ return FlightGeometry(fromEnd, toEnd, place.leftMargin, place.topMargin, holds)
40
271
  }
41
272
  }
42
273
  }
43
274
 
275
+ /**
276
+ * The properties a moving `to` is drawn with in the layer when its flight starts, put back when it ends: the
277
+ * flight draws it unscaled (but mirrored where an end is) and untilted at the flight's angle about the middle of what
278
+ * shows.
279
+ */
280
+ internal class MovedProperties(private val view: View) {
281
+ private val translationX = view.translationX
282
+ private val translationY = view.translationY
283
+ private val scaleX = view.scaleX
284
+ private val scaleY = view.scaleY
285
+ private val rotation = view.rotation
286
+ private val rotationX = view.rotationX
287
+ private val rotationY = view.rotationY
288
+ private val pivotX = view.pivotX
289
+ private val pivotY = view.pivotY
290
+ private val ownWidth = view.width
291
+ private val ownHeight = view.height
292
+
293
+ /** [view] at [frame], laid out at [width] × [height] from its margins (or showing that part from its corner). */
294
+ fun show(geometry: FlightGeometry, frame: FlightEnd, width: Int, height: Int) {
295
+ val move = geometry.moveOf(frame, width, height)
296
+ view.pivotX = move[2]
297
+ view.pivotY = move[3]
298
+ view.translationX = move[0]
299
+ view.translationY = move[1]
300
+ view.scaleX = frame.mirrorX
301
+ view.scaleY = frame.mirrorY
302
+ view.rotation = frame.rotation
303
+ view.rotationX = 0f
304
+ view.rotationY = 0f
305
+ }
306
+
307
+ /**
308
+ * [view] at the end of a flight that [landed]: kept at the flight's last frame, laid out by [layOut] at the size it
309
+ * shows, when the geometry holds it there; else as it was at the start, laid out by [layOut] at its own size.
310
+ */
311
+ fun end(geometry: FlightGeometry, landed: Boolean, layOut: (width: Int, height: Int) -> Unit) {
312
+ if (geometry.holdsAtEnd && landed) {
313
+ val width = geometry.to.width.roundToInt()
314
+ val height = geometry.to.height.roundToInt()
315
+ layOut(width, height)
316
+ show(geometry, geometry.to, width, height)
317
+ } else {
318
+ restore()
319
+ layOut(ownWidth, ownHeight)
320
+ }
321
+ }
322
+
323
+ /** [view] as it was at the start. */
324
+ fun restore() {
325
+ view.translationX = translationX
326
+ view.translationY = translationY
327
+ view.scaleX = scaleX
328
+ view.scaleY = scaleY
329
+ view.rotation = rotation
330
+ view.rotationX = rotationX
331
+ view.rotationY = rotationY
332
+ view.pivotX = pivotX
333
+ view.pivotY = pivotY
334
+ }
335
+ }
336
+
44
337
  /** An outline of the size and corners a flying view shows meanwhile: its own bounds are larger. */
45
338
  internal class FlightOutline : android.view.ViewOutlineProvider() {
46
339
  var width = 0