react-native-blur-vibe 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # React Native Blur-Vibe
2
2
 
3
- <img width="1500" height="500" alt="github-banner" src="https://github.com/user-attachments/assets/78b2e5ec-5b57-48c0-b984-69cb57cbcf26" />
3
+ <a href="https://www.npmjs.com/package/react-native-blur-vibe"><img width="100%" height="35%" alt="github-banner" src="https://github.com/user-attachments/assets/78b2e5ec-5b57-48c0-b984-69cb57cbcf26" /></a>
4
4
  <br></br>
5
5
 
6
- A modern, actively maintained blur view for React Native. Works on **iOS** and **Android** with full New Architecture (Fabric) support.
6
+ A modern, actively maintained blur view for React Native. Works on **iOS** and **Android** with both Old (Paper) and New (Fabric) Architecture support.
7
7
 
8
8
  > The key difference from other blur libraries: `overlayColor` works on **both iOS and Android** — letting you control blur visibility the same way CSS `backdrop-filter` + `background-color` works on the web.
9
9
 
@@ -7,6 +7,7 @@ import android.graphics.Canvas
7
7
  import android.graphics.Color
8
8
  import android.graphics.LinearGradient
9
9
  import android.graphics.Paint
10
+ import android.graphics.PixelFormat
10
11
  import android.graphics.PorterDuff
11
12
  import android.graphics.PorterDuffXfermode
12
13
  import android.graphics.RadialGradient
@@ -17,12 +18,10 @@ import android.os.Build
17
18
  import android.os.Handler
18
19
  import android.os.HandlerThread
19
20
  import android.os.Looper
20
- import android.renderscript.Allocation
21
- import android.renderscript.Element
22
- import android.renderscript.RenderScript
23
- import android.renderscript.ScriptIntrinsicBlur
24
21
  import android.util.TypedValue
25
22
  import android.view.Choreographer
23
+ import android.view.PixelCopy
24
+ import android.view.Surface
26
25
  import android.view.View
27
26
  import android.view.ViewGroup
28
27
  import android.view.ViewOutlineProvider
@@ -57,48 +56,40 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
57
56
  private var noiseBitmap: Bitmap? = null
58
57
  private val noisePaint = Paint()
59
58
 
60
- // ── Double-buffer bitmap pool ─────────────────────────────────────────────
59
+ // ── Bitmap double-buffer ──────────────────────────────────────────────────
60
+ //
61
+ // pixelCopyBitmap: written by PixelCopy (on its own callback thread)
62
+ // scaledBitmap: written by workerThread after downsampling
63
+ // readyBitmap: @Volatile — RenderThread reads this in onDraw()
61
64
 
62
- private var captureBitmap: Bitmap? = null
63
- private var scaledBitmap: Bitmap? = null
64
- @Volatile private var readyBitmap: Bitmap? = null
65
+ private var pixelCopyBitmap: Bitmap? = null
66
+ private var scaledBitmap: Bitmap? = null
67
+ @Volatile private var readyBitmap: Bitmap? = null
65
68
 
66
69
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
67
70
  private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
68
71
 
69
- // ── Worker thread — blur runs here, main thread never blocks ──────────────
72
+ // ── Worker thread ─────────────────────────────────────────────────────────
70
73
 
71
74
  private val workerThread = HandlerThread("BlurVibeWorker31-${hashCode()}")
72
75
  .also { it.start() }
73
76
  private val workerHandler = Handler(workerThread.looper)
74
77
  private val mainHandler = Handler(Looper.getMainLooper())
75
78
 
76
- // ── RenderScript (deprecated API 31 but still functional through API 35) ──
77
-
78
- @Suppress("DEPRECATION")
79
- private var rs: RenderScript? = null
80
- @Suppress("DEPRECATION")
81
- private var blurScript: ScriptIntrinsicBlur? = null
82
- @Suppress("DEPRECATION")
83
- private var inAlloc: Allocation? = null
84
- @Suppress("DEPRECATION")
85
- private var outAlloc: Allocation? = null
79
+ // ── Root / window ─────────────────────────────────────────────────────────
86
80
 
87
- // ── Root view ─────────────────────────────────────────────────────────────
88
-
89
- private var blurRoot: ViewGroup? = null
90
- private val myLoc = IntArray(2)
91
- private val rootLoc = IntArray(2)
81
+ private var blurRoot: ViewGroup? = null
82
+ private val myLoc = IntArray(2)
83
+ private val rootLoc = IntArray(2)
92
84
 
93
85
  // ── State ─────────────────────────────────────────────────────────────────
94
86
 
95
- // isCapturing: suppresses our own draw() during root.draw() capture
96
- // so we don't paint stale blur into the capture bitmap (static blur bug)
97
87
  var isCapturing = false
98
88
  private set
99
89
  private var blurEnabled = true
100
90
  private var autoUpdate = true
101
91
  private var frameScheduled = false
92
+ private var pixelCopyInFlight = false
102
93
 
103
94
  // ── Choreographer gate ────────────────────────────────────────────────────
104
95
 
@@ -126,13 +117,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
126
117
 
127
118
  init {
128
119
  setWillNotDraw(false)
129
- // DO NOT call setBackgroundColor — it replaces ReactViewGroup's
130
- // ReactViewBackgroundDrawable, killing all RN style prop handling.
131
- //
132
120
  // outlineProvider = BACKGROUND: ReactViewBackgroundDrawable implements
133
- // getOutline() for all RN borderRadius variants. clipToOutline=false
134
- // by default — only enabled when a non-zero radius is actually set,
135
- // to avoid GPU clip stack issues with overflow:hidden + Reanimated.
121
+ // getOutline() correctly for all RN borderRadius variants automatically.
136
122
  outlineProvider = ViewOutlineProvider.BACKGROUND
137
123
  }
138
124
 
@@ -143,22 +129,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
143
129
  blurRoot = findBlurRoot()
144
130
  safeAddPreDrawListener()
145
131
  generateNoiseBitmap()
146
- workerHandler.post { initRenderScript() }
147
132
  scheduleFrame()
148
133
  }
149
134
 
150
135
  override fun onDetachedFromWindow() {
151
136
  safeRemovePreDrawListener()
152
137
  Choreographer.getInstance().removeFrameCallback(frameCallback)
153
- frameScheduled = false
154
- isCapturing = false
155
- blurRoot = null
156
- readyBitmap = null
138
+ frameScheduled = false
139
+ isCapturing = false
140
+ pixelCopyInFlight = false
141
+ blurRoot = null
142
+ readyBitmap = null
157
143
  noiseBitmap?.recycle(); noiseBitmap = null
158
- workerHandler.post {
159
- releaseBitmapPool()
160
- releaseRenderScript()
161
- }
144
+ workerHandler.post { releaseBitmapPool() }
162
145
  super.onDetachedFromWindow()
163
146
  }
164
147
 
@@ -180,10 +163,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
180
163
  }
181
164
 
182
165
  // ── draw() — no-op during root capture ────────────────────────────────────
183
- //
184
- // Prevents stale blur output from being captured into the background bitmap.
185
- // When isCapturing=true, root.draw() is in progress — we skip ourselves
186
- // so only the content BEHIND us is captured.
187
166
 
188
167
  override fun draw(canvas: Canvas) {
189
168
  if (isCapturing) return
@@ -198,12 +177,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
198
177
  val h = height.toFloat(); if (h <= 0f) return
199
178
 
200
179
  val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
201
- // No blur ready yet — draw overlay only so view isn't invisible
180
+ // No blur ready yet — show overlay color as placeholder
202
181
  if (Color.alpha(overlayColor) > 0) {
203
182
  overlayPaint.color = overlayColor
204
183
  canvas.drawRect(0f, 0f, w, h, overlayPaint)
205
184
  }
206
- super.onDraw(canvas)
185
+ // Redraw border on top even when no blur ready
186
+ background?.draw(canvas)
207
187
  return
208
188
  }
209
189
 
@@ -212,7 +192,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
212
192
  canvas.saveLayer(0f, 0f, w, h, null)
213
193
  } else -1
214
194
 
215
- // Step 2: draw blurred bitmap
195
+ // Step 2: blurred bitmap
216
196
  canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
217
197
 
218
198
  // Step 3: progressive alpha mask
@@ -236,108 +216,250 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
236
216
  noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
237
217
  canvas.drawRect(0f, 0f, w, h, noisePaint)
238
218
  }
239
-
240
- // Step 6: let ReactViewGroup draw borders/radius on top
241
- super.onDraw(canvas)
219
+ background?.draw(canvas)
242
220
  }
243
221
 
244
- // ── Capture + blur pipeline ───────────────────────────────────────────────
222
+ // ── Capture pipeline PixelCopy (API 31+) ────────────────────────────────
245
223
 
246
224
  private fun captureAndBlur() {
247
- if (isCapturing) return
248
- val root = blurRoot ?: return
249
- val rw = root.width; if (rw <= 0) return
250
- val rh = root.height; if (rh <= 0) return
251
- val vw = width; if (vw <= 0) return
252
- val vh = height; if (vh <= 0) return
225
+ if (isCapturing || pixelCopyInFlight) return
226
+ val root = blurRoot ?: return
227
+ val vw = width; if (vw <= 0) return
228
+ val vh = height; if (vh <= 0) return
229
+
230
+ // Compute this view's screen rect for PixelCopy
231
+ getLocationInWindow(myLoc)
232
+ root.getLocationInWindow(rootLoc)
233
+
234
+ // Screen-space rect of the CONTENT BEHIND this view (use root location
235
+ // as origin since PixelCopy works in window coordinates)
236
+ val srcRect = Rect(
237
+ myLoc[0], myLoc[1],
238
+ myLoc[0] + vw, myLoc[1] + vh
239
+ )
253
240
 
254
241
  val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
255
242
  val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
256
243
 
257
- // Compute offset window coords, correct for split-screen/freeform/PiP
244
+ val destBitmap = reuseBitmap(pixelCopyBitmap, vw, vh)
245
+ .also { pixelCopyBitmap = it }
246
+
247
+ // Hide ourselves during PixelCopy so we capture ONLY content behind us
248
+ isCapturing = true
249
+ pixelCopyInFlight = true
250
+
251
+ val window = (context as? android.app.Activity)?.window
252
+ ?: run {
253
+ // Fallback to root.draw() if window not available
254
+ isCapturing = false
255
+ pixelCopyInFlight = false
256
+ captureWithRootDraw()
257
+ return
258
+ }
259
+
260
+ PixelCopy.request(
261
+ window,
262
+ srcRect,
263
+ destBitmap,
264
+ { result ->
265
+ isCapturing = false
266
+ pixelCopyInFlight = false
267
+
268
+ if (result != PixelCopy.SUCCESS) {
269
+ // PixelCopy failed — fall back to root.draw()
270
+ mainHandler.post { captureWithRootDraw() }
271
+ return@request
272
+ }
273
+
274
+ // Blur on worker thread
275
+ val captureRef = destBitmap
276
+ workerHandler.post {
277
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
278
+
279
+ // Downsample
280
+ Canvas(scaled).drawBitmap(
281
+ captureRef,
282
+ Rect(0, 0, captureRef.width, captureRef.height),
283
+ Rect(0, 0, scaled.width, scaled.height),
284
+ capturePaint
285
+ )
286
+
287
+ // Multi-pass software Gaussian blur
288
+ val radius = blurRadiusFromAmount(blurAmount)
289
+ repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
290
+
291
+ readyBitmap = scaled
292
+ mainHandler.post { invalidate() }
293
+ }
294
+ },
295
+ mainHandler
296
+ )
297
+ }
298
+
299
+ // ── Fallback: root.draw() when PixelCopy unavailable ─────────────────────
300
+
301
+ private fun captureWithRootDraw() {
302
+ if (isCapturing) return
303
+ val root = blurRoot ?: return
304
+ val vw = width; if (vw <= 0) return
305
+ val vh = height; if (vh <= 0) return
306
+ val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
307
+ val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
308
+
258
309
  root.getLocationInWindow(rootLoc)
259
310
  getLocationInWindow(myLoc)
260
311
  val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
261
312
  val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
262
313
 
263
- val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
264
- val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
314
+ val capture = reuseBitmap(pixelCopyBitmap, vw, vh).also { pixelCopyBitmap = it }
315
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
265
316
 
266
- // Capture — isCapturing suppresses our own draw() so root.draw() skips us
267
317
  isCapturing = true
268
318
  val c = Canvas(capture)
269
319
  c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
270
320
  c.translate(-offsetX, -offsetY)
271
- try {
272
- root.draw(c)
273
- } catch (_: Exception) {
274
- isCapturing = false
275
- return
276
- }
321
+ try { root.draw(c) } catch (_: Exception) { isCapturing = false; return }
277
322
  isCapturing = false
278
323
 
279
- // Downsample + blur on worker thread (never blocks main/RenderThread)
280
324
  val captureRef = capture
281
- val scaledRef = scaled
282
- val radius = blurRadiusFromAmount(blurAmount)
283
-
284
325
  workerHandler.post {
285
- // Downsample
286
- Canvas(scaledRef).drawBitmap(
326
+ Canvas(scaled).drawBitmap(
287
327
  captureRef,
288
328
  Rect(0, 0, captureRef.width, captureRef.height),
289
- Rect(0, 0, scaledRef.width, scaledRef.height),
329
+ Rect(0, 0, scaled.width, scaled.height),
290
330
  capturePaint
291
331
  )
292
- // Multi-pass blur for wide frosted-glass spread
293
- repeat(BLUR_ROUNDS) { blurBitmap(scaledRef, radius) }
294
-
295
- // Atomic swap: readyBitmap is @Volatile — RenderThread sees new value immediately
296
- // We never mutate scaledRef after this point until the next capture starts
297
- readyBitmap = scaledRef
298
-
332
+ val radius = blurRadiusFromAmount(blurAmount)
333
+ repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
334
+ readyBitmap = scaled
299
335
  mainHandler.post { invalidate() }
300
336
  }
301
337
  }
302
338
 
303
- // ── RenderScript blur ─────────────────────────────────────────────────────
304
-
305
- @Suppress("DEPRECATION")
306
- private fun initRenderScript() {
307
- try {
308
- rs = RenderScript.create(context)
309
- blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
310
- } catch (_: Exception) {}
311
- }
312
-
313
- @Suppress("DEPRECATION")
314
- private fun blurBitmap(bmp: Bitmap, radius: Float) {
315
- val r = rs ?: return softwareBlur(bmp, radius)
316
- val sc = blurScript ?: return softwareBlur(bmp, radius)
317
- try {
318
- val iA = reuseAlloc(inAlloc, bmp, r).also { inAlloc = it }
319
- val oA = reuseAlloc(outAlloc, bmp, r).also { outAlloc = it }
320
- iA.copyFrom(bmp)
321
- sc.setRadius(radius.coerceIn(1f, 25f))
322
- sc.setInput(iA)
323
- sc.forEach(oA)
324
- oA.copyTo(bmp)
325
- } catch (_: Exception) { softwareBlur(bmp, radius) }
326
- }
339
+ // ── Stack blur (pure Kotlin, no deprecated APIs) ──────────────────────────
340
+ //
341
+ // Mario Klingemann's StackBlur — O(w×h) regardless of radius.
342
+ // Fast, no RenderScript, works on all API levels, zero deprecation warnings.
343
+ // Used by many production apps including Facebook's Fresco library.
344
+ // radius clamped 1–254 (algorithm limit).
345
+
346
+ private fun stackBlur(bmp: Bitmap, radius: Int) {
347
+ val r = radius.coerceIn(1, 254)
348
+ val w = bmp.width
349
+ val h = bmp.height
350
+ val pixels = IntArray(w * h)
351
+ bmp.getPixels(pixels, 0, w, 0, 0, w, h)
352
+
353
+ val div = r + r + 1
354
+ val wm = w - 1
355
+ val hm = h - 1
356
+ val wh = w * h
357
+ val divSum = (div + 1) shr 1
358
+ val divSumSq = divSum * divSum
359
+ val dv = IntArray(256 * divSumSq) { it / divSumSq }
360
+
361
+ var yi = 0
362
+ val vmin = IntArray(maxOf(w, h))
363
+ val vmax = IntArray(maxOf(w, h))
364
+
365
+ val rStack = IntArray(div)
366
+ val gStack = IntArray(div)
367
+ val bStack = IntArray(div)
368
+
369
+ for (y in 0 until h) {
370
+ var rSum = 0; var gSum = 0; var bSum = 0
371
+ var rOut = 0; var gOut = 0; var bOut = 0
372
+
373
+ var p = pixels[yi]
374
+ var pr = (p shr 16) and 0xFF
375
+ var pg = (p shr 8) and 0xFF
376
+ var pb = p and 0xFF
377
+
378
+ for (i in 0 until divSum) {
379
+ rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
380
+ rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
381
+ rOut += pr; gOut += pg; bOut += pb
382
+ }
383
+ for (i in 1 until divSum) {
384
+ val ii = if (i <= wm) i else wm
385
+ p = pixels[yi + ii]
386
+ pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
387
+ rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
388
+ rSum += pr * (divSum - i)
389
+ gSum += pg * (divSum - i)
390
+ bSum += pb * (divSum - i)
391
+ }
327
392
 
328
- private fun softwareBlur(bmp: Bitmap, radius: Float) {
329
- val p = Paint(Paint.ANTI_ALIAS_FLAG).apply {
330
- maskFilter = android.graphics.BlurMaskFilter(radius, android.graphics.BlurMaskFilter.Blur.NORMAL)
393
+ var si = r
394
+ for (x in 0 until w) {
395
+ pixels[yi + x] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
396
+ rSum -= rOut; gSum -= gOut; bSum -= bOut
397
+ rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
398
+ var sip = si + divSum
399
+ if (sip >= div) sip -= div
400
+ pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
401
+ rOut += pr; gOut += pg; bOut += pb
402
+ rSum += rOut; gSum += gOut; bSum += bOut
403
+ if (x < r) vmin[x] = x + r + 1 else if (x + r < wm) vmin[x] = x + r + 1 else vmin[x] = wm
404
+ if (x > r) vmax[x] = x - r else vmax[x] = 0
405
+ val sp = pixels[yi + vmin[x]]
406
+ val vp = pixels[yi + vmax[x]]
407
+ rStack[sip] = (sp shr 16) and 0xFF
408
+ gStack[sip] = (sp shr 8) and 0xFF
409
+ bStack[sip] = sp and 0xFF
410
+ rOut += rStack[sip] - ((vp shr 16) and 0xFF)
411
+ gOut += gStack[sip] - ((vp shr 8) and 0xFF)
412
+ bOut += bStack[sip] - (vp and 0xFF)
413
+ if (++si >= div) si = 0
414
+ }
415
+ yi += w
331
416
  }
332
- Canvas(bmp).drawBitmap(bmp, 0f, 0f, p)
333
- }
334
417
 
335
- @Suppress("DEPRECATION")
336
- private fun releaseRenderScript() {
337
- inAlloc?.destroy(); inAlloc = null
338
- outAlloc?.destroy(); outAlloc = null
339
- blurScript?.destroy(); blurScript = null
340
- rs?.destroy(); rs = null
418
+ var xi = 0
419
+ for (x in 0 until w) {
420
+ var rSum = 0; var gSum = 0; var bSum = 0
421
+ var rOut = 0; var gOut = 0; var bOut = 0
422
+ var yp = -r * w
423
+ var p = pixels[xi]
424
+ var pr = (p shr 16) and 0xFF
425
+ var pg = (p shr 8) and 0xFF
426
+ var pb = p and 0xFF
427
+ for (i in 0 until divSum) {
428
+ rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
429
+ rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
430
+ rOut += pr; gOut += pg; bOut += pb
431
+ }
432
+ for (i in 1..r) {
433
+ if (i <= hm) yp += w
434
+ p = pixels[xi + yp]
435
+ pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
436
+ rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
437
+ rSum += pr * (divSum - i); gSum += pg * (divSum - i); bSum += pb * (divSum - i)
438
+ }
439
+ var si = r
440
+ for (y in 0 until h) {
441
+ pixels[xi + y * w] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
442
+ rSum -= rOut; gSum -= gOut; bSum -= bOut
443
+ rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
444
+ var sip = si + divSum; if (sip >= div) sip -= div
445
+ pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
446
+ rOut += pr; gOut += pg; bOut += pb
447
+ rSum += rOut; gSum += gOut; bSum += bOut
448
+ vmin[y] = if (y + r < hm) (y + r + 1) * w else hm * w
449
+ vmax[y] = if (y > r) (y - r) * w else 0
450
+ val sp = pixels[xi + vmin[y]]
451
+ val vp = pixels[xi + vmax[y]]
452
+ rStack[sip] = (sp shr 16) and 0xFF
453
+ gStack[sip] = (sp shr 8) and 0xFF
454
+ bStack[sip] = sp and 0xFF
455
+ rOut += rStack[sip] - ((vp shr 16) and 0xFF)
456
+ gOut += gStack[sip] - ((vp shr 8) and 0xFF)
457
+ bOut += bStack[sip] - (vp and 0xFF)
458
+ if (++si >= div) si = 0
459
+ }
460
+ xi++
461
+ }
462
+ bmp.setPixels(pixels, 0, w, 0, 0, w, h)
341
463
  }
342
464
 
343
465
  // ── Progressive shader ────────────────────────────────────────────────────
@@ -383,9 +505,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
383
505
  cornerRadiusPx = TypedValue.applyDimension(
384
506
  TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
385
507
  )
386
- // Only enable clipToOutline when radius > 0.
387
- // Keeping it false when not needed avoids GPU clip stack issues
388
- // when overflow:hidden is set on parent + Reanimated is animating.
389
508
  clipToOutline = cornerRadiusPx > 0f
390
509
  invalidate()
391
510
  }
@@ -413,9 +532,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
413
532
  else {
414
533
  safeRemovePreDrawListener()
415
534
  Choreographer.getInstance().removeFrameCallback(frameCallback)
416
- frameScheduled = false
417
- readyBitmap = null
418
- invalidate()
535
+ frameScheduled = false; readyBitmap = null; invalidate()
419
536
  }
420
537
  }
421
538
 
@@ -459,11 +576,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
459
576
  }
460
577
 
461
578
  private fun releaseBitmapPool() {
462
- captureBitmap?.recycle(); captureBitmap = null
463
- scaledBitmap?.recycle(); scaledBitmap = null
579
+ pixelCopyBitmap?.recycle(); pixelCopyBitmap = null
580
+ scaledBitmap?.recycle(); scaledBitmap = null
464
581
  }
465
582
 
466
- @Suppress("DEPRECATION")
467
583
  private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
468
584
  if (existing != null && !existing.isRecycled
469
585
  && existing.width == w && existing.height == h) return existing
@@ -471,13 +587,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
471
587
  return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
472
588
  }
473
589
 
474
- @Suppress("DEPRECATION")
475
- private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
476
- if (existing != null && existing.type.x == src.width && existing.type.y == src.height)
477
- return existing
478
- existing?.destroy()
479
- return Allocation.createFromBitmap(rs, src,
480
- Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
590
+ private fun blurRadiusFromAmount(amount: Float): Float {
591
+ val t = amount.coerceIn(0f, 100f) / 100f
592
+ return (2f + t * 22f) // 2–24, StackBlur works well in this range per pass
481
593
  }
482
594
 
483
595
  private fun parseHexColor(s: String): Int? {
@@ -502,19 +614,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
502
614
  } catch (_: NumberFormatException) { null }
503
615
  }
504
616
 
505
- private fun blurRadiusFromAmount(amount: Float): Float {
506
- // Linear 0→100 maps to 1→25 (RenderScript kernel max is 25).
507
- // With BLUR_ROUNDS=4 passes the effective spread is radius × √4 = radius × 2,
508
- // so blurAmount=100 gives effective spread of ~50px — properly frosted glass.
509
- val t = amount.coerceIn(0f, 100f) / 100f
510
- return (1f + t * 24f)
511
- }
512
-
513
617
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
514
618
 
515
619
  companion object {
516
- private const val DOWNSAMPLE = 2f // 1/4 pixels — higher quality than legacy
517
- private const val BLUR_ROUNDS = 4 // 4 passes — wider Gaussian spread for API 31+
620
+ private const val DOWNSAMPLE = 2f
621
+ private const val BLUR_ROUNDS = 3
518
622
  const val PROGRESSIVE_NONE = 0
519
623
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
520
624
  const val PROGRESSIVE_BOTTOM_TO_TOP = 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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",