@onekeyfe/react-native-native-overlay 3.0.162-alpha.268

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/LICENSE +21 -0
  2. package/NativeOverlay.podspec +30 -0
  3. package/README.md +35 -0
  4. package/android/build.gradle +82 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
  7. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
  8. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
  9. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
  10. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
  11. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
  12. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
  13. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
  14. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
  15. package/android/src/main/jni/CMakeLists.txt +44 -0
  16. package/android/src/main/jni/RNCNativeOverlay.h +19 -0
  17. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
  18. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
  19. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
  20. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
  21. package/docs/SPEC.md +693 -0
  22. package/ios/NativeOverlayAnimation.swift +160 -0
  23. package/ios/NativeOverlayContainerView.swift +688 -0
  24. package/ios/NativeOverlayKeyboard.swift +88 -0
  25. package/ios/NativeOverlayPageHostView.swift +98 -0
  26. package/ios/NativeOverlaySheetController.swift +391 -0
  27. package/ios/NativeOverlayWindowManager.swift +194 -0
  28. package/ios/RCTNativeOverlayComponentView.h +11 -0
  29. package/ios/RCTNativeOverlayComponentView.mm +193 -0
  30. package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
  31. package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
  32. package/lib/module/NativeOverlayNativeComponent.ts +61 -0
  33. package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
  34. package/lib/module/OverlayErrors.js +6 -0
  35. package/lib/module/OverlayErrors.js.map +1 -0
  36. package/lib/module/OverlayLevelContext.js +27 -0
  37. package/lib/module/OverlayLevelContext.js.map +1 -0
  38. package/lib/module/OverlayLevels.js +33 -0
  39. package/lib/module/OverlayLevels.js.map +1 -0
  40. package/lib/module/OverlayPageHost.js +24 -0
  41. package/lib/module/OverlayPageHost.js.map +1 -0
  42. package/lib/module/OverlayPageHost.web.js +49 -0
  43. package/lib/module/OverlayPageHost.web.js.map +1 -0
  44. package/lib/module/OverlayPageScope.js +41 -0
  45. package/lib/module/OverlayPageScope.js.map +1 -0
  46. package/lib/module/OverlayStore.js +305 -0
  47. package/lib/module/OverlayStore.js.map +1 -0
  48. package/lib/module/OverlayView.js +141 -0
  49. package/lib/module/OverlayView.js.map +1 -0
  50. package/lib/module/OverlayView.web.js +290 -0
  51. package/lib/module/OverlayView.web.js.map +1 -0
  52. package/lib/module/OverlayViewTypes.js +4 -0
  53. package/lib/module/OverlayViewTypes.js.map +1 -0
  54. package/lib/module/animation/motionPresets.js +57 -0
  55. package/lib/module/animation/motionPresets.js.map +1 -0
  56. package/lib/module/animation/platformMotionPresets.js +4 -0
  57. package/lib/module/animation/platformMotionPresets.js.map +1 -0
  58. package/lib/module/animation/platformMotionPresets.web.js +4 -0
  59. package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
  60. package/lib/module/animation/resolveAnimation.js +80 -0
  61. package/lib/module/animation/resolveAnimation.js.map +1 -0
  62. package/lib/module/animation/spring.js +69 -0
  63. package/lib/module/animation/spring.js.map +1 -0
  64. package/lib/module/animation/types.js +2 -0
  65. package/lib/module/animation/types.js.map +1 -0
  66. package/lib/module/index.js +16 -0
  67. package/lib/module/index.js.map +1 -0
  68. package/lib/module/package.json +1 -0
  69. package/lib/module/types.js +2 -0
  70. package/lib/module/types.js.map +1 -0
  71. package/lib/module/useOverlayController.js +127 -0
  72. package/lib/module/useOverlayController.js.map +1 -0
  73. package/lib/module/useSuspendedPageOwners.js +16 -0
  74. package/lib/module/useSuspendedPageOwners.js.map +1 -0
  75. package/lib/module/web/animateTransition.js +115 -0
  76. package/lib/module/web/animateTransition.js.map +1 -0
  77. package/lib/module/web/overlayLayers.js +147 -0
  78. package/lib/module/web/overlayLayers.js.map +1 -0
  79. package/lib/module/web/useKeyboardShift.js +110 -0
  80. package/lib/module/web/useKeyboardShift.js.map +1 -0
  81. package/lib/module/web/useSheetDrag.js +126 -0
  82. package/lib/module/web/useSheetDrag.js.map +1 -0
  83. package/lib/typescript/package.json +1 -0
  84. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
  85. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
  87. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
  88. package/lib/typescript/src/OverlayErrors.d.ts +4 -0
  89. package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
  90. package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
  91. package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
  92. package/lib/typescript/src/OverlayLevels.d.ts +13 -0
  93. package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
  94. package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
  95. package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
  96. package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
  97. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
  98. package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
  99. package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
  100. package/lib/typescript/src/OverlayStore.d.ts +72 -0
  101. package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
  102. package/lib/typescript/src/OverlayView.d.ts +3 -0
  103. package/lib/typescript/src/OverlayView.d.ts.map +1 -0
  104. package/lib/typescript/src/OverlayView.web.d.ts +3 -0
  105. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
  106. package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
  107. package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
  108. package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
  109. package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
  110. package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
  111. package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
  112. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
  113. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
  114. package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
  115. package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
  116. package/lib/typescript/src/animation/spring.d.ts +24 -0
  117. package/lib/typescript/src/animation/spring.d.ts.map +1 -0
  118. package/lib/typescript/src/animation/types.d.ts +56 -0
  119. package/lib/typescript/src/animation/types.d.ts.map +1 -0
  120. package/lib/typescript/src/index.d.ts +18 -0
  121. package/lib/typescript/src/index.d.ts.map +1 -0
  122. package/lib/typescript/src/types.d.ts +59 -0
  123. package/lib/typescript/src/types.d.ts.map +1 -0
  124. package/lib/typescript/src/useOverlayController.d.ts +29 -0
  125. package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
  126. package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
  127. package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
  128. package/lib/typescript/src/web/animateTransition.d.ts +18 -0
  129. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
  130. package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
  131. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
  132. package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
  133. package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
  134. package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
  135. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
  136. package/package.json +188 -0
  137. package/react-native.config.js +16 -0
  138. package/src/NativeOverlayNativeComponent.ts +61 -0
  139. package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
  140. package/src/OverlayErrors.ts +3 -0
  141. package/src/OverlayLevelContext.tsx +32 -0
  142. package/src/OverlayLevels.ts +40 -0
  143. package/src/OverlayPageHost.tsx +21 -0
  144. package/src/OverlayPageHost.web.tsx +53 -0
  145. package/src/OverlayPageScope.tsx +42 -0
  146. package/src/OverlayStore.ts +401 -0
  147. package/src/OverlayView.tsx +165 -0
  148. package/src/OverlayView.web.tsx +356 -0
  149. package/src/OverlayViewTypes.ts +81 -0
  150. package/src/animation/motionPresets.ts +52 -0
  151. package/src/animation/platformMotionPresets.ts +1 -0
  152. package/src/animation/platformMotionPresets.web.ts +1 -0
  153. package/src/animation/resolveAnimation.ts +103 -0
  154. package/src/animation/spring.ts +80 -0
  155. package/src/animation/types.ts +71 -0
  156. package/src/index.ts +55 -0
  157. package/src/types.ts +79 -0
  158. package/src/useOverlayController.ts +199 -0
  159. package/src/useSuspendedPageOwners.ts +26 -0
  160. package/src/web/animateTransition.ts +137 -0
  161. package/src/web/overlayLayers.ts +182 -0
  162. package/src/web/useKeyboardShift.ts +130 -0
  163. package/src/web/useSheetDrag.ts +143 -0
@@ -0,0 +1,122 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.view.ViewGroup
6
+ import com.facebook.react.uimanager.PointerEvents
7
+ import com.facebook.react.uimanager.ReactPointerEventsView
8
+ import java.lang.ref.WeakReference
9
+
10
+ /**
11
+ * Page-scope overlays of one root route render inside this view. It sits in
12
+ * the React tree (last child of the root-route screen), so the ReactRootView
13
+ * already dispatches touches to the moved content.
14
+ */
15
+ @SuppressLint("ViewConstructor")
16
+ class NativeOverlayPageHostView(context: Context) : ViewGroup(context), ReactPointerEventsView {
17
+ // RN's TouchTargetHelper walks the native tree; without box-none this
18
+ // full-screen host would become the JS target of every touch in the route.
19
+ override val pointerEvents: PointerEvents = PointerEvents.BOX_NONE
20
+
21
+ var hostKey: String = ""
22
+ set(value) {
23
+ if (field == value) return
24
+ if (field.isNotEmpty() && registry[field]?.get() === this) registry.remove(field)
25
+ field = value
26
+ if (value.isNotEmpty()) registry[value] = WeakReference(this)
27
+ }
28
+
29
+ private var suspendedOwners: Set<String> = emptySet()
30
+
31
+ fun setSuspendedOwners(joined: String?) {
32
+ val owners = joined.orEmpty().split('\n').filter { it.isNotEmpty() }.toSet()
33
+ if (owners == suspendedOwners) return
34
+ suspendedOwners = owners
35
+ entries().forEach(::applySuspension)
36
+ }
37
+
38
+ internal fun attach(entry: NativeOverlayEntryRootView) {
39
+ if (entry.parent !== this) {
40
+ (entry.parent as? ViewGroup)?.removeView(entry)
41
+ val index = entries().indexOfFirst { sortKey(it) > sortKey(entry) }
42
+ .let { if (it < 0) childCount else indexOfChild(entries()[it]) }
43
+ addView(entry, index, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
44
+ }
45
+ layoutEntries()
46
+ applySuspension(entry)
47
+ }
48
+
49
+ internal fun detach(entry: NativeOverlayEntryRootView) {
50
+ removeView(entry)
51
+ }
52
+
53
+ /** Visible, input-ready entries in bottom-to-top order. */
54
+ internal fun shownEntries(): List<NativeOverlayEntryRootView> =
55
+ entries().filter { it.visibility == VISIBLE && it.isShownForInput }
56
+
57
+ fun invalidateHost() {
58
+ if (hostKey.isNotEmpty() && registry[hostKey]?.get() === this) registry.remove(hostKey)
59
+ }
60
+
61
+ private var layoutScheduled = false
62
+ private val layoutRunnable = Runnable {
63
+ layoutScheduled = false
64
+ layoutEntries()
65
+ }
66
+
67
+ /**
68
+ * This view sits inside Fabric's tree, where parents ignore requestLayout
69
+ * and only Fabric calls layout(). Entries are native-only children, so lay
70
+ * them out here whenever they (or their content) ask for it.
71
+ */
72
+ override fun requestLayout() {
73
+ super.requestLayout()
74
+ if (!layoutScheduled) {
75
+ layoutScheduled = true
76
+ post(layoutRunnable)
77
+ }
78
+ }
79
+
80
+ private fun layoutEntries() {
81
+ onLayout(false, left, top, right, bottom)
82
+ }
83
+
84
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
85
+ val width = r - l
86
+ val height = b - t
87
+ if (width <= 0 || height <= 0) return
88
+ for (i in 0 until childCount) {
89
+ val child = getChildAt(i)
90
+ child.measure(
91
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
92
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
93
+ )
94
+ child.layout(0, 0, width, height)
95
+ }
96
+ }
97
+
98
+ private fun applySuspension(entry: NativeOverlayEntryRootView) {
99
+ val hidden = entry.ownerKey in suspendedOwners
100
+ val target = if (hidden) INVISIBLE else VISIBLE
101
+ if (entry.visibility == target) return
102
+ entry.visibility = target
103
+ if (!hidden) {
104
+ entry.alpha = 0f
105
+ entry.animate().alpha(1f).setDuration(150).start()
106
+ }
107
+ }
108
+
109
+ private fun entries(): List<NativeOverlayEntryRootView> =
110
+ (0 until childCount).mapNotNull { getChildAt(it) as? NativeOverlayEntryRootView }
111
+
112
+ private fun sortKey(entry: NativeOverlayEntryRootView): Long =
113
+ entry.levelOrder.toLong() * 1_000_000_000L + entry.stackOrder
114
+
115
+ companion object {
116
+ private val registry = HashMap<String, WeakReference<NativeOverlayPageHostView>>()
117
+
118
+ internal fun host(key: String): NativeOverlayPageHostView? = registry[key]?.get()
119
+
120
+ internal fun allHosts(): List<NativeOverlayPageHostView> = registry.values.mapNotNull { it.get() }
121
+ }
122
+ }
@@ -0,0 +1,40 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.viewmanagers.RNCNativeOverlayPageHostManagerDelegate
8
+ import com.facebook.react.viewmanagers.RNCNativeOverlayPageHostManagerInterface
9
+
10
+ /** No React children: entries are attached natively by `NativeOverlayView`. */
11
+ @ReactModule(name = NativeOverlayPageHostViewManager.NAME)
12
+ class NativeOverlayPageHostViewManager :
13
+ SimpleViewManager<NativeOverlayPageHostView>(),
14
+ RNCNativeOverlayPageHostManagerInterface<NativeOverlayPageHostView> {
15
+ private val delegate =
16
+ RNCNativeOverlayPageHostManagerDelegate<NativeOverlayPageHostView, NativeOverlayPageHostViewManager>(this)
17
+
18
+ override fun getName(): String = NAME
19
+
20
+ override fun getDelegate(): ViewManagerDelegate<NativeOverlayPageHostView> = delegate
21
+
22
+ override fun createViewInstance(context: ThemedReactContext) = NativeOverlayPageHostView(context)
23
+
24
+ override fun onDropViewInstance(view: NativeOverlayPageHostView) {
25
+ view.invalidateHost()
26
+ super.onDropViewInstance(view)
27
+ }
28
+
29
+ override fun setHostKey(view: NativeOverlayPageHostView, value: String?) {
30
+ view.hostKey = value.orEmpty()
31
+ }
32
+
33
+ override fun setSuspendedOwners(view: NativeOverlayPageHostView, value: String?) {
34
+ view.setSuspendedOwners(value)
35
+ }
36
+
37
+ companion object {
38
+ const val NAME = "RNCNativeOverlayPageHost"
39
+ }
40
+ }
@@ -0,0 +1,465 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.content.res.Configuration
7
+ import android.graphics.Color
8
+ import android.provider.Settings
9
+ import android.util.TypedValue
10
+ import android.view.View
11
+ import android.view.View.OnLayoutChangeListener
12
+ import android.view.ViewGroup
13
+ import android.view.accessibility.AccessibilityEvent
14
+ import com.facebook.react.bridge.Arguments
15
+ import com.facebook.react.uimanager.StateWrapper
16
+ import com.facebook.react.uimanager.ThemedReactContext
17
+ import com.facebook.react.uimanager.events.EventDispatcher
18
+ import kotlin.math.abs
19
+
20
+ /** Visual state of the content between "hidden" (off stage) and identity. */
21
+ private data class OverlayVisualState(
22
+ val alpha: Float = 1f,
23
+ val translationX: Float = 0f,
24
+ val translationY: Float = 0f,
25
+ val scale: Float = 1f,
26
+ ) {
27
+ fun lerp(to: OverlayVisualState, t: Float) = OverlayVisualState(
28
+ alpha + (to.alpha - alpha) * t,
29
+ translationX + (to.translationX - translationX) * t,
30
+ translationY + (to.translationY - translationY) * t,
31
+ scale + (to.scale - scale) * t,
32
+ )
33
+
34
+ fun applyTo(view: View) {
35
+ view.alpha = alpha.coerceIn(0f, 1f)
36
+ view.translationX = translationX
37
+ view.translationY = translationY
38
+ view.scaleX = scale
39
+ view.scaleY = scale
40
+ }
41
+
42
+ companion object {
43
+ val IDENTITY = OverlayVisualState()
44
+
45
+ fun of(view: View) = OverlayVisualState(view.alpha, view.translationX, view.translationY, view.scaleX)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The React-facing view. It never draws: its single React child is moved
51
+ * into a [NativeOverlayEntryRootView] inside the activity's
52
+ * [NativeOverlayHost] while visible.
53
+ */
54
+ class NativeOverlayView(
55
+ private val reactContext: ThemedReactContext,
56
+ ) : ViewGroup(reactContext) {
57
+ var visible = false
58
+ var level: String = "modal"
59
+ var scope: String = "global"
60
+ var hostKey: String = ""
61
+ var ownerKey: String = ""
62
+ var presentation: String = "center"
63
+ var stackOrder = 0
64
+ var blocking = true
65
+ var dismissOnBackPress = true
66
+ var dismissOnBackdropPress = false
67
+ var backdropColor: Int? = null
68
+ var sheetHeight = 0.0
69
+ var sheetCornerRadius = 24.0
70
+ var showHandle = false
71
+ var sheetBackgroundColor: Int? = null
72
+ var dismissOnPanDown = true
73
+ var animationConfig: String? = null
74
+ var onPresented: ((Int) -> Unit)? = null
75
+ var onDismissed: ((String) -> Unit)? = null
76
+ var onRequestDismiss: ((String) -> Unit)? = null
77
+
78
+ /**
79
+ * Fabric state of the hand-written shadow node: the window origin (dp) of
80
+ * wherever the content is drawn, so `measure` reports on-screen frames.
81
+ */
82
+ var stateWrapper: StateWrapper? = null
83
+ set(value) {
84
+ field = value
85
+ pushContentOffset()
86
+ }
87
+ private var contentOffsetX = 0.0
88
+ private var contentOffsetY = 0.0
89
+ private val contentLayoutListener =
90
+ OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> reportContentOffset() }
91
+ var eventDispatcher: EventDispatcher? = null
92
+ set(value) {
93
+ field = value
94
+ entry?.eventDispatcher = value
95
+ }
96
+
97
+ private enum class Phase { HIDDEN, ENTERING, SHOWN, EXITING }
98
+
99
+ private var phase = Phase.HIDDEN
100
+ private var contentChild: View? = null
101
+ private var entry: NativeOverlayEntryRootView? = null
102
+ private var host: NativeOverlayHost? = null
103
+ private var pageHost: NativeOverlayPageHostView? = null
104
+ private var contentAnimator: ValueAnimator? = null
105
+ private var backdropAnimator: ValueAnimator? = null
106
+ private var parsedConfigSource: String? = null
107
+ private var config = OverlayAnimationConfig()
108
+ private var cycleOpen = false
109
+
110
+ init {
111
+ visibility = INVISIBLE
112
+ }
113
+
114
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit
115
+
116
+ fun addReactChild(child: View) {
117
+ contentChild?.takeIf { it !== child }?.let(::removeReactChild)
118
+ contentChild = child
119
+ entry?.let { attachContent(it) } ?: stageContent()
120
+ }
121
+
122
+ fun getReactChildCount(): Int = if (contentChild == null) 0 else 1
123
+
124
+ fun getReactChildAt(index: Int): View? = if (index == 0) contentChild else null
125
+
126
+ fun removeReactChild(child: View) {
127
+ if (contentChild !== child) return
128
+ (child.parent as? ViewGroup)?.removeView(child)
129
+ contentChild = null
130
+ }
131
+
132
+ fun commitConfiguration() {
133
+ if (parsedConfigSource != animationConfig) {
134
+ parsedConfigSource = animationConfig
135
+ config = OverlayAnimationConfig.parse(animationConfig)
136
+ }
137
+ entry?.let(::applyEntryConfiguration)
138
+ if (visible) {
139
+ cycleOpen = true
140
+ if (phase == Phase.HIDDEN || phase == Phase.EXITING) present()
141
+ } else if (cycleOpen) {
142
+ when (phase) {
143
+ Phase.ENTERING, Phase.SHOWN -> dismiss()
144
+ Phase.HIDDEN -> {
145
+ cycleOpen = false
146
+ onDismissed?.invoke("programmatic")
147
+ }
148
+ Phase.EXITING -> Unit
149
+ }
150
+ }
151
+ }
152
+
153
+ fun onDropViewInstance() {
154
+ if (phase != Phase.HIDDEN) finishDismiss(null)
155
+ onPresented = null
156
+ onDismissed = null
157
+ onRequestDismiss = null
158
+ eventDispatcher = null
159
+ }
160
+
161
+ override fun onAttachedToWindow() {
162
+ super.onAttachedToWindow()
163
+ commitConfiguration()
164
+ }
165
+
166
+ // Detach from window (screen removed, activity destroyed) ends the overlay.
167
+ override fun onDetachedFromWindow() {
168
+ super.onDetachedFromWindow()
169
+ // A page overlay's owner screen detaches when another screen covers it;
170
+ // the page host hides the entry, it is not closed.
171
+ if (phase != Phase.HIDDEN && scope != "page") finishDismiss("system")
172
+ }
173
+
174
+ private fun present() {
175
+ val activity = reactContext.currentActivity ?: return
176
+ if (!isAttachedToWindow && scope != "page") return
177
+ val overlayHost = NativeOverlayHost.of(activity)
178
+ val target = entry ?: makeEntry()
179
+ applyEntryConfiguration(target)
180
+ if (scope == "page") {
181
+ val page = NativeOverlayPageHostView.host(hostKey) ?: return
182
+ target.dispatchesJsTouches = false
183
+ target.levelOrder = NativeOverlayLevel.from(level).order
184
+ target.ownerKey = ownerKey
185
+ pageHost = page
186
+ page.attach(target)
187
+ // Back for page entries is resolved through the activity interceptor.
188
+ overlayHost.installBackInterceptor()
189
+ } else {
190
+ host = overlayHost
191
+ overlayHost.attach(target, NativeOverlayLevel.from(level))
192
+ }
193
+ attachContent(target)
194
+
195
+ val transition = config.enter.reduceMotionAdjusted(isReduceMotionEnabled())
196
+ if (phase == Phase.HIDDEN) {
197
+ hiddenState(target, transition).applyTo(target.animatedView)
198
+ target.backdropView.alpha = 0f
199
+ }
200
+ cancelAnimators()
201
+ phase = Phase.ENTERING
202
+ target.isShownForInput = true
203
+ host?.onEntriesChanged()
204
+ if (blocking) {
205
+ target.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
206
+ }
207
+ if (transition.kind == "none") {
208
+ OverlayVisualState.IDENTITY.applyTo(target.animatedView)
209
+ target.backdropView.alpha = 1f
210
+ phase = Phase.SHOWN
211
+ reportContentOffset()
212
+ onPresented?.invoke(stackOrder)
213
+ return
214
+ }
215
+ animate(target, OverlayVisualState.IDENTITY, 1f, transition.motion) {
216
+ if (phase == Phase.ENTERING) {
217
+ phase = Phase.SHOWN
218
+ reportContentOffset()
219
+ onPresented?.invoke(stackOrder)
220
+ }
221
+ }
222
+ }
223
+
224
+ private fun dismiss() {
225
+ val target = entry ?: return finishDismiss("programmatic")
226
+ cancelAnimators()
227
+ phase = Phase.EXITING
228
+ target.isShownForInput = false
229
+ host?.onEntriesChanged()
230
+ val transition = config.exit.reduceMotionAdjusted(isReduceMotionEnabled())
231
+ if (transition.kind == "none") return finishDismiss("programmatic")
232
+ animate(target, hiddenState(target, transition), 0f, transition.motion) {
233
+ if (phase == Phase.EXITING) finishDismiss("programmatic")
234
+ }
235
+ }
236
+
237
+ /** `reason == null` tears down without notifying JS (view dropped). */
238
+ private fun finishDismiss(reason: String?) {
239
+ cancelAnimators()
240
+ phase = Phase.HIDDEN
241
+ entry?.let { target ->
242
+ target.isShownForInput = false
243
+ stageContent()
244
+ host?.detach(target)
245
+ pageHost?.detach(target)
246
+ }
247
+ entry = null
248
+ host = null
249
+ pageHost = null
250
+ if (reason == null || !cycleOpen) return
251
+ cycleOpen = visible
252
+ onDismissed?.invoke(reason)
253
+ }
254
+
255
+ private fun animate(
256
+ target: NativeOverlayEntryRootView,
257
+ to: OverlayVisualState,
258
+ backdropTo: Float,
259
+ motion: OverlayMotion,
260
+ onEnd: () -> Unit,
261
+ ) {
262
+ // Start from the current values so an interrupted run retargets smoothly.
263
+ val animated = target.animatedView
264
+ val from = OverlayVisualState.of(animated)
265
+ contentAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
266
+ duration = motion.durationMs()
267
+ interpolator = motion.interpolator()
268
+ addUpdateListener { from.lerp(to, it.animatedValue as Float).applyTo(animated) }
269
+ addListener(object : AnimatorListenerAdapter() {
270
+ private var cancelled = false
271
+
272
+ override fun onAnimationCancel(animation: Animator) {
273
+ cancelled = true
274
+ }
275
+
276
+ override fun onAnimationEnd(animation: Animator) {
277
+ if (!cancelled) onEnd()
278
+ }
279
+ })
280
+ start()
281
+ }
282
+ val backdropFrom = target.backdropView.alpha
283
+ val backdropMotion = config.backdropMotion
284
+ backdropAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
285
+ duration = backdropMotion.durationMs()
286
+ interpolator = backdropMotion.interpolator()
287
+ addUpdateListener {
288
+ val t = it.animatedValue as Float
289
+ target.backdropView.alpha = (backdropFrom + (backdropTo - backdropFrom) * t).coerceIn(0f, 1f)
290
+ }
291
+ start()
292
+ }
293
+ }
294
+
295
+ private fun cancelAnimators() {
296
+ contentAnimator?.cancel()
297
+ backdropAnimator?.cancel()
298
+ contentAnimator = null
299
+ backdropAnimator = null
300
+ }
301
+
302
+ /** The off-stage state: where enter starts and exit ends. */
303
+ private fun hiddenState(target: NativeOverlayEntryRootView, transition: OverlayTransition): OverlayVisualState {
304
+ val alpha = if (transition.hidesOpacity) 0f else 1f
305
+ val extent = target.contentExtent()
306
+ val density = resources.displayMetrics.density
307
+ val isSheet = target.sheetView != null
308
+ return when (transition.kind) {
309
+ "slide" -> {
310
+ val travel = transition.distanceDp?.let { (it * density).toFloat() } ?: when {
311
+ // The sheet may not be laid out yet; its own height is the travel.
312
+ isSheet && transition.edge == "bottom" ->
313
+ maxOf(target.sheetHeightPx(), (sheetHeight * density).toInt()).toFloat()
314
+ else -> when (transition.edge) {
315
+ "top" -> extent.bottom.toFloat()
316
+ "left" -> extent.right.toFloat()
317
+ "right" -> (target.width - extent.left).toFloat()
318
+ else -> (target.height - extent.top).toFloat()
319
+ }
320
+ }
321
+ when (transition.edge) {
322
+ "top" -> OverlayVisualState(alpha, 0f, -travel, 1f)
323
+ "left" -> OverlayVisualState(alpha, -travel, 0f, 1f)
324
+ "right" -> OverlayVisualState(alpha, travel, 0f, 1f)
325
+ else -> OverlayVisualState(alpha, 0f, travel, 1f)
326
+ }
327
+ }
328
+ "scale" -> {
329
+ // Scale around the content's center, not the full-window root's.
330
+ target.animatedView.pivotX = extent.exactCenterX() - target.animatedView.left
331
+ target.animatedView.pivotY = extent.exactCenterY() - target.animatedView.top
332
+ OverlayVisualState(alpha, 0f, (transition.offsetYDp * density).toFloat(), transition.scale)
333
+ }
334
+ else -> OverlayVisualState(alpha = alpha)
335
+ }
336
+ }
337
+
338
+ private fun makeEntry(): NativeOverlayEntryRootView {
339
+ val target = NativeOverlayEntryRootView(reactContext)
340
+ target.eventDispatcher = eventDispatcher
341
+ target.onRequestDismiss = { reason -> onRequestDismiss?.invoke(reason) }
342
+ target.onKeyboardShiftSettled = { reportContentOffset() }
343
+ target.onSheetHiddenByPan = {
344
+ if (phase == Phase.SHOWN || phase == Phase.ENTERING) {
345
+ // Already off screen: tear down, then let JS close the entry; the
346
+ // next visible=false commit reports onDismissed.
347
+ finishDismiss(null)
348
+ onRequestDismiss?.invoke("pan")
349
+ }
350
+ }
351
+ entry = target
352
+ return target
353
+ }
354
+
355
+ /**
356
+ * Without an explicit color the sheet follows the theme (iOS uses
357
+ * `systemBackground`); a fixed white showed as a strip under dark content
358
+ * in the navigation bar inset.
359
+ */
360
+ private fun defaultSheetBackgroundColor(): Int {
361
+ val theme = (context as? ThemedReactContext)?.currentActivity?.theme ?: context.theme
362
+ val value = TypedValue()
363
+ if (
364
+ theme.resolveAttribute(android.R.attr.colorBackground, value, true) &&
365
+ value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
366
+ value.type <= TypedValue.TYPE_LAST_COLOR_INT
367
+ ) {
368
+ return value.data
369
+ }
370
+ val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
371
+ return if (nightMode == Configuration.UI_MODE_NIGHT_YES) Color.BLACK else Color.WHITE
372
+ }
373
+
374
+ private fun applyEntryConfiguration(target: NativeOverlayEntryRootView) {
375
+ target.stackOrder = stackOrder
376
+ target.updateBlocking(blocking)
377
+ target.dismissOnBackPress = dismissOnBackPress
378
+ target.dismissOnBackdropPress = dismissOnBackdropPress
379
+ target.backdropView.setBackgroundColor(backdropColor ?: Color.TRANSPARENT)
380
+ if (presentation == "sheet") {
381
+ val density = resources.displayMetrics.density
382
+ target.configureSheet(
383
+ heightPx = (sheetHeight * density).toInt().coerceAtLeast(1),
384
+ cornerRadiusPx = (sheetCornerRadius * density).toFloat(),
385
+ backgroundColor = sheetBackgroundColor ?: defaultSheetBackgroundColor(),
386
+ showHandle = showHandle,
387
+ draggable = dismissOnPanDown,
388
+ )
389
+ }
390
+ }
391
+
392
+ /**
393
+ * While hidden, keep the React child inside this invisible view. Fabric only
394
+ * reports onLayout for attached views, and fitted sheets size themselves
395
+ * from that measurement before presenting.
396
+ */
397
+ private fun stageContent() {
398
+ val child = contentChild ?: return
399
+ if (child.parent === this) return
400
+ moveContent(child, this)
401
+ }
402
+
403
+ private fun attachContent(target: NativeOverlayEntryRootView) {
404
+ val child = contentChild ?: return
405
+ if (child.parent === target.contentView) return
406
+ moveContent(child, target.contentView)
407
+ }
408
+
409
+ private fun moveContent(child: View, container: ViewGroup) {
410
+ (child.parent as? ViewGroup)?.let { previous ->
411
+ previous.removeOnLayoutChangeListener(contentLayoutListener)
412
+ previous.removeView(child)
413
+ }
414
+ container.addView(child)
415
+ container.addOnLayoutChangeListener(contentLayoutListener)
416
+ reportContentOffset()
417
+ }
418
+
419
+ /** Window origin of the content's container, ignoring running animations. */
420
+ private fun reportContentOffset() {
421
+ val container = contentChild?.parent as? View ?: return
422
+ var x = 0f
423
+ var y = 0f
424
+ var current: View? = container
425
+ while (current != null) {
426
+ x += current.left
427
+ y += current.top
428
+ // The keyboard lift is a resting position, unlike animation transforms.
429
+ if (current is NativeOverlayKeyboardLayer) {
430
+ x += current.translationX
431
+ y += current.translationY
432
+ }
433
+ val parent = current.parent as? View
434
+ if (parent != null) {
435
+ x -= parent.scrollX
436
+ y -= parent.scrollY
437
+ }
438
+ current = parent
439
+ }
440
+ val density = resources.displayMetrics.density
441
+ contentOffsetX = (x / density).toDouble()
442
+ contentOffsetY = (y / density).toDouble()
443
+ pushContentOffset()
444
+ }
445
+
446
+ private fun pushContentOffset() {
447
+ val wrapper = stateWrapper ?: return
448
+ val data = wrapper.stateData
449
+ if (data != null && data.hasKey("x") && data.hasKey("y") &&
450
+ abs(data.getDouble("x") - contentOffsetX) < 0.5 &&
451
+ abs(data.getDouble("y") - contentOffsetY) < 0.5
452
+ ) {
453
+ return
454
+ }
455
+ wrapper.updateState(
456
+ Arguments.createMap().apply {
457
+ putDouble("x", contentOffsetX)
458
+ putDouble("y", contentOffsetY)
459
+ },
460
+ )
461
+ }
462
+
463
+ private fun isReduceMotionEnabled(): Boolean =
464
+ Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
465
+ }