@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,415 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Rect
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.view.Gravity
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.widget.FrameLayout
11
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
12
+ import androidx.core.view.ViewCompat
13
+ import androidx.core.view.WindowInsetsAnimationCompat
14
+ import androidx.core.view.WindowInsetsCompat
15
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
16
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
17
+ import com.facebook.react.config.ReactFeatureFlags
18
+ import com.facebook.react.uimanager.JSPointerDispatcher
19
+ import com.facebook.react.uimanager.JSTouchDispatcher
20
+ import com.facebook.react.uimanager.PointerEvents
21
+ import com.facebook.react.uimanager.ReactPointerEventsView
22
+ import com.facebook.react.uimanager.RootView
23
+ import com.facebook.react.uimanager.ThemedReactContext
24
+ import com.facebook.react.uimanager.TouchTargetHelper
25
+ import com.facebook.react.uimanager.events.EventDispatcher
26
+
27
+ /**
28
+ * Keeps Fabric-assigned frames: the React root inside is laid out by Yoga
29
+ * (window size from JS), so this container must not re-layout it.
30
+ */
31
+ internal class NativeOverlayContentView(context: ThemedReactContext) : ViewGroup(context) {
32
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit
33
+ }
34
+
35
+ /**
36
+ * Carries the keyboard avoidance lift (translationY), so it composes with the
37
+ * enter / exit animation on the content or sheet inside it.
38
+ */
39
+ internal class NativeOverlayKeyboardLayer(context: ThemedReactContext) : FrameLayout(context) {
40
+ init {
41
+ clipChildren = false
42
+ }
43
+ }
44
+
45
+ /**
46
+ * One overlay entry. React children mounted here are outside their
47
+ * ReactRootView, so this root mirrors RN Modal's touch dispatch boundary
48
+ * (JSTouchDispatcher / JSPointerDispatcher) to keep Pressability and native
49
+ * gesture cancellation working after reparenting.
50
+ */
51
+ @SuppressLint("ViewConstructor")
52
+ internal class NativeOverlayEntryRootView(
53
+ private val reactContext: ThemedReactContext,
54
+ ) : FrameLayout(reactContext), RootView, ReactPointerEventsView {
55
+ var eventDispatcher: EventDispatcher? = null
56
+
57
+ /**
58
+ * Page entries are inside the ReactRootView, whose TouchTargetHelper reads
59
+ * this: blocking entries stop the search (touches never reach the page
60
+ * below), non-blocking ones let it continue past the empty area.
61
+ */
62
+ override val pointerEvents: PointerEvents
63
+ get() = if (blocking && isShownForInput) PointerEvents.AUTO else PointerEvents.BOX_NONE
64
+ var stackOrder = 0
65
+ var levelOrder = 0
66
+ /** Page scope: the owning page, used by the page host to hide the entry. */
67
+ var ownerKey = ""
68
+ /**
69
+ * False for page scope: the entry is inside the ReactRootView, which already
70
+ * dispatches its touches; a second dispatcher would double every event.
71
+ */
72
+ var dispatchesJsTouches = true
73
+ var blocking = true
74
+ var dismissOnBackPress = true
75
+ var dismissOnBackdropPress = false
76
+ var isShownForInput = false
77
+ var onRequestDismiss: ((String) -> Unit)? = null
78
+
79
+ val backdropView = View(reactContext).apply { alpha = 0f }
80
+ val keyboardLayer = NativeOverlayKeyboardLayer(reactContext)
81
+ val contentView = NativeOverlayContentView(reactContext)
82
+
83
+ /** The lift finished changing; the owner re-reports where its content is. */
84
+ var onKeyboardShiftSettled: (() -> Unit)? = null
85
+ private var imeBottom = 0
86
+ private var safeTop = 0
87
+ private var sheetBottomPadding = 0
88
+ private var keyboardAnimating = false
89
+
90
+ /** `sheet` presentation: the draggable surface; otherwise null. */
91
+ var sheetView: FrameLayout? = null
92
+ private set
93
+ private var sheetBehavior: BottomSheetBehavior<FrameLayout>? = null
94
+ private var handleView: View? = null
95
+ var onSheetHiddenByPan: (() -> Unit)? = null
96
+
97
+ /** The view that enter / exit animations move. */
98
+ val animatedView: View get() = sheetView ?: contentView
99
+
100
+ private val touchDispatcher = JSTouchDispatcher(this)
101
+ private val pointerDispatcher = if (ReactFeatureFlags.dispatchPointerEvents) {
102
+ JSPointerDispatcher(this)
103
+ } else {
104
+ null
105
+ }
106
+ private var passingThrough = false
107
+ private val hitRect = Rect()
108
+ private val targetViewTag = IntArray(1)
109
+
110
+ init {
111
+ clipChildren = false
112
+ addView(backdropView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
113
+ addView(keyboardLayer, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
114
+ keyboardLayer.addView(contentView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
115
+ installKeyboardTracking()
116
+ backdropView.setOnClickListener {
117
+ if (dismissOnBackdropPress) onRequestDismiss?.invoke("backdrop")
118
+ }
119
+ }
120
+
121
+ fun updateBlocking(value: Boolean) {
122
+ blocking = value
123
+ // A clickable backdrop consumes touches that miss the content.
124
+ backdropView.isClickable = value
125
+ backdropView.importantForAccessibility =
126
+ if (value) IMPORTANT_FOR_ACCESSIBILITY_NO else IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
127
+ }
128
+
129
+ /**
130
+ * Hosts the content in a bottom sheet driven by BottomSheetBehavior, the
131
+ * same drag / nested-scroll mechanics as the previous BottomSheetDialog,
132
+ * but inside this layer instead of a separate window.
133
+ */
134
+ fun configureSheet(
135
+ heightPx: Int,
136
+ cornerRadiusPx: Float,
137
+ backgroundColor: Int,
138
+ showHandle: Boolean,
139
+ draggable: Boolean,
140
+ ) {
141
+ val sheet = sheetView ?: createSheet()
142
+ (sheet.background as? GradientDrawable)?.apply {
143
+ setColor(backgroundColor)
144
+ cornerRadii = floatArrayOf(
145
+ cornerRadiusPx, cornerRadiusPx, cornerRadiusPx, cornerRadiusPx, 0f, 0f, 0f, 0f,
146
+ )
147
+ }
148
+ handleView?.visibility = if (showHandle) VISIBLE else GONE
149
+ sheetBehavior?.apply {
150
+ isDraggable = draggable
151
+ isHideable = draggable
152
+ }
153
+ // The content sits above the navigation bar, as UIKit sheets keep it
154
+ // above the home indicator; the surface still reaches the screen edge.
155
+ sheetBottomPadding = bottomSystemInset()
156
+ val targetHeight = heightPx + sheetBottomPadding
157
+ val params = sheet.layoutParams
158
+ if (params.height != targetHeight) {
159
+ params.height = targetHeight
160
+ sheet.layoutParams = params
161
+ }
162
+ }
163
+
164
+ /** The sheet's full height, navigation bar padding included. */
165
+ fun sheetHeightPx(): Int = sheetView?.layoutParams?.height ?: 0
166
+
167
+ // The entry may not be attached yet when the sheet is configured; the
168
+ // activity's decor view always carries the window insets.
169
+ private fun bottomSystemInset(): Int {
170
+ val root = reactContext.currentActivity?.window?.decorView ?: this
171
+ return ViewCompat.getRootWindowInsets(root)
172
+ ?.getInsets(WindowInsetsCompat.Type.navigationBars())
173
+ ?.bottom ?: 0
174
+ }
175
+
176
+ private fun createSheet(): FrameLayout {
177
+ keyboardLayer.removeView(contentView)
178
+ val coordinator = CoordinatorLayout(reactContext)
179
+ keyboardLayer.addView(coordinator, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
180
+ val sheet = FrameLayout(reactContext).apply {
181
+ background = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE }
182
+ clipToOutline = true
183
+ isClickable = true
184
+ }
185
+ sheet.addView(contentView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
186
+ val density = resources.displayMetrics.density
187
+ val handle = View(reactContext).apply {
188
+ background = GradientDrawable().apply {
189
+ cornerRadius = 2.5f * density
190
+ setColor(0x33000000)
191
+ }
192
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
193
+ }
194
+ sheet.addView(
195
+ handle,
196
+ LayoutParams((36 * density).toInt(), (5 * density).toInt()).apply {
197
+ gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
198
+ topMargin = (8 * density).toInt()
199
+ },
200
+ )
201
+ val behavior = BottomSheetBehavior<FrameLayout>().apply {
202
+ skipCollapsed = true
203
+ isFitToContents = true
204
+ state = BottomSheetBehavior.STATE_EXPANDED
205
+ addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
206
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
207
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) onSheetHiddenByPan?.invoke()
208
+ }
209
+
210
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
211
+ // slideOffset runs from 0 (expanded) to -1 (hidden) while dragging down.
212
+ if (slideOffset <= 0f) backdropView.alpha = (1f + slideOffset).coerceIn(0f, 1f)
213
+ }
214
+ })
215
+ }
216
+ coordinator.addView(
217
+ sheet,
218
+ CoordinatorLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
219
+ this.behavior = behavior
220
+ },
221
+ )
222
+ sheetView = sheet
223
+ sheetBehavior = behavior
224
+ handleView = handle
225
+ return sheet
226
+ }
227
+
228
+ /** Union of the rendered React children; the React root fills the window. */
229
+ fun contentExtent(): Rect {
230
+ sheetView?.let { sheet ->
231
+ val top = height - sheet.layoutParams.height
232
+ return Rect(0, top, width, height)
233
+ }
234
+ val union = Rect()
235
+ for (i in 0 until contentView.childCount) {
236
+ val root = contentView.getChildAt(i) as? ViewGroup ?: continue
237
+ for (j in 0 until root.childCount) {
238
+ val child = root.getChildAt(j)
239
+ if (child.visibility != View.VISIBLE) continue
240
+ child.getHitRect(hitRect)
241
+ hitRect.offset(root.left, root.top)
242
+ union.union(hitRect)
243
+ }
244
+ }
245
+ if (union.isEmpty) union.set(0, 0, width, height)
246
+ return union
247
+ }
248
+
249
+ // region Keyboard
250
+
251
+ /**
252
+ * IME insets arrive through this entry: applied insets for the resting
253
+ * state, the insets animation for the frames in between. Nothing is
254
+ * consumed, so the React content still sees them.
255
+ */
256
+ private fun installKeyboardTracking() {
257
+ ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
258
+ readInsets(insets)
259
+ if (!keyboardAnimating) {
260
+ updateKeyboardShift()
261
+ onKeyboardShiftSettled?.invoke()
262
+ }
263
+ insets
264
+ }
265
+ ViewCompat.setWindowInsetsAnimationCallback(
266
+ this,
267
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
268
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
269
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) keyboardAnimating = true
270
+ }
271
+
272
+ override fun onProgress(
273
+ insets: WindowInsetsCompat,
274
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>,
275
+ ): WindowInsetsCompat {
276
+ if (keyboardAnimating) {
277
+ readInsets(insets)
278
+ updateKeyboardShift()
279
+ }
280
+ return insets
281
+ }
282
+
283
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
284
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() == 0) return
285
+ keyboardAnimating = false
286
+ ViewCompat.getRootWindowInsets(this@NativeOverlayEntryRootView)?.let(::readInsets)
287
+ updateKeyboardShift()
288
+ onKeyboardShiftSettled?.invoke()
289
+ }
290
+ },
291
+ )
292
+ }
293
+
294
+ private fun readInsets(insets: WindowInsetsCompat) {
295
+ imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
296
+ safeTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
297
+ }
298
+
299
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
300
+ super.onLayout(changed, l, t, r, b)
301
+ if (changed) updateKeyboardShift()
302
+ }
303
+
304
+ /**
305
+ * Lifts the content above the IME, the same rule as iOS: a sheet until its
306
+ * content bottom clears the keyboard, other content until its bottom is
307
+ * 16dp above it, never above the status bar; full-window content stays.
308
+ */
309
+ fun updateKeyboardShift() {
310
+ val shift = if (imeBottom <= 0 || !isAttachedToWindow || height == 0) {
311
+ 0f
312
+ } else {
313
+ val location = IntArray(2)
314
+ getLocationInWindow(location)
315
+ val keyboardTop = (rootView.height - imeBottom - location[1]).toFloat()
316
+ val sheet = sheetView
317
+ val needed: Float
318
+ val room: Float
319
+ if (sheet != null) {
320
+ val sheetTop = (height - sheet.layoutParams.height).toFloat()
321
+ needed = (height - sheetBottomPadding) - keyboardTop
322
+ room = sheetTop - safeTop
323
+ } else {
324
+ val extent = contentExtent()
325
+ needed = extent.bottom + KEYBOARD_MARGIN_DP * resources.displayMetrics.density - keyboardTop
326
+ room = (extent.top - safeTop).toFloat()
327
+ }
328
+ maxOf(0f, minOf(needed, room))
329
+ }
330
+ if (keyboardLayer.translationY != -shift) keyboardLayer.translationY = -shift
331
+ }
332
+
333
+ // endregion
334
+
335
+ /**
336
+ * Hit-tests the React content the way the JS touch dispatcher would, so
337
+ * `box-none` wrappers (a full-window toaster, say) let touches through.
338
+ */
339
+ private fun hitsContent(event: MotionEvent): Boolean {
340
+ val x = event.x - keyboardLayer.translationX - contentView.left - contentView.translationX
341
+ val y = event.y - keyboardLayer.translationY - contentView.top - contentView.translationY
342
+ for (i in contentView.childCount - 1 downTo 0) {
343
+ val root = contentView.getChildAt(i) as? ViewGroup ?: continue
344
+ if (root.visibility != View.VISIBLE) continue
345
+ targetViewTag[0] = View.NO_ID
346
+ TouchTargetHelper.findTargetTagForTouch(x - root.left, y - root.top, root, targetViewTag)
347
+ if (targetViewTag[0] != View.NO_ID) return true
348
+ }
349
+ return false
350
+ }
351
+
352
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
353
+ if (!isShownForInput) return false
354
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
355
+ // Non-blocking overlays (toasts) only take touches that land on content.
356
+ passingThrough = !blocking && !hitsContent(event)
357
+ }
358
+ if (passingThrough) return false
359
+ return super.dispatchTouchEvent(event)
360
+ }
361
+
362
+ override fun handleException(t: Throwable) {
363
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
364
+ }
365
+
366
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
367
+ if (!dispatchesJsTouches) return super.onInterceptTouchEvent(event)
368
+ eventDispatcher?.let { dispatcher ->
369
+ touchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
370
+ pointerDispatcher?.handleMotionEvent(event, dispatcher, true)
371
+ }
372
+ return super.onInterceptTouchEvent(event)
373
+ }
374
+
375
+ @SuppressLint("ClickableViewAccessibility")
376
+ override fun onTouchEvent(event: MotionEvent): Boolean {
377
+ if (!dispatchesJsTouches) {
378
+ super.onTouchEvent(event)
379
+ return blocking
380
+ }
381
+ eventDispatcher?.let { dispatcher ->
382
+ touchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
383
+ pointerDispatcher?.handleMotionEvent(event, dispatcher, false)
384
+ }
385
+ super.onTouchEvent(event)
386
+ return true
387
+ }
388
+
389
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
390
+ eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, true) }
391
+ return super.onInterceptHoverEvent(event)
392
+ }
393
+
394
+ override fun onHoverEvent(event: MotionEvent): Boolean {
395
+ eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, false) }
396
+ return super.onHoverEvent(event)
397
+ }
398
+
399
+ @OptIn(UnstableReactNativeAPI::class)
400
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
401
+ eventDispatcher?.let { dispatcher ->
402
+ touchDispatcher.onChildStartedNativeGesture(ev, dispatcher, reactContext)
403
+ pointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
404
+ }
405
+ }
406
+
407
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
408
+ eventDispatcher?.let { touchDispatcher.onChildEndedNativeGesture(ev, it) }
409
+ pointerDispatcher?.onChildEndedNativeGesture()
410
+ }
411
+
412
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) = Unit
413
+ }
414
+
415
+ private const val KEYBOARD_MARGIN_DP = 16f
@@ -0,0 +1,38 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ internal class NativeOverlayPresentedEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val stackOrder: Int,
11
+ ) : Event<NativeOverlayPresentedEvent>(surfaceId, viewTag) {
12
+ override fun getEventName(): String = "topPresented"
13
+
14
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
15
+ putInt("stackOrder", stackOrder)
16
+ }
17
+ }
18
+
19
+ internal class NativeOverlayReasonEvent(
20
+ surfaceId: Int,
21
+ viewTag: Int,
22
+ private val name: String,
23
+ private val reason: String,
24
+ ) : Event<NativeOverlayReasonEvent>(surfaceId, viewTag) {
25
+ override fun getEventName(): String = name
26
+
27
+ // Every request / dismissal must reach JS; never coalesce them.
28
+ override fun canCoalesce(): Boolean = false
29
+
30
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
31
+ putString("reason", reason)
32
+ }
33
+
34
+ companion object {
35
+ const val DISMISSED = "topDismissed"
36
+ const val REQUEST_DISMISS = "topRequestDismiss"
37
+ }
38
+ }
@@ -0,0 +1,174 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import android.app.Activity
4
+ import android.view.KeyEvent
5
+ import android.view.View
6
+ import android.view.ViewGroup
7
+ import android.view.Window
8
+ import android.widget.FrameLayout
9
+ import androidx.activity.ComponentActivity
10
+ import androidx.activity.OnBackPressedCallback
11
+ import java.util.WeakHashMap
12
+
13
+ /** Mirrors `OVERLAY_LEVEL_ORDER`. */
14
+ internal enum class NativeOverlayLevel(val order: Int) {
15
+ MODAL(100),
16
+ HARDWARE(200),
17
+ SECURE(300),
18
+ TOAST(400),
19
+ LOCK(500),
20
+ DEBUG(900);
21
+
22
+ companion object {
23
+ fun from(value: String?): NativeOverlayLevel =
24
+ entries.firstOrNull { it.name.equals(value, ignoreCase = true) } ?: MODAL
25
+ }
26
+ }
27
+
28
+ private class BackInterceptingCallback(
29
+ private val delegate: Window.Callback,
30
+ private val host: NativeOverlayHost,
31
+ ) : Window.Callback by delegate {
32
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
33
+ if (event.keyCode == KeyEvent.KEYCODE_BACK && host.handleBackKey(event)) return true
34
+ return delegate.dispatchKeyEvent(event)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * One per activity: a FrameLayout added after the ReactRootView inside
40
+ * `android.R.id.content`, with one child container per level. Overlays are
41
+ * views, not windows, so order between levels is plain child order and
42
+ * touches that nothing consumes fall through to the React root below.
43
+ */
44
+ internal class NativeOverlayHost private constructor(
45
+ private val activity: Activity,
46
+ ) : FrameLayout(activity) {
47
+ private val levelContainers = sortedMapOf<NativeOverlayLevel, FrameLayout>(
48
+ compareBy { it.order },
49
+ )
50
+ private val backCallback = object : OnBackPressedCallback(false) {
51
+ override fun handleOnBackPressed() {
52
+ topBlockingEntry()?.let { entry ->
53
+ if (entry.dismissOnBackPress) entry.onRequestDismiss?.invoke("back")
54
+ }
55
+ }
56
+ }
57
+ private val hiddenForAccessibility = mutableMapOf<View, Int>()
58
+
59
+ init {
60
+ clipChildren = false
61
+ }
62
+
63
+ /**
64
+ * Called for KEYCODE_BACK before the activity sees it. ReactActivity's
65
+ * `onBackPressed` forwards to JS BackHandler (react-navigation pops the
66
+ * screen) before the OnBackPressedDispatcher, and the app opts out of
67
+ * predictive back, so the dispatcher alone never runs first.
68
+ */
69
+ fun handleBackKey(event: KeyEvent): Boolean {
70
+ val entry = topBlockingEntry() ?: return false
71
+ if (event.action == KeyEvent.ACTION_UP && !event.isCanceled && entry.dismissOnBackPress) {
72
+ entry.onRequestDismiss?.invoke("back")
73
+ }
74
+ return true
75
+ }
76
+
77
+ internal fun installBackInterceptor() {
78
+ val window = activity.window ?: return
79
+ val current = window.callback ?: return
80
+ if (current !is BackInterceptingCallback) {
81
+ window.callback = BackInterceptingCallback(current, this)
82
+ }
83
+ }
84
+
85
+ fun attach(entry: NativeOverlayEntryRootView, level: NativeOverlayLevel) {
86
+ installBackInterceptor()
87
+ val content = activity.findViewById<ViewGroup>(android.R.id.content) ?: return
88
+ if (parent !== content) {
89
+ (parent as? ViewGroup)?.removeView(this)
90
+ content.addView(this, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
91
+ } else if (content.getChildAt(content.childCount - 1) !== this) {
92
+ // Something (e.g. a dev overlay) was added later; stay on top.
93
+ bringToFront()
94
+ }
95
+ val container = levelContainer(level)
96
+ if (entry.parent !== container) {
97
+ (entry.parent as? ViewGroup)?.removeView(entry)
98
+ val index = (0 until container.childCount).firstOrNull { i ->
99
+ (container.getChildAt(i) as? NativeOverlayEntryRootView)?.let { it.stackOrder > entry.stackOrder } == true
100
+ } ?: container.childCount
101
+ container.addView(entry, index, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
102
+ }
103
+ onEntriesChanged()
104
+ }
105
+
106
+ fun detach(entry: NativeOverlayEntryRootView) {
107
+ (entry.parent as? ViewGroup)?.removeView(entry)
108
+ onEntriesChanged()
109
+ }
110
+
111
+ fun onEntriesChanged() {
112
+ val hasBlocking = topBlockingEntry() != null
113
+ if (hasBlocking && !backCallback.isEnabled) {
114
+ // The most recently added callback wins; re-add so ours runs before
115
+ // ReactActivity's and react-native-screens' callbacks.
116
+ backCallback.remove()
117
+ (activity as? ComponentActivity)?.onBackPressedDispatcher?.addCallback(backCallback)
118
+ }
119
+ backCallback.isEnabled = hasBlocking
120
+ updateAccessibility()
121
+ }
122
+
123
+ private fun levelContainer(level: NativeOverlayLevel): FrameLayout =
124
+ levelContainers.getOrPut(level) {
125
+ val container = FrameLayout(activity).apply { clipChildren = false }
126
+ val index = levelContainers.keys.count { it.order < level.order }
127
+ addView(container, index, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
128
+ container
129
+ }
130
+
131
+ private fun orderedEntries(): List<NativeOverlayEntryRootView> =
132
+ levelContainers.values.flatMap { container ->
133
+ (0 until container.childCount).mapNotNull { container.getChildAt(it) as? NativeOverlayEntryRootView }
134
+ }
135
+
136
+ /** Global overlays win; otherwise the topmost visible page overlay. */
137
+ private fun topBlockingEntry(): NativeOverlayEntryRootView? =
138
+ orderedEntries().lastOrNull { it.blocking && it.isShownForInput }
139
+ ?: NativeOverlayPageHostView.allHosts()
140
+ .filter { it.isAttachedToWindow && it.isShown }
141
+ .flatMap { it.shownEntries() }
142
+ .lastOrNull { it.blocking }
143
+
144
+ /** TalkBack only reads the topmost blocking overlay and what is above it. */
145
+ private fun updateAccessibility() {
146
+ val entries = orderedEntries()
147
+ val topIndex = entries.indexOfLast { it.blocking && it.isShownForInput }
148
+ val content = parent as? ViewGroup
149
+ val below = mutableListOf<View>()
150
+ if (topIndex >= 0) {
151
+ content?.let { group ->
152
+ (0 until group.childCount).map(group::getChildAt).filter { it !== this }.forEach(below::add)
153
+ }
154
+ below.addAll(entries.subList(0, topIndex))
155
+ }
156
+ val restore = hiddenForAccessibility.keys.filter { it !in below }
157
+ restore.forEach { view ->
158
+ view.importantForAccessibility = hiddenForAccessibility.remove(view) ?: IMPORTANT_FOR_ACCESSIBILITY_AUTO
159
+ }
160
+ below.forEach { view ->
161
+ if (view !in hiddenForAccessibility) {
162
+ hiddenForAccessibility[view] = view.importantForAccessibility
163
+ view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
164
+ }
165
+ }
166
+ }
167
+
168
+ companion object {
169
+ private val hosts = WeakHashMap<Activity, NativeOverlayHost>()
170
+
171
+ fun of(activity: Activity): NativeOverlayHost =
172
+ hosts.getOrPut(activity) { NativeOverlayHost(activity) }
173
+ }
174
+ }
@@ -0,0 +1,16 @@
1
+ package com.onekey.nativeoverlay
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class NativeOverlayPackage : ReactPackage {
9
+ override fun createViewManagers(
10
+ reactContext: ReactApplicationContext,
11
+ ): List<ViewManager<*, *>> = listOf(NativeOverlayViewManager(), NativeOverlayPageHostViewManager())
12
+
13
+ override fun createNativeModules(
14
+ reactContext: ReactApplicationContext,
15
+ ): List<NativeModule> = emptyList()
16
+ }