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

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 (125) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +10 -4
  4. package/android/CORE.md +4 -1
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  7. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  8. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  9. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  11. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  12. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  13. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  16. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  17. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  18. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +12 -8
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +12 -11
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +2 -2
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +1 -1
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -3
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +2 -1
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +2 -1
  29. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  30. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  31. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  32. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  33. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  34. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  35. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  36. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  37. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  38. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  42. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  43. package/cli/app.js +1 -1
  44. package/cli/project.js +6 -2
  45. package/cli/setup/index.js +13 -0
  46. package/cli/setup/steps/appDelegate.js +66 -11
  47. package/cli/setup/steps/sceneDelegate.js +108 -0
  48. package/docs-md/architecture/transitions.md +1 -1
  49. package/docs-md/guides/deep-links.md +15 -33
  50. package/docs-md/start/installation.md +1 -0
  51. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  52. package/ios/Animations/ANNTransitionWait.mm +6 -7
  53. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  54. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  55. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  56. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  57. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  58. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  59. package/ios/CLAUDE.md +10 -2
  60. package/ios/CORE.md +3 -2
  61. package/ios/Contract/ANNCommandAnswer.h +4 -3
  62. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  63. package/ios/Contract/ANNCommandRunner.h +2 -2
  64. package/ios/Contract/ANNCommandRunner.mm +1 -1
  65. package/ios/Contract/ANNErrorTexts.h +6 -2
  66. package/ios/Contract/ANNErrorTexts.mm +18 -10
  67. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  68. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  69. package/ios/Core/ANNErrorHandler.h +8 -3
  70. package/ios/Core/ANNErrorHandler.mm +17 -1
  71. package/ios/Core/ANNEventContract.h +246 -0
  72. package/ios/Core/ANNEventContract.mm +225 -0
  73. package/ios/Core/ANNEventEmitter.h +5 -1
  74. package/ios/Core/ANNEventEmitter.mm +117 -80
  75. package/ios/Core/ANNHinge.mm +6 -4
  76. package/ios/Core/ANNSceneDelegate.mm +25 -15
  77. package/ios/Core/ANNSystemUI.h +3 -2
  78. package/ios/Core/ANNSystemUI.mm +7 -0
  79. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  80. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  81. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  82. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  83. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  84. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  85. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  86. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  87. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  88. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  89. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  90. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  92. package/ios/Options/ANNSheetOptions.h +4 -1
  93. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  94. package/ios/RECIPES.md +2 -1
  95. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  96. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  97. package/lib/module/ARCHITECTURE.md +7 -4
  98. package/lib/module/api/app/createNavigation.js +2 -0
  99. package/lib/module/api/app/createNavigation.js.map +1 -1
  100. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  101. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  102. package/lib/module/interfaces/contract/errors.json +275 -0
  103. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  104. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  105. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  106. package/lib/typescript/interfaces/Events.d.ts +9 -0
  107. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  108. package/lib/typescript/interfaces/options/animations.d.ts +17 -13
  109. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  110. package/lib/typescript/interfaces/options/modals.d.ts +15 -11
  111. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  112. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  113. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  114. package/lib/typescript/interfaces/options/system.d.ts +4 -3
  115. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  116. package/package.json +1 -1
  117. package/src/ARCHITECTURE.md +7 -4
  118. package/src/api/app/createNavigation.ts +2 -0
  119. package/src/api/linking/dispatchedLinks.ts +18 -0
  120. package/src/interfaces/Events.ts +10 -0
  121. package/src/interfaces/contract/errors.json +275 -0
  122. package/src/interfaces/options/animations.ts +17 -13
  123. package/src/interfaces/options/modals.ts +15 -11
  124. package/src/interfaces/options/sideMenu.ts +7 -6
  125. package/src/interfaces/options/system.ts +4 -3
@@ -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
@@ -1,8 +1,6 @@
1
1
  package dev.apexnative.navigation.views.element.animators
2
2
 
3
3
  import android.animation.Animator
4
- import androidx.core.animation.doOnEnd
5
- import androidx.core.animation.doOnStart
6
4
  import com.facebook.drawee.drawable.ScalingUtils
7
5
  import com.facebook.react.views.image.ReactImageView
8
6
  import dev.apexnative.navigation.views.animations.ValueInterpolation
@@ -45,14 +43,14 @@ internal class ReactImageAnimator(
45
43
  to.rotation = ValueInterpolation.number(fromRotation, toRotation, f, time)
46
44
  to.invalidate()
47
45
  }
48
- doOnStart {
46
+ onStart {
49
47
  savedFade = to.hierarchy.fadeDuration
50
48
  to.hierarchy.fadeDuration = 0
51
49
  to.hierarchy.setActualImageScaleType(scale)
52
50
  frame(0f)
53
51
  }
54
- addUpdateListener { frame(it.animatedFraction) }
55
- doOnEnd {
52
+ onFrame { frame(it) }
53
+ onEnd {
56
54
  to.hierarchy.setActualImageScaleType(toType)
57
55
  to.hierarchy.fadeDuration = savedFade
58
56
  to.translationX = 0f
@@ -10,6 +10,7 @@ import dev.apexnative.navigation.options.SharedElementTransitionOptions
10
10
  import dev.apexnative.navigation.views.animations.AnimationRules
11
11
  import dev.apexnative.navigation.views.animations.Curves
12
12
  import dev.apexnative.navigation.views.element.TransitionLayer
13
+ import dev.apexnative.navigation.views.element.pairName
13
14
 
14
15
  /**
15
16
  * The animator of a shared element by the kinds of its two views, chosen by explicit classes, never by reflection:
@@ -55,6 +56,7 @@ internal object SharedAnimatorFactory {
55
56
  else -> moves(PlainViewAnimator(from, to, layer).create())
56
57
  } ?: return null
57
58
  val animator = flight.animator
59
+ (animator as? MoveAnimator)?.element = pairName(options.fromId.get(""), options.toId.get(""))
58
60
  val duration = AnimationRules.durationOf(options.duration.get(null), fallbackDuration)
59
61
  animator.duration = duration
60
62
  animator.startDelay = AnimationRules.delayOf(options.startDelay.get(null))
@@ -11,8 +11,6 @@ import android.text.Layout
11
11
  import android.view.View
12
12
  import android.view.ViewGroup
13
13
  import android.widget.TextView
14
- import androidx.core.animation.doOnEnd
15
- import androidx.core.animation.doOnStart
16
14
  import com.facebook.react.views.text.ReactTextView
17
15
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
18
16
  import dev.apexnative.navigation.utils.TextViewUtils
@@ -42,16 +40,16 @@ class TextChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactTe
42
40
  val drawable = TextMorphDrawable(start, end)
43
41
  // Everything changes between the start and the end of the animation: a cancelled one ends too.
44
42
  return MoveAnimator().apply {
45
- doOnStart {
43
+ onStart {
46
44
  drawable.setBounds(0, 0, layer.width, layer.height)
47
45
  layer.overlay.add(drawable)
48
46
  to.alpha = 0f
49
47
  }
50
- addUpdateListener {
51
- drawable.update(it.animatedFraction, curve.getInterpolation(it.animatedFraction))
48
+ onFrame {
49
+ drawable.update(it, curve.getInterpolation(it))
52
50
  layer.invalidate()
53
51
  }
54
- doOnEnd {
52
+ onEnd {
55
53
  layer.overlay.remove(drawable)
56
54
  layer.invalidate()
57
55
  to.alpha = end.alpha
@@ -12,8 +12,6 @@ import android.graphics.RectF
12
12
  import android.graphics.drawable.Drawable
13
13
  import android.view.View
14
14
  import android.view.ViewGroup
15
- import androidx.core.animation.doOnEnd
16
- import androidx.core.animation.doOnStart
17
15
  import com.facebook.react.views.view.ReactViewGroup
18
16
  import dev.apexnative.navigation.options.SharedElementTransitionOptions
19
17
  import dev.apexnative.navigation.utils.ViewUtils
@@ -38,16 +36,16 @@ class ViewChangeAnimator(from: View, to: View) : PropertyAnimatorCreator<ReactVi
38
36
  val drawable = ViewMorphDrawable(start, end)
39
37
  // Everything changes between the start and the end of the animation: a cancelled one ends too.
40
38
  return MoveAnimator().apply {
41
- doOnStart {
39
+ onStart {
42
40
  drawable.setBounds(0, 0, layer.width, layer.height)
43
41
  layer.overlay.add(drawable)
44
42
  to.alpha = 0f
45
43
  }
46
- addUpdateListener {
47
- drawable.progress = curve.getInterpolation(it.animatedFraction)
44
+ onFrame {
45
+ drawable.progress = curve.getInterpolation(it)
48
46
  layer.invalidate()
49
47
  }
50
- doOnEnd {
48
+ onEnd {
51
49
  layer.overlay.remove(drawable)
52
50
  layer.invalidate()
53
51
  to.alpha = end.alpha
@@ -37,6 +37,7 @@ import dev.apexnative.navigation.utils.MaterialTheme
37
37
  import dev.apexnative.navigation.utils.TestIds
38
38
  import dev.apexnative.navigation.utils.UiUtils
39
39
  import java.util.WeakHashMap
40
+ import kotlin.math.roundToInt
40
41
 
41
42
  /**
42
43
  * The buttons of one side of the bar in its toolbar: each button a menu item — a text, an icon (on its background), or
@@ -171,7 +172,7 @@ internal class ButtonBar(
171
172
  options.fontSize.get(0.0).toFloat(),
172
173
  context.resources.displayMetrics,
173
174
  )
174
- spannable.setSpan(AbsoluteSizeSpan(size.toInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
175
+ spannable.setSpan(AbsoluteSizeSpan(size.roundToInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
175
176
  }
176
177
  if (options.font.hasValue()) {
177
178
  val typeface = options.font.getTypeface(typefaces, Typeface.DEFAULT)
@@ -207,8 +208,8 @@ internal class ButtonBar(
207
208
  icon,
208
209
  color,
209
210
  UiUtils.dpToPx(context, background.cornerRadius.get(0.0).toFloat()),
210
- UiUtils.dpToPx(context, background.width.get(0.0).toFloat()).toInt(),
211
- UiUtils.dpToPx(context, background.height.get(0.0).toFloat()).toInt(),
211
+ UiUtils.dpToPx(context, background.width.get(0.0).toFloat()).roundToInt(),
212
+ UiUtils.dpToPx(context, background.height.get(0.0).toFloat()).roundToInt(),
212
213
  )
213
214
  }
214
215
 
package/cli/app.js CHANGED
@@ -9,7 +9,7 @@ const { exec } = require('./exec');
9
9
  const MINIMUM = [0, 85];
10
10
 
11
11
  /** The newest React Native the navigation was checked with: `init` makes apps of its newest patch. */
12
- const TESTED = [0, 87];
12
+ const TESTED = [0, 88];
13
13
 
14
14
  /** The name the navigation is installed under. */
15
15
  const PACKAGE = '@apexnative/navigation';
package/cli/project.js CHANGED
@@ -8,8 +8,9 @@ const { exec } = require('./exec');
8
8
  /**
9
9
  * The files of the app the CLI sets up, checks and adds to, where the app has them; `undefined` for one it couldn't
10
10
  * find. `styles` is the file of the app's Android theme, `styles.xml` or `themes.xml`; `manifest` its
11
- * `AndroidManifest.xml`. `xcodeProject` is the app's `project.pbxproj`; `androidPackage` the package of its Kotlin code (the `namespace`
12
- * of `android/app/build.gradle`), and `androidSources` the folder it is in.
11
+ * `AndroidManifest.xml`. `sceneDelegate` is the `SceneDelegate.swift` next to `AppDelegate.swift`, which apps made
12
+ * from React Native 0.88's template have. `xcodeProject` is the app's `project.pbxproj`; `androidPackage` the package
13
+ * of its Kotlin code (the `namespace` of `android/app/build.gradle`), and `androidSources` the folder it is in.
13
14
  * @typedef {{
14
15
  * root: string;
15
16
  * mainApplication?: string;
@@ -17,6 +18,7 @@ const { exec } = require('./exec');
17
18
  * styles?: string;
18
19
  * manifest?: string;
19
20
  * appDelegate?: string;
21
+ * sceneDelegate?: string;
20
22
  * infoPlist?: string;
21
23
  * entry?: string;
22
24
  * app?: string;
@@ -64,6 +66,8 @@ function findProject(root, run = exec) {
64
66
  existing(path.join(androidMain, 'res', 'values', 'themes.xml')),
65
67
  manifest: existing(path.join(androidMain, 'AndroidManifest.xml')),
66
68
  appDelegate,
69
+ sceneDelegate:
70
+ appDelegate && existing(path.join(path.dirname(appDelegate), 'SceneDelegate.swift')),
67
71
  infoPlist: appDelegate && existing(path.join(path.dirname(appDelegate), 'Info.plist')),
68
72
  entry: ['index.js', 'index.ts', 'index.tsx']
69
73
  .map((name) => path.join(root, name))
@@ -16,6 +16,7 @@ const { planEntry, navigationFile, HOW_TO: ENTRY_HOW_TO } = require('./steps/ent
16
16
  const { planEnv, targetOf } = require('./steps/env');
17
17
  const { planInfoPlist } = require('./steps/infoPlist');
18
18
  const { planMainActivity } = require('./steps/mainActivity');
19
+ const { planSceneDelegate } = require('./steps/sceneDelegate');
19
20
  const { planMainApplication } = require('./steps/mainApplication');
20
21
  const { planManifest } = require('./steps/manifest');
21
22
  const { planStyles } = require('./steps/styles');
@@ -83,6 +84,18 @@ function planProject(project) {
83
84
  planAppDelegate,
84
85
  notFound('AppDelegate.swift')
85
86
  ),
87
+ // React Native 0.88's template starts React Native in its scene delegate; the earlier templates have none.
88
+ ...(project.sceneDelegate
89
+ ? [
90
+ planFile(
91
+ 'SceneDelegate.swift',
92
+ 'ios',
93
+ project.sceneDelegate,
94
+ planSceneDelegate,
95
+ notFound('SceneDelegate.swift')
96
+ ),
97
+ ]
98
+ : []),
86
99
  planFile('Info.plist', 'ios', project.infoPlist, planInfoPlist, notFound('Info.plist')),
87
100
  planFile('index.js', 'js', project.entry, planEntry, ENTRY_HOW_TO),
88
101
  ];
@@ -19,7 +19,10 @@ its delegate (the bundle URL) before calling super.
19
19
  self.reactNativeDelegate = ReactNativeDelegate()
20
20
  return super.application(application, didFinishLaunchingWithOptions: launchOptions)
21
21
  }
22
- }`;
22
+ }
23
+
24
+ ReactNativeDelegate is the app's RCTDefaultReactNativeFactoryDelegate, which gives the bundle URL (bundleURL()); in
25
+ React Native 0.88's template that method is in SceneDelegate.`;
23
26
 
24
27
  // Lines of the template's launch: ANNAppDelegate does all of it.
25
28
  const TEMPLATE_LINES = [
@@ -42,6 +45,20 @@ const LAUNCH_OVERRIDE =
42
45
  const DELEGATE = /^([ \t]*)let\s+delegate\s*=\s*ReactNativeDelegate\(\)[ \t]*$/m;
43
46
  const RETURN_TRUE = /^([ \t]*)return\s+true[ \t]*$/m;
44
47
 
48
+ // React Native 0.88's template starts React Native in its scene delegate: the app delegate only returns true. The
49
+ // bundle URL moves into a delegate of React Native's factory, as the earlier templates had it.
50
+ const REACT_NATIVE_DELEGATE = `class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
51
+ override func bundleURL() -> URL? {
52
+ #if DEBUG
53
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
54
+ #else
55
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
56
+ #endif
57
+ }
58
+ }
59
+ `;
60
+ const RETURN_TRUE_OR_TRUE = /^([ \t]*)(?:return\s+)?true[ \t]*$/m;
61
+
45
62
  /**
46
63
  * AppDelegate.swift: the app delegate becomes an ANNAppDelegate.
47
64
  * @param {string} text
@@ -55,13 +72,30 @@ function planAppDelegate(text) {
55
72
  note: 'changed by hand, see below',
56
73
  howTo: HOW_TO,
57
74
  });
58
- if (!HEADER.test(text) || !LAUNCH.test(text) || !DELEGATE.test(text)) return manual;
75
+ if (!HEADER.test(text) || !LAUNCH.test(text)) return manual;
76
+ let next = DELEGATE.test(text) ? fromFactoryLaunch(text) : fromSceneLaunch(text);
77
+ if (next === undefined) return manual;
59
78
 
79
+ // The dependency provider is ANNAppDelegate's now.
80
+ if (!/\bRCTAppDependencyProvider\b/.test(next)) {
81
+ next = next.replace(/^import\s+ReactAppDependencyProvider[ \t]*\n/m, '');
82
+ }
83
+ next = addImport(next, 'import ApexNavigation', { sorted: false });
84
+ return { status: 'change', next, note: 'ANNAppDelegate' };
85
+ }
86
+
87
+ /**
88
+ * The app delegate of the templates before React Native 0.88, which makes React Native's factory and window in its
89
+ * launch; undefined when the launch has code of the app's own that uses them.
90
+ * @param {string} text
91
+ * @returns {string | undefined}
92
+ */
93
+ function fromFactoryLaunch(text) {
60
94
  let next = text
61
95
  .replace(HEADER, 'class $1: ANNAppDelegate {')
62
96
  .replace(LAUNCH, '$1override func$2');
63
97
  for (const line of TEMPLATE_LINES) {
64
- if (!line.test(next)) return manual;
98
+ if (!line.test(next)) return undefined;
65
99
  next = next.replace(line, '');
66
100
  }
67
101
  next = next.replace(DELEGATE, '$1self.reactNativeDelegate = ReactNativeDelegate()');
@@ -69,10 +103,10 @@ function planAppDelegate(text) {
69
103
  // The launch now ends in super's, which starts React Native.
70
104
  const launchAt = next.search(LAUNCH_OVERRIDE);
71
105
  const returnAt = next.slice(launchAt).search(RETURN_TRUE);
72
- if (returnAt < 0) return manual;
106
+ if (returnAt < 0) return undefined;
73
107
  const body = next.slice(launchAt, launchAt + returnAt).replace(/self\.reactNativeDelegate/g, '');
74
108
  // Code of the app's own that used the template's factory, delegate or window: it has to move by hand.
75
- if (/\b(factory|delegate|window)\b/.test(body)) return manual;
109
+ if (/\b(factory|delegate|window)\b/.test(body)) return undefined;
76
110
  const rest = next
77
111
  .slice(launchAt + returnAt)
78
112
  .replace(
@@ -82,17 +116,38 @@ function planAppDelegate(text) {
82
116
  next = next.slice(0, launchAt + returnAt) + rest;
83
117
 
84
118
  // The blank lines the template's launch leaves behind.
85
- next = next
119
+ return next
86
120
  .replace(/(class\s+\w+:\s*ANNAppDelegate\s*\{\n)(?:[ \t]*\n)+/, '$1')
87
121
  .replace(/(self\.reactNativeDelegate = ReactNativeDelegate\(\)\n)(?:[ \t]*\n)+/, '$1')
88
122
  .replace(/\n(?:[ \t]*\n)+([ \t]*return super\.application\()/, '\n$1');
123
+ }
89
124
 
90
- // The dependency provider is ANNAppDelegate's now.
91
- if (!/\bRCTAppDependencyProvider\b/.test(next)) {
92
- next = next.replace(/^import\s+ReactAppDependencyProvider[ \t]*\n/m, '');
125
+ /**
126
+ * The app delegate of React Native 0.88's template, whose launch only returns true; undefined when the app delegate
127
+ * makes React Native or a window of its own.
128
+ * @param {string} text
129
+ * @returns {string | undefined}
130
+ */
131
+ function fromSceneLaunch(text) {
132
+ if (/\b(RCTReactNativeFactory|ReactNativeDelegate|window)\b/.test(text)) return undefined;
133
+ let next = text
134
+ .replace(HEADER, 'class $1: ANNAppDelegate {')
135
+ .replace(LAUNCH, '$1override func$2');
136
+ const launchAt = next.search(LAUNCH_OVERRIDE);
137
+ const returnAt = next.slice(launchAt).search(RETURN_TRUE_OR_TRUE);
138
+ if (returnAt < 0) return undefined;
139
+ const rest = next
140
+ .slice(launchAt + returnAt)
141
+ .replace(
142
+ RETURN_TRUE_OR_TRUE,
143
+ '$1self.reactNativeDelegate = ReactNativeDelegate()\n' +
144
+ '$1return super.application(application, didFinishLaunchingWithOptions: launchOptions)'
145
+ );
146
+ next = `${next.slice(0, launchAt + returnAt)}${rest.trimEnd()}\n\n${REACT_NATIVE_DELEGATE}`;
147
+ for (const module of ['React', 'React_RCTAppDelegate']) {
148
+ next = addImport(next, `import ${module}`, { sorted: false });
93
149
  }
94
- next = addImport(next, 'import ApexNavigation', { sorted: false });
95
- return { status: 'change', next, note: 'ANNAppDelegate' };
150
+ return next;
96
151
  }
97
152
 
98
153
  module.exports = { planAppDelegate };