react-native-blur-vibe 0.1.13 → 0.1.15

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
 
@@ -17,10 +17,6 @@ import android.os.Build
17
17
  import android.os.Handler
18
18
  import android.os.HandlerThread
19
19
  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
20
  import android.util.TypedValue
25
21
  import android.view.Choreographer
26
22
  import android.view.View
@@ -41,8 +37,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
41
37
 
42
38
  // ── Blur params ────────────────────────────────────────────────────────────
43
39
 
44
- private var blurAmount = 10f
45
- private var overlayColor = Color.TRANSPARENT
40
+ private var blurAmount = 10f
41
+ private var overlayColor = Color.TRANSPARENT
46
42
  private var cornerRadiusPx = 0f
47
43
 
48
44
  // ── Progressive blur ──────────────────────────────────────────────────────
@@ -57,44 +53,33 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
57
53
  private var noiseBitmap: Bitmap? = null
58
54
  private val noisePaint = Paint()
59
55
 
60
- // ── Double-buffer bitmap pool ─────────────────────────────────────────────
56
+ // ── Bitmap double-buffer ──────────────────────────────────────────────────
61
57
 
62
- private var captureBitmap: Bitmap? = null
63
- private var scaledBitmap: Bitmap? = null
64
- @Volatile private var readyBitmap: Bitmap? = null
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
65
62
 
66
63
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
67
64
  private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
68
65
 
69
- // ── Worker thread — blur runs here, main thread never blocks ──────────────
66
+ // ── Worker thread ─────────────────────────────────────────────────────────
70
67
 
71
68
  private val workerThread = HandlerThread("BlurVibeWorker31-${hashCode()}")
72
69
  .also { it.start() }
73
70
  private val workerHandler = Handler(workerThread.looper)
74
71
  private val mainHandler = Handler(Looper.getMainLooper())
75
72
 
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
86
-
87
- // ── Root view ─────────────────────────────────────────────────────────────
73
+ // ── Root ──────────────────────────────────────────────────────────────────
88
74
 
89
75
  private var blurRoot: ViewGroup? = null
90
- private val myLoc = IntArray(2)
91
- private val rootLoc = IntArray(2)
76
+ private val myLoc = IntArray(2)
77
+ private val rootLoc = IntArray(2)
92
78
 
93
79
  // ── State ─────────────────────────────────────────────────────────────────
94
80
 
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
- var isCapturing = false
81
+ // isCapturing: public read so BlurVibeViewApi31.draw() can check it
82
+ var isCapturing = false
98
83
  private set
99
84
  private var blurEnabled = true
100
85
  private var autoUpdate = true
@@ -126,13 +111,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
126
111
 
127
112
  init {
128
113
  setWillNotDraw(false)
129
- // DO NOT call setBackgroundColor — it replaces ReactViewGroup's
130
- // ReactViewBackgroundDrawable, killing all RN style prop handling.
131
- //
132
- // 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.
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.
136
117
  outlineProvider = ViewOutlineProvider.BACKGROUND
137
118
  }
138
119
 
@@ -143,7 +124,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
143
124
  blurRoot = findBlurRoot()
144
125
  safeAddPreDrawListener()
145
126
  generateNoiseBitmap()
146
- workerHandler.post { initRenderScript() }
147
127
  scheduleFrame()
148
128
  }
149
129
 
@@ -156,8 +136,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
156
136
  readyBitmap = null
157
137
  noiseBitmap?.recycle(); noiseBitmap = null
158
138
  workerHandler.post {
159
- releaseBitmapPool()
160
- releaseRenderScript()
139
+ captureBitmap?.recycle(); captureBitmap = null
140
+ scaledBitmap?.recycle(); scaledBitmap = null
161
141
  }
162
142
  super.onDetachedFromWindow()
163
143
  }
@@ -166,7 +146,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
166
146
  super.onSizeChanged(w, h, oldw, oldh)
167
147
  if (w > 0 && h > 0) {
168
148
  readyBitmap = null
169
- workerHandler.post { releaseBitmapPool() }
149
+ workerHandler.post {
150
+ captureBitmap?.recycle(); captureBitmap = null
151
+ scaledBitmap?.recycle(); scaledBitmap = null
152
+ }
170
153
  scheduleFrame()
171
154
  }
172
155
  }
@@ -179,14 +162,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
179
162
  }
180
163
  }
181
164
 
182
- // ── 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.
165
+ // ── draw() — skip self during root capture ────────────────────────────────
187
166
 
188
167
  override fun draw(canvas: Canvas) {
189
- if (isCapturing) return
168
+ if (isCapturing) return // prevents capturing own blur output
190
169
  super.draw(canvas)
191
170
  }
192
171
 
@@ -197,25 +176,25 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
197
176
  val w = width.toFloat(); if (w <= 0f) return
198
177
  val h = height.toFloat(); if (h <= 0f) return
199
178
 
179
+ // Show overlay as placeholder while first blur is loading
200
180
  val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
201
- // No blur ready yet — draw overlay only so view isn't invisible
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
+ background?.draw(canvas)
207
186
  return
208
187
  }
209
188
 
210
- // Step 1: progressive mask layer
211
- val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
189
+ // Step 1: save layer for progressive mask compositing
190
+ val saveCount = if (progressiveDirection != PROGRESSIVE_NONE)
212
191
  canvas.saveLayer(0f, 0f, w, h, null)
213
- } else -1
192
+ else -1
214
193
 
215
- // Step 2: draw blurred bitmap
194
+ // Step 2: blurred bitmap — fills entire view
216
195
  canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
217
196
 
218
- // Step 3: progressive alpha mask
197
+ // Step 3: progressive alpha mask (fades blur across view)
219
198
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
220
199
  buildProgressiveShader(w, h)?.let { shader ->
221
200
  maskPaint.shader = shader
@@ -237,24 +216,25 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
237
216
  canvas.drawRect(0f, 0f, w, h, noisePaint)
238
217
  }
239
218
 
240
- // Step 6: let ReactViewGroup draw borders/radius on top
241
- super.onDraw(canvas)
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.
223
+ background?.draw(canvas)
242
224
  }
243
225
 
244
- // ── Capture + blur pipeline ───────────────────────────────────────────────
226
+ // ── Capture + blur pipeline ────────────────────────────────────────────────
245
227
 
246
228
  private fun captureAndBlur() {
247
229
  if (isCapturing) return
248
230
  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
231
+ val vw = width; if (vw <= 0) return
232
+ val vh = height; if (vh <= 0) return
253
233
 
254
234
  val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
255
235
  val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
256
236
 
257
- // Compute offset — window coords, correct for split-screen/freeform/PiP
237
+ // Window-relative offset (correct for split-screen, freeform, PiP)
258
238
  root.getLocationInWindow(rootLoc)
259
239
  getLocationInWindow(myLoc)
260
240
  val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
@@ -263,7 +243,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
263
243
  val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
264
244
  val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
265
245
 
266
- // Capture isCapturing suppresses our own draw() so root.draw() skips us
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)
267
248
  isCapturing = true
268
249
  val c = Canvas(capture)
269
250
  c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
@@ -276,68 +257,112 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
276
257
  }
277
258
  isCapturing = false
278
259
 
279
- // Downsample + blur on worker thread (never blocks main/RenderThread)
260
+ // Blur on worker thread never blocks main/RenderThread
280
261
  val captureRef = capture
281
- val scaledRef = scaled
282
262
  val radius = blurRadiusFromAmount(blurAmount)
283
263
 
284
264
  workerHandler.post {
285
265
  // Downsample
286
- Canvas(scaledRef).drawBitmap(
266
+ Canvas(scaled).drawBitmap(
287
267
  captureRef,
288
268
  Rect(0, 0, captureRef.width, captureRef.height),
289
- Rect(0, 0, scaledRef.width, scaledRef.height),
269
+ Rect(0, 0, scaled.width, scaled.height),
290
270
  capturePaint
291
271
  )
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
272
+ // Multi-pass StackBlur (pure Kotlin, no deprecated APIs, all API levels)
273
+ repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
298
274
 
275
+ // Atomic @Volatile swap — RenderThread always reads a complete bitmap
276
+ readyBitmap = scaled
299
277
  mainHandler.post { invalidate() }
300
278
  }
301
279
  }
302
280
 
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
- }
327
-
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)
281
+ // ── StackBlur ─────────────────────────────────────────────────────────────
282
+ // Mario Klingemann's algorithm — O(w×h) regardless of radius.
283
+ // No RenderScript, no deprecated APIs. Works on all Android versions.
284
+
285
+ 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)
296
+ var yi = 0
297
+ for (y in 0 until h) {
298
+ var rSum = 0; var gSum = 0; var bSum = 0
299
+ 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
303
+ for (i in 0 until ds) {
304
+ rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
305
+ rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
306
+ rOut += pr; gOut += pg; bOut += pb
307
+ }
308
+ for (i in 1 until ds) {
309
+ 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
311
+ rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
312
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
313
+ }
314
+ var si = r
315
+ for (x in 0 until w) {
316
+ pixels[yi + x] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
317
+ rSum -= rOut; gSum -= gOut; bSum -= bOut
318
+ rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
319
+ var sip = si + ds; if (sip >= div) sip -= div
320
+ pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
321
+ rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
322
+ 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)]
324
+ rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
325
+ rOut += rStack[sip] - ((vp shr 16) and 0xFF)
326
+ gOut += gStack[sip] - ((vp shr 8) and 0xFF)
327
+ bOut += bStack[sip] - (vp and 0xFF)
328
+ if (++si >= div) si = 0
329
+ }
330
+ yi += w
331
331
  }
332
- Canvas(bmp).drawBitmap(bmp, 0f, 0f, p)
333
- }
334
-
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
332
+ for (x in 0 until w) {
333
+ var rSum = 0; var gSum = 0; var bSum = 0
334
+ 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
337
+ for (i in 0 until ds) {
338
+ rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
339
+ rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
340
+ rOut += pr; gOut += pg; bOut += pb
341
+ }
342
+ for (i in 1 until ds) {
343
+ 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
345
+ rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
346
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
347
+ }
348
+ var si = r
349
+ 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])
351
+ rSum -= rOut; gSum -= gOut; bSum -= bOut
352
+ rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
353
+ var sip = si + ds; if (sip >= div) sip -= div
354
+ pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
355
+ rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
356
+ 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)]
358
+ rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
359
+ rOut += rStack[sip] - ((vp shr 16) and 0xFF)
360
+ gOut += gStack[sip] - ((vp shr 8) and 0xFF)
361
+ bOut += bStack[sip] - (vp and 0xFF)
362
+ if (++si >= div) si = 0
363
+ }
364
+ }
365
+ bmp.setPixels(pixels, 0, w, 0, 0, w, h)
341
366
  }
342
367
 
343
368
  // ── Progressive shader ────────────────────────────────────────────────────
@@ -350,7 +375,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
350
375
  PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
351
376
  PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
352
377
  PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,sc,ec,Shader.TileMode.CLAMP)
353
- PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,min(w,h)/2f,sc,ec,Shader.TileMode.CLAMP)
378
+ PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,min(w,h)/2f,sc,ec,Shader.TileMode.CLAMP)
354
379
  else -> null
355
380
  }
356
381
  }
@@ -383,9 +408,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
383
408
  cornerRadiusPx = TypedValue.applyDimension(
384
409
  TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
385
410
  )
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
411
  clipToOutline = cornerRadiusPx > 0f
390
412
  invalidate()
391
413
  }
@@ -413,9 +435,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
413
435
  else {
414
436
  safeRemovePreDrawListener()
415
437
  Choreographer.getInstance().removeFrameCallback(frameCallback)
416
- frameScheduled = false
417
- readyBitmap = null
418
- invalidate()
438
+ frameScheduled = false; readyBitmap = null; invalidate()
419
439
  }
420
440
  }
421
441
 
@@ -447,23 +467,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
447
467
  private fun findBlurRoot(): ViewGroup? {
448
468
  var p = parent
449
469
  while (p != null) {
450
- if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
470
+ if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
471
+ return p as? ViewGroup
451
472
  p = (p as? View)?.parent
452
473
  }
453
474
  p = parent
454
475
  while (p != null) {
455
- if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
476
+ if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
477
+ return p as? ViewGroup
456
478
  p = (p as? View)?.parent
457
479
  }
458
480
  return rootView as? ViewGroup
459
481
  }
460
482
 
461
- private fun releaseBitmapPool() {
462
- captureBitmap?.recycle(); captureBitmap = null
463
- scaledBitmap?.recycle(); scaledBitmap = null
464
- }
465
-
466
- @Suppress("DEPRECATION")
467
483
  private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
468
484
  if (existing != null && !existing.isRecycled
469
485
  && existing.width == w && existing.height == h) return existing
@@ -471,13 +487,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
471
487
  return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
472
488
  }
473
489
 
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)
490
+ private fun blurRadiusFromAmount(amount: Float): Float {
491
+ val t = amount.coerceIn(0f, 100f) / 100f
492
+ return 2f + t * 22f // 2–24, with BLUR_ROUNDS=4 effective spread ≈ 4–48px
481
493
  }
482
494
 
483
495
  private fun parseHexColor(s: String): Int? {
@@ -502,19 +514,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
502
514
  } catch (_: NumberFormatException) { null }
503
515
  }
504
516
 
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
517
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
514
518
 
515
519
  companion object {
516
520
  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+
521
+ private const val BLUR_ROUNDS = 4 // 4 passes — wider spread than legacy's 3
518
522
  const val PROGRESSIVE_NONE = 0
519
523
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
520
524
  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.15",
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",