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.
Files changed (117) hide show
  1. package/LICENSE +20 -0
  2. package/NitroModal.podspec +29 -0
  3. package/README.md +39 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +284 -0
  9. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +312 -0
  10. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +50 -0
  11. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +115 -0
  12. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +253 -0
  13. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +52 -0
  14. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalPackage.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalViewManager.kt +67 -0
  16. package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +156 -0
  17. package/android/src/main/res/values/styles.xml +17 -0
  18. package/ios/BackdropView.swift +64 -0
  19. package/ios/HybridNitroModal.swift +329 -0
  20. package/ios/HybridNitroModalComponent+Children.mm +77 -0
  21. package/ios/ModalConfig.swift +73 -0
  22. package/ios/NitroModalHostView.swift +91 -0
  23. package/ios/PopupViewController.swift +223 -0
  24. package/ios/SheetViewController.swift +244 -0
  25. package/lib/module/NitroModal.js +188 -0
  26. package/lib/module/NitroModal.js.map +1 -0
  27. package/lib/module/NitroModal.nitro.js +4 -0
  28. package/lib/module/NitroModal.nitro.js.map +1 -0
  29. package/lib/module/NitroModalView.js +17 -0
  30. package/lib/module/NitroModalView.js.map +1 -0
  31. package/lib/module/NitroModalView.native.js +6 -0
  32. package/lib/module/NitroModalView.native.js.map +1 -0
  33. package/lib/module/index.js +4 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/typescript/package.json +1 -0
  37. package/lib/typescript/src/NitroModal.d.ts +59 -0
  38. package/lib/typescript/src/NitroModal.d.ts.map +1 -0
  39. package/lib/typescript/src/NitroModal.nitro.d.ts +78 -0
  40. package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -0
  41. package/lib/typescript/src/NitroModalView.d.ts +4 -0
  42. package/lib/typescript/src/NitroModalView.d.ts.map +1 -0
  43. package/lib/typescript/src/NitroModalView.native.d.ts +3 -0
  44. package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +3 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/nitro.json +23 -0
  48. package/nitrogen/generated/android/c++/JDismissReason.hpp +64 -0
  49. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_DismissReason.hpp +77 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_ModalContentArea.hpp +77 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  53. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +344 -0
  54. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +106 -0
  55. package/nitrogen/generated/android/c++/JKeyboardBehavior.hpp +61 -0
  56. package/nitrogen/generated/android/c++/JModalContentArea.hpp +61 -0
  57. package/nitrogen/generated/android/c++/JModalMode.hpp +58 -0
  58. package/nitrogen/generated/android/c++/JPopupAnimation.hpp +61 -0
  59. package/nitrogen/generated/android/c++/JSheetDetent.hpp +64 -0
  60. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +174 -0
  61. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.hpp +54 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/DismissReason.kt +25 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void.kt +78 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_DismissReason.kt +78 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_ModalContentArea.kt +78 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_double.kt +78 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +237 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/KeyboardBehavior.kt +24 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalContentArea.kt +56 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalMode.kt +23 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/PopupAnimation.kt +24 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +25 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/nitromodalOnLoad.kt +35 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalManager.kt +96 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalStateUpdater.kt +23 -0
  76. package/nitrogen/generated/android/nitromodal+autolinking.cmake +83 -0
  77. package/nitrogen/generated/android/nitromodal+autolinking.gradle +27 -0
  78. package/nitrogen/generated/android/nitromodalOnLoad.cpp +64 -0
  79. package/nitrogen/generated/android/nitromodalOnLoad.hpp +34 -0
  80. package/nitrogen/generated/ios/NitroModal+autolinking.rb +62 -0
  81. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.cpp +65 -0
  82. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +237 -0
  83. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +64 -0
  84. package/nitrogen/generated/ios/NitroModalAutolinking.mm +33 -0
  85. package/nitrogen/generated/ios/NitroModalAutolinking.swift +26 -0
  86. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.cpp +11 -0
  87. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +247 -0
  88. package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +264 -0
  89. package/nitrogen/generated/ios/swift/DismissReason.swift +48 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_DismissReason.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_ModalContentArea.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  94. package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +77 -0
  95. package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +554 -0
  96. package/nitrogen/generated/ios/swift/KeyboardBehavior.swift +44 -0
  97. package/nitrogen/generated/ios/swift/ModalContentArea.swift +34 -0
  98. package/nitrogen/generated/ios/swift/ModalMode.swift +40 -0
  99. package/nitrogen/generated/ios/swift/PopupAnimation.swift +44 -0
  100. package/nitrogen/generated/ios/swift/SheetDetent.swift +48 -0
  101. package/nitrogen/generated/shared/c++/DismissReason.hpp +84 -0
  102. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +65 -0
  103. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +124 -0
  104. package/nitrogen/generated/shared/c++/KeyboardBehavior.hpp +80 -0
  105. package/nitrogen/generated/shared/c++/ModalContentArea.hpp +87 -0
  106. package/nitrogen/generated/shared/c++/ModalMode.hpp +76 -0
  107. package/nitrogen/generated/shared/c++/PopupAnimation.hpp +80 -0
  108. package/nitrogen/generated/shared/c++/SheetDetent.hpp +84 -0
  109. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +74 -0
  110. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +151 -0
  111. package/nitrogen/generated/shared/json/NitroModalConfig.json +30 -0
  112. package/package.json +183 -0
  113. package/src/NitroModal.nitro.ts +92 -0
  114. package/src/NitroModal.tsx +292 -0
  115. package/src/NitroModalView.native.tsx +8 -0
  116. package/src/NitroModalView.tsx +24 -0
  117. package/src/index.tsx +12 -0
@@ -0,0 +1,253 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.app.Activity
4
+ import android.os.Build
5
+ import android.view.View
6
+ import android.view.ViewGroup
7
+ import android.view.WindowManager
8
+ import android.view.inputmethod.InputMethodManager
9
+ import androidx.activity.ComponentDialog
10
+ import androidx.activity.OnBackPressedCallback
11
+ import androidx.core.graphics.Insets
12
+ import androidx.core.view.ViewCompat
13
+ import androidx.core.view.WindowCompat
14
+ import androidx.core.view.WindowInsetsAnimationCompat
15
+ import androidx.core.view.WindowInsetsCompat
16
+ import com.facebook.react.uimanager.ThemedReactContext
17
+
18
+ /**
19
+ * Owns one modal window from show to dismiss. Subclasses build the layout and
20
+ * drive the enter/exit animations; this class handles the window, edge-to-edge
21
+ * insets, keyboard tracking, back handling and teardown.
22
+ *
23
+ * Everything here runs on the UI thread.
24
+ */
25
+ internal abstract class ModalPresenter(
26
+ protected val activity: Activity,
27
+ private val reactContext: ThemedReactContext,
28
+ protected val contentRoot: ModalContentRoot,
29
+ config: ModalConfig,
30
+ protected val listener: Listener,
31
+ ) {
32
+ interface Listener {
33
+ fun onPresented()
34
+ /** The window is gone. Called exactly once per presenter. */
35
+ fun onDismissed(reason: DismissReason)
36
+ fun onBackdropPress()
37
+ fun onBackPress()
38
+ fun onDetentChange(index: Int)
39
+ /** Area available to the content, in px. */
40
+ fun onContentAreaChange(width: Int, height: Int)
41
+ }
42
+
43
+ protected var config: ModalConfig = config
44
+ private set
45
+
46
+ protected val dialog = ComponentDialog(activity, R.style.NitroModal_Dialog)
47
+ protected val backdrop = View(activity)
48
+
49
+ /** System bars + display cutout. */
50
+ protected var systemInsets: Insets = Insets.NONE
51
+ private set
52
+
53
+ /** Current keyboard height, updated every frame while it animates. */
54
+ protected var imeHeight: Int = 0
55
+ private set
56
+
57
+ private var imeAnimating = false
58
+ private var presentedNotified = false
59
+ private var finished = false
60
+ private var dismissReason = DismissReason.PROGRAMMATIC
61
+
62
+ protected var isDismissing = false
63
+ private set
64
+
65
+ protected abstract fun createLayout(): ViewGroup
66
+ protected abstract fun animateIn()
67
+ protected abstract fun animateOut(onEnd: () -> Unit)
68
+ protected abstract fun onConfigChanged(previous: ModalConfig)
69
+
70
+ /** System insets or the settled keyboard height changed. */
71
+ protected abstract fun onInsetsChanged()
72
+
73
+ /** Called every frame while the keyboard animates. Must not trigger layout. */
74
+ protected abstract fun onKeyboardProgress()
75
+
76
+ abstract fun onContentSizeChanged()
77
+
78
+ open fun snapToDetent(index: Int) = Unit
79
+
80
+ fun show() {
81
+ (contentRoot.parent as? ViewGroup)?.removeView(contentRoot)
82
+ val root = createLayout()
83
+ backdrop.setBackgroundColor(config.effectiveBackdropColor)
84
+ backdrop.alpha = 0f
85
+ backdrop.setOnClickListener { listener.onBackdropPress() }
86
+
87
+ dialog.setContentView(root, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
88
+ dialog.setCancelable(false)
89
+ dialog.onBackPressedDispatcher.addCallback(dialog, object : OnBackPressedCallback(true) {
90
+ override fun handleOnBackPressed() = listener.onBackPress()
91
+ })
92
+ observeInsets(root)
93
+
94
+ val window = requireNotNull(dialog.window)
95
+ window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
96
+ WindowCompat.setDecorFitsSystemWindows(window, false)
97
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
98
+ window.attributes = window.attributes.apply {
99
+ layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
100
+ }
101
+ }
102
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
103
+ window.isStatusBarContrastEnforced = false
104
+ window.isNavigationBarContrastEnforced = false
105
+ }
106
+ activity.window?.let { activityWindow ->
107
+ val source = WindowCompat.getInsetsController(activityWindow, activityWindow.decorView)
108
+ WindowCompat.getInsetsController(window, window.decorView).apply {
109
+ isAppearanceLightStatusBars = source.isAppearanceLightStatusBars
110
+ isAppearanceLightNavigationBars = source.isAppearanceLightNavigationBars
111
+ }
112
+ if (activityWindow.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE != 0) {
113
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
114
+ }
115
+ }
116
+ applyBlur()
117
+
118
+ // Like RN's Modal: not focusable while showing so immersive system bars
119
+ // don't flash, then focusable again so text inputs work.
120
+ window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
121
+ dialog.show()
122
+ window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
123
+ reactContext.onExtraWindowCreate(window)
124
+
125
+ animateIn()
126
+ }
127
+
128
+ fun update(newConfig: ModalConfig) {
129
+ val previous = config
130
+ if (previous == newConfig) return
131
+ config = newConfig
132
+ backdrop.setBackgroundColor(config.effectiveBackdropColor)
133
+ if (previous.backdropBlurRadius != newConfig.backdropBlurRadius) applyBlur()
134
+ onConfigChanged(previous)
135
+ }
136
+
137
+ /** Animated dismissal. [Listener.onDismissed] follows when it completes. */
138
+ fun dismiss(reason: DismissReason) {
139
+ if (isDismissing || finished) return
140
+ isDismissing = true
141
+ dismissReason = reason
142
+ hideKeyboard()
143
+ animateOut { finish() }
144
+ }
145
+
146
+ /** Tears the window down without animating or notifying the listener. */
147
+ fun dismissImmediately() {
148
+ if (finished) return
149
+ finished = true
150
+ teardown()
151
+ }
152
+
153
+ /** For subclasses: the enter animation finished. */
154
+ protected fun notifyPresented() {
155
+ if (presentedNotified || isDismissing || finished) return
156
+ presentedNotified = true
157
+ listener.onPresented()
158
+ }
159
+
160
+ /** For subclasses: the user dismissed the modal natively (e.g. swipe). */
161
+ protected fun finishUserDismissal(reason: DismissReason) {
162
+ if (finished) return
163
+ if (!isDismissing) dismissReason = reason
164
+ finish()
165
+ }
166
+
167
+ protected fun reportContentArea(width: Int, height: Int) {
168
+ if (width > 0 && height > 0) listener.onContentAreaChange(width, height)
169
+ }
170
+
171
+ /** Keyboard overlap below the bottom system inset, in px. */
172
+ protected val keyboardOverlap: Int
173
+ get() = (imeHeight - systemInsets.bottom).coerceAtLeast(0)
174
+
175
+ protected fun dp(value: Float): Int = activity.dpToPx(value).toInt()
176
+
177
+ private fun finish() {
178
+ if (finished) return
179
+ finished = true
180
+ teardown()
181
+ listener.onDismissed(dismissReason)
182
+ }
183
+
184
+ private fun teardown() {
185
+ backdrop.animate().cancel()
186
+ dialog.window?.let { reactContext.onExtraWindowDestroy(it) }
187
+ if (dialog.isShowing && !activity.isFinishing && !activity.isDestroyed) {
188
+ try {
189
+ dialog.dismiss()
190
+ } catch (e: IllegalArgumentException) {
191
+ // The window was already detached from the window manager.
192
+ }
193
+ }
194
+ (contentRoot.parent as? ViewGroup)?.removeView(contentRoot)
195
+ }
196
+
197
+ private fun hideKeyboard() {
198
+ val token = dialog.window?.decorView?.windowToken ?: return
199
+ (activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager)
200
+ ?.hideSoftInputFromWindow(token, 0)
201
+ }
202
+
203
+ private fun applyBlur() {
204
+ val window = dialog.window ?: return
205
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
206
+ val radius = dp(config.backdropBlurRadius)
207
+ if (radius > 0 && activity.windowManager.isCrossWindowBlurEnabled) {
208
+ window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
209
+ window.attributes = window.attributes.apply { blurBehindRadius = radius }
210
+ } else {
211
+ window.clearFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
212
+ }
213
+ }
214
+
215
+ private fun observeInsets(root: View) {
216
+ ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
217
+ systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
218
+ // While the keyboard animates, the end state arrives up front; the
219
+ // animation callback below feeds the in-between frames instead.
220
+ if (!imeAnimating) imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
221
+ onInsetsChanged()
222
+ WindowInsetsCompat.CONSUMED
223
+ }
224
+ ViewCompat.setWindowInsetsAnimationCallback(
225
+ root,
226
+ object : WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
227
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
228
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) imeAnimating = true
229
+ }
230
+
231
+ override fun onProgress(
232
+ insets: WindowInsetsCompat,
233
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>,
234
+ ): WindowInsetsCompat {
235
+ if (imeAnimating) {
236
+ imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
237
+ onKeyboardProgress()
238
+ }
239
+ return insets
240
+ }
241
+
242
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
243
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() == 0) return
244
+ imeAnimating = false
245
+ ViewCompat.getRootWindowInsets(root)?.let {
246
+ imeHeight = it.getInsets(WindowInsetsCompat.Type.ime()).bottom
247
+ }
248
+ onInsetsChanged()
249
+ }
250
+ },
251
+ )
252
+ }
253
+ }
@@ -0,0 +1,52 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.view.View
5
+ import android.view.ViewGroup
6
+ import android.view.accessibility.AccessibilityEvent
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+
9
+ /**
10
+ * The view Nitro mounts into the React tree. It is an invisible placeholder:
11
+ * React children added to it (via [NitroModalViewManager]) are hosted by
12
+ * [contentRoot], which travels into the modal window.
13
+ */
14
+ @SuppressLint("ViewConstructor")
15
+ class NitroModalHostView(context: ThemedReactContext) : ViewGroup(context) {
16
+ internal val contentRoot = ModalContentRoot(context)
17
+ internal var onAttachedChange: ((attached: Boolean) -> Unit)? = null
18
+
19
+ internal val reactChildCount: Int
20
+ get() = contentRoot.childCount
21
+
22
+ internal fun addReactChild(child: View, index: Int) = contentRoot.addView(child, index)
23
+
24
+ internal fun getReactChildAt(index: Int): View? = contentRoot.getChildAt(index)
25
+
26
+ internal fun removeReactChildAt(index: Int) = contentRoot.removeViewAt(index)
27
+
28
+ override fun setId(id: Int) {
29
+ super.setId(id)
30
+ // Touches that hit no child target the root; give it our React tag.
31
+ contentRoot.id = id
32
+ }
33
+
34
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
35
+ // Laid out by Fabric; our children live in the modal window.
36
+ }
37
+
38
+ override fun onAttachedToWindow() {
39
+ super.onAttachedToWindow()
40
+ onAttachedChange?.invoke(true)
41
+ }
42
+
43
+ override fun onDetachedFromWindow() {
44
+ super.onDetachedFromWindow()
45
+ onAttachedChange?.invoke(false)
46
+ }
47
+
48
+ // Accessibility for the content is handled inside the modal window.
49
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) = Unit
50
+
51
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
52
+ }
@@ -0,0 +1,28 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class NitroModalPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return null
12
+ }
13
+
14
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
15
+ return ReactModuleInfoProvider { HashMap() }
16
+ }
17
+
18
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
19
+ // Not nitrogen's generated HybridNitroModalManager: the modal needs children.
20
+ return listOf(NitroModalViewManager())
21
+ }
22
+
23
+ companion object {
24
+ init {
25
+ System.loadLibrary("nitromodal")
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.view.View
4
+ import com.facebook.react.uimanager.ReactStylesDiffMap
5
+ import com.facebook.react.uimanager.StateWrapper
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.UIManagerHelper
8
+ import com.facebook.react.uimanager.ViewGroupManager
9
+ import com.margelo.nitro.R.id.associated_hybrid_view_tag
10
+ import com.margelo.nitro.nitromodal.views.HybridNitroModalStateUpdater
11
+
12
+ /**
13
+ * View manager for the "NitroModal" Nitro view.
14
+ *
15
+ * Nitrogen generates a `SimpleViewManager`, which cannot have children. This
16
+ * is the same manager as a `ViewGroupManager`: props still flow through the
17
+ * generated C++ state updater, and React children are redirected into the
18
+ * modal's [ModalContentRoot].
19
+ */
20
+ class NitroModalViewManager : ViewGroupManager<NitroModalHostView>() {
21
+ private class HybridViewHolder(val hybridView: HybridNitroModal, var lastState: StateWrapper? = null)
22
+
23
+ override fun getName(): String = "NitroModal"
24
+
25
+ override fun createViewInstance(reactContext: ThemedReactContext): NitroModalHostView {
26
+ val hybridView = HybridNitroModal(reactContext)
27
+ val view = hybridView.view
28
+ view.setTag(associated_hybrid_view_tag, HybridViewHolder(hybridView))
29
+ return view
30
+ }
31
+
32
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: NitroModalHostView) {
33
+ view.contentRoot.eventDispatcher = UIManagerHelper.getEventDispatcher(reactContext)
34
+ }
35
+
36
+ override fun updateState(view: NitroModalHostView, props: ReactStylesDiffMap, stateWrapper: StateWrapper): Any? {
37
+ val holder = holder(view) ?: throw IllegalStateException("Couldn't find the HybridNitroModal for $view")
38
+ val hybridView = holder.hybridView
39
+ hybridView.beforeUpdate()
40
+ HybridNitroModalStateUpdater.updateViewProps(hybridView, stateWrapper, holder.lastState)
41
+ hybridView.afterUpdate()
42
+ holder.lastState = stateWrapper
43
+ return super.updateState(view, props, stateWrapper)
44
+ }
45
+
46
+ override fun onDropViewInstance(view: NitroModalHostView) {
47
+ holder(view)?.let {
48
+ it.lastState = null
49
+ it.hybridView.onDropView()
50
+ }
51
+ super.onDropViewInstance(view)
52
+ }
53
+
54
+ // region Children live in the modal window, not in the host view.
55
+
56
+ override fun addView(parent: NitroModalHostView, child: View, index: Int) = parent.addReactChild(child, index)
57
+
58
+ override fun getChildCount(parent: NitroModalHostView): Int = parent.reactChildCount
59
+
60
+ override fun getChildAt(parent: NitroModalHostView, index: Int): View? = parent.getReactChildAt(index)
61
+
62
+ override fun removeViewAt(parent: NitroModalHostView, index: Int) = parent.removeReactChildAt(index)
63
+
64
+ // endregion
65
+
66
+ private fun holder(view: View): HybridViewHolder? = view.getTag(associated_hybrid_view_tag) as? HybridViewHolder
67
+ }
@@ -0,0 +1,156 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.app.Activity
4
+ import android.graphics.drawable.GradientDrawable
5
+ import android.view.Gravity
6
+ import android.view.ViewGroup
7
+ import android.view.ViewOutlineProvider
8
+ import android.view.animation.AccelerateInterpolator
9
+ import android.view.animation.PathInterpolator
10
+ import android.widget.FrameLayout
11
+ import androidx.core.graphics.Insets
12
+ import com.facebook.react.uimanager.ThemedReactContext
13
+
14
+ /**
15
+ * `popup` mode: a centered card over the backdrop. Enter/exit are
16
+ * [android.view.ViewPropertyAnimator]s on hardware layers, so they run on the
17
+ * UI/render threads without touching JS.
18
+ */
19
+ internal class PopupPresenter(
20
+ activity: Activity,
21
+ reactContext: ThemedReactContext,
22
+ contentRoot: ModalContentRoot,
23
+ config: ModalConfig,
24
+ listener: Listener,
25
+ ) : ModalPresenter(activity, reactContext, contentRoot, config, listener) {
26
+
27
+ private val root = FrameLayout(activity)
28
+ private val card = FrameLayout(activity)
29
+ private val cardBackground = GradientDrawable()
30
+
31
+ override fun createLayout(): ViewGroup {
32
+ root.addView(backdrop, FrameLayout.LayoutParams(MATCH, MATCH))
33
+ card.background = cardBackground
34
+ card.outlineProvider = ViewOutlineProvider.BACKGROUND
35
+ card.clipToOutline = true
36
+ card.addView(contentRoot, FrameLayout.LayoutParams(MATCH, MATCH))
37
+ root.addView(card, FrameLayout.LayoutParams(0, 0, Gravity.CENTER))
38
+ root.addOnLayoutChangeListener { _, l, t, r, b, oldL, oldT, oldR, oldB ->
39
+ if (r - l != oldR - oldL || b - t != oldB - oldT) reportArea()
40
+ }
41
+ applyAppearance()
42
+ updateCardSize()
43
+ return root
44
+ }
45
+
46
+ override fun animateIn() {
47
+ card.alpha = 0f
48
+ root.post {
49
+ if (isDismissing) return@post
50
+ if (config.popupAnimation == PopupAnimation.NONE) {
51
+ backdrop.alpha = 1f
52
+ card.alpha = 1f
53
+ notifyPresented()
54
+ return@post
55
+ }
56
+ val scale = if (config.popupAnimation == PopupAnimation.SCALE) 0.9f else 1f
57
+ card.scaleX = scale
58
+ card.scaleY = scale
59
+ backdrop.animate().alpha(1f).setDuration(ENTER_DURATION).setInterpolator(EMPHASIZED_DECELERATE).start()
60
+ card.animate()
61
+ .alpha(1f).scaleX(1f).scaleY(1f)
62
+ .setDuration(ENTER_DURATION)
63
+ .setInterpolator(EMPHASIZED_DECELERATE)
64
+ .withLayer()
65
+ .withEndAction { notifyPresented() }
66
+ .start()
67
+ }
68
+ }
69
+
70
+ override fun animateOut(onEnd: () -> Unit) {
71
+ if (config.popupAnimation == PopupAnimation.NONE) {
72
+ onEnd()
73
+ return
74
+ }
75
+ val scale = if (config.popupAnimation == PopupAnimation.SCALE) 0.95f else 1f
76
+ backdrop.animate().alpha(0f).setDuration(EXIT_DURATION).setInterpolator(ACCELERATE).start()
77
+ card.animate()
78
+ .alpha(0f).scaleX(scale).scaleY(scale)
79
+ .setDuration(EXIT_DURATION)
80
+ .setInterpolator(ACCELERATE)
81
+ .withLayer()
82
+ .withEndAction { onEnd() }
83
+ .start()
84
+ }
85
+
86
+ override fun onConfigChanged(previous: ModalConfig) {
87
+ applyAppearance()
88
+ onInsetsChanged()
89
+ }
90
+
91
+ override fun onInsetsChanged() {
92
+ val margin = dp(MARGIN_DP)
93
+ root.setPadding(
94
+ systemInsets.left + margin,
95
+ systemInsets.top + margin,
96
+ systemInsets.right + margin,
97
+ systemInsets.bottom + margin,
98
+ )
99
+ updateCardSize()
100
+ applyKeyboardOffset()
101
+ reportArea()
102
+ }
103
+
104
+ override fun onKeyboardProgress() = applyKeyboardOffset()
105
+
106
+ override fun onContentSizeChanged() = updateCardSize()
107
+
108
+ private fun reportArea() {
109
+ val width = root.width - root.paddingLeft - root.paddingRight
110
+ val height = root.height - root.paddingTop - root.paddingBottom -
111
+ if (config.keyboardBehavior == KeyboardBehavior.RESIZE) keyboardOverlap else 0
112
+ reportContentArea(width, height)
113
+ }
114
+
115
+ private fun applyAppearance() {
116
+ cardBackground.cornerRadius = activity.dpToPx(config.cornerRadius ?: DEFAULT_CORNER_RADIUS_DP)
117
+ cardBackground.setColor(config.surfaceColor(activity))
118
+ }
119
+
120
+ private fun updateCardSize() {
121
+ val params = card.layoutParams ?: return
122
+ val width = contentRoot.contentWidth
123
+ val height = contentRoot.contentHeight
124
+ if (params.width != width || params.height != height) {
125
+ params.width = width
126
+ params.height = height
127
+ card.layoutParams = params
128
+ }
129
+ }
130
+
131
+ /** Keeps the card centered in the space above the keyboard. */
132
+ private fun applyKeyboardOffset() {
133
+ if (config.keyboardBehavior == KeyboardBehavior.NONE) {
134
+ card.translationY = 0f
135
+ return
136
+ }
137
+ val headroom = (card.top - root.paddingTop).coerceAtLeast(0)
138
+ card.translationY = -(keyboardOverlap / 2).coerceAtMost(headroom).toFloat()
139
+ }
140
+
141
+ companion object {
142
+ private const val MATCH = ViewGroup.LayoutParams.MATCH_PARENT
143
+ private const val MARGIN_DP = 16f
144
+ private const val DEFAULT_CORNER_RADIUS_DP = 16f
145
+ private const val ENTER_DURATION = 280L
146
+ private const val EXIT_DURATION = 180L
147
+ private val EMPHASIZED_DECELERATE = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
148
+ private val ACCELERATE = AccelerateInterpolator()
149
+
150
+ /** Content area before the popup is on screen, in px. */
151
+ fun estimateContentArea(activity: Activity, width: Int, height: Int, insets: Insets): Pair<Int, Int> {
152
+ val margin = activity.dpToPx(MARGIN_DP).toInt() * 2
153
+ return Pair(width - insets.left - insets.right - margin, height - insets.top - insets.bottom - margin)
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!--
4
+ Full-screen, transparent, undimmed window. The backdrop, sheet and card are
5
+ regular views inside it so their animations can be driven per frame.
6
+ -->
7
+ <style name="NitroModal.Dialog" parent="@android:style/Theme.Translucent.NoTitleBar">
8
+ <item name="android:windowIsFloating">false</item>
9
+ <item name="android:windowBackground">@android:color/transparent</item>
10
+ <item name="android:backgroundDimEnabled">false</item>
11
+ <item name="android:windowAnimationStyle">@null</item>
12
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
13
+ <item name="android:statusBarColor">@android:color/transparent</item>
14
+ <item name="android:navigationBarColor">@android:color/transparent</item>
15
+ <item name="android:windowSoftInputMode">adjustResize</item>
16
+ </style>
17
+ </resources>
@@ -0,0 +1,64 @@
1
+ //
2
+ // BackdropView.swift
3
+ // NitroModal
4
+ //
5
+
6
+ import UIKit
7
+
8
+ /// Dimming (and optional blur) layer behind the modal. Its `alpha` is driven
9
+ /// by the presentation transitions, so it fades with the modal, including
10
+ /// interactive swipe-to-dismiss.
11
+ final class BackdropView: UIView {
12
+ var onTap: (() -> Void)?
13
+
14
+ private let dimView = UIView()
15
+ private var blurView: UIVisualEffectView?
16
+
17
+ override init(frame: CGRect) {
18
+ super.init(frame: frame)
19
+ dimView.frame = bounds
20
+ dimView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
21
+ addSubview(dimView)
22
+ addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
23
+ isAccessibilityElement = true
24
+ accessibilityTraits = .button
25
+ accessibilityLabel = "Dismiss"
26
+ }
27
+
28
+ @available(*, unavailable)
29
+ required init?(coder: NSCoder) {
30
+ fatalError("init(coder:) is not supported")
31
+ }
32
+
33
+ func configure(color: UIColor, opacity: CGFloat, blurRadius: CGFloat) {
34
+ var alpha: CGFloat = 1
35
+ color.getRed(nil, green: nil, blue: nil, alpha: &alpha)
36
+ dimView.backgroundColor = color.withAlphaComponent(alpha * min(max(opacity, 0), 1))
37
+
38
+ guard blurRadius > 0 else {
39
+ blurView?.removeFromSuperview()
40
+ blurView = nil
41
+ return
42
+ }
43
+ let blurView = self.blurView ?? {
44
+ let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial))
45
+ view.frame = bounds
46
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
47
+ insertSubview(view, belowSubview: dimView)
48
+ self.blurView = view
49
+ return view
50
+ }()
51
+ // UIKit has no public blur radius. Blend a thin system material instead,
52
+ // reaching full strength at ~30pt.
53
+ blurView.alpha = min(blurRadius / 30, 1)
54
+ }
55
+
56
+ @objc private func handleTap() {
57
+ onTap?()
58
+ }
59
+
60
+ override func accessibilityActivate() -> Bool {
61
+ onTap?()
62
+ return true
63
+ }
64
+ }