@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -29,9 +29,77 @@ export function rememberMergedOptions(id: string, options: Options): void {
29
29
  remembered.set(id, before ? mergeOptions(before, kept) : kept);
30
30
  }
31
31
 
32
+ /** Takes the options at the paths, such as `['topBar', 'subtitle']`, out of those remembered for the node with the id. */
33
+ export function forgetMergedPaths(id: string, paths: readonly string[][]): void {
34
+ const own = ownOptions.get(id);
35
+ if (own) own.options = withoutPaths(own.options, paths);
36
+ const options = remembered.get(id);
37
+ if (options) remembered.set(id, withoutPaths(options, paths));
38
+ }
39
+
40
+ // A copy of the options without the values at the paths.
41
+ function withoutPaths(options: Options, paths: readonly string[][]): Options {
42
+ const kept = structuredCopy(options);
43
+ for (const path of paths) {
44
+ const parent = path
45
+ .slice(0, -1)
46
+ .reduce<Record<string, unknown> | undefined>(
47
+ (group, key) => group?.[key] as Record<string, unknown> | undefined,
48
+ kept
49
+ );
50
+ if (parent) delete parent[path[path.length - 1]!];
51
+ }
52
+ return kept as Options;
53
+ }
54
+
55
+ // A copy of the plain objects of options, the other values shared.
56
+ function structuredCopy(options: Options): Record<string, unknown> {
57
+ return Object.fromEntries(
58
+ Object.entries(options).map(([key, value]) => [
59
+ key,
60
+ isGroup(value) ? structuredCopy(value as Options) : value,
61
+ ])
62
+ );
63
+ }
64
+
65
+ function isGroup(value: unknown): boolean {
66
+ return (
67
+ typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype
68
+ );
69
+ }
70
+
32
71
  /** Forgets the merged options of a node that went; a node of the last root keeps them until another root goes out. */
33
72
  export function forgetMergedOptions(id: string): void {
34
73
  if (!lastRoot?.ids.has(id)) remembered.delete(id);
74
+ ownOptions.delete(id);
75
+ }
76
+
77
+ // The own options of each screen as JavaScript sent them, in development: those it showed with, and every merge and
78
+ // clear since. An edit of a screen's options (Fast Refresh) changes only what still has the old definition's value.
79
+ const ownOptions = new Map<string, { name: string; options: Options }>();
80
+
81
+ /** Notes the own options a screen of the name shows with (development). */
82
+ export function noteOwnOptions(id: string, name: string, options: Options): void {
83
+ if (__DEV__) ownOptions.set(id, { name, options });
84
+ }
85
+
86
+ /** Notes a merge into a screen's own options (development). */
87
+ export function noteMergedOwnOptions(id: string, options: Options): void {
88
+ const noted = __DEV__ ? ownOptions.get(id) : undefined;
89
+ if (noted) noted.options = mergeOptions(noted.options, options);
90
+ }
91
+
92
+ /** The own options of a screen as JavaScript sent them (development); undefined for a screen it didn't note. */
93
+ export function ownOptionsOf(id: string): Options | undefined {
94
+ return ownOptions.get(id)?.options;
95
+ }
96
+
97
+ /**
98
+ * The ids of the screens of the name the native side has, mounted in React or not yet (a tab not selected yet), as
99
+ * development notes them.
100
+ */
101
+ export function notedScreens(name: string): string[] {
102
+ return [...ownOptions].filter(([, noted]) => noted.name === name).map(([id]) => id);
35
103
  }
36
104
 
37
105
  /** The options merged into the node with the id, as remembered; undefined without any. */
@@ -55,6 +123,8 @@ export function rootGoesOut(nodes: LayoutNode[], again: SentRoot | undefined): v
55
123
  const before = lastRoot;
56
124
  lastRoot = { nodes, ids: new Set(nodes.flatMap(idsOf)) };
57
125
  if (!before) return;
126
+ // The screens of the root before go with it, the tabs React never mounted too.
127
+ for (const id of before.ids) if (!lastRoot.ids.has(id)) ownOptions.delete(id);
58
128
  const moved = new Map<string, Options>();
59
129
  if (again === before) {
60
130
  pairNodes(before.nodes, nodes, (old, node) => {
@@ -68,6 +68,29 @@ let unmounted: ((screenId: string) => void) | undefined;
68
68
  // bar of every screen that shows it. The id lets its props go only with the last of them. A slot of the app's one tree
69
69
  // has no tag: while the one tree shows a screen with the id, no root of that id lets it go.
70
70
  const owners = new Map<string, Set<number>>();
71
+ // The screens that show by their ids: the name, and how many roots or slots of the id are mounted (a component of the
72
+ // options has a root in the bar of every screen that shows it).
73
+ const shown = new Map<string, { name: string; mounts: number }>();
74
+
75
+ /**
76
+ * Notes a screen as shown, a React root or slot of it or the controller of a native screen, until the function it
77
+ * gives is called.
78
+ */
79
+ export function noteShown(componentId: string, name: string): () => void {
80
+ const entry = shown.get(componentId);
81
+ if (entry?.name === name) entry.mounts++;
82
+ else shown.set(componentId, { name, mounts: 1 });
83
+ return () => {
84
+ const current = shown.get(componentId);
85
+ if (current?.name !== name) return;
86
+ if (--current.mounts === 0) shown.delete(componentId);
87
+ };
88
+ }
89
+
90
+ /** The ids of the screens with the name that show now. */
91
+ export function shownScreens(name: string): string[] {
92
+ return [...shown].filter(([, entry]) => entry.name === name).map(([id]) => id);
93
+ }
71
94
 
72
95
  /** Whether a React root of the screen with the id is mounted now (a slot of the app's one tree doesn't count). */
73
96
  export function hasRoot(componentId: string): boolean {
@@ -235,6 +258,7 @@ export function screenRoot(name: string): ComponentType<NativeRootProps> | undef
235
258
  [componentId]
236
259
  );
237
260
  const props = useSyncExternalStore(watch, () => propsOf(componentId));
261
+ useEffect(() => noteShown(componentId, name), [componentId]);
238
262
  useEffect(() => {
239
263
  if (rootTag !== undefined) {
240
264
  const tags = owners.get(componentId) ?? new Set<number>();
@@ -95,8 +95,8 @@ export interface SheetDetentChangedEvent {
95
95
  /** The id of the visible screen of the modal. */
96
96
  componentId: string;
97
97
  /**
98
- * The detent the sheet stopped at: `'medium'`, `'large'`, or a height (points on iOS, dp on Android). `'content'`
99
- * is sent on iOS for a detent that fits the content; Android names such a detent `'medium'`.
98
+ * The detent the sheet stopped at: `'medium'`, `'large'`, `'content'` (the detent that fits the content), or a
99
+ * height (points on iOS, dp on Android).
100
100
  */
101
101
  detent: 'medium' | 'large' | 'content' | number;
102
102
  }
@@ -262,8 +262,8 @@ export type ElementAnimation = AppearingElementAnimation | DisappearingElementAn
262
262
  * element's value, and a missing end is that value (a turn read from −180° to 180°, as React Native draws it). `x`
263
263
  * and `y` are the position in the parent of the element on both platforms. As on a side of a screen, don't set `x`
264
264
  * with `translationX` or `y` with `translationY` on one element: the translation plays and the position doesn't.
265
- * (Android) A view whose parent is a React view with `removeClippedSubviews` turned on, such as a child of a clipping
266
- * list, is not moved: it counts as not found.
265
+ * The element plays where it is, in its screen: it moves with its screen, its parents clip it, and the views drawn
266
+ * after it in its screen stay over it.
267
267
  */
268
268
  export interface ElementTransition {
269
269
  /** The `nativeID` of the view to animate. */
@@ -294,15 +294,20 @@ export interface ElementTransition {
294
294
  * A view that flies from the screen that goes to the screen that comes, changing its place, size, corners and look on
295
295
  * the way. A pair whose view is not found on either screen is skipped while the rest plays, and the transition waits
296
296
  * until the views are found. A pair that fails on the way (an error in its start or its flight) is skipped too, with a
297
- * warning in the log: its views show and hide with their screens, and the transition and its command go on. (iOS) So is
298
- * a pair with a view without a size, or one whose picture can't be taken. Without an animation of the screens of its
299
- * own, the screen that comes fades in (or the one that goes fades out) in 300 ms around the pair. Text keeps its
300
- * proportions and changes its picture within a quarter of the flight. A view turned or scaled by its own `transform` or
301
- * by a parent's flies from where it shows, and its angle goes to the other view's along the shorter arc. The angle is
302
- * the one React Native draws, from −180° to 180°: `rotate: '270deg'` turns back a quarter, and no pair spins a full
303
- * turn. (Android) A picture that doesn't load within a second drops out of the transition, and so does a pair with a
304
- * view whose parent is a React view with `removeClippedSubviews` turned on: it counts as not found. A skew and a tilt
305
- * (`skewX`, `rotateX`, …) don't fly: the view flies without them and has them again at the end.
297
+ * warning in the log: its views show and hide with their screens, and the transition and its command go on. So is a
298
+ * pair with a view without a size, or one whose picture can't be taken. Without an animation of the screens of its
299
+ * own, the screen that comes fades in (or the one that goes fades out) in 300 ms around the pair. What flies is a copy:
300
+ * pictures of the two views taken as the transition starts, while the views stay hidden in their places; what changes
301
+ * in them meanwhile shows when they are back. A view its parents cut (`overflow: hidden`, a scroll view, the edge of
302
+ * its screen) shows only the part they leave visible, and that part goes to the one of the other view on the way.
303
+ * Text keeps its proportions, flies whole and changes its picture within a quarter of the flight. A view turned or
304
+ * scaled by its own `transform` or by a parent's flies from where it shows, and its angle goes to the other view's
305
+ * along the shorter arc. The angle is the one React Native draws, from −180° to 180°:
306
+ * `rotate: '270deg'` turns back a quarter, and no pair spins a full turn. (Android) An image without a background or a
307
+ * border flies as the image of the view that comes, from the part and place the fit (`resizeMode`) shows in one view
308
+ * to those in the other, and an animated image plays on; images of other proportions fly as pictures. An image that
309
+ * doesn't load within a second drops out of the transition. A skew and a tilt (`skewX`, `rotateX`, …) don't fly: the
310
+ * view flies without them and has them again at the end.
306
311
  */
307
312
  export interface SharedElementTransition {
308
313
  /** The `nativeID` of the view on the screen that goes. */
@@ -411,14 +416,27 @@ export interface StackPushAnimationOptions extends StackAnimationOptions {
411
416
  }
412
417
 
413
418
  /**
414
- * The animation of a pop, read from the screen that goes. As {@link StackAnimationOptions}, with the preview of the
415
- * back gesture on Android.
419
+ * The animation of a pop, read from the screen that goes. As {@link StackAnimationOptions}, with the swipe back of iOS
420
+ * and the preview of the back gesture on Android.
416
421
  */
417
422
  export interface StackPopAnimationOptions extends StackAnimationOptions {
423
+ /**
424
+ * (iOS) Whether the swipe back plays this pop under the finger. Without it, the swipe back plays UIKit's slide, and
425
+ * the pop's own animation plays only for the back button and `pop`. With `true`, the swipe from the edge plays the
426
+ * pop's own animation as far as the finger has gone across the screen: the user can stop halfway, and letting go
427
+ * finishes the pop or puts the screen back, the rest playing faster after a quick flick. It applies only when the pop
428
+ * has an animation of its own (`content`, `elementTransitions`, `sharedElementTransitions`). The top bar shows the
429
+ * screen under from the start of the swipe, as UIKit does in a transition of its own, and comes back when the swipe
430
+ * is given up. On iOS 26 the swipe from the content stays off for a screen with an own pop.
431
+ * Android ignores it: a pop with an animation of its own plays when the back gesture ends.
432
+ * @default false
433
+ */
434
+ interactive?: boolean;
418
435
  /**
419
436
  * (Android 14+) Whether the back gesture on a pushed screen previews its pop as the user swipes: the screen shrinks
420
437
  * and follows the finger, the screen under it shows behind. `false` pops the screen when the gesture is released,
421
- * as on Android 13 and older; back still pops it. iOS ignores it: its swipe back is UIKit's.
438
+ * as on Android 13 and older; back still pops it. iOS ignores it: its swipe back is UIKit's, or the pop's own
439
+ * animation with `interactive`.
422
440
  * @default true
423
441
  */
424
442
  preview?: boolean;
@@ -474,6 +492,12 @@ export interface ModalAnimationOptions extends EnterExitAnimationOptions {
474
492
  * options of the command. A change by `mergeOptions` is for the next command and animates nothing itself. A theme
475
493
  * switch or a rotation does not change an animation. Without a key, the transition is the platform's own.
476
494
  *
495
+ * The system's setting to reduce motion is respected, with no option to turn that off. (iOS) With Reduce Motion on, a
496
+ * transition of these keys is a fade of the same length: the screen that comes fades in (a push, `setStackRoot`, a
497
+ * modal's show, `setRoot`), the screen that goes fades out (a pop, a dismissal); no element moves. (Android) With
498
+ * Remove animations on (or the animator duration scale at 0), the transitions end at once, no element moves and none
499
+ * is waited for, a side menu opens and closes without its slide, and a sheet shows and closes without it.
500
+ *
477
501
  * ```ts
478
502
  * const options: Options = {
479
503
  * animations: {
@@ -489,7 +513,8 @@ export interface AnimationOptions {
489
513
  push?: StackPushAnimationOptions;
490
514
  /**
491
515
  * The pop of a screen, also `popTo` and `popToRoot`, read from the screen that goes. The swipe back of iOS is
492
- * UIKit's; the back gesture of Android previews the pop (`preview`).
516
+ * UIKit's, or the pop's own animation under the finger (`interactive`); the back gesture of Android previews the pop
517
+ * (`preview`).
493
518
  */
494
519
  pop?: StackPopAnimationOptions;
495
520
  /** The replacing of the screens of a stack, read from the new top screen. */
@@ -199,8 +199,8 @@ export interface OptionsBottomTabs {
199
199
  */
200
200
  drawBehind?: boolean;
201
201
  /**
202
- * The background color of the bar; `null` gives the default one. On Android the default is the surface color of the
203
- * Material 3 theme. Before iOS 26, and in the compatibility mode (`UIDesignRequiresCompatibility` in Info.plist: iOS 26, and iOS 27
202
+ * The background color of the bar; `null` gives the default one. On Android the default is the surface container color
203
+ * (`colorSurfaceContainer`) of the Material 3 theme. Before iOS 26, and in the compatibility mode (`UIDesignRequiresCompatibility` in Info.plist: iOS 26, and iOS 27
204
204
  * only for an app built with an SDK before iOS 27), the bar is opaque with the color, or system opaque without it,
205
205
  * and a transparent color makes it transparent. On iOS 26 and later with glass the color is ignored
206
206
  * with one warning, since the system has no color for glass, while a transparent color gives a transparent bar. The
@@ -159,7 +159,7 @@ export interface OptionsTopBarButtonMenu {
159
159
  * A button of the top bar (`leftButtons`, `rightButtons`, `centerButtons`) or of the toolbar (`toolbar.buttons`). The
160
160
  * kinds are picked by the fields that are set. On iOS, the first that fits wins: a `component`, then an icon with text
161
161
  * and `showText`, then an icon with `iconBackground` or `iconInsets`, then an SF Symbol, an image `icon`, a `text`,
162
- * a `systemItem`; a button with none of them is not shown. On Android a button is a menu item of the bar: text, icon
162
+ * a `systemItem`; a button with none of them is not shown. On Android a button is a button of the bar's row: text, icon
163
163
  * or a component. Lists of buttons are replaced as a whole and take nothing from the lists of lower layers.
164
164
  *
165
165
  * Text, icon, color and similar fields are the same in all four places; where a place or a side differs, the field
@@ -204,7 +204,7 @@ export interface OptionsTopBarButton {
204
204
  systemItem?: SystemItemIcon;
205
205
  /**
206
206
  * The text of the button. A button with an icon keeps it as a label for the overflow menu, the customization panel
207
- * and VoiceOver (iOS) or as the text of the menu item (Android, where it also shows when there is room).
207
+ * and VoiceOver (iOS), or for TalkBack, the tooltip of a long press and the overflow menu (Android).
208
208
  */
209
209
  text?: string;
210
210
  /** The text a screen reader says for the button. */
@@ -51,8 +51,7 @@ export type OptionsModalTransitionStyle =
51
51
  *
52
52
  * On iOS `'medium'` and `'large'` are the system stops; a number and `'content'` need iOS 16 and are left out below it
53
53
  * with a warning in the log. On Android `'medium'` is half of the window, `'large'` the full height under the status
54
- * bar, a number is capped at `'large'`, and `'content'` stands in as `'medium'` because the Material sheet has no
55
- * height by content.
54
+ * bar, a number is capped at `'large'`, and `'content'` is the height of the content, measured as on iOS.
56
55
  */
57
56
  export type OptionsModalSheetDetent = 'medium' | 'large' | 'content' | number;
58
57
 
@@ -67,11 +66,11 @@ export type OptionsModalSheetDetent = 'medium' | 'large' | 'content' | number;
67
66
  export interface OptionsModalSheet {
68
67
  /**
69
68
  * The stops the sheet can rest on, and the user drags it between, in any order: on both platforms they are sorted by
70
- * height and repeats dropped. On iOS `'medium'` counts as half of `'large'` there, and `'content'` takes its place by
71
- * the height of the content, again whenever that changes; a list left empty by unsupported values gives
72
- * `['large']`. On Android the Material sheet has three resting places at most: the lowest stop of the list, the
73
- * highest, and `'medium'` between them when the list has both `'medium'` and `'large'`. A stop without a place is
74
- * dropped.
69
+ * height and repeats dropped. On iOS `'medium'` counts as half of `'large'` there, and on both `'content'` takes its
70
+ * place by the height of the content, again whenever that changes; on iOS a list left empty by unsupported values
71
+ * gives `['large']`. On Android the Material sheet has three resting places at most: the lowest stop of the list, the
72
+ * highest, and one between them, `selectedDetent` when it stands there, else the lowest of the others; the rest are
73
+ * dropped. Stops of the same height there are one.
75
74
  * @default ['large']
76
75
  */
77
76
  detents?: OptionsModalSheetDetent[];
@@ -39,13 +39,18 @@ export interface HardwareBackButtonOptions {
39
39
 
40
40
  /**
41
41
  * (Android) A floating action button over the screen, or an entry of its menu. The button shows while its screen is
42
- * on top of the stack; without the option there is none. `mergeOptions` and the default options of the app apply, and
43
- * the colors of the theme are read again when the theme changes.
42
+ * on top of the stack; without the option there is none. It changes as a push or a pop starts and moves with its
43
+ * screen, as the tab bar does: the new screen's button comes in with it, the old one goes out with the screen it
44
+ * belongs to, and an open menu closes. With an animation of the screens' own the old button shrinks away and the new
45
+ * one grows as it starts; during the back gesture the button of the screen under the top shows with that screen.
46
+ * `mergeOptions` and the default options of the app apply, and the colors of the theme are read again when the theme
47
+ * changes.
44
48
  */
45
49
  export interface OptionsFab {
46
50
  /**
47
51
  * The identifier of the button. A press reports it, and the app finds the `onPress` of the button by it. A button
48
- * without an `id` doesn't show. The same `id` on a later show or merge updates the button; another `id` replaces it.
52
+ * without an `id` doesn't show. The same `id` on a later show or merge updates the button, which stays in place
53
+ * while the screens move; another `id` replaces it.
49
54
  * For an entry of `actions` it is the id of that entry's own press.
50
55
  */
51
56
  id: string;
@@ -654,6 +654,20 @@ export interface OptionsTopBar {
654
654
  centerButtonColor?: Color;
655
655
  /** (iOS 16+) The color of the disabled center buttons that have no `disabledColor` of their own. */
656
656
  centerButtonDisabledColor?: Color;
657
+ /**
658
+ * (Android) The background of the bar's menus: "⋮" and the menus of the buttons, their submenus too. Without it the
659
+ * bar's color (`background.color`, else the theme's surface), unless the bar lets something through; then the
660
+ * theme's surface container color. It follows light and dark mode; an open menu closes as the mode switches. iOS
661
+ * draws its system menus in the system's colors.
662
+ */
663
+ menuBackgroundColor?: Color;
664
+ /**
665
+ * (Android) The color of the text and the icons of the items of the bar's menus, and of the titles of their
666
+ * submenus. Without it the title's color (`title.color`) when the menu takes the bar's color, else the theme's
667
+ * colors. A destructive item stays red, and a button moved into "⋮" keeps its own color or the side's
668
+ * (`rightButtonColor`). A disabled item is dimmed. An open menu closes when either menu color is merged.
669
+ */
670
+ menuTextColor?: Color;
657
671
  /**
658
672
  * Whether the content of the screen goes under the bar, starting at the top of the screen. The screen goes under the
659
673
  * bar by itself in some cases (a large title that may show, `hideOnScroll`, a hidden bar, an explicit translucent
@@ -1,31 +0,0 @@
1
- package dev.apexnative.navigation.views.element
2
-
3
- import android.annotation.SuppressLint
4
- import android.content.Context
5
- import android.graphics.Canvas
6
- import android.view.MotionEvent
7
- import android.view.View
8
-
9
- /**
10
- * The empty view that stands in an element's place in its parent while the element is in the transition layer: the same
11
- * id at the same index with the same layout, so Fabric finds the parent's children as it left them; React removing it
12
- * removes the element for good. It has no `nativeID`, so no search finds it as the element. It draws nothing and takes
13
- * no touches. Main thread only.
14
- */
15
- @SuppressLint("ViewConstructor")
16
- internal class ElementStub(context: Context, element: View) : View(context) {
17
- init {
18
- id = element.id
19
- importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
20
- isClickable = false
21
- isFocusable = false
22
- }
23
-
24
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) =
25
- setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
26
-
27
- override fun draw(canvas: Canvas) = Unit
28
-
29
- @SuppressLint("ClickableViewAccessibility")
30
- override fun onTouchEvent(event: MotionEvent?): Boolean = false
31
- }
@@ -1,134 +0,0 @@
1
- package dev.apexnative.navigation.views.element.animators
2
-
3
- import android.animation.Animator
4
- import android.graphics.Matrix
5
- import android.view.View
6
- import android.view.ViewOutlineProvider
7
- import android.widget.ImageView
8
- import com.facebook.react.views.view.ReactViewGroup
9
- import dev.apexnative.navigation.utils.borderRadius
10
- import dev.apexnative.navigation.views.animations.ValueInterpolation
11
- import dev.apexnative.navigation.views.element.TransitionLayer
12
- import kotlin.math.max
13
- import kotlin.math.min
14
- import kotlin.math.roundToInt
15
-
16
- /**
17
- * A shared image of another kind (an `ImageView` that isn't React Native's): from where `from` is drawn to where it is
18
- * drawn in its screen ([FlightGeometry]), unscaled and turned about its middle; its image matrix from `from`'s fit
19
- * (`centerCrop`, `fitXY`, `fitCenter`, `centerInside`) to its own, the part seen from `from`'s size to its own, the
20
- * corner radius of the parent that clips it (`borderRadius` with `overflow: hidden`) by its outline. Another fit: the
21
- * pair moves without the change of its matrix, and a line goes to the log. Everything goes back as it was at the end.
22
- * Main thread only.
23
- */
24
- internal class ImageMatrixAnimator(
25
- private val from: ImageView,
26
- private val to: ImageView,
27
- /** Where [to] was in its screen: what it flies to. */
28
- private val toStandIn: View?,
29
- private val layer: TransitionLayer,
30
- /** The parents the views are clipped by in their screens: [to]'s is the one it had before the layer. */
31
- private val fromParent: View?,
32
- private val toParent: View?,
33
- private val id: String,
34
- private val log: (String) -> Unit,
35
- ) {
36
- /** The animator; none when [to] has no place in the layer. */
37
- fun create(): Animator? {
38
- val geometry = FlightGeometry.of(from, to, toStandIn, layer) ?: return null
39
- val fromFit = from.scaleType
40
- val toFit = to.scaleType
41
- // The image whose matrix changes; none when a fit isn't one of the four or there is no image to fit.
42
- val image = to.drawable?.takeIf {
43
- fromFit in FITS && toFit in FITS && it.intrinsicWidth > 0 && it.intrinsicHeight > 0
44
- }
45
- val fits = image != null
46
- if (!fits) log("The image '$id' fits by ${fromFit.name} and ${toFit.name}: it moves without a change of fit")
47
- val fromRadius = clipRadius(fromParent) * geometry.from.scale
48
- val toRadius = clipRadius(toParent) * geometry.to.scale
49
- val outline = FlightOutline()
50
- val moved = MovedProperties(to)
51
- var savedProvider: ViewOutlineProvider? = null
52
- var savedClip = false
53
- val savedMatrix = Matrix(to.imageMatrix)
54
- val start = FloatArray(MATRIX)
55
- val end = FloatArray(MATRIX)
56
- val values = FloatArray(MATRIX)
57
- val matrix = Matrix()
58
- return MoveAnimator().apply {
59
- fun frame(time: Float) {
60
- val f = curve.getInterpolation(time)
61
- val at = geometry.at(f, time)
62
- val width = at.width.roundToInt()
63
- val height = at.height.roundToInt()
64
- layer.place(to, geometry.toLeft, geometry.toTop, width, height)
65
- moved.show(geometry, at, width, height)
66
- outline.width = width
67
- outline.height = height
68
- outline.radius = ValueInterpolation.number(fromRadius, toRadius, f, time)
69
- to.invalidateOutline()
70
- if (image != null) {
71
- val imageWidth = image.intrinsicWidth
72
- val imageHeight = image.intrinsicHeight
73
- fitMatrix(fromFit, imageWidth, imageHeight, width, height).getValues(start)
74
- fitMatrix(toFit, imageWidth, imageHeight, width, height).getValues(end)
75
- for (i in 0 until MATRIX) values[i] = ValueInterpolation.number(start[i], end[i], f, time)
76
- matrix.setValues(values)
77
- to.imageMatrix = matrix
78
- }
79
- }
80
- onStart {
81
- savedProvider = to.outlineProvider
82
- savedClip = to.clipToOutline
83
- to.outlineProvider = outline
84
- to.clipToOutline = true
85
- if (fits) to.scaleType = ImageView.ScaleType.MATRIX
86
- frame(0f)
87
- }
88
- onFrame { frame(it) }
89
- onEnd {
90
- if (fits) {
91
- to.scaleType = toFit
92
- to.imageMatrix = savedMatrix
93
- }
94
- to.outlineProvider = savedProvider
95
- to.clipToOutline = savedClip
96
- moved.end(geometry, landed) { w, h -> layer.place(to, geometry.toLeft, geometry.toTop, w, h) }
97
- }
98
- }
99
- }
100
-
101
- // The radius of a React parent that clips what is in it; 0 for another.
102
- private fun clipRadius(parent: View?): Float =
103
- (parent as? ReactViewGroup)?.takeIf { it.overflow == HIDDEN }?.borderRadius ?: 0f
104
-
105
- private companion object {
106
- const val MATRIX = 9
107
- const val HIDDEN = "hidden"
108
- val FITS = setOf(
109
- ImageView.ScaleType.CENTER_CROP,
110
- ImageView.ScaleType.FIT_XY,
111
- ImageView.ScaleType.FIT_CENTER,
112
- ImageView.ScaleType.CENTER_INSIDE,
113
- )
114
-
115
- // The matrix of a fit of an image of [imageWidth] × [imageHeight] into [width] × [height].
116
- fun fitMatrix(fit: ImageView.ScaleType, imageWidth: Int, imageHeight: Int, width: Int, height: Int): Matrix {
117
- val sx = width.toFloat() / imageWidth
118
- val sy = height.toFloat() / imageHeight
119
- val matrix = Matrix()
120
- if (fit == ImageView.ScaleType.FIT_XY) {
121
- matrix.setScale(sx, sy)
122
- return matrix
123
- }
124
- val scale = when (fit) {
125
- ImageView.ScaleType.CENTER_CROP -> max(sx, sy)
126
- ImageView.ScaleType.CENTER_INSIDE -> min(1f, min(sx, sy))
127
- else -> min(sx, sy)
128
- }
129
- matrix.setScale(scale, scale)
130
- matrix.postTranslate((width - imageWidth * scale) / 2f, (height - imageHeight * scale) / 2f)
131
- return matrix
132
- }
133
- }
134
- }
@@ -1,78 +0,0 @@
1
- package dev.apexnative.navigation.views.element.animators
2
-
3
- import android.animation.Animator
4
- import android.view.View
5
- import android.view.ViewOutlineProvider
6
- import com.facebook.react.uimanager.BackgroundStyleApplicator
7
- import com.facebook.react.uimanager.style.BorderRadiusProp
8
- import dev.apexnative.navigation.utils.borderRadius
9
- import dev.apexnative.navigation.views.animations.ValueInterpolation
10
- import dev.apexnative.navigation.views.element.TransitionLayer
11
- import kotlin.math.roundToInt
12
-
13
- /**
14
- * A shared React view without children moves itself: from where `from` is drawn to where it is drawn in its screen
15
- * ([FlightGeometry]); its size and corner radius by an outline that grows from `from`'s (at the scale it is drawn at)
16
- * to its own while it is laid out at the larger of the two sizes, unscaled and turned about the outline's middle; its
17
- * own radius taken off meanwhile and put back as it was; its background color from `from`'s to its own in Lab, ending
18
- * exactly on its own (`BackgroundStyleApplicator`). Main thread only.
19
- */
20
- internal class PlainViewAnimator(
21
- private val from: View,
22
- private val to: View,
23
- /** Where [to] was in its screen: what it flies to. */
24
- private val toStandIn: View?,
25
- private val layer: TransitionLayer,
26
- ) {
27
- /** The animator; none when [to] has no place in the layer. */
28
- fun create(): Animator? {
29
- val geometry = FlightGeometry.of(from, to, toStandIn, layer) ?: return null
30
- val fromRadius = from.borderRadius * geometry.from.scale
31
- val toRadius = to.borderRadius * geometry.to.scale
32
- val fromColor = BackgroundStyleApplicator.getBackgroundColor(from)
33
- val toColor = BackgroundStyleApplicator.getBackgroundColor(to)
34
- val ownRadius = BackgroundStyleApplicator.getBorderRadius(to, RADIUS)
35
- val outline = FlightOutline()
36
- val moved = MovedProperties(to)
37
- var savedProvider: ViewOutlineProvider? = null
38
- var savedClip = false
39
- val (width, height) = geometry.largest
40
- return MoveAnimator().apply {
41
- fun frame(time: Float) {
42
- val f = curve.getInterpolation(time)
43
- val at = geometry.at(f, time)
44
- outline.width = at.width.roundToInt()
45
- outline.height = at.height.roundToInt()
46
- outline.radius = ValueInterpolation.number(fromRadius, toRadius, f, time)
47
- moved.show(geometry, at, outline.width, outline.height)
48
- to.invalidateOutline()
49
- if (fromColor != toColor) {
50
- val color = ValueInterpolation.color(fromColor, toColor, f, time)
51
- BackgroundStyleApplicator.setBackgroundColor(to, color)
52
- }
53
- }
54
- onStart {
55
- savedProvider = to.outlineProvider
56
- savedClip = to.clipToOutline
57
- if (ownRadius != null) BackgroundStyleApplicator.setBorderRadius(to, RADIUS, null)
58
- to.outlineProvider = outline
59
- to.clipToOutline = true
60
- layer.place(to, geometry.toLeft, geometry.toTop, width, height)
61
- frame(0f)
62
- }
63
- onFrame { frame(it) }
64
- // A cancelled flight ends too: the view goes back whole.
65
- onEnd {
66
- if (fromColor != toColor) BackgroundStyleApplicator.setBackgroundColor(to, toColor)
67
- if (ownRadius != null) BackgroundStyleApplicator.setBorderRadius(to, RADIUS, ownRadius)
68
- to.outlineProvider = savedProvider
69
- to.clipToOutline = savedClip
70
- moved.end(geometry, landed) { w, h -> layer.place(to, geometry.toLeft, geometry.toTop, w, h) }
71
- }
72
- }
73
- }
74
-
75
- private companion object {
76
- val RADIUS = BorderRadiusProp.BORDER_RADIUS
77
- }
78
- }
@@ -1,33 +0,0 @@
1
- package dev.apexnative.navigation.views.element.animators
2
-
3
- import android.animation.Animator
4
- import android.view.View
5
- import dev.apexnative.navigation.options.SharedElementTransitionOptions
6
-
7
- /**
8
- * One animator of a shared element: how [from], on the screen that goes, becomes [to], on the screen that comes, when
9
- * both are of [type]. The core picks one by the views' classes (`SharedAnimatorFactory`) and runs it in the
10
- * transition's layer.
11
- */
12
- abstract class PropertyAnimatorCreator<T : View>(
13
- protected val from: View,
14
- protected val to: View,
15
- private val type: Class<T>,
16
- ) {
17
- /** Whether this animator takes part: both views are of its type and differ in what it animates. */
18
- fun shouldAnimate(): Boolean {
19
- if (!type.isInstance(from) || !type.isInstance(to)) return false
20
- val fromChild = type.cast(from) ?: return false
21
- val toChild = type.cast(to) ?: return false
22
- return shouldAnimateProperty(fromChild, toChild)
23
- }
24
-
25
- /** Whether [fromChild] and [toChild] differ in what this animator animates. */
26
- abstract fun shouldAnimateProperty(fromChild: T, toChild: T): Boolean
27
-
28
- /**
29
- * The animator of the pair for [options]; the core gives it the pair's duration, delay and curve (each animator
30
- * its own, never through a set around it).
31
- */
32
- abstract fun create(options: SharedElementTransitionOptions): Animator
33
- }