react-native-blur-vibe 0.1.7 → 0.1.9

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 (31) hide show
  1. package/android/build.gradle +2 -2
  2. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +95 -193
  3. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +158 -185
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +77 -28
  5. package/android/src/main/java/com/blurvibe/LegacyBlurController.kt +238 -0
  6. package/ios/BlurVibeView.swift +2 -0
  7. package/ios/BlurVibeViewFabric.mm +112 -0
  8. package/ios/BlurVibeViewManager.m +12 -2
  9. package/ios/BlurVibeViewManager.swift +9 -9
  10. package/lib/commonjs/BlurVibeViewNativeComponent.ts +14 -25
  11. package/lib/commonjs/BlurView.js +9 -30
  12. package/lib/commonjs/BlurView.js.map +1 -1
  13. package/lib/module/BlurVibeViewNativeComponent.ts +14 -25
  14. package/lib/module/BlurView.js +9 -30
  15. package/lib/module/BlurView.js.map +1 -1
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +11 -9
  17. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/src/BlurView.d.ts +6 -31
  19. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/types.d.ts +26 -1
  21. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +11 -9
  23. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/module/src/BlurView.d.ts +6 -31
  25. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  26. package/lib/typescript/module/src/types.d.ts +26 -1
  27. package/lib/typescript/module/src/types.d.ts.map +1 -1
  28. package/package.json +11 -2
  29. package/src/BlurVibeViewNativeComponent.ts +14 -25
  30. package/src/BlurView.tsx +10 -33
  31. package/src/types.ts +30 -1
@@ -28,28 +28,6 @@ import com.facebook.react.views.view.ReactViewGroup
28
28
  import kotlin.math.min
29
29
  import kotlin.random.Random
30
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
31
  @RequiresApi(Build.VERSION_CODES.S)
54
32
  class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
55
33
 
@@ -62,35 +40,48 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
62
40
 
63
41
  // ── Progressive blur params ────────────────────────────────────────────────
64
42
 
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
43
+ private var progressiveDirection = PROGRESSIVE_NONE
44
+ private var progressiveStartIntensity = 1f
45
+ private var progressiveEndIntensity = 0f
68
46
 
69
47
  // ── Noise params ──────────────────────────────────────────────────────────
70
48
 
71
- private var noiseFactor = 0.08f // Haze default is 0.15 — we use 0.08 as default (subtler)
49
+ private var noiseFactor = 0.08f
72
50
  private var noiseBitmap: Bitmap? = null
73
- private val noisePaint = Paint().apply { alpha = (noiseFactor * 255).toInt() }
51
+ private val noisePaint = Paint()
74
52
 
75
53
  // ── RenderNodes ───────────────────────────────────────────────────────────
54
+ //
55
+ // contentNode: records root-view draw commands ("what's behind me")
56
+ // blurNode: crops + translates contentNode to this view's position,
57
+ // with RenderEffect blur applied
58
+ //
59
+ // IMPORTANT — NO setUseCompositingLayer(true) on either node.
60
+ // Compositing layer on a re-recorded RenderNode causes GPU memory
61
+ // thrashing and SIGSEGV on some API 31 drivers.
62
+ //
63
+ // IMPORTANT — NO LAYER_TYPE_HARDWARE on the view itself.
64
+ // canvas.drawRenderNode() is only valid on a hardware-accelerated canvas
65
+ // that is NOT itself a hardware layer — mixing them causes SIGSEGV
66
+ // in RenderThread (the exact crash we saw in logcat).
76
67
 
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
- }
68
+ private val contentNode = RenderNode("BlurVibeContent")
69
+ private val blurNode = RenderNode("BlurVibeBlur")
70
+
71
+ // ── Recording guard — prevents double-beginRecording crashes ─────────────
72
+ //
73
+ // If captureRootIntoNode fires twice in the same frame (e.g. during
74
+ // layout + invalidate), a second beginRecording() on an active recording
75
+ // crashes the RenderThread. This flag gates it.
81
76
 
82
- /** Holds contentNode cropped to this view's position, with RenderEffect blur applied */
83
- private val blurNode = RenderNode("BlurVibeBlur")
77
+ private var isCapturing = false
84
78
 
85
- // ── Paint objects (reused, no per-frame allocation) ───────────────────────
79
+ // ── Paint objects ──────────────────────────────────────────────────────────
86
80
 
87
- private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
88
- private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
81
+ private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
82
+ private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
89
83
  xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
90
84
  }
91
- private val clearPaint = Paint().apply {
92
- xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
93
- }
94
85
 
95
86
  // ── Root view ─────────────────────────────────────────────────────────────
96
87
 
@@ -106,6 +97,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
106
97
  invalidate()
107
98
  }
108
99
  }
100
+
109
101
  private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
110
102
  if (!frameScheduled) {
111
103
  frameScheduled = true
@@ -120,8 +112,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
120
112
  setWillNotDraw(false)
121
113
  super.setBackgroundColor(Color.TRANSPARENT)
122
114
  clipToOutline = true
123
- // Enable hardware layer so onDraw() runs on GPU
124
- setLayerType(LAYER_TYPE_HARDWARE, null)
115
+ // DO NOT call setLayerType(LAYER_TYPE_HARDWARE) here
116
+ // it conflicts with canvas.drawRenderNode() and causes SIGSEGV in RenderThread.
117
+ // The view uses the default layer type (LAYER_TYPE_NONE) so its canvas is
118
+ // the hardware-accelerated display list canvas — which supports drawRenderNode.
125
119
  }
126
120
 
127
121
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -137,55 +131,68 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
137
131
  blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
138
132
  Choreographer.getInstance().removeFrameCallback(frameCallback)
139
133
  frameScheduled = false
140
- blurRoot = null
134
+ isCapturing = false
135
+ blurRoot = null
141
136
  noiseBitmap?.recycle()
142
137
  noiseBitmap = null
138
+ // Discard RenderNode display lists to free GPU memory
139
+ contentNode.discardDisplayList()
140
+ blurNode.discardDisplayList()
143
141
  super.onDetachedFromWindow()
144
142
  }
145
143
 
146
144
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
147
145
  super.onSizeChanged(w, h, oldw, oldh)
148
- blurNode.setPosition(0, 0, w, h)
149
- applyBlurRenderEffect()
146
+ // Update blurNode bounds contentNode bounds are set in captureRootIntoNode
147
+ if (w > 0 && h > 0) {
148
+ blurNode.setPosition(0, 0, w, h)
149
+ applyBlurRenderEffect()
150
+ }
150
151
  }
151
152
 
152
- // ── Capture ────────────────────────────────────────────────────────────────
153
+ // ── Capture pipeline ───────────────────────────────────────────────────────
153
154
 
154
155
  private fun captureRootIntoNode() {
156
+ if (isCapturing) return // guard against re-entrant / double recording crash
155
157
  val root = blurRoot ?: return
156
- if (root.width <= 0 || root.height <= 0) return
157
-
158
- contentNode.setPosition(0, 0, root.width, root.height)
158
+ val rw = root.width; if (rw <= 0) return
159
+ val rh = root.height; if (rh <= 0) return
160
+ val vw = width; if (vw <= 0) return
161
+ val vh = height; if (vh <= 0) return
159
162
 
160
- val canvas = contentNode.beginRecording()
163
+ isCapturing = true
161
164
  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()
165
+ // Step 1: record root-view draw into contentNode
166
+ contentNode.setPosition(0, 0, rw, rh)
167
+ val contentCanvas = contentNode.beginRecording()
168
+ try {
169
+ contentCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
170
+ root.draw(contentCanvas)
171
+ } finally {
172
+ contentNode.endRecording() // always end — even if draw() throws
173
+ }
179
174
 
180
- blurNode.setPosition(0, 0, width, height)
181
- applyBlurRenderEffect()
175
+ // Step 2: contentNode recording is FINISHED before we reference it
176
+ // in blurNode. This is critical — drawing an actively-recording
177
+ // RenderNode into another canvas is undefined behaviour.
178
+ val myLoc = IntArray(2); getLocationInWindow(myLoc)
179
+ val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
180
+ val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
181
+ val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
182
+
183
+ blurNode.setPosition(0, 0, vw, vh)
184
+ applyBlurRenderEffect()
185
+
186
+ val blurCanvas = blurNode.beginRecording()
187
+ try {
188
+ blurCanvas.translate(-offsetX, -offsetY)
189
+ blurCanvas.drawRenderNode(contentNode) // safe: contentNode recording is done
190
+ } finally {
191
+ blurNode.endRecording()
192
+ }
182
193
 
183
- val canvas = blurNode.beginRecording()
184
- try {
185
- canvas.translate(-offsetX, -offsetY)
186
- canvas.drawRenderNode(contentNode)
187
194
  } finally {
188
- blurNode.endRecording()
195
+ isCapturing = false
189
196
  }
190
197
  }
191
198
 
@@ -202,34 +209,30 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
202
209
  // ── Draw ───────────────────────────────────────────────────────────────────
203
210
 
204
211
  override fun onDraw(canvas: Canvas) {
205
- val w = width.toFloat()
206
- val h = height.toFloat()
207
- if (w <= 0f || h <= 0f) return
212
+ val w = width.toFloat(); if (w <= 0f) return
213
+ val h = height.toFloat(); if (h <= 0f) return
208
214
 
215
+ // Guard: only draw if blurNode has a valid recorded display list
209
216
  if (!blurNode.hasDisplayList()) return
210
217
 
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
218
+ // Step 1: save layer for progressive mask compositing
213
219
  val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
214
220
  canvas.saveLayer(0f, 0f, w, h, null)
215
- } else {
216
- -1
217
- }
221
+ } else -1
218
222
 
219
- // ── Step 2: Draw blurred backdrop ─────────────────────────────────────────
223
+ // Step 2: draw the blurred backdrop
220
224
  canvas.drawRenderNode(blurNode)
221
225
 
222
- // ── Step 3: Progressive mask (alpha gradient fades the blur) ──────────────
226
+ // Step 3: progressive alpha mask
223
227
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
224
- val shader = buildProgressiveShader(w, h)
225
- if (shader != null) {
228
+ buildProgressiveShader(w, h)?.let { shader ->
226
229
  maskPaint.shader = shader
227
230
  canvas.drawRect(0f, 0f, w, h, maskPaint)
228
231
  }
229
232
  canvas.restoreToCount(saveCount)
230
233
  }
231
234
 
232
- // ── Step 4: Overlay tint ──────────────────────────────────────────────────
235
+ // Step 4: overlay tint
233
236
  if (Color.alpha(overlayColor) > 0) {
234
237
  overlayPaint.color = overlayColor
235
238
  if (cornerRadiusPx > 0f) {
@@ -239,58 +242,38 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
239
242
  }
240
243
  }
241
244
 
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)
245
+ // Step 5: noise grain
246
+ noiseBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
247
+ if (noiseFactor > 0f) {
248
+ noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
249
+ noisePaint.shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
250
+ canvas.drawRect(0f, 0f, w, h, noisePaint)
251
+ }
248
252
  }
249
253
  }
250
254
 
251
- // ── Progressive shader builder ────────────────────────────────────────────
255
+ // ── Progressive shader ────────────────────────────────────────────────────
252
256
 
253
257
  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
-
258
+ val startColor = Color.argb((progressiveStartIntensity.coerceIn(0f,1f) * 255).toInt(), 0,0,0)
259
+ val endColor = Color.argb((progressiveEndIntensity.coerceIn(0f,1f) * 255).toInt(), 0,0,0)
260
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
261
+ PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h, startColor,endColor, Shader.TileMode.CLAMP)
262
+ PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f, startColor,endColor, Shader.TileMode.CLAMP)
263
+ PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f, startColor,endColor, Shader.TileMode.CLAMP)
264
+ PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f, startColor,endColor, Shader.TileMode.CLAMP)
265
+ PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f, min(w,h)/2f, startColor,endColor, Shader.TileMode.CLAMP)
266
+ else -> null
280
267
  }
281
268
  }
282
269
 
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.
270
+ // ── Noise bitmap ──────────────────────────────────────────────────────────
288
271
 
289
272
  private fun generateNoiseBitmap() {
290
- if (noiseBitmap != null && !noiseBitmap!!.isRecycled) return
273
+ if (noiseBitmap?.isRecycled == false) return
291
274
  val size = 64
292
275
  val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
293
- val rng = Random(42) // fixed seed = deterministic noise, no shimmer on re-render
276
+ val rng = Random(42)
294
277
  for (x in 0 until size) {
295
278
  for (y in 0 until size) {
296
279
  val v = rng.nextInt(256)
@@ -303,10 +286,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
303
286
  // ── Public setters ─────────────────────────────────────────────────────────
304
287
 
305
288
  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
289
+ val t = amount.coerceIn(0f, 100f) / 100f
290
+ blurRadiusX = t * t * MAX_BLUR_RADIUS
291
+ blurRadiusY = blurRadiusX
310
292
  applyBlurRenderEffect()
311
293
  scheduleFrame()
312
294
  }
@@ -329,55 +311,54 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
329
311
  invalidate()
330
312
  }
331
313
 
332
- fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
333
- // iOS-only — no-op on Android
334
- }
314
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) { }
335
315
 
336
- /**
337
- * Progressive blur direction.
338
- * @param direction one of: "none", "topToBottom", "bottomToTop",
339
- * "leftToRight", "rightToLeft", "radial"
340
- */
341
316
  fun setProgressiveBlurDirection(direction: String?) {
342
317
  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
318
+ "topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
319
+ "bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
320
+ "leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
321
+ "rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
322
+ "radial" -> PROGRESSIVE_RADIAL
323
+ else -> PROGRESSIVE_NONE
349
324
  }
350
325
  invalidate()
351
326
  }
352
327
 
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
328
  fun setProgressiveStartIntensity(intensity: Float) {
359
- progressiveStartIntensity = intensity.coerceIn(0f, 1f)
360
- invalidate()
329
+ progressiveStartIntensity = intensity.coerceIn(0f, 1f); invalidate()
361
330
  }
362
331
 
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
332
  fun setProgressiveEndIntensity(intensity: Float) {
369
- progressiveEndIntensity = intensity.coerceIn(0f, 1f)
370
- invalidate()
333
+ progressiveEndIntensity = intensity.coerceIn(0f, 1f); invalidate()
371
334
  }
372
335
 
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
336
  fun setNoiseFactor(factor: Float) {
379
- noiseFactor = factor.coerceIn(0f, 1f)
380
- invalidate()
337
+ noiseFactor = factor.coerceIn(0f, 1f); invalidate()
338
+ }
339
+
340
+ fun applyBlurEnabled(enabled: Boolean) {
341
+ if (!enabled) {
342
+ blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
343
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
344
+ frameScheduled = false
345
+ blurNode.discardDisplayList()
346
+ contentNode.discardDisplayList()
347
+ invalidate()
348
+ } else {
349
+ blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
350
+ scheduleFrame()
351
+ }
352
+ }
353
+
354
+ fun setAutoUpdate(autoUpdate: Boolean) {
355
+ if (autoUpdate) {
356
+ blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
357
+ } else {
358
+ blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
359
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
360
+ frameScheduled = false
361
+ }
381
362
  }
382
363
 
383
364
  // ── Helpers ────────────────────────────────────────────────────────────────
@@ -392,14 +373,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
392
373
  private fun findBlurRoot(): ViewGroup? {
393
374
  var p = parent
394
375
  while (p != null) {
395
- if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
396
- return p as? ViewGroup
376
+ if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
397
377
  p = (p as? View)?.parent
398
378
  }
399
379
  p = parent
400
380
  while (p != null) {
401
- if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
402
- return p as? ViewGroup
381
+ if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
403
382
  p = (p as? View)?.parent
404
383
  }
405
384
  return rootView as? ViewGroup
@@ -412,32 +391,26 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
412
391
  val hex = t.removePrefix("#")
413
392
  return try {
414
393
  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))
394
+ 3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
395
+ hex[1].toString().repeat(2).toInt(16),
396
+ hex[2].toString().repeat(2).toInt(16))
397
+ 6 -> Color.argb(255, hex.substring(0,2).toInt(16),
398
+ hex.substring(2,4).toInt(16),
399
+ hex.substring(4,6).toInt(16))
400
+ 8 -> Color.argb(hex.substring(6,8).toInt(16),
401
+ hex.substring(0,2).toInt(16),
402
+ hex.substring(2,4).toInt(16),
403
+ hex.substring(4,6).toInt(16))
428
404
  else -> null
429
405
  }
430
406
  } catch (_: NumberFormatException) { null }
431
407
  }
432
408
 
433
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
434
- // Yoga handles all layout
435
- }
409
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { }
436
410
 
437
411
  companion object {
438
412
  private const val MAX_BLUR_RADIUS = 25f
439
413
  private const val DEFAULT_BLUR_RADIUS = 2.5f
440
-
441
414
  const val PROGRESSIVE_NONE = 0
442
415
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
443
416
  const val PROGRESSIVE_BOTTOM_TO_TOP = 2