react-native-blur-vibe 0.1.6 → 0.1.8

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 (30) hide show
  1. package/README.md +374 -181
  2. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +7 -5
  3. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +70 -17
  5. package/ios/BlurVibeView.swift +28 -27
  6. package/ios/BlurVibeViewManager.m +9 -9
  7. package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
  8. package/ios/Views/ProgressiveBlurView.swift +255 -0
  9. package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
  10. package/lib/commonjs/BlurView.js +34 -7
  11. package/lib/commonjs/BlurView.js.map +1 -1
  12. package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
  13. package/lib/module/BlurView.js +34 -7
  14. package/lib/module/BlurView.js.map +1 -1
  15. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
  18. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  19. package/lib/typescript/commonjs/src/types.d.ts +236 -18
  20. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  21. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  23. package/lib/typescript/module/src/BlurView.d.ts +27 -8
  24. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  25. package/lib/typescript/module/src/types.d.ts +236 -18
  26. package/lib/typescript/module/src/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/BlurVibeViewNativeComponent.ts +10 -16
  29. package/src/BlurView.tsx +34 -7
  30. package/src/types.ts +267 -18
@@ -0,0 +1,448 @@
1
+ package com.blurvibe
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapShader
6
+ import android.graphics.Canvas
7
+ import android.graphics.Color
8
+ import android.graphics.LinearGradient
9
+ import android.graphics.Outline
10
+ import android.graphics.Paint
11
+ import android.graphics.PorterDuff
12
+ import android.graphics.PorterDuffXfermode
13
+ import android.graphics.RadialGradient
14
+ import android.graphics.RectF
15
+ import android.graphics.RenderEffect
16
+ import android.graphics.RenderNode
17
+ import android.graphics.Shader
18
+ import android.os.Build
19
+ import android.util.TypedValue
20
+ import android.view.Choreographer
21
+ import android.view.View
22
+ import android.view.ViewGroup
23
+ import android.view.ViewOutlineProvider
24
+ import android.view.ViewTreeObserver
25
+ import androidx.annotation.RequiresApi
26
+ import androidx.core.graphics.toColorInt
27
+ import com.facebook.react.views.view.ReactViewGroup
28
+ import kotlin.math.min
29
+ import kotlin.random.Random
30
+
31
+ /**
32
+ * BlurVibeViewApi31 — GPU backdrop blur for Android API 31+
33
+ *
34
+ * Features:
35
+ * • Dual-RenderNode blur (backdrop-filter CSS semantics)
36
+ * • Progressive / gradient blur (vertical, horizontal, radial)
37
+ * • Noise texture overlay (tactile frosted-glass feel, like Haze)
38
+ * • Overlay tint with full RGBA support
39
+ * • Corner radius with hardware clipping
40
+ * • Choreographer-gated updates (max 1 capture per vsync)
41
+ *
42
+ * Progressive blur technique (from Haze docs):
43
+ * Uses a mask approach — a LinearGradient/RadialGradient is drawn as an
44
+ * alpha mask over the blur output using PorterDuff.DST_IN.
45
+ * This fades the blur from full-strength to zero across the view.
46
+ * Per Haze docs: "masks are much faster with negligible performance cost"
47
+ * vs true per-pixel radius variation which costs ~25% more on API 33+.
48
+ *
49
+ * Noise texture:
50
+ * Haze uses noise at 15% opacity by default for tactility.
51
+ * We generate a small tileable noise bitmap once and draw it with low alpha.
52
+ */
53
+ @RequiresApi(Build.VERSION_CODES.S)
54
+ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
55
+
56
+ // ── Blur params ────────────────────────────────────────────────────────────
57
+
58
+ private var blurRadiusX = DEFAULT_BLUR_RADIUS
59
+ private var blurRadiusY = DEFAULT_BLUR_RADIUS
60
+ private var overlayColor = Color.TRANSPARENT
61
+ private var cornerRadiusPx = 0f
62
+
63
+ // ── Progressive blur params ────────────────────────────────────────────────
64
+
65
+ private var progressiveDirection = PROGRESSIVE_NONE
66
+ private var progressiveStartIntensity = 1f // 0.0–1.0, full blur at start
67
+ private var progressiveEndIntensity = 0f // 0.0–1.0, no blur at end
68
+
69
+ // ── Noise params ──────────────────────────────────────────────────────────
70
+
71
+ private var noiseFactor = 0.08f // Haze default is 0.15 — we use 0.08 as default (subtler)
72
+ private var noiseBitmap: Bitmap? = null
73
+ private val noisePaint = Paint().apply { alpha = (noiseFactor * 255).toInt() }
74
+
75
+ // ── RenderNodes ───────────────────────────────────────────────────────────
76
+
77
+ /** Records the root-view content — "what's behind me" */
78
+ private val contentNode = RenderNode("BlurVibeContent").apply {
79
+ setUseCompositingLayer(true, null) // caches as GPU texture — repeated reads are free
80
+ }
81
+
82
+ /** Holds contentNode cropped to this view's position, with RenderEffect blur applied */
83
+ private val blurNode = RenderNode("BlurVibeBlur")
84
+
85
+ // ── Paint objects (reused, no per-frame allocation) ───────────────────────
86
+
87
+ private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
88
+ private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
89
+ xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
90
+ }
91
+ private val clearPaint = Paint().apply {
92
+ xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
93
+ }
94
+
95
+ // ── Root view ─────────────────────────────────────────────────────────────
96
+
97
+ private var blurRoot: ViewGroup? = null
98
+
99
+ // ── Choreographer gate ────────────────────────────────────────────────────
100
+
101
+ private var frameScheduled = false
102
+ private val frameCallback = Choreographer.FrameCallback {
103
+ frameScheduled = false
104
+ if (isAttachedToWindow) {
105
+ captureRootIntoNode()
106
+ invalidate()
107
+ }
108
+ }
109
+ private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
110
+ if (!frameScheduled) {
111
+ frameScheduled = true
112
+ Choreographer.getInstance().postFrameCallback(frameCallback)
113
+ }
114
+ true
115
+ }
116
+
117
+ // ── Init ───────────────────────────────────────────────────────────────────
118
+
119
+ init {
120
+ setWillNotDraw(false)
121
+ super.setBackgroundColor(Color.TRANSPARENT)
122
+ clipToOutline = true
123
+ // Enable hardware layer so onDraw() runs on GPU
124
+ setLayerType(LAYER_TYPE_HARDWARE, null)
125
+ }
126
+
127
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
128
+
129
+ override fun onAttachedToWindow() {
130
+ super.onAttachedToWindow()
131
+ blurRoot = findBlurRoot()
132
+ blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
133
+ generateNoiseBitmap()
134
+ }
135
+
136
+ override fun onDetachedFromWindow() {
137
+ blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
138
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
139
+ frameScheduled = false
140
+ blurRoot = null
141
+ noiseBitmap?.recycle()
142
+ noiseBitmap = null
143
+ super.onDetachedFromWindow()
144
+ }
145
+
146
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
147
+ super.onSizeChanged(w, h, oldw, oldh)
148
+ blurNode.setPosition(0, 0, w, h)
149
+ applyBlurRenderEffect()
150
+ }
151
+
152
+ // ── Capture ────────────────────────────────────────────────────────────────
153
+
154
+ private fun captureRootIntoNode() {
155
+ val root = blurRoot ?: return
156
+ if (root.width <= 0 || root.height <= 0) return
157
+
158
+ contentNode.setPosition(0, 0, root.width, root.height)
159
+
160
+ val canvas = contentNode.beginRecording()
161
+ try {
162
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
163
+ root.draw(canvas)
164
+ } finally {
165
+ contentNode.endRecording()
166
+ }
167
+
168
+ rebuildBlurNode()
169
+ }
170
+
171
+ private fun rebuildBlurNode() {
172
+ val root = blurRoot ?: return
173
+ if (width <= 0 || height <= 0) return
174
+
175
+ val myLoc = IntArray(2); getLocationInWindow(myLoc)
176
+ val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
177
+ val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
178
+ val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
179
+
180
+ blurNode.setPosition(0, 0, width, height)
181
+ applyBlurRenderEffect()
182
+
183
+ val canvas = blurNode.beginRecording()
184
+ try {
185
+ canvas.translate(-offsetX, -offsetY)
186
+ canvas.drawRenderNode(contentNode)
187
+ } finally {
188
+ blurNode.endRecording()
189
+ }
190
+ }
191
+
192
+ private fun applyBlurRenderEffect() {
193
+ if (blurRadiusX < 0.5f && blurRadiusY < 0.5f) {
194
+ blurNode.setRenderEffect(null)
195
+ return
196
+ }
197
+ blurNode.setRenderEffect(
198
+ RenderEffect.createBlurEffect(blurRadiusX, blurRadiusY, Shader.TileMode.CLAMP)
199
+ )
200
+ }
201
+
202
+ // ── Draw ───────────────────────────────────────────────────────────────────
203
+
204
+ override fun onDraw(canvas: Canvas) {
205
+ val w = width.toFloat()
206
+ val h = height.toFloat()
207
+ if (w <= 0f || h <= 0f) return
208
+
209
+ if (!blurNode.hasDisplayList()) return
210
+
211
+ // ── Step 1: Save layer so we can apply mask on top of blur ────────────────
212
+ // saveLayer lets us composite blur + progressive mask as a unit
213
+ val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
214
+ canvas.saveLayer(0f, 0f, w, h, null)
215
+ } else {
216
+ -1
217
+ }
218
+
219
+ // ── Step 2: Draw blurred backdrop ─────────────────────────────────────────
220
+ canvas.drawRenderNode(blurNode)
221
+
222
+ // ── Step 3: Progressive mask (alpha gradient fades the blur) ──────────────
223
+ if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
224
+ val shader = buildProgressiveShader(w, h)
225
+ if (shader != null) {
226
+ maskPaint.shader = shader
227
+ canvas.drawRect(0f, 0f, w, h, maskPaint)
228
+ }
229
+ canvas.restoreToCount(saveCount)
230
+ }
231
+
232
+ // ── Step 4: Overlay tint ──────────────────────────────────────────────────
233
+ if (Color.alpha(overlayColor) > 0) {
234
+ overlayPaint.color = overlayColor
235
+ if (cornerRadiusPx > 0f) {
236
+ canvas.drawRoundRect(RectF(0f, 0f, w, h), cornerRadiusPx, cornerRadiusPx, overlayPaint)
237
+ } else {
238
+ canvas.drawRect(0f, 0f, w, h, overlayPaint)
239
+ }
240
+ }
241
+
242
+ // ── Step 5: Noise texture (tactile frosted-glass feel) ────────────────────
243
+ if (noiseFactor > 0f && noiseBitmap != null && !noiseBitmap!!.isRecycled) {
244
+ noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
245
+ val noiseShader = BitmapShader(noiseBitmap!!, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
246
+ noisePaint.shader = noiseShader
247
+ canvas.drawRect(0f, 0f, w, h, noisePaint)
248
+ }
249
+ }
250
+
251
+ // ── Progressive shader builder ────────────────────────────────────────────
252
+
253
+ private fun buildProgressiveShader(w: Float, h: Float): Shader? {
254
+ // Map intensity values to alpha: 1.0 = fully opaque (full blur), 0.0 = fully transparent (no blur)
255
+ val startAlpha = progressiveStartIntensity.coerceIn(0f, 1f)
256
+ val endAlpha = progressiveEndIntensity.coerceIn(0f, 1f)
257
+ val startColor = Color.argb((startAlpha * 255).toInt(), 0, 0, 0)
258
+ val endColor = Color.argb((endAlpha * 255).toInt(), 0, 0, 0)
259
+
260
+ return when (progressiveDirection) {
261
+ PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(
262
+ 0f, 0f, 0f, h, startColor, endColor, Shader.TileMode.CLAMP
263
+ )
264
+ PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(
265
+ 0f, h, 0f, 0f, startColor, endColor, Shader.TileMode.CLAMP
266
+ )
267
+ PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(
268
+ 0f, 0f, w, 0f, startColor, endColor, Shader.TileMode.CLAMP
269
+ )
270
+ PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(
271
+ w, 0f, 0f, 0f, startColor, endColor, Shader.TileMode.CLAMP
272
+ )
273
+ PROGRESSIVE_RADIAL -> RadialGradient(
274
+ w / 2f, h / 2f,
275
+ min(w, h) / 2f,
276
+ startColor, endColor,
277
+ Shader.TileMode.CLAMP
278
+ )
279
+ else -> null
280
+ }
281
+ }
282
+
283
+ // ── Noise generation ─────────────────────────────────────────────────────
284
+ //
285
+ // Generates a small (64×64) tileable noise bitmap once.
286
+ // Haze uses noise at 15% opacity for tactility — the fine grain
287
+ // breaks up the uniform blur and makes it look more like real frosted glass.
288
+
289
+ private fun generateNoiseBitmap() {
290
+ if (noiseBitmap != null && !noiseBitmap!!.isRecycled) return
291
+ val size = 64
292
+ val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
293
+ val rng = Random(42) // fixed seed = deterministic noise, no shimmer on re-render
294
+ for (x in 0 until size) {
295
+ for (y in 0 until size) {
296
+ val v = rng.nextInt(256)
297
+ bmp.setPixel(x, y, Color.argb(255, v, v, v))
298
+ }
299
+ }
300
+ noiseBitmap = bmp
301
+ }
302
+
303
+ // ── Public setters ─────────────────────────────────────────────────────────
304
+
305
+ fun setBlurAmount(amount: Float) {
306
+ val t = amount.coerceIn(0f, 100f) / 100f
307
+ val radius = t * t * MAX_BLUR_RADIUS // quadratic — matches CSS backdrop-blur feel
308
+ blurRadiusX = radius
309
+ blurRadiusY = radius
310
+ applyBlurRenderEffect()
311
+ scheduleFrame()
312
+ }
313
+
314
+ fun setOverlayColor(colorString: String?) {
315
+ overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
316
+ invalidate()
317
+ }
318
+
319
+ fun applyBorderRadius(radiusDp: Float) {
320
+ cornerRadiusPx = TypedValue.applyDimension(
321
+ TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
322
+ )
323
+ outlineProvider = object : ViewOutlineProvider() {
324
+ override fun getOutline(view: View, outline: Outline) {
325
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
326
+ }
327
+ }
328
+ clipToOutline = cornerRadiusPx > 0f
329
+ invalidate()
330
+ }
331
+
332
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
333
+ // iOS-only — no-op on Android
334
+ }
335
+
336
+ /**
337
+ * Progressive blur direction.
338
+ * @param direction one of: "none", "topToBottom", "bottomToTop",
339
+ * "leftToRight", "rightToLeft", "radial"
340
+ */
341
+ fun setProgressiveBlurDirection(direction: String?) {
342
+ progressiveDirection = when (direction) {
343
+ "topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
344
+ "bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
345
+ "leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
346
+ "rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
347
+ "radial" -> PROGRESSIVE_RADIAL
348
+ else -> PROGRESSIVE_NONE
349
+ }
350
+ invalidate()
351
+ }
352
+
353
+ /**
354
+ * Progressive blur start intensity (0.0 = no blur, 1.0 = full blur).
355
+ * This is the intensity at the START of the gradient direction.
356
+ * Default 1.0 — full blur at top/left/center.
357
+ */
358
+ fun setProgressiveStartIntensity(intensity: Float) {
359
+ progressiveStartIntensity = intensity.coerceIn(0f, 1f)
360
+ invalidate()
361
+ }
362
+
363
+ /**
364
+ * Progressive blur end intensity (0.0 = no blur, 1.0 = full blur).
365
+ * This is the intensity at the END of the gradient direction.
366
+ * Default 0.0 — fades to no blur at bottom/right/edge.
367
+ */
368
+ fun setProgressiveEndIntensity(intensity: Float) {
369
+ progressiveEndIntensity = intensity.coerceIn(0f, 1f)
370
+ invalidate()
371
+ }
372
+
373
+ /**
374
+ * Noise factor — grain overlay strength for frosted-glass tactility.
375
+ * 0.0 = no noise, 1.0 = full noise. Default 0.08 (8%).
376
+ * Haze's default is 0.15. Set 0 to disable.
377
+ */
378
+ fun setNoiseFactor(factor: Float) {
379
+ noiseFactor = factor.coerceIn(0f, 1f)
380
+ invalidate()
381
+ }
382
+
383
+ // ── Helpers ────────────────────────────────────────────────────────────────
384
+
385
+ private fun scheduleFrame() {
386
+ if (!frameScheduled) {
387
+ frameScheduled = true
388
+ Choreographer.getInstance().postFrameCallback(frameCallback)
389
+ }
390
+ }
391
+
392
+ private fun findBlurRoot(): ViewGroup? {
393
+ var p = parent
394
+ while (p != null) {
395
+ if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
396
+ return p as? ViewGroup
397
+ p = (p as? View)?.parent
398
+ }
399
+ p = parent
400
+ while (p != null) {
401
+ if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
402
+ return p as? ViewGroup
403
+ p = (p as? View)?.parent
404
+ }
405
+ return rootView as? ViewGroup
406
+ }
407
+
408
+ private fun parseHexColor(s: String): Int? {
409
+ val t = s.trim()
410
+ if (t.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
411
+ if (!t.startsWith("#")) return try { t.toColorInt() } catch (_: Exception) { null }
412
+ val hex = t.removePrefix("#")
413
+ return try {
414
+ when (hex.length) {
415
+ 3 -> Color.argb(255,
416
+ hex[0].toString().repeat(2).toInt(16),
417
+ hex[1].toString().repeat(2).toInt(16),
418
+ hex[2].toString().repeat(2).toInt(16))
419
+ 6 -> Color.argb(255,
420
+ hex.substring(0, 2).toInt(16),
421
+ hex.substring(2, 4).toInt(16),
422
+ hex.substring(4, 6).toInt(16))
423
+ 8 -> Color.argb(
424
+ hex.substring(6, 8).toInt(16),
425
+ hex.substring(0, 2).toInt(16),
426
+ hex.substring(2, 4).toInt(16),
427
+ hex.substring(4, 6).toInt(16))
428
+ else -> null
429
+ }
430
+ } catch (_: NumberFormatException) { null }
431
+ }
432
+
433
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
434
+ // Yoga handles all layout
435
+ }
436
+
437
+ companion object {
438
+ private const val MAX_BLUR_RADIUS = 25f
439
+ private const val DEFAULT_BLUR_RADIUS = 2.5f
440
+
441
+ const val PROGRESSIVE_NONE = 0
442
+ const val PROGRESSIVE_TOP_TO_BOTTOM = 1
443
+ const val PROGRESSIVE_BOTTOM_TO_TOP = 2
444
+ const val PROGRESSIVE_LEFT_TO_RIGHT = 3
445
+ const val PROGRESSIVE_RIGHT_TO_LEFT = 4
446
+ const val PROGRESSIVE_RADIAL = 5
447
+ }
448
+ }
@@ -1,5 +1,7 @@
1
1
  package com.blurvibe
2
2
 
3
+ import android.os.Build
4
+ import android.view.ViewGroup
3
5
  import com.facebook.react.uimanager.ThemedReactContext
4
6
  import com.facebook.react.uimanager.ViewGroupManager
5
7
  import com.facebook.react.uimanager.annotations.ReactProp
@@ -7,45 +9,96 @@ import com.facebook.react.uimanager.annotations.ReactProp
7
9
  /**
8
10
  * BlurVibeViewManager
9
11
  *
10
- * ViewGroupManager BlurVibeView (which extends BlurViewGroup/FrameLayout)
11
- * hosts React children, so we must use ViewGroupManager, not SimpleViewManager.
12
+ * Extends ViewGroupManager<ViewGroup> so that both BlurVibeView (which extends
13
+ * BlurViewGroup, not ReactViewGroup) and BlurVibeViewApi31 (which extends
14
+ * ReactViewGroup) satisfy the type bound.
15
+ *
16
+ * @ReactProp handlers receive ViewGroup and smart-cast via `when`.
17
+ *
18
+ * Naming rules to avoid supertype collisions on the VIEW classes:
19
+ * Manager method → View method called
20
+ * setBlurBorderRadius → applyBorderRadius (ReactViewGroup has setBorderRadius)
21
+ * setBlurRadiusProp → setBlurRadius (unique name on BlurVibeView)
22
+ * setOverlayColorProp → setOverlayColor (unique — not in ReactViewGroup)
23
+ * setBlurTypeProp → no-op
12
24
  */
13
- class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
25
+ class BlurVibeViewManager : ViewGroupManager<ViewGroup>() {
14
26
 
15
27
  override fun getName() = "BlurVibeView"
16
28
 
17
- override fun createViewInstance(context: ThemedReactContext) = BlurVibeView(context)
29
+ override fun createViewInstance(context: ThemedReactContext): ViewGroup =
30
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) BlurVibeViewApi31(context)
31
+ else BlurVibeView(context)
32
+
33
+ // ── Core props ─────────────────────────────────────────────────────────────
18
34
 
19
35
  @ReactProp(name = "blurAmount", defaultFloat = 10f)
20
- fun setBlurAmount(view: BlurVibeView, amount: Float) {
21
- view.setBlurAmount(amount)
36
+ fun setBlurAmount(view: ViewGroup, amount: Float) {
37
+ when (view) {
38
+ is BlurVibeViewApi31 -> view.setBlurAmount(amount)
39
+ is BlurVibeView -> view.setBlurAmount(amount)
40
+ }
22
41
  }
23
42
 
24
43
  @ReactProp(name = "blurType")
25
- fun setBlurType(view: BlurVibeView, type: String?) {
26
- // No-op on AndroidblurType maps to iOS UIBlurEffectStyle only
44
+ fun setBlurTypeProp(view: ViewGroup, @Suppress("UNUSED_PARAMETER") type: String?) {
45
+ // iOS UIBlurEffectStyle onlyno-op on Android
27
46
  }
28
47
 
29
48
  @ReactProp(name = "overlayColor")
30
- fun setOverlayColor(view: BlurVibeView, color: String?) {
31
- view.setOverlayColor(color)
49
+ fun setOverlayColorProp(view: ViewGroup, color: String?) {
50
+ when (view) {
51
+ is BlurVibeViewApi31 -> view.setOverlayColor(color)
52
+ is BlurVibeView -> view.setOverlayColor(color)
53
+ }
32
54
  }
33
55
 
34
56
  @ReactProp(name = "reducedTransparencyFallbackColor")
35
- fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) {
36
- view.setReducedTransparencyFallbackColor(color)
57
+ fun setReducedTransparencyFallbackColor(view: ViewGroup, color: String?) {
58
+ when (view) {
59
+ is BlurVibeViewApi31 -> view.setReducedTransparencyFallbackColor(color)
60
+ is BlurVibeView -> view.setReducedTransparencyFallbackColor(color)
61
+ }
37
62
  }
38
63
 
39
64
  @ReactProp(name = "blurRadius", defaultInt = 4)
40
- fun setBlurRadius(view: BlurVibeView, radius: Int) {
41
- view.setBlurRadius(radius)
65
+ fun setBlurRadiusProp(view: ViewGroup, radius: Int) {
66
+ // API < 31 only — QmBlurView downsample factor
67
+ // API 31+ uses full-res RenderNode, downsample irrelevant
68
+ if (view is BlurVibeView) view.setBlurRadius(radius)
42
69
  }
43
70
 
44
71
  @ReactProp(name = "borderRadius", defaultFloat = 0f)
45
- fun setBlurBorderRadius(view: BlurVibeView, radius: Float) {
46
- view.setBorderRadius(radius)
72
+ fun setBlurBorderRadius(view: ViewGroup, radius: Float) {
73
+ when (view) {
74
+ is BlurVibeViewApi31 -> view.applyBorderRadius(radius) // renamed — avoids ReactViewGroup.setBorderRadius
75
+ is BlurVibeView -> view.setBorderRadius(radius)
76
+ }
77
+ }
78
+
79
+ // ── Progressive blur props (API 31+ only) ──────────────────────────────────
80
+
81
+ @ReactProp(name = "progressiveBlurDirection")
82
+ fun setProgressiveBlurDirection(view: ViewGroup, direction: String?) {
83
+ if (view is BlurVibeViewApi31) view.setProgressiveBlurDirection(direction)
84
+ }
85
+
86
+ @ReactProp(name = "progressiveStartIntensity", defaultFloat = 1f)
87
+ fun setProgressiveStartIntensity(view: ViewGroup, intensity: Float) {
88
+ if (view is BlurVibeViewApi31) view.setProgressiveStartIntensity(intensity)
89
+ }
90
+
91
+ @ReactProp(name = "progressiveEndIntensity", defaultFloat = 0f)
92
+ fun setProgressiveEndIntensity(view: ViewGroup, intensity: Float) {
93
+ if (view is BlurVibeViewApi31) view.setProgressiveEndIntensity(intensity)
94
+ }
95
+
96
+ // ── Noise prop (API 31+ only) ──────────────────────────────────────────────
97
+
98
+ @ReactProp(name = "noiseFactor", defaultFloat = 0.08f)
99
+ fun setNoiseFactorProp(view: ViewGroup, factor: Float) {
100
+ if (view is BlurVibeViewApi31) view.setNoiseFactor(factor)
47
101
  }
48
102
 
49
- // React Native's Yoga handles child layout — return false
50
103
  override fun needsCustomLayoutForChildren(): Boolean = false
51
104
  }