@apexnative/navigation 1.0.0-alpha.1 → 1.0.0-alpha.3

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 (163) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +31 -6
  4. package/android/CORE.md +7 -2
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  8. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  9. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  10. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  11. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  12. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  13. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  17. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  18. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +76 -13
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +86 -16
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +41 -23
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +24 -20
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +42 -4
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +29 -6
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +2 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -3
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +16 -2
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +148 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +24 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +51 -11
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +105 -2
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +123 -8
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +19 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  51. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  52. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  53. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  54. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  55. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  56. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  57. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  63. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  64. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  65. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  68. package/cli/app.js +1 -1
  69. package/cli/project.js +6 -2
  70. package/cli/setup/index.js +13 -0
  71. package/cli/setup/steps/appDelegate.js +66 -11
  72. package/cli/setup/steps/sceneDelegate.js +108 -0
  73. package/docs-md/architecture/transitions.md +7 -1
  74. package/docs-md/containers/modals.md +4 -2
  75. package/docs-md/containers/overlays.md +3 -1
  76. package/docs-md/containers/side-menu.md +4 -2
  77. package/docs-md/containers/tabs.md +2 -0
  78. package/docs-md/flows/forms-in-a-modal.md +1 -1
  79. package/docs-md/guides/deep-links.md +15 -33
  80. package/docs-md/help/glossary.md +1 -1
  81. package/docs-md/index.json +8 -0
  82. package/docs-md/screen-ui/back-and-gestures.md +6 -0
  83. package/docs-md/screen-ui/system-ui.md +1 -1
  84. package/docs-md/start/installation.md +1 -0
  85. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  86. package/ios/Animations/ANNTransitionWait.mm +6 -7
  87. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  88. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  89. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  90. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  91. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  92. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  93. package/ios/CLAUDE.md +10 -2
  94. package/ios/CORE.md +3 -2
  95. package/ios/Contract/ANNCommandAnswer.h +4 -3
  96. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  97. package/ios/Contract/ANNCommandRunner.h +2 -2
  98. package/ios/Contract/ANNCommandRunner.mm +1 -1
  99. package/ios/Contract/ANNErrorTexts.h +6 -2
  100. package/ios/Contract/ANNErrorTexts.mm +18 -10
  101. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  102. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  103. package/ios/Core/ANNErrorHandler.h +8 -3
  104. package/ios/Core/ANNErrorHandler.mm +17 -1
  105. package/ios/Core/ANNEventContract.h +246 -0
  106. package/ios/Core/ANNEventContract.mm +225 -0
  107. package/ios/Core/ANNEventEmitter.h +5 -1
  108. package/ios/Core/ANNEventEmitter.mm +117 -80
  109. package/ios/Core/ANNHinge.mm +6 -4
  110. package/ios/Core/ANNSceneDelegate.mm +25 -15
  111. package/ios/Core/ANNSystemUI.h +3 -2
  112. package/ios/Core/ANNSystemUI.mm +7 -0
  113. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  114. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  115. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  116. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  117. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  118. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  119. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  120. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  121. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  122. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  123. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  124. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  125. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  126. package/ios/Options/ANNSheetOptions.h +4 -1
  127. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  128. package/ios/RECIPES.md +2 -1
  129. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  130. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  131. package/lib/module/ARCHITECTURE.md +7 -4
  132. package/lib/module/api/app/createNavigation.js +2 -0
  133. package/lib/module/api/app/createNavigation.js.map +1 -1
  134. package/lib/module/api/hooks/useNavigation.js +8 -3
  135. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  136. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  137. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  138. package/lib/module/interfaces/contract/errors.json +275 -0
  139. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  140. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  141. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  142. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  143. package/lib/typescript/interfaces/Events.d.ts +9 -0
  144. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  145. package/lib/typescript/interfaces/options/animations.d.ts +27 -18
  146. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  147. package/lib/typescript/interfaces/options/modals.d.ts +33 -24
  148. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  149. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  150. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  151. package/lib/typescript/interfaces/options/system.d.ts +10 -6
  152. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  153. package/package.json +1 -1
  154. package/src/ARCHITECTURE.md +7 -4
  155. package/src/api/app/createNavigation.ts +2 -0
  156. package/src/api/hooks/useNavigation.ts +8 -3
  157. package/src/api/linking/dispatchedLinks.ts +18 -0
  158. package/src/interfaces/Events.ts +10 -0
  159. package/src/interfaces/contract/errors.json +275 -0
  160. package/src/interfaces/options/animations.ts +27 -18
  161. package/src/interfaces/options/modals.ts +33 -24
  162. package/src/interfaces/options/sideMenu.ts +7 -6
  163. package/src/interfaces/options/system.ts +10 -6
@@ -94,6 +94,19 @@ abstract class ViewController<V : ViewGroup>(
94
94
  open val isInSheet: Boolean
95
95
  get() = shownAsSheet || parent?.isInSheet == true
96
96
 
97
+ /**
98
+ * The node is the root of an overlay: the overlays set it before the node's view is made, so its screens are drawn
99
+ * under the status bar from their first layout (`StatusBarOptions.isHiddenOrDrawBehind`).
100
+ */
101
+ internal var shownAsOverlay = false
102
+
103
+ /**
104
+ * The node is in an overlay: it covers the whole window, and is drawn under the status bar by default. It holds from
105
+ * the start of the show, while `OverlayCoordinator.isInOverlay` answers only for the overlays it lists.
106
+ */
107
+ open val isInOverlay: Boolean
108
+ get() = shownAsOverlay || parent?.isInOverlay == true
109
+
97
110
  /**
98
111
  * The node is in a side of a side menu (its left or right menu, not its center): the side goes the whole height of
99
112
  * the window, and the node keeps no room for the status bar. The layout around answers for it
@@ -265,6 +278,12 @@ abstract class ViewController<V : ViewGroup>(
265
278
  /** Whether [handleBack] would go back. */
266
279
  open fun canHandleBack(): Boolean = false
267
280
 
281
+ /**
282
+ * The stack whose top [handleBack] would take away with the default pop, which a back gesture previews; none when
283
+ * back would do anything else (hold, close a menu or a pane, a pop of its own) or nothing.
284
+ */
285
+ internal open fun stackPoppedByBack(): StackController? = null
286
+
268
287
  /** Starts the node's content (a native screen's view, a React screen's surface). */
269
288
  open fun start() = Unit
270
289
 
@@ -96,6 +96,9 @@ internal object Curves {
96
96
  * linear between them — the values of AndroidX's `FastOutSlowInInterpolator`, computed here.
97
97
  */
98
98
  object FastOutSlowIn : Curve() {
99
+ // Before the table, which is made from it.
100
+ private val curve = CubicBezier(0.4, 0.0, 0.2, 1.0)
101
+
99
102
  /** The 201 values. */
100
103
  val table: DoubleArray = DoubleArray(TABLE_STEPS + 1) {
101
104
  round(bezierAt(it.toDouble() / TABLE_STEPS) * DECIMALS) / DECIMALS
@@ -109,7 +112,25 @@ internal object Curves {
109
112
  }
110
113
 
111
114
  /** y of the Bézier at its x = [x], exactly (to 1e-12). */
112
- fun bezierAt(x: Double): Double {
115
+ fun bezierAt(x: Double): Double = curve.yAt(x)
116
+
117
+ private const val DECIMALS = 1e4
118
+ }
119
+
120
+ /**
121
+ * A cubic Bézier from (0, 0) to (1, 1) with the control points ([x1], [y1]) and ([x2], [y2]), as the framework's
122
+ * `PathInterpolator` draws it, computed exactly: the system's curves without a framework class.
123
+ */
124
+ class CubicBezier(
125
+ private val x1: Double,
126
+ private val y1: Double,
127
+ private val x2: Double,
128
+ private val y2: Double,
129
+ ) : Curve() {
130
+ override fun at(p: Double): Double = yAt(p)
131
+
132
+ /** y of the curve at its x = [x], exactly (to 1e-12). */
133
+ fun yAt(x: Double): Double {
113
134
  if (x <= 0.0) return 0.0
114
135
  if (x >= 1.0) return 1.0
115
136
  // x(t) only grows with t: a bisection finds t, then y(t).
@@ -118,23 +139,20 @@ internal object Curves {
118
139
  var t = x
119
140
  repeat(BISECTION_STEPS) {
120
141
  t = (low + high) / 2
121
- if (bezier(t, X1, X2) < x) low = t else high = t
142
+ if (coordinate(t, x1, x2) < x) low = t else high = t
122
143
  }
123
- return bezier(t, Y1, Y2)
144
+ return coordinate(t, y1, y2)
124
145
  }
125
146
 
126
- // One coordinate of the curve from (0, 0) to (1, 1) with the control points' coordinates [c1], [c2].
127
- private fun bezier(t: Double, c1: Double, c2: Double): Double {
147
+ // One coordinate of the curve with the control points' coordinates [c1], [c2].
148
+ private fun coordinate(t: Double, c1: Double, c2: Double): Double {
128
149
  val u = 1 - t
129
150
  return 3 * u * u * t * c1 + 3 * u * t * t * c2 + t * t * t
130
151
  }
131
152
 
132
- private const val X1 = 0.4
133
- private const val Y1 = 0.0
134
- private const val X2 = 0.2
135
- private const val Y2 = 1.0
136
- private const val BISECTION_STEPS = 60
137
- private const val DECIMALS = 1e4
153
+ private companion object {
154
+ const val BISECTION_STEPS = 60
155
+ }
138
156
  }
139
157
 
140
158
  /**
@@ -98,8 +98,8 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
98
98
  var iconInactiveColor: Int? = null
99
99
  var titleActiveColor: Int? = null
100
100
  var titleInactiveColor: Int? = null
101
- var iconWidth: Int? = null
102
- var iconHeight: Int? = null
101
+ var iconWidth: Double? = null
102
+ var iconHeight: Double? = null
103
103
  var typeface: Typeface? = null
104
104
  var activeTextSize: Float? = null
105
105
  var inactiveTextSize: Float? = null
@@ -246,9 +246,9 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
246
246
 
247
247
  fun setTabTestId(index: Int, testId: String?) = updateStyle(index) { it.testId = testId }
248
248
 
249
- fun setIconWidth(index: Int, width: Int?) = updateStyle(index) { it.iconWidth = width }
249
+ fun setIconWidth(index: Int, width: Double?) = updateStyle(index) { it.iconWidth = width }
250
250
 
251
- fun setIconHeight(index: Int, height: Int?) = updateStyle(index) { it.iconHeight = height }
251
+ fun setIconHeight(index: Int, height: Double?) = updateStyle(index) { it.iconHeight = height }
252
252
 
253
253
  fun setTitleTypeface(index: Int, typeface: Typeface?) = updateStyle(index) { it.typeface = typeface }
254
254
 
@@ -475,7 +475,7 @@ class BottomTabs(context: Context) : BottomNavigationView(MaterialTheme.context(
475
475
  view.setIconTintList(tint(style.iconActiveColor, style.iconInactiveColor, itemIconTintList))
476
476
  view.setTextColor(tint(style.titleActiveColor, style.titleInactiveColor, itemTextColor))
477
477
  val iconSize = style.iconWidth ?: style.iconHeight
478
- view.setIconSize(if (iconSize != null) UiUtils.dpToPx(context, iconSize) else itemIconSize)
478
+ view.setIconSize(if (iconSize != null) UiUtils.dpToPixels(context, iconSize) else itemIconSize)
479
479
  view.findViewById<TextView>(MaterialR.id.navigation_bar_item_small_label_view)?.let {
480
480
  style.typeface?.let { typeface -> it.typeface = typeface }
481
481
  style.inactiveTextSize?.let { size -> it.setTextSize(TypedValue.COMPLEX_UNIT_SP, size) }
@@ -0,0 +1,31 @@
1
+ package dev.apexnative.navigation.views.element
2
+
3
+ import dev.apexnative.navigation.utils.DevelopmentLog
4
+
5
+ /**
6
+ * The isolation of every element of the transitions: [work], a step of the element named [element] (its `nativeID` in
7
+ * quotes, or a pair's two), runs; an exception in it skips that element only — one line in the log
8
+ * ([elementSkipped]), and false — while the rest of the transition plays and its command completes.
9
+ */
10
+ internal inline fun elementStep(
11
+ element: String,
12
+ noinline log: (String) -> Unit = DevelopmentLog::warn,
13
+ work: () -> Unit,
14
+ ): Boolean = try {
15
+ work()
16
+ true
17
+ } catch (e: Exception) {
18
+ elementSkipped(element, e.toString(), log)
19
+ false
20
+ }
21
+
22
+ /** The line in the log for the element named [element], skipped for [reason]: it shows and hides with its screen. */
23
+ internal fun elementSkipped(element: String, reason: String, log: (String) -> Unit = DevelopmentLog::warn) =
24
+ log("[ApexNavigation] the transition's element $element is skipped, it shows and hides with its screen while the " +
25
+ "rest plays: $reason")
26
+
27
+ /** The name of an element in the log. */
28
+ internal fun elementName(nativeId: String) = "'$nativeId'"
29
+
30
+ /** The name of a shared pair in the log. */
31
+ internal fun pairName(fromId: String, toId: String) = "'$fromId' -> '$toId'"
@@ -7,7 +7,6 @@ import android.os.Build
7
7
  import android.util.Log
8
8
  import android.view.View
9
9
  import android.view.ViewGroup
10
- import com.facebook.react.uimanager.util.ReactFindViewUtil
11
10
  import com.facebook.react.views.view.ReactViewGroup
12
11
  import dev.apexnative.navigation.options.ElementTransitionOptions
13
12
  import dev.apexnative.navigation.options.LayoutAnimation
@@ -23,12 +22,13 @@ import dev.apexnative.navigation.views.element.animators.SharedAnimatorFactory
23
22
  * The elements of one transition: the element transitions and the targets of the shared elements found by their
24
23
  * `nativeID` (depth first, the first match) in the whole view of the screen each side shows — an element in the screen
25
24
  * that goes first, then in the one that comes; a pair's `fromId` in the screen that goes, its `toId` in the one that
26
- * comes. What isn't found, what is in another transition's layer and an image that isn't loaded is skipped; the rest
27
- * plays. Each view found moves into a [TransitionLayer] at its place on the screen, in the order its hierarchy draws
28
- * it, and an [ElementStub] takes its index in its parent. A pair's `from` stays where it is, hidden while its copy
29
- * flies. [finish] puts every view back at its index with its layout, its pivot and the values of its properties read
30
- * before — unless React removed its stub meanwhile: then it is gone for good —, shows the `from`s again and takes the
31
- * layer away. Main thread only.
25
+ * comes; each screen is walked once for all of them ([TransitionViews]). What isn't found, what is in another
26
+ * transition's layer and an image that isn't loaded is skipped; so is an element whose start fails ([elementStep]):
27
+ * it goes back at once and the rest plays. Each view found moves into a [TransitionLayer] at its place on the screen,
28
+ * in the order its hierarchy draws it, and an [ElementStub] takes its index in its parent. A pair's `from` stays where
29
+ * it is, hidden while its copy flies. [finish] puts every view back at its index with its layout, its pivot and the
30
+ * values of its properties read before — unless React removed its stub meanwhile: then it is gone for good —, shows
31
+ * the `from`s again and takes the layer away. Main thread only.
32
32
  */
33
33
  internal class ElementTransition private constructor(private val layer: TransitionLayer) {
34
34
  // A view in the layer: where it came from and what it was.
@@ -89,18 +89,41 @@ internal class ElementTransition private constructor(private val layer: Transiti
89
89
  view.rotationX, view.rotationY, view.pivotX, view.pivotY,
90
90
  )
91
91
  val stub = ElementStub(view.context, view)
92
- parent.removeViewAt(index)
93
- parent.addView(stub, index, params)
94
- stub.layout(frame.left, frame.top, frame.right, frame.bottom)
95
- val left = originX + frame.left
96
- val top = originY + frame.top
97
- layer.put(view, left, top, frame.width(), frame.height())
92
+ try {
93
+ parent.removeViewAt(index)
94
+ parent.addView(stub, index, params)
95
+ stub.layout(frame.left, frame.top, frame.right, frame.bottom)
96
+ val left = originX + frame.left
97
+ val top = originY + frame.top
98
+ layer.put(view, left, top, frame.width(), frame.height())
99
+ } catch (e: RuntimeException) {
100
+ putBack(view, stub, parent, index, params, frame)
101
+ throw e
102
+ }
98
103
  // Scale and rotations turn about the center.
99
104
  view.pivotX = frame.width() / 2f
100
105
  view.pivotY = frame.height() / 2f
101
106
  return Moved(view, stub, params, frame, values, pivotSet, originX, originY).also { moved += it }
102
107
  }
103
108
 
109
+ // A move that failed half way undone: the view back at its index in its parent, the stub gone.
110
+ private fun putBack(
111
+ view: View,
112
+ stub: ElementStub,
113
+ parent: ViewGroup,
114
+ index: Int,
115
+ params: ViewGroup.LayoutParams,
116
+ frame: Rect,
117
+ ) {
118
+ if (view.parent === layer) layer.removeView(view)
119
+ val stubIndex = parent.indexOfChild(stub)
120
+ if (stubIndex >= 0) parent.removeViewAt(stubIndex)
121
+ if (view.parent == null) {
122
+ parent.addView(view, index.coerceAtMost(parent.childCount), params)
123
+ view.layout(frame.left, frame.top, frame.right, frame.bottom)
124
+ }
125
+ }
126
+
104
127
  private fun giveBack(item: Moved) {
105
128
  val view = item.view
106
129
  // A commit of Fabric meanwhile laid it out in its parent's coordinates: the newest such frame holds.
@@ -156,13 +179,26 @@ internal class ElementTransition private constructor(private val layer: Transiti
156
179
  from, to, toParent, layer, options, fallbackDuration, DevelopmentLog::warn,
157
180
  ) ?: return false
158
181
  val alpha = item.values[ALPHA]
182
+ val fromAlpha = from.alpha
159
183
  to.alpha = 0f
160
- hidden += from to from.alpha
184
+ hidden += from to fromAlpha
161
185
  flight.animator.addListener(object : AnimatorListenerAdapter() {
186
+ private var stopped = false
187
+
162
188
  override fun onAnimationStart(animation: Animator) {
189
+ if (stopped) return
163
190
  from.alpha = 0f
164
191
  if (!flight.drawsPictures) to.alpha = alpha
165
192
  }
193
+
194
+ // A flight whose start or frame failed stops (`MoveAnimator`), maybe inside its own start: `from` shows
195
+ // again in its screen, and a `to` that moves itself in its place, as at any stop (a flight of pictures
196
+ // shows its `to` at its end).
197
+ override fun onAnimationCancel(animation: Animator) {
198
+ stopped = true
199
+ from.alpha = fromAlpha
200
+ if (!flight.drawsPictures) to.alpha = alpha
201
+ }
166
202
  })
167
203
  animators += flight.animator
168
204
  return true
@@ -195,20 +231,23 @@ internal class ElementTransition private constructor(private val layer: Transiti
195
231
  ): ElementTransition? {
196
232
  val fromView = from?.let { AppearanceDispatcher.visibleNode(it).peekView() }
197
233
  val toView = AppearanceDispatcher.visibleNode(to).peekView()
234
+ val transitions = animation.elementTransitions.transitions
235
+ val shared = animation.sharedElements.get()
236
+ val views = TransitionViews(shared, transitions, fromView, toView)
198
237
  val taken = HashSet<View>()
199
238
  val elements = ArrayList<Pair<ElementTransitionOptions, View>>()
200
- for (element in animation.elementTransitions.transitions) {
239
+ for (element in transitions) {
201
240
  if (element.id.isEmpty()) continue
202
- val view = find(fromView, element.id) ?: find(toView, element.id) ?: continue
241
+ val view = found(views.leaving(element.id)) ?: found(views.arriving(element.id)) ?: continue
203
242
  if (TransitionLayer.holds(view) || !taken.add(view)) continue
204
243
  elements += element to view
205
244
  }
206
245
  val pairs = ArrayList<Triple<SharedElementTransitionOptions, View, View>>()
207
- for (pair in animation.sharedElements.get()) {
246
+ for (pair in shared) {
208
247
  val fromId = pair.fromId.get(null) ?: continue
209
248
  val toId = pair.toId.get(null) ?: continue
210
- val source = find(fromView, fromId) ?: continue
211
- val target = find(toView, toId) ?: continue
249
+ val source = found(views.leaving(fromId)) ?: continue
250
+ val target = found(views.arriving(toId)) ?: continue
212
251
  if (source === target || TransitionLayer.holds(source) || TransitionLayer.holds(target)) continue
213
252
  if (target in taken || source in taken) continue
214
253
  if (!loaded(source, fromId) || !loaded(target, toId)) continue
@@ -246,8 +285,7 @@ internal class ElementTransition private constructor(private val layer: Transiti
246
285
  null
247
286
  }
248
287
 
249
- private fun find(root: View?, nativeId: String): View? =
250
- root?.let { ReactFindViewUtil.findView(it, nativeId) }?.takeUnless { it is ElementStub }
288
+ private fun found(view: View?): View? = view?.takeUnless { it is ElementStub }
251
289
 
252
290
  // An image not loaded drops out of the transition; its wait logged it already, else it is logged now.
253
291
  private fun loaded(view: View, id: String): Boolean {
@@ -258,24 +296,44 @@ internal class ElementTransition private constructor(private val layer: Transiti
258
296
  }
259
297
 
260
298
  // The views into the layer in the order they are drawn, then their animators; a pair that doesn't fly goes back at
261
- // once.
299
+ // once. Each element's steps run on their own ([elementStep]): one that fails goes back, the others play.
262
300
  private fun fill(
263
301
  host: ViewGroup,
264
302
  elements: List<Pair<ElementTransitionOptions, View>>,
265
303
  pairs: List<Triple<SharedElementTransitionOptions, View, View>>,
266
304
  fallbackDuration: Long,
267
305
  ) {
306
+ val names = HashMap<View, String>()
307
+ for ((options, view) in elements) names[view] = elementName(options.id)
308
+ for ((options, _, target) in pairs) names[target] = pairName(options.fromId.get(""), options.toId.get(""))
268
309
  val views = elements.map { it.second } + pairs.map { it.third }
269
310
  val parents = views.associateWith { it.parent as? View }
270
311
  val placed = HashMap<View, Moved>()
271
- for (view in DrawOrder.sorted(views, host) { it }) moveIntoLayer(view)?.let { placed[view] = it }
272
- for ((options, view) in elements) placed[view]?.let { addElement(options, it) }
312
+ for (view in DrawOrder.sorted(views, host) { it }) {
313
+ var item: Moved? = null
314
+ if (elementStep(names.getValue(view)) { item = moveIntoLayer(view) }) item?.let { placed[view] = it }
315
+ }
316
+ for ((options, view) in elements) {
317
+ val item = placed[view] ?: continue
318
+ if (!elementStep(names.getValue(view)) { addElement(options, item) }) goBack(item)
319
+ }
273
320
  for ((options, source, target) in pairs) {
274
321
  val item = placed[target] ?: continue
275
- if (!addPair(options, source, item, parents[target], fallbackDuration)) {
276
- moved -= item
277
- giveBack(item)
322
+ var flies = false
323
+ elementStep(names.getValue(target)) {
324
+ flies = addPair(options, source, item, parents[target], fallbackDuration)
278
325
  }
326
+ if (!flies) goBack(item)
327
+ }
328
+ }
329
+
330
+ // An element that doesn't play leaves the layer now; one that can't go back is logged, the others go on.
331
+ private fun goBack(item: Moved) {
332
+ moved -= item
333
+ try {
334
+ giveBack(item)
335
+ } catch (e: RuntimeException) {
336
+ Log.w(NavigationLog.TAG, "[ApexNavigation] An element of a transition couldn't go back", e)
279
337
  }
280
338
  }
281
339
  }
@@ -3,7 +3,6 @@ package dev.apexnative.navigation.views.element
3
3
  import android.util.Log
4
4
  import android.view.Choreographer
5
5
  import android.view.View
6
- import com.facebook.react.uimanager.util.ReactFindViewUtil
7
6
  import dev.apexnative.navigation.options.LayoutAnimation
8
7
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
9
8
  import dev.apexnative.navigation.utils.DevelopmentLog
@@ -72,12 +71,14 @@ internal object TargetsWait {
72
71
  ): () -> Unit {
73
72
  val fromView = from?.let { AppearanceDispatcher.visibleNode(it).peekView() }
74
73
  val toView = AppearanceDispatcher.visibleNode(to).peekView()
74
+ val pairs = animation.sharedElements.get()
75
+ val views = TransitionViews(pairs, emptyList(), fromView, toView)
75
76
  val images = ArrayList<ImageWait.Image?>()
76
- for (pair in animation.sharedElements.get()) {
77
+ for (pair in pairs) {
77
78
  val fromId = pair.fromId.get(null) ?: continue
78
79
  val toId = pair.toId.get(null) ?: continue
79
- val source = fromView?.let { ReactFindViewUtil.findView(it, fromId) } ?: continue
80
- val target = toView?.let { ReactFindViewUtil.findView(it, toId) } ?: continue
80
+ val source = views.leaving(fromId) ?: continue
81
+ val target = views.arriving(toId) ?: continue
81
82
  images += imageOf(source, fromId)
82
83
  images += imageOf(target, toId)
83
84
  }
@@ -2,7 +2,6 @@ package dev.apexnative.navigation.views.element
2
2
 
3
3
  import android.os.SystemClock
4
4
  import android.view.ViewGroup
5
- import com.facebook.react.uimanager.util.ReactFindViewUtil
6
5
  import dev.apexnative.navigation.options.LayoutAnimation
7
6
  import dev.apexnative.navigation.utils.DRAW_WAIT_LIMIT
8
7
  import dev.apexnative.navigation.viewcontrollers.component.ContentReadiness
@@ -38,13 +37,17 @@ private fun areReady(animation: LayoutAnimation, from: ViewController<*>, to: Vi
38
37
  val screen = to.shownScreen()
39
38
  val content = screen.view as? ViewGroup
40
39
  if (content != null && !ContentReadiness.isReady(screen)) return false
41
- val sharedFound = animation.sharedElements.get().all { isIn(to, it.toId.get("")) }
42
- return sharedFound && animation.elementTransitions.transitions.all { isIn(to, it.id) || isIn(from, it.id) }
40
+ // Each screen walked once for all the ids ([TransitionViews.findViews]); an element is looked for in the screen
41
+ // that goes only when the one that comes doesn't have it.
42
+ val targets = animation.sharedElements.get().map { it.toId.get("") }.filter { it.isNotEmpty() }
43
+ val elements = animation.elementTransitions.transitions.map { it.id }.filter { it.isNotEmpty() }
44
+ if (targets.isEmpty() && elements.isEmpty()) return true
45
+ val inTo = TransitionViews.findViews(to.view, (targets + elements).toSet())
46
+ if (!targets.all { it in inTo }) return false
47
+ val missing = elements.filterNot { it in inTo }.toSet()
48
+ return missing.isEmpty() || TransitionViews.findViews(from.view, missing).size == missing.size
43
49
  }
44
50
 
45
- private fun isIn(screen: ViewController<*>, nativeId: String) =
46
- nativeId.isEmpty() || ReactFindViewUtil.findView(screen.view, nativeId) != null
47
-
48
51
  // The screen a layout shows: a modal of a stack shows the screen on its top.
49
52
  private fun ViewController<*>.shownScreen(): ViewController<*> {
50
53
  var controller: ViewController<*> = this
@@ -0,0 +1,100 @@
1
+ package dev.apexnative.navigation.views.element
2
+
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+ import com.facebook.react.R as ReactR
6
+ import dev.apexnative.navigation.options.ElementTransitionOptions
7
+ import dev.apexnative.navigation.options.SharedElementTransitionOptions
8
+
9
+ /**
10
+ * The views of a transition's elements in its two screens: each screen is walked once, at its first lookup, for the ids
11
+ * looked for there — the screen that goes ([fromRoot]) for the pairs' `fromId`s and the elements' ids, the one that
12
+ * comes ([toRoot]) for the pairs' `toId`s and the elements' ids the other doesn't have. A lookup finds what
13
+ * `ReactFindViewUtil.findView` finds in that screen as it was at its walk (depth first, the first match). Main thread
14
+ * only.
15
+ */
16
+ internal class TransitionViews(
17
+ pairs: List<SharedElementTransitionOptions>,
18
+ elements: List<ElementTransitionOptions>,
19
+ private val fromRoot: View?,
20
+ private val toRoot: View?,
21
+ ) {
22
+ // The ids each screen is walked for: the screen that goes for the sources and the elements, the one that comes for
23
+ // the targets (and the elements the other screen doesn't have, added at its walk).
24
+ private val leavingIds = HashSet<String>()
25
+ private val arrivingIds = HashSet<String>()
26
+ private val elementIds = ArrayList<String>()
27
+ private var leavingViews: Map<String, View>? = null
28
+ private var arrivingViews: Map<String, View>? = null
29
+
30
+ init {
31
+ for (pair in pairs) {
32
+ pair.fromId.get(null)?.takeIf { it.isNotEmpty() }?.let(leavingIds::add)
33
+ pair.toId.get(null)?.takeIf { it.isNotEmpty() }?.let(arrivingIds::add)
34
+ }
35
+ for (element in elements) {
36
+ if (element.id.isEmpty()) continue
37
+ leavingIds += element.id
38
+ elementIds += element.id
39
+ }
40
+ }
41
+
42
+ /** The view with [nativeId] in the screen that goes; none when none has it or it isn't one of the transition's. */
43
+ fun leaving(nativeId: String): View? {
44
+ val views = leavingViews ?: findViews(fromRoot, leavingIds).also { leavingViews = it }
45
+ return views[nativeId]
46
+ }
47
+
48
+ /** The view with [nativeId] in the screen that comes; none when none has it or it isn't one of the transition's. */
49
+ fun arriving(nativeId: String): View? {
50
+ val views = arrivingViews ?: run {
51
+ // An element is looked for here only when the screen that goes doesn't have it: the walk stops sooner.
52
+ for (id in elementIds) if (leaving(id) == null) arrivingIds += id
53
+ findViews(toRoot, arrivingIds).also { arrivingViews = it }
54
+ }
55
+ return views[nativeId]
56
+ }
57
+
58
+ companion object {
59
+ /** The views with the `nativeID`s of [nativeIds] under [root], in one walk ([firstByKey]). */
60
+ fun findViews(root: View?, nativeIds: Set<String>): Map<String, View> {
61
+ if (root == null || nativeIds.isEmpty()) return emptyMap()
62
+ return firstByKey(
63
+ root,
64
+ nativeIds,
65
+ keyOf = { it.getTag(ReactR.id.view_tag_native_id) as? String },
66
+ childCount = { (it as? ViewGroup)?.childCount ?: 0 },
67
+ childAt = { view, index -> (view as ViewGroup).getChildAt(index) },
68
+ )
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * For each key of [keys], the first node with it in a depth-first walk from [root] (the node, then its children in
75
+ * order): what a search for that one key finds, for all of them in one walk, which stops once every key is found. A key
76
+ * no node has is left out. Inline: the walk calls no function objects.
77
+ */
78
+ internal inline fun <N : Any> firstByKey(
79
+ root: N,
80
+ keys: Set<String>,
81
+ keyOf: (N) -> String?,
82
+ childCount: (N) -> Int,
83
+ childAt: (N, Int) -> N,
84
+ ): Map<String, N> {
85
+ val found = HashMap<String, N>()
86
+ if (keys.isEmpty()) return found
87
+ // A stack of the nodes still to visit, the next one on top: the order of the recursive search without its depth.
88
+ val stack = ArrayList<N>()
89
+ stack.add(root)
90
+ while (stack.isNotEmpty()) {
91
+ val node = stack.removeAt(stack.size - 1)
92
+ val key = keyOf(node)
93
+ if (key != null && key in keys && key !in found) {
94
+ found[key] = node
95
+ if (found.size == keys.size) break
96
+ }
97
+ for (index in childCount(node) - 1 downTo 0) stack.add(childAt(node, index))
98
+ }
99
+ return found
100
+ }
@@ -1,5 +1,7 @@
1
1
  package dev.apexnative.navigation.views.element.animators
2
2
 
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
3
5
  import android.animation.TimeInterpolator
4
6
  import android.animation.ValueAnimator
5
7
  import android.graphics.Picture
@@ -7,6 +9,7 @@ import android.util.Log
7
9
  import android.view.View
8
10
  import android.view.animation.LinearInterpolator
9
11
  import dev.apexnative.navigation.utils.NavigationLog
12
+ import dev.apexnative.navigation.views.element.elementStep
10
13
 
11
14
  /**
12
15
  * The view's own drawing with its children, as drawing commands: sharp at any scale. Its opacity and place belong to
@@ -28,14 +31,55 @@ internal fun recordPicture(view: View): Picture? = try {
28
31
  }
29
32
 
30
33
  // Runs in linear time and keeps the curve it is given for the move: an element moves along the transition's curve,
31
- // while what has to follow the clock (the pictures of a text changing over) can.
34
+ // while what has to follow the clock (the pictures of a text changing over) can. Its start and its frames each run in
35
+ // [elementStep]: one that fails stops the element — it is cancelled, so its ends put back what it changed and the
36
+ // transition plays on without it.
32
37
  internal class MoveAnimator : ValueAnimator() {
33
38
  var curve: TimeInterpolator = LinearInterpolator()
34
39
  private set
35
40
 
41
+ /** The element's name in the log ([pairName]). */
42
+ var element = "''"
43
+
44
+ private val starts = ArrayList<() -> Unit>()
45
+ private val frames = ArrayList<(Float) -> Unit>()
46
+ private val ends = ArrayList<() -> Unit>()
47
+ private var failed = false
48
+
36
49
  init {
37
50
  setFloatValues(0f, 1f)
38
51
  super.setInterpolator(LinearInterpolator())
52
+ addListener(object : AnimatorListenerAdapter() {
53
+ override fun onAnimationStart(animation: Animator) = guarded { for (start in starts) start() }
54
+
55
+ override fun onAnimationEnd(animation: Animator) {
56
+ for (end in ends) elementStep(element) { end() }
57
+ }
58
+ })
59
+ addUpdateListener { animation ->
60
+ if (!failed) guarded { for (frame in frames) frame(animation.animatedFraction) }
61
+ }
62
+ }
63
+
64
+ /** At the start of the flight, after its delay. */
65
+ fun onStart(action: () -> Unit) {
66
+ starts += action
67
+ }
68
+
69
+ /** Each frame, with the fraction of the time, linear. */
70
+ fun onFrame(action: (fraction: Float) -> Unit) {
71
+ frames += action
72
+ }
73
+
74
+ /** At its end, also when it is cancelled or its start or a frame failed. */
75
+ fun onEnd(action: () -> Unit) {
76
+ ends += action
77
+ }
78
+
79
+ private fun guarded(work: () -> Unit) {
80
+ if (failed || elementStep(element, work = work)) return
81
+ failed = true
82
+ cancel()
39
83
  }
40
84
 
41
85
  override fun setInterpolator(value: TimeInterpolator?) {
@@ -5,8 +5,6 @@ import android.graphics.Matrix
5
5
  import android.view.View
6
6
  import android.view.ViewOutlineProvider
7
7
  import android.widget.ImageView
8
- import androidx.core.animation.doOnEnd
9
- import androidx.core.animation.doOnStart
10
8
  import com.facebook.react.views.view.ReactViewGroup
11
9
  import dev.apexnative.navigation.utils.borderRadius
12
10
  import dev.apexnative.navigation.views.animations.ValueInterpolation
@@ -78,7 +76,7 @@ internal class ImageMatrixAnimator(
78
76
  to.imageMatrix = matrix
79
77
  }
80
78
  }
81
- doOnStart {
79
+ onStart {
82
80
  savedProvider = to.outlineProvider
83
81
  savedClip = to.clipToOutline
84
82
  to.outlineProvider = outline
@@ -86,8 +84,8 @@ internal class ImageMatrixAnimator(
86
84
  if (fits) to.scaleType = ImageView.ScaleType.MATRIX
87
85
  frame(0f)
88
86
  }
89
- addUpdateListener { frame(it.animatedFraction) }
90
- doOnEnd {
87
+ onFrame { frame(it) }
88
+ onEnd {
91
89
  if (fits) {
92
90
  to.scaleType = toFit
93
91
  to.imageMatrix = savedMatrix
@@ -3,8 +3,6 @@ package dev.apexnative.navigation.views.element.animators
3
3
  import android.animation.Animator
4
4
  import android.view.View
5
5
  import android.view.ViewOutlineProvider
6
- import androidx.core.animation.doOnEnd
7
- import androidx.core.animation.doOnStart
8
6
  import com.facebook.react.uimanager.BackgroundStyleApplicator
9
7
  import com.facebook.react.uimanager.style.BorderRadiusProp
10
8
  import dev.apexnative.navigation.utils.borderRadius
@@ -57,7 +55,7 @@ internal class PlainViewAnimator(
57
55
  }
58
56
  to.rotation = ValueInterpolation.number(fromRotation, toRotation, f, time)
59
57
  }
60
- doOnStart {
58
+ onStart {
61
59
  savedProvider = to.outlineProvider
62
60
  savedClip = to.clipToOutline
63
61
  if (ownRadius != null) BackgroundStyleApplicator.setBorderRadius(to, RADIUS, null)
@@ -66,9 +64,9 @@ internal class PlainViewAnimator(
66
64
  layer.place(to, toRect.left.toInt(), toRect.top.toInt(), width, height)
67
65
  frame(0f)
68
66
  }
69
- addUpdateListener { frame(it.animatedFraction) }
67
+ onFrame { frame(it) }
70
68
  // A cancelled flight ends too: the view goes back whole.
71
- doOnEnd {
69
+ onEnd {
72
70
  to.translationX = 0f
73
71
  to.translationY = 0f
74
72
  to.rotation = toRotation