react-native-nitro-modal 0.1.0
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 +20 -0
- package/NitroModal.podspec +29 -0
- package/README.md +39 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +284 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +312 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +50 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +115 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +253 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +52 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalPackage.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalViewManager.kt +67 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +156 -0
- package/android/src/main/res/values/styles.xml +17 -0
- package/ios/BackdropView.swift +64 -0
- package/ios/HybridNitroModal.swift +329 -0
- package/ios/HybridNitroModalComponent+Children.mm +77 -0
- package/ios/ModalConfig.swift +73 -0
- package/ios/NitroModalHostView.swift +91 -0
- package/ios/PopupViewController.swift +223 -0
- package/ios/SheetViewController.swift +244 -0
- package/lib/module/NitroModal.js +188 -0
- package/lib/module/NitroModal.js.map +1 -0
- package/lib/module/NitroModal.nitro.js +4 -0
- package/lib/module/NitroModal.nitro.js.map +1 -0
- package/lib/module/NitroModalView.js +17 -0
- package/lib/module/NitroModalView.js.map +1 -0
- package/lib/module/NitroModalView.native.js +6 -0
- package/lib/module/NitroModalView.native.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroModal.d.ts +59 -0
- package/lib/typescript/src/NitroModal.d.ts.map +1 -0
- package/lib/typescript/src/NitroModal.nitro.d.ts +78 -0
- package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroModalView.d.ts +4 -0
- package/lib/typescript/src/NitroModalView.d.ts.map +1 -0
- package/lib/typescript/src/NitroModalView.native.d.ts +3 -0
- package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JDismissReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_DismissReason.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_ModalContentArea.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +344 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +106 -0
- package/nitrogen/generated/android/c++/JKeyboardBehavior.hpp +61 -0
- package/nitrogen/generated/android/c++/JModalContentArea.hpp +61 -0
- package/nitrogen/generated/android/c++/JModalMode.hpp +58 -0
- package/nitrogen/generated/android/c++/JPopupAnimation.hpp +61 -0
- package/nitrogen/generated/android/c++/JSheetDetent.hpp +64 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +174 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.hpp +54 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/DismissReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_DismissReason.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_ModalContentArea.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +237 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/KeyboardBehavior.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalContentArea.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalMode.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/PopupAnimation.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/nitromodalOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalManager.kt +96 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalStateUpdater.kt +23 -0
- package/nitrogen/generated/android/nitromodal+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitromodal+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitromodalOnLoad.cpp +64 -0
- package/nitrogen/generated/android/nitromodalOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroModal+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.cpp +65 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +237 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +64 -0
- package/nitrogen/generated/ios/NitroModalAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroModalAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +247 -0
- package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +264 -0
- package/nitrogen/generated/ios/swift/DismissReason.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_DismissReason.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ModalContentArea.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +77 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +554 -0
- package/nitrogen/generated/ios/swift/KeyboardBehavior.swift +44 -0
- package/nitrogen/generated/ios/swift/ModalContentArea.swift +34 -0
- package/nitrogen/generated/ios/swift/ModalMode.swift +40 -0
- package/nitrogen/generated/ios/swift/PopupAnimation.swift +44 -0
- package/nitrogen/generated/ios/swift/SheetDetent.swift +48 -0
- package/nitrogen/generated/shared/c++/DismissReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +65 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +124 -0
- package/nitrogen/generated/shared/c++/KeyboardBehavior.hpp +80 -0
- package/nitrogen/generated/shared/c++/ModalContentArea.hpp +87 -0
- package/nitrogen/generated/shared/c++/ModalMode.hpp +76 -0
- package/nitrogen/generated/shared/c++/PopupAnimation.hpp +80 -0
- package/nitrogen/generated/shared/c++/SheetDetent.hpp +84 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +74 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +151 -0
- package/nitrogen/generated/shared/json/NitroModalConfig.json +30 -0
- package/package.json +183 -0
- package/src/NitroModal.nitro.ts +92 -0
- package/src/NitroModal.tsx +292 -0
- package/src/NitroModalView.native.tsx +8 -0
- package/src/NitroModalView.tsx +24 -0
- package/src/index.tsx +12 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromodal
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.view.WindowManager
|
|
6
|
+
import androidx.core.graphics.Insets
|
|
7
|
+
import androidx.core.view.ViewCompat
|
|
8
|
+
import androidx.core.view.WindowInsetsCompat
|
|
9
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
10
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The Nitro Hybrid View. Props arrive on the UI thread (Fabric `updateState`);
|
|
15
|
+
* methods arrive on the JS thread and hop to the UI thread.
|
|
16
|
+
*
|
|
17
|
+
* Presentation is a small state machine so that rapid `isOpen` toggles,
|
|
18
|
+
* native dismissals and unmounts always settle on a consistent state, and
|
|
19
|
+
* every accepted present request ends with exactly one `onDismiss`.
|
|
20
|
+
*/
|
|
21
|
+
@DoNotStrip
|
|
22
|
+
class HybridNitroModal(private val reactContext: ThemedReactContext) :
|
|
23
|
+
HybridNitroModalSpec(), LifecycleEventListener, ModalPresenter.Listener {
|
|
24
|
+
|
|
25
|
+
override val view = NitroModalHostView(reactContext)
|
|
26
|
+
|
|
27
|
+
// region Props
|
|
28
|
+
|
|
29
|
+
override var isOpen: Boolean = false
|
|
30
|
+
set(value) {
|
|
31
|
+
if (field == value) return
|
|
32
|
+
field = value
|
|
33
|
+
if (value) requestPresent() else requestDismiss(DismissReason.PROGRAMMATIC)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override var mode: ModalMode = ModalMode.BOTTOMSHEET
|
|
37
|
+
override var detents: Array<SheetDetent> = arrayOf(SheetDetent.FITCONTENT)
|
|
38
|
+
override var initialDetentIndex: Double = 0.0
|
|
39
|
+
override var backdropColor: Double = 0xFF000000.toDouble()
|
|
40
|
+
override var backdropOpacity: Double = 0.4
|
|
41
|
+
override var backdropBlurRadius: Double = 0.0
|
|
42
|
+
override var dismissOnBackdropPress: Boolean = true
|
|
43
|
+
override var dismissOnSwipe: Boolean = true
|
|
44
|
+
override var dismissOnBackButton: Boolean = true
|
|
45
|
+
override var grabberVisible: Boolean = false
|
|
46
|
+
override var cornerRadius: Double = -1.0
|
|
47
|
+
override var contentBackgroundColor: Double? = null
|
|
48
|
+
override var keyboardBehavior: KeyboardBehavior = KeyboardBehavior.PAN
|
|
49
|
+
override var popupAnimation: PopupAnimation = PopupAnimation.SCALE
|
|
50
|
+
|
|
51
|
+
override var onPresent: (() -> Unit)? = null
|
|
52
|
+
override var onDismiss: ((reason: DismissReason) -> Unit)? = null
|
|
53
|
+
override var onDetentChange: ((index: Double) -> Unit)? = null
|
|
54
|
+
override var onBackdropPress: (() -> Unit)? = null
|
|
55
|
+
override var onBackButtonPress: (() -> Unit)? = null
|
|
56
|
+
override var onContentAreaChange: ((area: ModalContentArea) -> Unit)? = null
|
|
57
|
+
|
|
58
|
+
// endregion
|
|
59
|
+
|
|
60
|
+
private enum class Phase { IDLE, PRESENTING, PRESENTED, DISMISSING }
|
|
61
|
+
|
|
62
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
63
|
+
private var config = ModalConfig()
|
|
64
|
+
private var phase = Phase.IDLE
|
|
65
|
+
|
|
66
|
+
/** What React (or an imperative call) asked for. */
|
|
67
|
+
private var wantsOpen = false
|
|
68
|
+
|
|
69
|
+
/** A present request was accepted and its `onDismiss` is still owed. */
|
|
70
|
+
private var sessionActive = false
|
|
71
|
+
private var dismissReason = DismissReason.PROGRAMMATIC
|
|
72
|
+
private var reconcileScheduled = false
|
|
73
|
+
private var isDropped = false
|
|
74
|
+
private var presenter: ModalPresenter? = null
|
|
75
|
+
private var lastContentArea: ModalContentArea? = null
|
|
76
|
+
|
|
77
|
+
private val reconcileRunnable = Runnable {
|
|
78
|
+
reconcileScheduled = false
|
|
79
|
+
reconcile()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
init {
|
|
83
|
+
reactContext.addLifecycleEventListener(this)
|
|
84
|
+
view.onAttachedChange = { attached ->
|
|
85
|
+
if (attached) {
|
|
86
|
+
if (presenter == null) reportEstimatedContentArea()
|
|
87
|
+
scheduleReconcile()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
view.contentRoot.onContentSizeChange = { presenter?.onContentSizeChanged() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// region HybridView
|
|
94
|
+
|
|
95
|
+
override fun afterUpdate() {
|
|
96
|
+
val next = makeConfig()
|
|
97
|
+
if (next == config) return
|
|
98
|
+
config = next
|
|
99
|
+
presenter?.update(next) ?: reportEstimatedContentArea()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun onDropView() {
|
|
103
|
+
isDropped = true
|
|
104
|
+
wantsOpen = false
|
|
105
|
+
sessionActive = false
|
|
106
|
+
mainHandler.removeCallbacksAndMessages(null)
|
|
107
|
+
reactContext.removeLifecycleEventListener(this)
|
|
108
|
+
teardownPresenter()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// endregion
|
|
112
|
+
|
|
113
|
+
// region Methods (JS thread)
|
|
114
|
+
|
|
115
|
+
override fun present() {
|
|
116
|
+
mainHandler.post { requestPresent() }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
override fun dismiss() {
|
|
120
|
+
mainHandler.post { requestDismiss(DismissReason.PROGRAMMATIC) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override fun snapToDetent(index: Double) {
|
|
124
|
+
if (index.isNaN()) return
|
|
125
|
+
mainHandler.post { presenter?.snapToDetent(index.toInt()) }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// endregion
|
|
129
|
+
|
|
130
|
+
// region State machine
|
|
131
|
+
|
|
132
|
+
private fun requestPresent() {
|
|
133
|
+
if (isDropped) return
|
|
134
|
+
wantsOpen = true
|
|
135
|
+
sessionActive = true
|
|
136
|
+
scheduleReconcile()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun requestDismiss(reason: DismissReason) {
|
|
140
|
+
if (wantsOpen) dismissReason = reason
|
|
141
|
+
wantsOpen = false
|
|
142
|
+
scheduleReconcile()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Posted so children and layout from the same Fabric batch are mounted first. */
|
|
146
|
+
private fun scheduleReconcile() {
|
|
147
|
+
if (reconcileScheduled) return
|
|
148
|
+
reconcileScheduled = true
|
|
149
|
+
mainHandler.post(reconcileRunnable)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun reconcile() {
|
|
153
|
+
when (phase) {
|
|
154
|
+
Phase.PRESENTING, Phase.DISMISSING -> Unit // The transition reconciles when done.
|
|
155
|
+
Phase.PRESENTED -> if (!wantsOpen) startDismiss()
|
|
156
|
+
Phase.IDLE -> when {
|
|
157
|
+
wantsOpen -> startPresent()
|
|
158
|
+
sessionActive -> endSession() // Cancelled before anything appeared.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun startPresent() {
|
|
164
|
+
// Without an attached host or activity there is nothing to present over;
|
|
165
|
+
// attaching / resuming retries.
|
|
166
|
+
if (!view.isAttachedToWindow) return
|
|
167
|
+
val activity = reactContext.currentActivity ?: return
|
|
168
|
+
if (activity.isFinishing || activity.isDestroyed) return
|
|
169
|
+
|
|
170
|
+
val next = when (config.mode) {
|
|
171
|
+
ModalMode.POPUP -> PopupPresenter(activity, reactContext, view.contentRoot, config, this)
|
|
172
|
+
ModalMode.BOTTOMSHEET -> BottomSheetPresenter(activity, reactContext, view.contentRoot, config, this)
|
|
173
|
+
}
|
|
174
|
+
presenter = next
|
|
175
|
+
phase = Phase.PRESENTING
|
|
176
|
+
try {
|
|
177
|
+
next.show()
|
|
178
|
+
} catch (e: WindowManager.BadTokenException) {
|
|
179
|
+
// The activity window isn't ready; retry on the next resume.
|
|
180
|
+
next.dismissImmediately()
|
|
181
|
+
presenter = null
|
|
182
|
+
phase = Phase.IDLE
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun startDismiss() {
|
|
187
|
+
val current = presenter ?: return finishDismiss()
|
|
188
|
+
phase = Phase.DISMISSING
|
|
189
|
+
current.dismiss(dismissReason)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun finishDismiss() {
|
|
193
|
+
phase = Phase.IDLE
|
|
194
|
+
presenter = null
|
|
195
|
+
endSession()
|
|
196
|
+
reconcile()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun endSession() {
|
|
200
|
+
if (!sessionActive) return
|
|
201
|
+
val reason = dismissReason
|
|
202
|
+
dismissReason = DismissReason.PROGRAMMATIC
|
|
203
|
+
// A present requested while we were dismissing starts a new session.
|
|
204
|
+
sessionActive = wantsOpen
|
|
205
|
+
if (!isDropped) onDismiss?.invoke(reason)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun teardownPresenter() {
|
|
209
|
+
presenter?.dismissImmediately()
|
|
210
|
+
presenter = null
|
|
211
|
+
phase = Phase.IDLE
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// endregion
|
|
215
|
+
|
|
216
|
+
// region ModalPresenter.Listener
|
|
217
|
+
|
|
218
|
+
override fun onPresented() {
|
|
219
|
+
if (phase != Phase.PRESENTING) return
|
|
220
|
+
phase = Phase.PRESENTED
|
|
221
|
+
if (!isDropped) onPresent?.invoke()
|
|
222
|
+
reconcile()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override fun onDismissed(reason: DismissReason) {
|
|
226
|
+
if (phase != Phase.DISMISSING) {
|
|
227
|
+
// Dismissed natively (swipe) rather than by us.
|
|
228
|
+
wantsOpen = false
|
|
229
|
+
dismissReason = reason
|
|
230
|
+
}
|
|
231
|
+
finishDismiss()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun onBackdropPress() {
|
|
235
|
+
onBackdropPress?.invoke()
|
|
236
|
+
if (!config.dismissOnBackdropPress) return
|
|
237
|
+
requestDismiss(DismissReason.BACKDROP)
|
|
238
|
+
reconcile()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
override fun onBackPress() {
|
|
242
|
+
onBackButtonPress?.invoke()
|
|
243
|
+
if (!config.dismissOnBackButton) return
|
|
244
|
+
requestDismiss(DismissReason.BACKBUTTON)
|
|
245
|
+
reconcile()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override fun onDetentChange(index: Int) {
|
|
249
|
+
onDetentChange?.invoke(index.toDouble())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onContentAreaChange(width: Int, height: Int) {
|
|
253
|
+
val area = ModalContentArea(
|
|
254
|
+
kotlin.math.floor(reactContext.pxToDp(width)),
|
|
255
|
+
kotlin.math.floor(reactContext.pxToDp(height)),
|
|
256
|
+
)
|
|
257
|
+
if (area == lastContentArea || area.width <= 0 || area.height <= 0) return
|
|
258
|
+
lastContentArea = area
|
|
259
|
+
onContentAreaChange?.invoke(area)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// endregion
|
|
263
|
+
|
|
264
|
+
// region LifecycleEventListener
|
|
265
|
+
|
|
266
|
+
override fun onHostResume() = scheduleReconcile()
|
|
267
|
+
|
|
268
|
+
override fun onHostPause() = Unit
|
|
269
|
+
|
|
270
|
+
override fun onHostDestroy() {
|
|
271
|
+
// The activity is going away; its windows go with it.
|
|
272
|
+
teardownPresenter()
|
|
273
|
+
if (sessionActive) {
|
|
274
|
+
wantsOpen = false
|
|
275
|
+
endSession()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// endregion
|
|
280
|
+
|
|
281
|
+
private fun makeConfig() = ModalConfig(
|
|
282
|
+
mode = mode,
|
|
283
|
+
detents = detents.toList(),
|
|
284
|
+
initialDetentIndex = if (initialDetentIndex.isNaN()) 0 else initialDetentIndex.toInt(),
|
|
285
|
+
backdropColor = backdropColor.toProcessedColor(),
|
|
286
|
+
backdropOpacity = backdropOpacity.toFloat(),
|
|
287
|
+
backdropBlurRadius = backdropBlurRadius.toFloat(),
|
|
288
|
+
dismissOnBackdropPress = dismissOnBackdropPress,
|
|
289
|
+
dismissOnSwipe = dismissOnSwipe,
|
|
290
|
+
dismissOnBackButton = dismissOnBackButton,
|
|
291
|
+
grabberVisible = grabberVisible,
|
|
292
|
+
cornerRadius = cornerRadius.takeIf { it >= 0 }?.toFloat(),
|
|
293
|
+
contentBackgroundColor = contentBackgroundColor?.toProcessedColor(),
|
|
294
|
+
keyboardBehavior = keyboardBehavior,
|
|
295
|
+
popupAnimation = popupAnimation,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
/** Lets React size the content before the first presentation. */
|
|
299
|
+
private fun reportEstimatedContentArea() {
|
|
300
|
+
val activity = reactContext.currentActivity ?: return
|
|
301
|
+
val decor = activity.window?.decorView ?: return
|
|
302
|
+
if (decor.width == 0 || decor.height == 0) return
|
|
303
|
+
val insets = ViewCompat.getRootWindowInsets(decor)
|
|
304
|
+
?.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
305
|
+
?: Insets.NONE
|
|
306
|
+
val (width, height) = when (config.mode) {
|
|
307
|
+
ModalMode.POPUP -> PopupPresenter.estimateContentArea(activity, decor.width, decor.height, insets)
|
|
308
|
+
ModalMode.BOTTOMSHEET -> BottomSheetPresenter.estimateContentArea(activity, config.detents, decor.width, decor.height, insets)
|
|
309
|
+
}
|
|
310
|
+
onContentAreaChange(width, height)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromodal
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
|
|
7
|
+
/** Snapshot of the React props that shape a presentation. */
|
|
8
|
+
internal data class ModalConfig(
|
|
9
|
+
val mode: ModalMode = ModalMode.BOTTOMSHEET,
|
|
10
|
+
val detents: List<SheetDetent> = listOf(SheetDetent.FITCONTENT),
|
|
11
|
+
val initialDetentIndex: Int = 0,
|
|
12
|
+
val backdropColor: Int = Color.BLACK,
|
|
13
|
+
val backdropOpacity: Float = 0.4f,
|
|
14
|
+
/** dp, 0 disables the blur. */
|
|
15
|
+
val backdropBlurRadius: Float = 0f,
|
|
16
|
+
val dismissOnBackdropPress: Boolean = true,
|
|
17
|
+
val dismissOnSwipe: Boolean = true,
|
|
18
|
+
val dismissOnBackButton: Boolean = true,
|
|
19
|
+
val grabberVisible: Boolean = false,
|
|
20
|
+
/** dp, `null` uses the platform default. */
|
|
21
|
+
val cornerRadius: Float? = null,
|
|
22
|
+
/** `null` uses the system surface color. */
|
|
23
|
+
val contentBackgroundColor: Int? = null,
|
|
24
|
+
val keyboardBehavior: KeyboardBehavior = KeyboardBehavior.PAN,
|
|
25
|
+
val popupAnimation: PopupAnimation = PopupAnimation.SCALE,
|
|
26
|
+
) {
|
|
27
|
+
val clampedInitialDetentIndex: Int
|
|
28
|
+
get() = initialDetentIndex.coerceIn(0, (detents.size - 1).coerceAtLeast(0))
|
|
29
|
+
|
|
30
|
+
/** Backdrop color with `backdropOpacity` folded into its alpha. */
|
|
31
|
+
val effectiveBackdropColor: Int
|
|
32
|
+
get() {
|
|
33
|
+
val alpha = (Color.alpha(backdropColor) * backdropOpacity.coerceIn(0f, 1f)).toInt()
|
|
34
|
+
return Color.argb(alpha, Color.red(backdropColor), Color.green(backdropColor), Color.blue(backdropColor))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun surfaceColor(context: Context): Int {
|
|
38
|
+
contentBackgroundColor?.let { return it }
|
|
39
|
+
val night = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
|
40
|
+
Configuration.UI_MODE_NIGHT_YES
|
|
41
|
+
return if (night) Color.rgb(0x1C, 0x1B, 0x1F) else Color.WHITE
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Decodes a color produced by React Native's `processColor` (ARGB int). */
|
|
46
|
+
internal fun Double.toProcessedColor(): Int = toLong().toInt()
|
|
47
|
+
|
|
48
|
+
internal fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density
|
|
49
|
+
|
|
50
|
+
internal fun Context.pxToDp(px: Int): Double = (px / resources.displayMetrics.density).toDouble()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromodal
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.view.MotionEvent
|
|
5
|
+
import android.view.View
|
|
6
|
+
import com.facebook.react.config.ReactFeatureFlags
|
|
7
|
+
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
8
|
+
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
9
|
+
import com.facebook.react.uimanager.RootView
|
|
10
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
12
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Holds the React children inside the modal window.
|
|
16
|
+
*
|
|
17
|
+
* The dialog is a separate window, so touches there never reach the React
|
|
18
|
+
* root view. Like React Native's own `Modal`, this view is a [RootView] that
|
|
19
|
+
* forwards touch and pointer events to JS itself. Children are laid out by
|
|
20
|
+
* Fabric; [ReactViewGroup.onLayout] is a no-op.
|
|
21
|
+
*/
|
|
22
|
+
@SuppressLint("ViewConstructor")
|
|
23
|
+
internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
|
|
24
|
+
ReactViewGroup(reactContext), RootView {
|
|
25
|
+
|
|
26
|
+
var eventDispatcher: EventDispatcher? = null
|
|
27
|
+
|
|
28
|
+
/** Called when a child's laid-out bounds change. */
|
|
29
|
+
var onContentSizeChange: (() -> Unit)? = null
|
|
30
|
+
|
|
31
|
+
private val touchDispatcher = JSTouchDispatcher(this)
|
|
32
|
+
private val pointerDispatcher: JSPointerDispatcher? =
|
|
33
|
+
if (ReactFeatureFlags.dispatchPointerEvents) JSPointerDispatcher(this) else null
|
|
34
|
+
|
|
35
|
+
private val childLayoutListener =
|
|
36
|
+
OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
|
37
|
+
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
|
38
|
+
onContentSizeChange?.invoke()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bounding size of the React children, in px. */
|
|
43
|
+
val contentWidth: Int
|
|
44
|
+
get() = (0 until childCount).maxOfOrNull { getChildAt(it).right } ?: 0
|
|
45
|
+
|
|
46
|
+
val contentHeight: Int
|
|
47
|
+
get() = (0 until childCount).maxOfOrNull { getChildAt(it).bottom } ?: 0
|
|
48
|
+
|
|
49
|
+
override fun onViewAdded(child: View) {
|
|
50
|
+
super.onViewAdded(child)
|
|
51
|
+
child.addOnLayoutChangeListener(childLayoutListener)
|
|
52
|
+
onContentSizeChange?.invoke()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun onViewRemoved(child: View) {
|
|
56
|
+
super.onViewRemoved(child)
|
|
57
|
+
child.removeOnLayoutChangeListener(childLayoutListener)
|
|
58
|
+
onContentSizeChange?.invoke()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// region Touch forwarding (mirrors ReactModalHostView.DialogRootViewGroup)
|
|
62
|
+
|
|
63
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
64
|
+
eventDispatcher?.let {
|
|
65
|
+
touchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
66
|
+
pointerDispatcher?.handleMotionEvent(event, it, true)
|
|
67
|
+
}
|
|
68
|
+
return super.onInterceptTouchEvent(event)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
72
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
73
|
+
eventDispatcher?.let {
|
|
74
|
+
touchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
75
|
+
pointerDispatcher?.handleMotionEvent(event, it, false)
|
|
76
|
+
}
|
|
77
|
+
super.onTouchEvent(event)
|
|
78
|
+
// Keep receiving the gesture even if no child is interested in it.
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
83
|
+
eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, true) }
|
|
84
|
+
return super.onInterceptHoverEvent(event)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
88
|
+
eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, false) }
|
|
89
|
+
return super.onHoverEvent(event)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
93
|
+
eventDispatcher?.let {
|
|
94
|
+
touchDispatcher.onChildStartedNativeGesture(ev, it, reactContext)
|
|
95
|
+
pointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
100
|
+
eventDispatcher?.let { touchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
101
|
+
pointerDispatcher?.onChildEndedNativeGesture()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun handleException(t: Throwable) {
|
|
105
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
109
|
+
// Keep seeing events in onInterceptTouchEvent, but still let a nested
|
|
110
|
+
// scroller stop the sheet's drag behavior from stealing its gesture.
|
|
111
|
+
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// endregion
|
|
115
|
+
}
|