@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.
- package/CHANGELOG.md +16 -0
- package/README.md +147 -51
- package/android/CLAUDE.md +10 -4
- package/android/CORE.md +4 -1
- package/android/RECIPES.md +4 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
- package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
- package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +12 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +12 -11
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
- package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
- package/cli/app.js +1 -1
- package/cli/project.js +6 -2
- package/cli/setup/index.js +13 -0
- package/cli/setup/steps/appDelegate.js +66 -11
- package/cli/setup/steps/sceneDelegate.js +108 -0
- package/docs-md/architecture/transitions.md +1 -1
- package/docs-md/guides/deep-links.md +15 -33
- package/docs-md/start/installation.md +1 -0
- package/ios/Animations/ANNTransitionAnimator.mm +13 -11
- package/ios/Animations/ANNTransitionWait.mm +6 -7
- package/ios/Animations/Elements/ANNElementFinder.h +57 -0
- package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
- package/ios/Animations/Elements/ANNElementTransition.h +7 -5
- package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
- package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
- package/ios/CLAUDE.md +10 -2
- package/ios/CORE.md +3 -2
- package/ios/Contract/ANNCommandAnswer.h +4 -3
- package/ios/Contract/ANNCommandAnswer.mm +12 -2
- package/ios/Contract/ANNCommandRunner.h +2 -2
- package/ios/Contract/ANNCommandRunner.mm +1 -1
- package/ios/Contract/ANNErrorTexts.h +6 -2
- package/ios/Contract/ANNErrorTexts.mm +18 -10
- package/ios/Core/ANNDeepLinkQueue.h +20 -1
- package/ios/Core/ANNDeepLinkQueue.mm +107 -12
- package/ios/Core/ANNErrorHandler.h +8 -3
- package/ios/Core/ANNErrorHandler.mm +17 -1
- package/ios/Core/ANNEventContract.h +246 -0
- package/ios/Core/ANNEventContract.mm +225 -0
- package/ios/Core/ANNEventEmitter.h +5 -1
- package/ios/Core/ANNEventEmitter.mm +117 -80
- package/ios/Core/ANNHinge.mm +6 -4
- package/ios/Core/ANNSceneDelegate.mm +25 -15
- package/ios/Core/ANNSystemUI.h +3 -2
- package/ios/Core/ANNSystemUI.mm +7 -0
- package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
- package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
- package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
- package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
- package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
- package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
- package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
- package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
- package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
- package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
- package/ios/Options/ANNSheetOptions.h +4 -1
- package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
- package/ios/RECIPES.md +2 -1
- package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
- package/ios/TurboModules/ANNTurboModule.mm +5 -2
- package/lib/module/ARCHITECTURE.md +7 -4
- package/lib/module/api/app/createNavigation.js +2 -0
- package/lib/module/api/app/createNavigation.js.map +1 -1
- package/lib/module/api/linking/dispatchedLinks.js +23 -0
- package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
- package/lib/module/interfaces/contract/errors.json +275 -0
- package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
- package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
- package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
- package/lib/typescript/interfaces/Events.d.ts +9 -0
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +17 -13
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/modals.d.ts +15 -11
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
- package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +4 -3
- package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +7 -4
- package/src/api/app/createNavigation.ts +2 -0
- package/src/api/linking/dispatchedLinks.ts +18 -0
- package/src/interfaces/Events.ts +10 -0
- package/src/interfaces/contract/errors.json +275 -0
- package/src/interfaces/options/animations.ts +17 -13
- package/src/interfaces/options/modals.ts +15 -11
- package/src/interfaces/options/sideMenu.ts +7 -6
- 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
|
-
|
|
42
|
-
|
|
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
|
+
}
|
package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
87
|
+
onFrame { frame(it) }
|
|
88
|
+
onEnd {
|
|
91
89
|
if (fits) {
|
|
92
90
|
to.scaleType = toFit
|
|
93
91
|
to.imageMatrix = savedMatrix
|
package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
67
|
+
onFrame { frame(it) }
|
|
70
68
|
// A cancelled flight ends too: the view goes back whole.
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
drawable.update(it
|
|
48
|
+
onFrame {
|
|
49
|
+
drawable.update(it, curve.getInterpolation(it))
|
|
52
50
|
layer.invalidate()
|
|
53
51
|
}
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
drawable.progress = curve.getInterpolation(it
|
|
44
|
+
onFrame {
|
|
45
|
+
drawable.progress = curve.getInterpolation(it)
|
|
48
46
|
layer.invalidate()
|
|
49
47
|
}
|
|
50
|
-
|
|
48
|
+
onEnd {
|
|
51
49
|
layer.overlay.remove(drawable)
|
|
52
50
|
layer.invalidate()
|
|
53
51
|
to.alpha = end.alpha
|
package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt
CHANGED
|
@@ -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.
|
|
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()).
|
|
211
|
-
UiUtils.dpToPx(context, background.height.get(0.0).toFloat()).
|
|
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,
|
|
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`. `
|
|
12
|
-
*
|
|
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))
|
package/cli/setup/index.js
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
return { status: 'change', next, note: 'ANNAppDelegate' };
|
|
150
|
+
return next;
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
module.exports = { planAppDelegate };
|