@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.
- package/LICENSE +21 -0
- package/NativeOverlay.podspec +30 -0
- package/README.md +35 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
- package/android/src/main/jni/CMakeLists.txt +44 -0
- package/android/src/main/jni/RNCNativeOverlay.h +19 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
- package/docs/SPEC.md +693 -0
- package/ios/NativeOverlayAnimation.swift +160 -0
- package/ios/NativeOverlayContainerView.swift +688 -0
- package/ios/NativeOverlayKeyboard.swift +88 -0
- package/ios/NativeOverlayPageHostView.swift +98 -0
- package/ios/NativeOverlaySheetController.swift +391 -0
- package/ios/NativeOverlayWindowManager.swift +194 -0
- package/ios/RCTNativeOverlayComponentView.h +11 -0
- package/ios/RCTNativeOverlayComponentView.mm +193 -0
- package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
- package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
- package/lib/module/NativeOverlayNativeComponent.ts +61 -0
- package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/lib/module/OverlayErrors.js +6 -0
- package/lib/module/OverlayErrors.js.map +1 -0
- package/lib/module/OverlayLevelContext.js +27 -0
- package/lib/module/OverlayLevelContext.js.map +1 -0
- package/lib/module/OverlayLevels.js +33 -0
- package/lib/module/OverlayLevels.js.map +1 -0
- package/lib/module/OverlayPageHost.js +24 -0
- package/lib/module/OverlayPageHost.js.map +1 -0
- package/lib/module/OverlayPageHost.web.js +49 -0
- package/lib/module/OverlayPageHost.web.js.map +1 -0
- package/lib/module/OverlayPageScope.js +41 -0
- package/lib/module/OverlayPageScope.js.map +1 -0
- package/lib/module/OverlayStore.js +305 -0
- package/lib/module/OverlayStore.js.map +1 -0
- package/lib/module/OverlayView.js +141 -0
- package/lib/module/OverlayView.js.map +1 -0
- package/lib/module/OverlayView.web.js +290 -0
- package/lib/module/OverlayView.web.js.map +1 -0
- package/lib/module/OverlayViewTypes.js +4 -0
- package/lib/module/OverlayViewTypes.js.map +1 -0
- package/lib/module/animation/motionPresets.js +57 -0
- package/lib/module/animation/motionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.js +4 -0
- package/lib/module/animation/platformMotionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.web.js +4 -0
- package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
- package/lib/module/animation/resolveAnimation.js +80 -0
- package/lib/module/animation/resolveAnimation.js.map +1 -0
- package/lib/module/animation/spring.js +69 -0
- package/lib/module/animation/spring.js.map +1 -0
- package/lib/module/animation/types.js +2 -0
- package/lib/module/animation/types.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useOverlayController.js +127 -0
- package/lib/module/useOverlayController.js.map +1 -0
- package/lib/module/useSuspendedPageOwners.js +16 -0
- package/lib/module/useSuspendedPageOwners.js.map +1 -0
- package/lib/module/web/animateTransition.js +115 -0
- package/lib/module/web/animateTransition.js.map +1 -0
- package/lib/module/web/overlayLayers.js +147 -0
- package/lib/module/web/overlayLayers.js.map +1 -0
- package/lib/module/web/useKeyboardShift.js +110 -0
- package/lib/module/web/useKeyboardShift.js.map +1 -0
- package/lib/module/web/useSheetDrag.js +126 -0
- package/lib/module/web/useSheetDrag.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/OverlayErrors.d.ts +4 -0
- package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevels.d.ts +13 -0
- package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
- package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
- package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
- package/lib/typescript/src/OverlayStore.d.ts +72 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.d.ts +3 -0
- package/lib/typescript/src/OverlayView.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.web.d.ts +3 -0
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
- package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
- package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
- package/lib/typescript/src/animation/spring.d.ts +24 -0
- package/lib/typescript/src/animation/spring.d.ts.map +1 -0
- package/lib/typescript/src/animation/types.d.ts +56 -0
- package/lib/typescript/src/animation/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useOverlayController.d.ts +29 -0
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
- package/lib/typescript/src/web/animateTransition.d.ts +18 -0
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
- package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
- package/package.json +188 -0
- package/react-native.config.js +16 -0
- package/src/NativeOverlayNativeComponent.ts +61 -0
- package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/src/OverlayErrors.ts +3 -0
- package/src/OverlayLevelContext.tsx +32 -0
- package/src/OverlayLevels.ts +40 -0
- package/src/OverlayPageHost.tsx +21 -0
- package/src/OverlayPageHost.web.tsx +53 -0
- package/src/OverlayPageScope.tsx +42 -0
- package/src/OverlayStore.ts +401 -0
- package/src/OverlayView.tsx +165 -0
- package/src/OverlayView.web.tsx +356 -0
- package/src/OverlayViewTypes.ts +81 -0
- package/src/animation/motionPresets.ts +52 -0
- package/src/animation/platformMotionPresets.ts +1 -0
- package/src/animation/platformMotionPresets.web.ts +1 -0
- package/src/animation/resolveAnimation.ts +103 -0
- package/src/animation/spring.ts +80 -0
- package/src/animation/types.ts +71 -0
- package/src/index.ts +55 -0
- package/src/types.ts +79 -0
- package/src/useOverlayController.ts +199 -0
- package/src/useSuspendedPageOwners.ts +26 -0
- package/src/web/animateTransition.ts +137 -0
- package/src/web/overlayLayers.ts +182 -0
- package/src/web/useKeyboardShift.ts +130 -0
- 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
|
+
}
|