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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +14 -12
  4. package/android/CORE.md +9 -6
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +36 -5
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  7. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  8. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +0 -2
  9. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  10. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +12 -49
  11. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  12. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -6
  13. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  14. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +9 -7
  15. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  16. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +13 -10
  17. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  18. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +7 -8
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -0
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +4 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  33. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  34. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  35. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  36. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  37. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  38. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  46. package/docs-md/architecture/transitions.md +4 -2
  47. package/docs-md/containers/modals.md +2 -0
  48. package/docs-md/containers/overlays.md +1 -1
  49. package/docs-md/containers/side-menu.md +1 -1
  50. package/docs-md/containers/tabs.md +2 -0
  51. package/docs-md/flows/bottom-sheets.md +1 -1
  52. package/docs-md/fundamentals/starting-the-app.md +1 -1
  53. package/docs-md/help/glossary.md +2 -2
  54. package/docs-md/help/troubleshooting.md +21 -0
  55. package/docs-md/index.json +12 -0
  56. package/docs-md/migration/from-expo-router.md +1 -1
  57. package/docs-md/migration/from-react-native-navigation.md +1 -1
  58. package/docs-md/migration/from-react-navigation.md +2 -2
  59. package/docs-md/screen-ui/back-and-gestures.md +14 -0
  60. package/docs-md/screen-ui/bar-buttons.md +4 -4
  61. package/docs-md/screen-ui/search.md +1 -1
  62. package/docs-md/screen-ui/system-ui.md +16 -11
  63. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  64. package/docs-md/screen-ui/top-bar.md +8 -4
  65. package/ios/Animations/ANNFrameDriver.mm +2 -0
  66. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  67. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  68. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  69. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  70. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  71. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  72. package/ios/CLAUDE.md +17 -15
  73. package/ios/CORE.md +24 -20
  74. package/ios/Contract/ANNCommandRunner.h +8 -7
  75. package/ios/Contract/ANNCommandRunner.mm +21 -27
  76. package/ios/Contract/ANNLayoutNode.h +3 -3
  77. package/ios/Contract/ANNLayoutReader.h +3 -3
  78. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  79. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  80. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  81. package/ios/Core/ApexNavigation+Internal.h +1 -1
  82. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  83. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  84. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  87. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  88. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  89. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  90. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  91. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  92. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  93. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  94. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  95. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  96. package/ios/Layouts/Stack/ANNPreviewPush.mm +57 -5
  97. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  98. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  100. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  103. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  104. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  105. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  106. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  107. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  108. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  109. package/ios/Options/ANNOptions.h +18 -0
  110. package/ios/Options/ANNOptions.mm +112 -1
  111. package/ios/Options/ANNOptionsReader.mm +110 -0
  112. package/ios/Options/Params/ANNParam.h +15 -0
  113. package/ios/Options/Params/ANNParam.mm +61 -0
  114. package/ios/Options/Params/Color.h +3 -0
  115. package/ios/Options/Params/Color.mm +21 -0
  116. package/ios/Options/Params/Image.h +4 -0
  117. package/ios/Options/Params/Image.mm +20 -0
  118. package/ios/React/ANNReactView.mm +6 -25
  119. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  120. package/ios/React/ANNSurfaceHostingView.h +6 -0
  121. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  122. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  123. package/ios/TopBar/ANNBarAppearance.h +2 -1
  124. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  125. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  126. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  127. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  128. package/ios/TopBar/ANNBarButtons.h +5 -3
  129. package/ios/TopBar/ANNBarButtons.mm +186 -17
  130. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  131. package/ios/TopBar/ANNMainList.h +3 -0
  132. package/ios/TopBar/ANNMainList.mm +4 -0
  133. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  134. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  135. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  136. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  137. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  138. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  139. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  140. package/lib/module/ARCHITECTURE.md +8 -4
  141. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  142. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  143. package/lib/module/api/options/OptionsChain.js +2 -2
  144. package/lib/module/engine/commands.js +21 -1
  145. package/lib/module/engine/commands.js.map +1 -1
  146. package/lib/module/engine/screens.js +29 -4
  147. package/lib/module/engine/screens.js.map +1 -1
  148. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  149. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  150. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  151. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  152. package/lib/typescript/engine/commands.d.ts.map +1 -1
  153. package/lib/typescript/engine/screens.d.ts +20 -6
  154. package/lib/typescript/engine/screens.d.ts.map +1 -1
  155. package/lib/typescript/interfaces/options/animations.d.ts +12 -7
  156. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  157. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  158. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  159. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  160. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  161. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  162. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  163. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  164. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  165. package/package.json +1 -1
  166. package/src/ARCHITECTURE.md +8 -4
  167. package/src/api/app/createNavigation.ts +3 -3
  168. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  169. package/src/api/options/OptionsChain.ts +2 -2
  170. package/src/engine/commands.ts +30 -6
  171. package/src/engine/screens.tsx +29 -8
  172. package/src/interfaces/options/animations.ts +12 -7
  173. package/src/interfaces/options/bottomTabs.ts +18 -5
  174. package/src/interfaces/options/buttons.ts +8 -8
  175. package/src/interfaces/options/system.ts +8 -5
  176. package/src/interfaces/options/toolbar.ts +2 -2
  177. package/src/interfaces/options/topBar.ts +32 -29
  178. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  179. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  180. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  181. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  182. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
@@ -14,15 +14,18 @@ import kotlin.math.min
14
14
  import kotlin.math.roundToInt
15
15
 
16
16
  /**
17
- * A shared image of another kind (an `ImageView` that isn't React Native's): its place by a translation; its image
18
- * matrix from `from`'s fit (`centerCrop`, `fitXY`, `fitCenter`, `centerInside`) to its own, the part seen from `from`'s
19
- * size to its own, the corner radius of the parent that clips it (`borderRadius` with `overflow: hidden`) by its
20
- * outline. Another fit: the pair moves without the change of its matrix, and a line goes to the log. Everything goes
21
- * back as it was at the end. Main thread only.
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.
22
23
  */
23
24
  internal class ImageMatrixAnimator(
24
25
  private val from: ImageView,
25
26
  private val to: ImageView,
27
+ /** Where [to] was in its screen: what it flies to. */
28
+ private val toStandIn: View?,
26
29
  private val layer: TransitionLayer,
27
30
  /** The parents the views are clipped by in their screens: [to]'s is the one it had before the layer. */
28
31
  private val fromParent: View?,
@@ -32,8 +35,7 @@ internal class ImageMatrixAnimator(
32
35
  ) {
33
36
  /** The animator; none when [to] has no place in the layer. */
34
37
  fun create(): Animator? {
35
- val geometry = FlightGeometry.of(from, to, layer) ?: return null
36
- val toRect = geometry.to
38
+ val geometry = FlightGeometry.of(from, to, toStandIn, layer) ?: return null
37
39
  val fromFit = from.scaleType
38
40
  val toFit = to.scaleType
39
41
  // The image whose matrix changes; none when a fit isn't one of the four or there is no image to fit.
@@ -42,9 +44,10 @@ internal class ImageMatrixAnimator(
42
44
  }
43
45
  val fits = image != null
44
46
  if (!fits) log("The image '$id' fits by ${fromFit.name} and ${toFit.name}: it moves without a change of fit")
45
- val fromRadius = clipRadius(fromParent) * geometry.fromScale
46
- val toRadius = clipRadius(toParent)
47
+ val fromRadius = clipRadius(fromParent) * geometry.from.scale
48
+ val toRadius = clipRadius(toParent) * geometry.to.scale
47
49
  val outline = FlightOutline()
50
+ val moved = MovedProperties(to)
48
51
  var savedProvider: ViewOutlineProvider? = null
49
52
  var savedClip = false
50
53
  val savedMatrix = Matrix(to.imageMatrix)
@@ -55,13 +58,11 @@ internal class ImageMatrixAnimator(
55
58
  return MoveAnimator().apply {
56
59
  fun frame(time: Float) {
57
60
  val f = curve.getInterpolation(time)
58
- val left = ValueInterpolation.number(geometry.from.left, toRect.left, f, time)
59
- val top = ValueInterpolation.number(geometry.from.top, toRect.top, f, time)
60
- val width = ValueInterpolation.number(geometry.from.width(), toRect.width(), f, time).roundToInt()
61
- val height = ValueInterpolation.number(geometry.from.height(), toRect.height(), f, time).roundToInt()
62
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), width, height)
63
- to.translationX = left - toRect.left
64
- to.translationY = top - toRect.top
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)
65
66
  outline.width = width
66
67
  outline.height = height
67
68
  outline.radius = ValueInterpolation.number(fromRadius, toRadius, f, time)
@@ -92,10 +93,7 @@ internal class ImageMatrixAnimator(
92
93
  }
93
94
  to.outlineProvider = savedProvider
94
95
  to.clipToOutline = savedClip
95
- to.translationX = 0f
96
- to.translationY = 0f
97
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), toRect.width().toInt(),
98
- toRect.height().toInt())
96
+ moved.end(geometry, landed) { w, h -> layer.place(to, geometry.toLeft, geometry.toTop, w, h) }
99
97
  }
100
98
  }
101
99
  }
@@ -11,49 +11,45 @@ import dev.apexnative.navigation.views.element.TransitionLayer
11
11
  import kotlin.math.roundToInt
12
12
 
13
13
  /**
14
- * A shared React view without children moves itself: from `from`'s place on the screen to its own by a translation; its
15
- * size and corner radius by an outline that grows from `from`'s (at the scale of its parents) to its own while it is
16
- * laid out at the larger of the two sizes; its own radius taken off meanwhile and put back as it was; its background
17
- * color from `from`'s to its own in Lab, ending exactly on its own (`BackgroundStyleApplicator`); its rotation from
18
- * `from`'s to its own. Main thread only.
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
19
  */
20
20
  internal class PlainViewAnimator(
21
21
  private val from: View,
22
22
  private val to: View,
23
+ /** Where [to] was in its screen: what it flies to. */
24
+ private val toStandIn: View?,
23
25
  private val layer: TransitionLayer,
24
26
  ) {
25
27
  /** The animator; none when [to] has no place in the layer. */
26
28
  fun create(): Animator? {
27
- val geometry = FlightGeometry.of(from, to, layer) ?: return null
28
- val fromRadius = from.borderRadius * geometry.fromScale
29
- val toRadius = to.borderRadius
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
30
32
  val fromColor = BackgroundStyleApplicator.getBackgroundColor(from)
31
33
  val toColor = BackgroundStyleApplicator.getBackgroundColor(to)
32
- val fromRotation = from.rotation
33
- val toRotation = to.rotation
34
34
  val ownRadius = BackgroundStyleApplicator.getBorderRadius(to, RADIUS)
35
35
  val outline = FlightOutline()
36
+ val moved = MovedProperties(to)
36
37
  var savedProvider: ViewOutlineProvider? = null
37
38
  var savedClip = false
38
39
  val (width, height) = geometry.largest
39
- val toRect = geometry.to
40
40
  return MoveAnimator().apply {
41
41
  fun frame(time: Float) {
42
42
  val f = curve.getInterpolation(time)
43
- val left = ValueInterpolation.number(geometry.from.left, toRect.left, f, time)
44
- val top = ValueInterpolation.number(geometry.from.top, toRect.top, f, time)
45
- to.translationX = left - toRect.left
46
- to.translationY = top - toRect.top
47
- val from = geometry.from
48
- outline.width = ValueInterpolation.number(from.width(), toRect.width(), f, time).roundToInt()
49
- outline.height = ValueInterpolation.number(from.height(), toRect.height(), f, time).roundToInt()
43
+ val at = geometry.at(f, time)
44
+ outline.width = at.width.roundToInt()
45
+ outline.height = at.height.roundToInt()
50
46
  outline.radius = ValueInterpolation.number(fromRadius, toRadius, f, time)
47
+ moved.show(geometry, at, outline.width, outline.height)
51
48
  to.invalidateOutline()
52
49
  if (fromColor != toColor) {
53
50
  val color = ValueInterpolation.color(fromColor, toColor, f, time)
54
51
  BackgroundStyleApplicator.setBackgroundColor(to, color)
55
52
  }
56
- to.rotation = ValueInterpolation.number(fromRotation, toRotation, f, time)
57
53
  }
58
54
  onStart {
59
55
  savedProvider = to.outlineProvider
@@ -61,21 +57,17 @@ internal class PlainViewAnimator(
61
57
  if (ownRadius != null) BackgroundStyleApplicator.setBorderRadius(to, RADIUS, null)
62
58
  to.outlineProvider = outline
63
59
  to.clipToOutline = true
64
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), width, height)
60
+ layer.place(to, geometry.toLeft, geometry.toTop, width, height)
65
61
  frame(0f)
66
62
  }
67
63
  onFrame { frame(it) }
68
64
  // A cancelled flight ends too: the view goes back whole.
69
65
  onEnd {
70
- to.translationX = 0f
71
- to.translationY = 0f
72
- to.rotation = toRotation
73
66
  if (fromColor != toColor) BackgroundStyleApplicator.setBackgroundColor(to, toColor)
74
67
  if (ownRadius != null) BackgroundStyleApplicator.setBorderRadius(to, RADIUS, ownRadius)
75
68
  to.outlineProvider = savedProvider
76
69
  to.clipToOutline = savedClip
77
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), toRect.width().toInt(),
78
- toRect.height().toInt())
70
+ moved.end(geometry, landed) { w, h -> layer.place(to, geometry.toLeft, geometry.toTop, w, h) }
79
71
  }
80
72
  }
81
73
  }
@@ -1,46 +1,43 @@
1
1
  package dev.apexnative.navigation.views.element.animators
2
2
 
3
3
  import android.animation.Animator
4
+ import android.view.View
4
5
  import com.facebook.drawee.drawable.ScalingUtils
5
6
  import com.facebook.react.views.image.ReactImageView
6
- import dev.apexnative.navigation.views.animations.ValueInterpolation
7
7
  import dev.apexnative.navigation.views.element.TransitionLayer
8
8
  import kotlin.math.roundToInt
9
9
 
10
10
  /**
11
- * A shared React image: its place by a translation from `from`'s on the screen; its content by Fresco's scale type
12
- * going from `from`'s fit to its own (`ScalingUtils.InterpolatingScaleType`); the part seen from `from`'s size to its
13
- * own (it is laid out at the size of each frame); its rotation from `from`'s to its own; Fresco's own fade off
14
- * meanwhile. Everything goes back as it was at the end. Main thread only.
11
+ * A shared React image: from where `from` is drawn to where it is drawn in its screen ([FlightGeometry]), laid out at
12
+ * the size of each frame, unscaled and turned about its middle; its content by Fresco's scale type going from `from`'s
13
+ * fit to its own (`ScalingUtils.InterpolatingScaleType`); Fresco's own fade off meanwhile. Everything goes back as it
14
+ * was at the end. Main thread only.
15
15
  */
16
16
  internal class ReactImageAnimator(
17
17
  private val from: ReactImageView,
18
18
  private val to: ReactImageView,
19
+ /** Where [to] was in its screen: what it flies to. */
20
+ private val toStandIn: View?,
19
21
  private val layer: TransitionLayer,
20
22
  ) {
21
23
  /** The animator; none when [to] has no place in the layer or no hierarchy. */
22
24
  fun create(): Animator? {
23
25
  if (!from.hasHierarchy() || !to.hasHierarchy()) return null
24
- val geometry = FlightGeometry.of(from, to, layer) ?: return null
25
- val toRect = geometry.to
26
+ val geometry = FlightGeometry.of(from, to, toStandIn, layer) ?: return null
26
27
  val toType = to.hierarchy.actualImageScaleType ?: ScalingUtils.ScaleType.CENTER_CROP
27
28
  val fromType = from.hierarchy.actualImageScaleType ?: ScalingUtils.ScaleType.CENTER_CROP
28
29
  val scale = ScalingUtils.InterpolatingScaleType(fromType, toType)
29
- val fromRotation = from.rotation
30
- val toRotation = to.rotation
30
+ val moved = MovedProperties(to)
31
31
  var savedFade = 0
32
32
  return MoveAnimator().apply {
33
33
  fun frame(time: Float) {
34
34
  val f = curve.getInterpolation(time)
35
- val left = ValueInterpolation.number(geometry.from.left, toRect.left, f, time)
36
- val top = ValueInterpolation.number(geometry.from.top, toRect.top, f, time)
37
- val width = ValueInterpolation.number(geometry.from.width(), toRect.width(), f, time).roundToInt()
38
- val height = ValueInterpolation.number(geometry.from.height(), toRect.height(), f, time).roundToInt()
39
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), width, height)
40
- to.translationX = left - toRect.left
41
- to.translationY = top - toRect.top
35
+ val at = geometry.at(f, time)
36
+ val width = at.width.roundToInt()
37
+ val height = at.height.roundToInt()
38
+ layer.place(to, geometry.toLeft, geometry.toTop, width, height)
39
+ moved.show(geometry, at, width, height)
42
40
  scale.setValue(if (time >= 1f) 1f else f)
43
- to.rotation = ValueInterpolation.number(fromRotation, toRotation, f, time)
44
41
  to.invalidate()
45
42
  }
46
43
  onStart {
@@ -53,11 +50,7 @@ internal class ReactImageAnimator(
53
50
  onEnd {
54
51
  to.hierarchy.setActualImageScaleType(toType)
55
52
  to.hierarchy.fadeDuration = savedFade
56
- to.translationX = 0f
57
- to.translationY = 0f
58
- to.rotation = toRotation
59
- layer.place(to, toRect.left.toInt(), toRect.top.toInt(), toRect.width().toInt(),
60
- toRect.height().toInt())
53
+ moved.end(geometry, landed) { w, h -> layer.place(to, geometry.toLeft, geometry.toTop, w, h) }
61
54
  }
62
55
  }
63
56
  }
@@ -29,13 +29,14 @@ internal object SharedAnimatorFactory {
29
29
 
30
30
  /**
31
31
  * The flight of the pair [from] → [to]; [to] is in [layer] at its margins, at its real alpha (the pictures are
32
- * taken now). [toParent] is the parent [to] had in its screen. None: the pair doesn't fly (a text that didn't
33
- * change, a picture that can't be taken, no place).
32
+ * taken now). [toParent] is the parent [to] had in its screen, [toStandIn] the view in its place there. None: the
33
+ * pair doesn't fly (a text that didn't change, a picture that can't be taken, no place).
34
34
  */
35
35
  fun create(
36
36
  from: View,
37
37
  to: View,
38
38
  toParent: View?,
39
+ toStandIn: View?,
39
40
  layer: TransitionLayer,
40
41
  options: SharedElementTransitionOptions,
41
42
  fallbackDuration: Long,
@@ -43,17 +44,19 @@ internal object SharedAnimatorFactory {
43
44
  ): Flight? {
44
45
  val flight = when {
45
46
  from is ReactTextView && to is ReactTextView -> pictures(
46
- TextChangeAnimator(from, to).takeIf { it.shouldAnimate() }?.create(options),
47
+ TextChangeAnimator(from, to, toStandIn).takeIf { it.shouldAnimate() }?.create(options),
47
48
  )
48
49
  hasChildren(from) || hasChildren(to) -> pictures(
49
- ViewChangeAnimator(from, to).takeIf { it.shouldAnimate() }?.create(options),
50
- ) ?: moves(PlainViewAnimator(from, to, layer).create())
51
- from is ReactImageView && to is ReactImageView -> moves(ReactImageAnimator(from, to, layer).create())
50
+ ViewChangeAnimator(from, to, toStandIn).takeIf { it.shouldAnimate() }?.create(options),
51
+ ) ?: moves(PlainViewAnimator(from, to, toStandIn, layer).create())
52
+ from is ReactImageView && to is ReactImageView ->
53
+ moves(ReactImageAnimator(from, to, toStandIn, layer).create())
52
54
  from is ImageView && to is ImageView -> moves(
53
- ImageMatrixAnimator(from, to, layer, from.parent as? View, toParent, options.toId.get(""), log)
54
- .create(),
55
+ ImageMatrixAnimator(
56
+ from, to, toStandIn, layer, from.parent as? View, toParent, options.toId.get(""), log,
57
+ ).create(),
55
58
  )
56
- else -> moves(PlainViewAnimator(from, to, layer).create())
59
+ else -> moves(PlainViewAnimator(from, to, toStandIn, layer).create())
57
60
  } ?: return null
58
61
  val animator = flight.animator
59
62
  (animator as? MoveAnimator)?.element = pairName(options.fromId.get(""), options.toId.get(""))
@@ -14,24 +14,30 @@ import android.widget.TextView
14
14
  import com.facebook.react.views.text.ReactTextView
15
15
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
16
16
  import dev.apexnative.navigation.utils.TextViewUtils
17
- import dev.apexnative.navigation.utils.ViewUtils
17
+ import dev.apexnative.navigation.views.animations.ValueInterpolation
18
+ import kotlin.math.abs
19
+ import kotlin.math.min
18
20
  import kotlin.math.roundToInt
19
21
 
20
22
  // The text moves as two pictures of itself, of the text that goes away and of the text that comes, the same text or
21
23
  // another. Both keep their proportions, scaled by the font size from a point of the first line, and one turns into the
22
24
  // other in a quarter of the move: at its start when the text grows, at its end when it shrinks, so the larger picture
23
- // shows most of the way (as on iOS). The pictures are drawing commands, not pixels: they stay sharp at any scale and
24
- // keep the colors, emoji and background of the text. They are drawn in the overlay of the transition layer while `to`
25
- // is hidden.
26
- class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTextView>(from, to, ReactTextView::class.java) {
25
+ // shows most of the way (as on iOS). Each text is where it is drawn, at the scale and angle its parents and its own
26
+ // properties give it; the angle goes along the shorter arc (FlightGeometry). The pictures are drawing commands, not
27
+ // pixels: they stay sharp at any scale and keep the colors, emoji and background of the text. They are drawn in the
28
+ // overlay of the transition layer while `to` is hidden.
29
+ class TextChangeAnimator internal constructor(
30
+ from: View,
31
+ to: View,
32
+ // Where `to` was in its screen: what the text flies to.
33
+ private val toStandIn: View?,
34
+ ) : PropertyAnimatorCreator<ReactTextView>(from, to, ReactTextView::class.java) {
27
35
  private val pictures by lazy { pictures() }
28
36
 
29
37
  override fun shouldAnimateProperty(fromChild: ReactTextView, toChild: ReactTextView): Boolean {
30
- val fromPlace = ViewUtils.getLocationOnScreen(from)
31
- val toPlace = ViewUtils.getLocationOnScreen(to)
32
- return (fromChild.text.toString() != toChild.text.toString() ||
33
- TextViewUtils.getTextSize(fromChild) != TextViewUtils.getTextSize(toChild) || fromPlace != toPlace
34
- ) && pictures != null
38
+ val (start, end) = pictures ?: return false
39
+ return fromChild.text.toString() != toChild.text.toString() ||
40
+ TextViewUtils.getTextSize(fromChild) != TextViewUtils.getTextSize(toChild) || start.drawnElsewhere(end)
35
41
  }
36
42
 
37
43
  override fun create(options: SharedElementTransitionOptions): Animator {
@@ -49,43 +55,63 @@ class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTe
49
55
  drawable.update(it, curve.getInterpolation(it))
50
56
  layer.invalidate()
51
57
  }
58
+ // A flight that landed where `to` shows only in its screen keeps its last picture: the transition takes the
59
+ // layer away with it and shows `to` in its place.
60
+ val holds = FlightEnd.holdsAtEnd(to, toStandIn, layer)
52
61
  onEnd {
53
- layer.overlay.remove(drawable)
54
- layer.invalidate()
55
- to.alpha = end.alpha
62
+ if (!holds || !landed) {
63
+ layer.overlay.remove(drawable)
64
+ layer.invalidate()
65
+ to.alpha = end.alpha
66
+ }
56
67
  }
57
68
  }
58
69
  }
59
70
 
60
- // `to` is in the transition layer already, at its margins (the coordinates of the other animators); `from` is where
61
- // it is on the screen. Texts that line up the same way go from that point of their first lines (the same text lies
62
- // over itself); others from the start of the line, as on iOS: a centered text no longer sticks out of a text at the
63
- // start.
71
+ // `to` is in the transition layer already: the pictures are drawn in its coordinates. `from` is where it is drawn
72
+ // on the screen, `to` where it is drawn once back in its screen. Texts that line up the same way go from that point
73
+ // of their first lines (the same text lies over itself); others from the start of the line, as on iOS: a centered
74
+ // text no longer sticks out of a text at the start.
64
75
  private fun pictures(): Pair<TextPicture, TextPicture>? {
65
- if (to.parent !is ViewGroup) return null
66
- val toPlace = to.layoutParams as? ViewGroup.MarginLayoutParams ?: return null
76
+ val layer = to.parent as? ViewGroup ?: return null
67
77
  val fromLayout = (from as TextView).layout ?: return null
68
78
  val toLayout = (to as TextView).layout ?: return null
69
79
  val edge = lineEdge(fromLayout).takeIf { it == lineEdge(toLayout) } ?: LineEdge.START
70
- val fromPlace = ViewUtils.getLocationOnScreen(from)
71
- .apply { ViewUtils.getLocationOnScreen(to.parent as View).let { offset(-it.x, -it.y) } }
72
- val start = TextPicture.of(from, fromPlace.x, fromPlace.y, edge) ?: return null
73
- val end = TextPicture.of(to, toPlace.leftMargin, toPlace.topMargin, edge) ?: return null
80
+ val start = TextPicture.of(from, LayerPlace.of(from, layer), edge) ?: return null
81
+ val end = TextPicture.of(to, LayerPlace.of(to, layer, toStandIn), edge) ?: return null
74
82
  return start to end
75
83
  }
76
84
  }
77
85
 
78
- // A text's picture, the point of its first line it is scaled from (in the view) and where that point is in the
79
- // transition layer, its font size and opacity.
86
+ // A text's picture, the point of its first line it is scaled from (in the view) and where that point is drawn in the
87
+ // transition layer, its font size, the scale and angle it is drawn at, and its opacity.
80
88
  private class TextPicture(
81
89
  val picture: Picture,
82
90
  val anchor: PointF,
83
91
  val position: PointF,
84
92
  val fontSize: Float,
93
+ val scale: Float,
94
+ val rotation: Float,
95
+ val mirrorX: Float,
96
+ val mirrorY: Float,
85
97
  val alpha: Float
86
98
  ) {
99
+ /** The font size it shows at. */
100
+ val shownSize: Float get() = fontSize * scale
101
+
102
+ /** Whether [other] is drawn elsewhere, larger or smaller, or turned. */
103
+ fun drawnElsewhere(other: TextPicture): Boolean =
104
+ abs(position.x - other.position.x) > SAME_PLACE || abs(position.y - other.position.y) > SAME_PLACE ||
105
+ abs(scale - other.scale) > SAME_SCALE || abs(rotation - other.rotation) > SAME_ANGLE ||
106
+ mirrorX != other.mirrorX || mirrorY != other.mirrorY
107
+
87
108
  companion object {
88
- fun of(view: TextView, left: Int, top: Int, edge: LineEdge): TextPicture? {
109
+ // Within half a pixel, a thousandth of the scale and a tenth of a degree: the same.
110
+ private const val SAME_PLACE = 0.5f
111
+ private const val SAME_SCALE = 0.001f
112
+ private const val SAME_ANGLE = 0.1f
113
+
114
+ fun of(view: TextView, place: LayerPlace, edge: LineEdge): TextPicture? {
89
115
  val layout = view.layout ?: return null
90
116
  val fontSize = TextViewUtils.getTextSize(view).takeIf { it > 0 } ?: view.textSize
91
117
  if (view.width <= 0 || view.height <= 0 || layout.lineCount == 0 || fontSize <= 0) return null
@@ -95,7 +121,14 @@ private class TextPicture(
95
121
  view.totalPaddingLeft + edgeX(layout, edge),
96
122
  view.totalPaddingTop + layout.getLineBaseline(0).toFloat()
97
123
  )
98
- return TextPicture(picture, anchor, PointF(left + anchor.x, top + anchor.y), fontSize, view.alpha)
124
+ val position = place.point(anchor.x, anchor.y)
125
+ val scale = min(abs(place.scaleX), abs(place.scaleY))
126
+ return TextPicture(
127
+ picture, anchor, PointF(position[0], position[1]), fontSize, scale, place.rotation,
128
+ mirrorX = if (place.scaleX < 0) -1f else 1f,
129
+ mirrorY = if (place.scaleY < 0) -1f else 1f,
130
+ alpha = view.alpha,
131
+ )
99
132
  }
100
133
 
101
134
  // That point of the first line, in the direction of the text.
@@ -127,15 +160,19 @@ private fun lineEdge(layout: Layout): LineEdge {
127
160
  }
128
161
  }
129
162
 
130
- // Both pictures where the move has brought the text: the font size and the point of the first line go from the start
131
- // to the end along the transition's curve, the pictures change over by the clock.
163
+ // Both pictures where the move has brought the text: the font size it shows at, its angle and the point of the first
164
+ // line go from the start to the end along the transition's curve, the pictures change over by the clock.
132
165
  private class TextMorphDrawable(private val start: TextPicture, private val end: TextPicture) : Drawable() {
133
- private val grows = end.fontSize >= start.fontSize
166
+ private val grows = end.shownSize >= start.shownSize
134
167
  private var progress = 0f
168
+ private var time = 0f
135
169
  private var changeOver = 0f
170
+ private var mirrorX = 1f
171
+ private var mirrorY = 1f
136
172
 
137
173
  fun update(time: Float, progress: Float) {
138
174
  this.progress = progress
175
+ this.time = time
139
176
  val changeOverStart = if (grows) 0f else 1f - CHANGE_OVER
140
177
  changeOver = ((time - changeOverStart) / CHANGE_OVER).coerceIn(0f, 1f)
141
178
  }
@@ -143,18 +180,30 @@ private class TextMorphDrawable(private val start: TextPicture, private val end:
143
180
  override fun draw(canvas: Canvas) {
144
181
  val x = start.position.x + (end.position.x - start.position.x) * progress
145
182
  val y = start.position.y + (end.position.y - start.position.y) * progress
146
- val fontSize = start.fontSize + (end.fontSize - start.fontSize) * progress
147
- draw(canvas, start, x, y, fontSize, 1f - changeOver)
148
- draw(canvas, end, x, y, fontSize, changeOver)
183
+ val fontSize = start.shownSize + (end.shownSize - start.shownSize) * progress
184
+ val rotation = ValueInterpolation.angle(start.rotation, end.rotation, progress, time)
185
+ mirrorX = ValueInterpolation.number(start.mirrorX, end.mirrorX, progress, time)
186
+ mirrorY = ValueInterpolation.number(start.mirrorY, end.mirrorY, progress, time)
187
+ draw(canvas, start, x, y, fontSize, rotation, 1f - changeOver)
188
+ draw(canvas, end, x, y, fontSize, rotation, changeOver)
149
189
  }
150
190
 
151
- private fun draw(canvas: Canvas, text: TextPicture, x: Float, y: Float, fontSize: Float, shown: Float) {
191
+ private fun draw(
192
+ canvas: Canvas,
193
+ text: TextPicture,
194
+ x: Float,
195
+ y: Float,
196
+ fontSize: Float,
197
+ rotation: Float,
198
+ shown: Float,
199
+ ) {
152
200
  val alpha = (shown * text.alpha * 255).roundToInt().coerceIn(0, 255)
153
201
  if (alpha == 0) return
154
202
  val scale = fontSize / text.fontSize
155
203
  val count = canvas.save()
156
204
  canvas.translate(x, y)
157
- canvas.scale(scale, scale)
205
+ canvas.rotate(rotation)
206
+ canvas.scale(scale * mirrorX, scale * mirrorY)
158
207
  canvas.translate(-text.anchor.x, -text.anchor.y)
159
208
  // The picture fades as a whole: its letters and background don't show through each other.
160
209
  if (alpha < 255) {
@@ -14,17 +14,22 @@ import android.view.View
14
14
  import android.view.ViewGroup
15
15
  import com.facebook.react.views.view.ReactViewGroup
16
16
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
17
- import dev.apexnative.navigation.utils.ViewUtils
18
17
  import dev.apexnative.navigation.utils.borderRadius
19
- import dev.apexnative.navigation.utils.computeInheritedScale
20
18
  import kotlin.math.roundToInt
21
19
 
22
20
  // A view with children moves as two pictures of itself, as on iOS: of the view that goes away and of the one that
23
- // comes, each with its children. The copy goes from the place, size and corner radius of the one to those of the other,
24
- // both pictures fill it, and the picture of the destination shows over the other on the way. The pictures are drawing
25
- // commands: an avatar's letter grows sharp. They are drawn in the overlay of the transition layer while `to` is hidden,
26
- // and nothing else moves the pair. A view without children moves itself, its outline growing, as before.
27
- class ViewChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactViewGroup>(from, to, ReactViewGroup::class.java) {
21
+ // comes, each with its children. The copy goes from where the one is drawn — its center, its size at the scale it is
22
+ // drawn at, its angle and corner radius — to where the other is drawn in its screen (FlightGeometry), the angle along
23
+ // the shorter arc; both pictures fill it, and the picture of the destination shows over the other on the way. The
24
+ // pictures are drawing commands: an avatar's letter grows sharp. They are drawn in the overlay of the transition layer
25
+ // while `to` is hidden, and nothing else moves the pair. A view without children moves itself, its outline growing, as
26
+ // before.
27
+ class ViewChangeAnimator internal constructor(
28
+ from: View,
29
+ to: View,
30
+ // Where `to` was in its screen: what the copy flies to.
31
+ private val toStandIn: View?,
32
+ ) : PropertyAnimatorCreator<ReactViewGroup>(from, to, ReactViewGroup::class.java) {
28
33
  private val pictures by lazy { pictures() }
29
34
 
30
35
  override fun shouldAnimateProperty(fromChild: ReactViewGroup, toChild: ReactViewGroup): Boolean =
@@ -42,68 +47,72 @@ class ViewChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactVi
42
47
  to.alpha = 0f
43
48
  }
44
49
  onFrame {
45
- drawable.progress = curve.getInterpolation(it)
50
+ drawable.update(it, curve.getInterpolation(it))
46
51
  layer.invalidate()
47
52
  }
53
+ // A flight that landed where `to` shows only in its screen keeps its last picture: the transition takes the
54
+ // layer away with it and shows `to` in its place.
55
+ val holds = FlightEnd.holdsAtEnd(to, toStandIn, layer)
48
56
  onEnd {
49
- layer.overlay.remove(drawable)
50
- layer.invalidate()
51
- to.alpha = end.alpha
57
+ if (!holds || !landed) {
58
+ layer.overlay.remove(drawable)
59
+ layer.invalidate()
60
+ to.alpha = end.alpha
61
+ }
52
62
  }
53
63
  }
54
64
  }
55
65
 
56
- // `to` is in the transition layer already, at its margins (the coordinates of the other animators); `from` is where
57
- // it is on the screen, at the scale of its parents.
66
+ // `to` is in the transition layer already: the pictures are drawn in its coordinates. `from` is where it is drawn
67
+ // on the screen, `to` where it is drawn once back in its screen.
58
68
  private fun pictures(): Pair<ViewPicture, ViewPicture>? {
59
- if (to.parent !is ViewGroup) return null
60
- val toPlace = to.layoutParams as? ViewGroup.MarginLayoutParams ?: return null
61
- val fromPlace = ViewUtils.getLocationOnScreen(from)
62
- .apply { ViewUtils.getLocationOnScreen(to.parent as View).let { offset(-it.x, -it.y) } }
63
- val (scaleX, scaleY) = computeInheritedScale(from)
64
- val fromRect = RectF(0f, 0f, from.width * scaleX, from.height * scaleY).apply {
65
- offset(fromPlace.x.toFloat(), fromPlace.y.toFloat())
66
- }
67
- val toRect = RectF(0f, 0f, to.width.toFloat(), to.height.toFloat()).apply {
68
- offset(toPlace.leftMargin.toFloat(), toPlace.topMargin.toFloat())
69
- }
70
- val start = ViewPicture.of(from as ReactViewGroup, fromRect, scaleX) ?: return null
71
- val end = ViewPicture.of(to as ReactViewGroup, toRect, 1f) ?: return null
69
+ val layer = to.parent as? ViewGroup ?: return null
70
+ val start = ViewPicture.of(from as ReactViewGroup, LayerPlace.of(from, layer)) ?: return null
71
+ val end = ViewPicture.of(to as ReactViewGroup, LayerPlace.of(to, layer, toStandIn)) ?: return null
72
72
  return start to end
73
73
  }
74
74
  }
75
75
 
76
- // A view's picture, its place in the transition layer, its corner radius there and its opacity.
77
- private class ViewPicture(val picture: Picture, val rect: RectF, val radius: Float, val alpha: Float) {
76
+ // A view's picture, where it is drawn in the transition layer, its corner radius there and its opacity.
77
+ private class ViewPicture(val picture: Picture, val end: FlightEnd, val radius: Float, val alpha: Float) {
78
78
  companion object {
79
- fun of(view: ReactViewGroup, rect: RectF, scale: Float): ViewPicture? {
79
+ fun of(view: ReactViewGroup, place: LayerPlace): ViewPicture? {
80
80
  if (view.width <= 0 || view.height <= 0) return null
81
81
  val picture = recordPicture(view) ?: return null
82
- return ViewPicture(picture, rect, view.borderRadius * scale, view.alpha)
82
+ val end = place.end(view.width, view.height)
83
+ return ViewPicture(picture, end, view.borderRadius * end.scale, view.alpha)
83
84
  }
84
85
  }
85
86
  }
86
87
 
87
- // The copy where the move has brought it: its place, size and corner radius go from the start to the end along the
88
- // transition's curve, and so does the picture of the end over the picture of the start.
88
+ // The copy where the move has brought it: its center, size, angle and corner radius go from the start to the end along
89
+ // the transition's curve, and so does the picture of the end over the picture of the start.
89
90
  private class ViewMorphDrawable(private val start: ViewPicture, private val end: ViewPicture) : Drawable() {
90
- var progress = 0f
91
+ private val flight = FlightGeometry(start.end, end.end, 0, 0)
92
+ private var at = start.end
93
+ private var progress = 0f
91
94
  private val rect = RectF()
92
95
  private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
93
96
  // What is drawn over the mask keeps only the mask's pixels: the corners stay smooth (a clip path on the hardware
94
97
  // canvas is not anti-aliased).
95
98
  private val contentPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) }
96
99
 
100
+ /** The copy at [time] (the part of the time), [progress] the curve's value. */
101
+ fun update(time: Float, progress: Float) {
102
+ this.progress = progress
103
+ at = flight.at(progress, time)
104
+ }
105
+
97
106
  override fun draw(canvas: Canvas) {
98
- rect.set(
99
- lerp(start.rect.left, end.rect.left),
100
- lerp(start.rect.top, end.rect.top),
101
- lerp(start.rect.right, end.rect.right),
102
- lerp(start.rect.bottom, end.rect.bottom)
103
- )
107
+ rect.set(-at.width / 2, -at.height / 2, at.width / 2, at.height / 2)
104
108
  if (rect.isEmpty) return
105
109
  val radius = lerp(start.radius, end.radius)
106
- val count = canvas.saveLayer(rect, null)
110
+ // The copy turns about its center, mirrored where a view is.
111
+ val count = canvas.save()
112
+ canvas.translate(at.centerX, at.centerY)
113
+ canvas.rotate(at.rotation)
114
+ canvas.scale(at.mirrorX, at.mirrorY)
115
+ canvas.saveLayer(rect, null)
107
116
  canvas.drawRoundRect(rect, radius, radius, maskPaint)
108
117
  canvas.saveLayer(rect, contentPaint)
109
118
  draw(canvas, start, start.alpha)