react-native-blur-vibe 0.1.15 → 0.1.16

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.
@@ -18,7 +18,6 @@ import android.os.Handler
18
18
  import android.os.HandlerThread
19
19
  import android.os.Looper
20
20
  import android.util.TypedValue
21
- import android.view.Choreographer
22
21
  import android.view.View
23
22
  import android.view.ViewGroup
24
23
  import android.view.ViewOutlineProvider
@@ -31,7 +30,64 @@ import kotlin.random.Random
31
30
 
32
31
  /**
33
32
  * BlurVibeViewApi31 — Backdrop blur for Android API 31+
34
- **/
33
+ *
34
+ * ─── Why Compose Modifier.blur() doesn't work for this ───────────────────────
35
+ *
36
+ * Modifier.blur() blurs the Composable's OWN content — equivalent to CSS
37
+ * filter:blur(), not backdrop-filter:blur(). It cannot see RN views behind it.
38
+ * ComposeView also creates its own Choreographer loop which conflicts with
39
+ * Reanimated causing SIGSEGV. Compose is not the right tool here.
40
+ *
41
+ * ─── Root cause of API 35 glitch + SIGSEGV ───────────────────────────────────
42
+ *
43
+ * The Choreographer.FrameCallback approach was the problem:
44
+ *
45
+ * OnPreDrawListener fires → schedules Choreographer callback
46
+ * Choreographer callback fires → calls root.draw()
47
+ *
48
+ * On Android 15+ (API 35), Choreographer.FrameCallback fires at the COMMIT
49
+ * phase — AFTER the RenderThread has already started processing the frame.
50
+ * Calling root.draw() at this point means:
51
+ * 1. Torn frames: capture sees RenderThread mid-draw → blinking/glitch
52
+ * 2. Reanimated's RenderNodes being written by UI thread while RenderThread
53
+ * reads them → use-after-free in GPU driver → SIGSEGV
54
+ *
55
+ * ─── THE FIX ─────────────────────────────────────────────────────────────────
56
+ *
57
+ * Call root.draw() DIRECTLY inside OnPreDrawListener — NOT deferred to
58
+ * Choreographer. OnPreDrawListener is guaranteed to fire BEFORE the
59
+ * RenderThread starts. This is the same approach used by Dimezis BlurView
60
+ * and it works on ALL Android versions including API 35.
61
+ *
62
+ * Throttling: simple System.nanoTime() check (16ms cap = 60fps).
63
+ * No Choreographer needed for throttling.
64
+ *
65
+ * StackBlur still runs on workerThread — main thread is never blocked.
66
+ *
67
+ * ─── Thread model ─────────────────────────────────────────────────────────────
68
+ *
69
+ * OnPreDrawListener (main thread, PRE-DRAW guaranteed):
70
+ * → throttle check
71
+ * → root.draw() into captureBitmap (isCapturing guard skips us)
72
+ * → post StackBlur to workerThread
73
+ * → return true immediately (never blocks draw pass)
74
+ *
75
+ * workerThread:
76
+ * → downsample + StackBlur × 3 passes
77
+ * → readyBitmap = scaledBitmap (@Volatile atomic swap)
78
+ * → post invalidate() to main thread
79
+ *
80
+ * RenderThread (onDraw):
81
+ * → canvas.drawBitmap(readyBitmap) ← reads fully-written bitmap, zero race
82
+ *
83
+ * ─── Reanimated safety ───────────────────────────────────────────────────────
84
+ *
85
+ * root.draw() is called in OnPreDrawListener BEFORE RenderThread starts.
86
+ * Reanimated's UI-thread animations have completed their prop updates by
87
+ * the time OnPreDrawListener fires (it fires after layout, after animations
88
+ * update view properties, but before the GPU draw begins).
89
+ * Zero SIGSEGV risk.
90
+ */
35
91
  @RequiresApi(Build.VERSION_CODES.S)
36
92
  class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
37
93
 
@@ -54,11 +110,17 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
54
110
  private val noisePaint = Paint()
55
111
 
56
112
  // ── Bitmap double-buffer ──────────────────────────────────────────────────
57
-
58
- private var captureBitmap: Bitmap? = null // full-size capture (main thread)
59
- private var scaledBitmap: Bitmap? = null // downsampled + blurred (worker thread)
60
- @Volatile
61
- private var readyBitmap: Bitmap? = null // @Volatile pointer — RenderThread reads this
113
+ //
114
+ // captureBitmap: written by main thread in OnPreDrawListener
115
+ // scaledBitmap: written by workerThread (back buffer)
116
+ // readyBitmap: @Volatile pointer — RenderThread reads this in onDraw()
117
+ //
118
+ // We NEVER mutate the bitmap readyBitmap currently points to.
119
+ // scaledBitmap becomes the next back buffer after the @Volatile swap.
120
+
121
+ private var captureBitmap: Bitmap? = null
122
+ private var scaledBitmap: Bitmap? = null
123
+ @Volatile private var readyBitmap: Bitmap? = null
62
124
 
63
125
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
64
126
  private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
@@ -78,26 +140,25 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
78
140
 
79
141
  // ── State ─────────────────────────────────────────────────────────────────
80
142
 
81
- // isCapturing: public read so BlurVibeViewApi31.draw() can check it
82
- var isCapturing = false
143
+ var isCapturing = false
83
144
  private set
84
- private var blurEnabled = true
85
- private var autoUpdate = true
86
- private var frameScheduled = false
87
145
 
88
- // ── Choreographer gate ────────────────────────────────────────────────────
146
+ private var blurEnabled = true
147
+ private var autoUpdate = true
148
+ private var workerBusy = false // prevent queuing multiple blur jobs
149
+ private var lastCaptureNano = 0L // throttle: nano timestamp of last capture
89
150
 
90
- private val frameCallback = Choreographer.FrameCallback {
91
- frameScheduled = false
92
- if (isAttachedToWindow && blurEnabled) captureAndBlur()
93
- }
151
+ // ── PreDraw listener fires PRE-DRAW, guaranteed before RenderThread ─────
94
152
 
95
153
  private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
96
- if (!frameScheduled && blurEnabled && autoUpdate) {
97
- frameScheduled = true
98
- Choreographer.getInstance().postFrameCallback(frameCallback)
154
+ if (blurEnabled && autoUpdate && !workerBusy) {
155
+ val now = System.nanoTime()
156
+ if (now - lastCaptureNano >= FRAME_INTERVAL_NS) {
157
+ lastCaptureNano = now
158
+ captureNow() // synchronous capture in pre-draw — safe on all API levels
159
+ }
99
160
  }
100
- true
161
+ true // MUST return true — never block the draw pass
101
162
  }
102
163
 
103
164
  // ── Paint objects ─────────────────────────────────────────────────────────
@@ -111,9 +172,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
111
172
 
112
173
  init {
113
174
  setWillNotDraw(false)
114
- // outlineProvider = BACKGROUND uses ReactViewBackgroundDrawable.getOutline()
115
- // which correctly handles all RN borderRadius variants automatically.
116
- // clipToOutline is set to true only when a non-zero radius is applied.
175
+ // outlineProvider = BACKGROUND: ReactViewBackgroundDrawable.getOutline()
176
+ // handles all RN borderRadius variants automatically. clipToOutline only
177
+ // enabled when non-zero radius is set (avoids GPU clip stack issues).
117
178
  outlineProvider = ViewOutlineProvider.BACKGROUND
118
179
  }
119
180
 
@@ -124,16 +185,14 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
124
185
  blurRoot = findBlurRoot()
125
186
  safeAddPreDrawListener()
126
187
  generateNoiseBitmap()
127
- scheduleFrame()
128
188
  }
129
189
 
130
190
  override fun onDetachedFromWindow() {
131
191
  safeRemovePreDrawListener()
132
- Choreographer.getInstance().removeFrameCallback(frameCallback)
133
- frameScheduled = false
134
- isCapturing = false
135
- blurRoot = null
136
- readyBitmap = null
192
+ isCapturing = false
193
+ workerBusy = false
194
+ blurRoot = null
195
+ readyBitmap = null
137
196
  noiseBitmap?.recycle(); noiseBitmap = null
138
197
  workerHandler.post {
139
198
  captureBitmap?.recycle(); captureBitmap = null
@@ -146,11 +205,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
146
205
  super.onSizeChanged(w, h, oldw, oldh)
147
206
  if (w > 0 && h > 0) {
148
207
  readyBitmap = null
208
+ workerBusy = false
149
209
  workerHandler.post {
150
210
  captureBitmap?.recycle(); captureBitmap = null
151
211
  scaledBitmap?.recycle(); scaledBitmap = null
152
212
  }
153
- scheduleFrame()
154
213
  }
155
214
  }
156
215
 
@@ -158,14 +217,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
158
217
  super.onWindowFocusChanged(hasWindowFocus)
159
218
  if (hasWindowFocus && blurEnabled && autoUpdate) {
160
219
  safeAddPreDrawListener()
161
- scheduleFrame()
162
220
  }
163
221
  }
164
222
 
165
- // ── draw() — skip self during root capture ────────────────────────────────
223
+ // ── draw() — skip self during capture ────────────────────────────────────
166
224
 
167
225
  override fun draw(canvas: Canvas) {
168
- if (isCapturing) return // prevents capturing own blur output
226
+ if (isCapturing) return
169
227
  super.draw(canvas)
170
228
  }
171
229
 
@@ -176,7 +234,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
176
234
  val w = width.toFloat(); if (w <= 0f) return
177
235
  val h = height.toFloat(); if (h <= 0f) return
178
236
 
179
- // Show overlay as placeholder while first blur is loading
180
237
  val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
181
238
  if (Color.alpha(overlayColor) > 0) {
182
239
  overlayPaint.color = overlayColor
@@ -186,15 +243,15 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
186
243
  return
187
244
  }
188
245
 
189
- // Step 1: save layer for progressive mask compositing
246
+ // Progressive mask layer
190
247
  val saveCount = if (progressiveDirection != PROGRESSIVE_NONE)
191
248
  canvas.saveLayer(0f, 0f, w, h, null)
192
249
  else -1
193
250
 
194
- // Step 2: blurred bitmap — fills entire view
251
+ // Blurred bitmap
195
252
  canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
196
253
 
197
- // Step 3: progressive alpha mask (fades blur across view)
254
+ // Progressive alpha mask
198
255
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
199
256
  buildProgressiveShader(w, h)?.let { shader ->
200
257
  maskPaint.shader = shader
@@ -203,33 +260,31 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
203
260
  canvas.restoreToCount(saveCount)
204
261
  }
205
262
 
206
- // Step 4: overlay tint
263
+ // Overlay tint
207
264
  if (Color.alpha(overlayColor) > 0) {
208
265
  overlayPaint.color = overlayColor
209
266
  canvas.drawRect(0f, 0f, w, h, overlayPaint)
210
267
  }
211
268
 
212
- // Step 5: noise grain
269
+ // Noise grain
213
270
  noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
214
271
  noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
215
272
  noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
216
273
  canvas.drawRect(0f, 0f, w, h, noisePaint)
217
274
  }
218
275
 
219
- // Step 6: redraw ReactViewBackgroundDrawable ON TOP of blur.
220
- // View.draw() drew the background BEFORE onDraw(), but our bitmap
221
- // covered it. Redrawing here makes borders/borderColor/borderRadius
222
- // appear on top of the blur — not hidden underneath it.
276
+ // Redraw ReactViewBackgroundDrawable ON TOP of blur so borders/radius
277
+ // are visible above the blur layer, not hidden underneath it.
223
278
  background?.draw(canvas)
224
279
  }
225
280
 
226
- // ── Capture + blur pipeline ────────────────────────────────────────────────
281
+ // ── Capture called in OnPreDrawListener (pre-draw, main thread) ─────────
227
282
 
228
- private fun captureAndBlur() {
229
- if (isCapturing) return
283
+ private fun captureNow() {
284
+ if (isCapturing || workerBusy) return
230
285
  val root = blurRoot ?: return
231
- val vw = width; if (vw <= 0) return
232
- val vh = height; if (vh <= 0) return
286
+ val vw = width; if (vw <= 0) return
287
+ val vh = height; if (vh <= 0) return
233
288
 
234
289
  val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
235
290
  val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
@@ -241,10 +296,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
241
296
  val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
242
297
 
243
298
  val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
244
- val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
245
299
 
246
- // isCapturing = true → our draw() is a no-op → root.draw() skips us
247
- // → capture contains ONLY the content behind us (not our own blur output)
300
+ // isCapturing = true → draw() returns immediately → root.draw() skips us
301
+ // → capture contains ONLY the content behind us, not our own blur output
248
302
  isCapturing = true
249
303
  val c = Canvas(capture)
250
304
  c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
@@ -257,11 +311,14 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
257
311
  }
258
312
  isCapturing = false
259
313
 
260
- // Blur on worker thread — never blocks main/RenderThread
314
+ // Hand off to worker thread — main thread returns immediately
315
+ workerBusy = true
261
316
  val captureRef = capture
262
317
  val radius = blurRadiusFromAmount(blurAmount)
263
318
 
264
319
  workerHandler.post {
320
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
321
+
265
322
  // Downsample
266
323
  Canvas(scaled).drawBitmap(
267
324
  captureRef,
@@ -269,37 +326,45 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
269
326
  Rect(0, 0, scaled.width, scaled.height),
270
327
  capturePaint
271
328
  )
272
- // Multi-pass StackBlur (pure Kotlin, no deprecated APIs, all API levels)
273
- repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
274
329
 
275
- // Atomic @Volatile swap RenderThread always reads a complete bitmap
330
+ // Multi-pass StackBlurpure Kotlin, no deprecated APIs, all versions
331
+ val r = radius.toInt().coerceAtLeast(1)
332
+ repeat(BLUR_ROUNDS) { stackBlur(scaled, r) }
333
+
334
+ // @Volatile atomic swap — RenderThread always sees a complete bitmap
276
335
  readyBitmap = scaled
336
+ workerBusy = false
337
+
277
338
  mainHandler.post { invalidate() }
278
339
  }
279
340
  }
280
341
 
281
342
  // ── StackBlur ─────────────────────────────────────────────────────────────
282
- // Mario Klingemann's algorithm — O(w×h) regardless of radius.
283
- // No RenderScript, no deprecated APIs. Works on all Android versions.
284
343
 
285
344
  private fun stackBlur(bmp: Bitmap, radius: Int) {
286
- val r = radius.coerceIn(1, 254)
287
- val w = bmp.width; val h = bmp.height
288
- val pixels = IntArray(w * h)
289
- bmp.getPixels(pixels, 0, w, 0, 0, w, h)
290
- val div = r + r + 1
291
- val wm = w - 1; val hm = h - 1
292
- val divSumSq = ((div + 1) shr 1).let { it * it }
293
- val dv = IntArray(256 * divSumSq) { it / divSumSq }
294
- val vmin = IntArray(maxOf(w, h))
295
- val rStack = IntArray(div); val gStack = IntArray(div); val bStack = IntArray(div)
345
+ val r = radius.coerceIn(1, 254)
346
+ val w = bmp.width
347
+ val h = bmp.height
348
+ val px = IntArray(w * h)
349
+ bmp.getPixels(px, 0, w, 0, 0, w, h)
350
+
351
+ val div = r + r + 1
352
+ val wm = w - 1
353
+ val hm = h - 1
354
+ val ds = (div + 1) shr 1
355
+ val dsSq = ds * ds
356
+ val dv = IntArray(256 * dsSq) { it / dsSq }
357
+ val vmin = IntArray(maxOf(w, h))
358
+ val rStack = IntArray(div)
359
+ val gStack = IntArray(div)
360
+ val bStack = IntArray(div)
361
+
362
+ // Horizontal pass
296
363
  var yi = 0
297
364
  for (y in 0 until h) {
298
365
  var rSum = 0; var gSum = 0; var bSum = 0
299
366
  var rOut = 0; var gOut = 0; var bOut = 0
300
- var p = pixels[yi]
301
- var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
302
- val ds = (div + 1) shr 1
367
+ var p = px[yi]; var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
303
368
  for (i in 0 until ds) {
304
369
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
305
370
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
@@ -307,20 +372,20 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
307
372
  }
308
373
  for (i in 1 until ds) {
309
374
  val xi = if (i <= wm) i else wm
310
- p = pixels[yi + xi]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
375
+ p = px[yi + xi]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
311
376
  rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
312
377
  rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
313
378
  }
314
379
  var si = r
315
380
  for (x in 0 until w) {
316
- pixels[yi + x] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
381
+ px[yi + x] = -0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum]
317
382
  rSum -= rOut; gSum -= gOut; bSum -= bOut
318
383
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
319
384
  var sip = si + ds; if (sip >= div) sip -= div
320
385
  pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
321
386
  rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
322
387
  vmin[x] = if (x + r < wm) x + r + 1 else wm
323
- val sp = pixels[yi + vmin[x]]; val vp = pixels[yi + (if (x > r) x - r else 0)]
388
+ val sp = px[yi + vmin[x]]; val vp = px[yi + if (x > r) x - r else 0]
324
389
  rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
325
390
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
326
391
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
@@ -329,11 +394,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
329
394
  }
330
395
  yi += w
331
396
  }
397
+
398
+ // Vertical pass
332
399
  for (x in 0 until w) {
333
400
  var rSum = 0; var gSum = 0; var bSum = 0
334
401
  var rOut = 0; var gOut = 0; var bOut = 0
335
- val ds = (div + 1) shr 1
336
- var p = pixels[x]; var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
402
+ var p = px[x]; var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
337
403
  for (i in 0 until ds) {
338
404
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
339
405
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
@@ -341,20 +407,20 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
341
407
  }
342
408
  for (i in 1 until ds) {
343
409
  val yi2 = if (i <= hm) i * w else hm * w
344
- p = pixels[x + yi2]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
410
+ p = px[x + yi2]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
345
411
  rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
346
412
  rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
347
413
  }
348
414
  var si = r
349
415
  for (y in 0 until h) {
350
- pixels[x + y * w] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
416
+ px[x + y * w] = -0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum]
351
417
  rSum -= rOut; gSum -= gOut; bSum -= bOut
352
418
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
353
419
  var sip = si + ds; if (sip >= div) sip -= div
354
420
  pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
355
421
  rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
356
422
  vmin[y] = if (y + r < hm) (y + r + 1) * w else hm * w
357
- val sp = pixels[x + vmin[y]]; val vp = pixels[x + (if (y > r) (y - r) * w else 0)]
423
+ val sp = px[x + vmin[y]]; val vp = px[x + if (y > r) (y - r) * w else 0]
358
424
  rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
359
425
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
360
426
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
@@ -362,20 +428,20 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
362
428
  if (++si >= div) si = 0
363
429
  }
364
430
  }
365
- bmp.setPixels(pixels, 0, w, 0, 0, w, h)
431
+ bmp.setPixels(px, 0, w, 0, 0, w, h)
366
432
  }
367
433
 
368
434
  // ── Progressive shader ────────────────────────────────────────────────────
369
435
 
370
436
  private fun buildProgressiveShader(w: Float, h: Float): Shader? {
371
- val sc = Color.argb((progressiveStartIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
372
- val ec = Color.argb((progressiveEndIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
437
+ val sc = Color.argb((progressiveStartIntensity.coerceIn(0f, 1f) * 255).toInt(), 0, 0, 0)
438
+ val ec = Color.argb((progressiveEndIntensity.coerceIn(0f, 1f) * 255).toInt(), 0, 0, 0)
373
439
  return when (progressiveDirection) {
374
- PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h,sc,ec,Shader.TileMode.CLAMP)
375
- PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
376
- PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
377
- PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,sc,ec,Shader.TileMode.CLAMP)
378
- PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,min(w,h)/2f,sc,ec,Shader.TileMode.CLAMP)
440
+ PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f, 0f, 0f, h, sc, ec, Shader.TileMode.CLAMP)
441
+ PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f, h, 0f, 0f, sc, ec, Shader.TileMode.CLAMP)
442
+ PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f, 0f, w, 0f, sc, ec, Shader.TileMode.CLAMP)
443
+ PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w, 0f, 0f, 0f, sc, ec, Shader.TileMode.CLAMP)
444
+ PROGRESSIVE_RADIAL -> RadialGradient(w / 2f, h / 2f, min(w, h) / 2f, sc, ec, Shader.TileMode.CLAMP)
379
445
  else -> null
380
446
  }
381
447
  }
@@ -396,7 +462,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
396
462
  // ── Public setters ─────────────────────────────────────────────────────────
397
463
 
398
464
  fun setBlurAmount(amount: Float) {
399
- blurAmount = amount.coerceIn(0f, 100f); scheduleFrame()
465
+ blurAmount = amount.coerceIn(0f, 100f); invalidate()
400
466
  }
401
467
 
402
468
  fun setOverlayColor(colorString: String?) {
@@ -425,17 +491,16 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
425
491
  }; invalidate()
426
492
  }
427
493
 
428
- fun setProgressiveStartIntensity(v: Float) { progressiveStartIntensity = v.coerceIn(0f,1f); invalidate() }
429
- fun setProgressiveEndIntensity(v: Float) { progressiveEndIntensity = v.coerceIn(0f,1f); invalidate() }
430
- fun setNoiseFactor(v: Float) { noiseFactor = v.coerceIn(0f,1f); invalidate() }
494
+ fun setProgressiveStartIntensity(v: Float) { progressiveStartIntensity = v.coerceIn(0f, 1f); invalidate() }
495
+ fun setProgressiveEndIntensity(v: Float) { progressiveEndIntensity = v.coerceIn(0f, 1f); invalidate() }
496
+ fun setNoiseFactor(v: Float) { noiseFactor = v.coerceIn(0f, 1f); invalidate() }
431
497
 
432
498
  fun applyBlurEnabled(enabled: Boolean) {
433
499
  blurEnabled = enabled
434
- if (enabled) { safeAddPreDrawListener(); scheduleFrame() }
500
+ if (enabled) safeAddPreDrawListener()
435
501
  else {
436
502
  safeRemovePreDrawListener()
437
- Choreographer.getInstance().removeFrameCallback(frameCallback)
438
- frameScheduled = false; readyBitmap = null; invalidate()
503
+ workerBusy = false; readyBitmap = null; invalidate()
439
504
  }
440
505
  }
441
506
 
@@ -446,13 +511,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
446
511
 
447
512
  // ── Helpers ────────────────────────────────────────────────────────────────
448
513
 
449
- private fun scheduleFrame() {
450
- if (!frameScheduled && blurEnabled) {
451
- frameScheduled = true
452
- Choreographer.getInstance().postFrameCallback(frameCallback)
453
- }
454
- }
455
-
456
514
  private fun safeAddPreDrawListener() {
457
515
  val root = blurRoot ?: return
458
516
  val vto = root.viewTreeObserver
@@ -489,7 +547,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
489
547
 
490
548
  private fun blurRadiusFromAmount(amount: Float): Float {
491
549
  val t = amount.coerceIn(0f, 100f) / 100f
492
- return 2f + t * 22f // 2–24, with BLUR_ROUNDS=4 effective spread ≈ 4–48px
550
+ return 2f + t * 22f // 2–24px per pass, × BLUR_ROUNDS = wide spread
493
551
  }
494
552
 
495
553
  private fun parseHexColor(s: String): Int? {
@@ -502,13 +560,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
502
560
  3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
503
561
  hex[1].toString().repeat(2).toInt(16),
504
562
  hex[2].toString().repeat(2).toInt(16))
505
- 6 -> Color.argb(255, hex.substring(0,2).toInt(16),
506
- hex.substring(2,4).toInt(16),
507
- hex.substring(4,6).toInt(16))
508
- 8 -> Color.argb(hex.substring(6,8).toInt(16),
509
- hex.substring(0,2).toInt(16),
510
- hex.substring(2,4).toInt(16),
511
- hex.substring(4,6).toInt(16))
563
+ 6 -> Color.argb(255, hex.substring(0, 2).toInt(16),
564
+ hex.substring(2, 4).toInt(16),
565
+ hex.substring(4, 6).toInt(16))
566
+ 8 -> Color.argb(hex.substring(6, 8).toInt(16),
567
+ hex.substring(0, 2).toInt(16),
568
+ hex.substring(2, 4).toInt(16),
569
+ hex.substring(4, 6).toInt(16))
512
570
  else -> null
513
571
  }
514
572
  } catch (_: NumberFormatException) { null }
@@ -517,8 +575,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
517
575
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
518
576
 
519
577
  companion object {
520
- private const val DOWNSAMPLE = 2f // 1/4 pixels — higher quality than legacy
521
- private const val BLUR_ROUNDS = 4 // 4 passes wider spread than legacy's 3
578
+ private const val DOWNSAMPLE = 2f // 1/4 pixels
579
+ private const val BLUR_ROUNDS = 3 // passes per frame
580
+ private const val FRAME_INTERVAL_NS = 16_666_666L // ~60 fps cap
581
+
522
582
  const val PROGRESSIVE_NONE = 0
523
583
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
524
584
  const val PROGRESSIVE_BOTTOM_TO_TOP = 2
@@ -526,4 +586,4 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
526
586
  const val PROGRESSIVE_RIGHT_TO_LEFT = 4
527
587
  const val PROGRESSIVE_RADIAL = 5
528
588
  }
529
- }
589
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "React Native package implementing Blur View in iOS and Android",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -53,8 +53,12 @@
53
53
  "blurvibe",
54
54
  "blureffect",
55
55
  "react-native-blur-vibe",
56
+ "reactnativeblurvibe",
57
+ "reactnativeblur",
58
+ "reactnativeblurview",
56
59
  "blur-vibe",
57
60
  "glassmorphism",
61
+ "frostedglass",
58
62
  "ios",
59
63
  "android",
60
64
  "overlay",