react-native-blur-vibe 0.1.14 → 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.
@@ -7,7 +7,6 @@ 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
11
10
  import android.graphics.PorterDuff
12
11
  import android.graphics.PorterDuffXfermode
13
12
  import android.graphics.RadialGradient
@@ -20,8 +19,6 @@ import android.os.HandlerThread
20
19
  import android.os.Looper
21
20
  import android.util.TypedValue
22
21
  import android.view.Choreographer
23
- import android.view.PixelCopy
24
- import android.view.Surface
25
22
  import android.view.View
26
23
  import android.view.ViewGroup
27
24
  import android.view.ViewOutlineProvider
@@ -40,8 +37,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
40
37
 
41
38
  // ── Blur params ────────────────────────────────────────────────────────────
42
39
 
43
- private var blurAmount = 10f
44
- private var overlayColor = Color.TRANSPARENT
40
+ private var blurAmount = 10f
41
+ private var overlayColor = Color.TRANSPARENT
45
42
  private var cornerRadiusPx = 0f
46
43
 
47
44
  // ── Progressive blur ──────────────────────────────────────────────────────
@@ -57,14 +54,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
57
54
  private val noisePaint = Paint()
58
55
 
59
56
  // ── 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()
64
57
 
65
- private var pixelCopyBitmap: Bitmap? = null
66
- private var scaledBitmap: Bitmap? = null
67
- @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
68
62
 
69
63
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
70
64
  private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
@@ -76,20 +70,20 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
76
70
  private val workerHandler = Handler(workerThread.looper)
77
71
  private val mainHandler = Handler(Looper.getMainLooper())
78
72
 
79
- // ── Root / window ─────────────────────────────────────────────────────────
73
+ // ── Root ──────────────────────────────────────────────────────────────────
80
74
 
81
- private var blurRoot: ViewGroup? = null
75
+ private var blurRoot: ViewGroup? = null
82
76
  private val myLoc = IntArray(2)
83
77
  private val rootLoc = IntArray(2)
84
78
 
85
79
  // ── State ─────────────────────────────────────────────────────────────────
86
80
 
87
- var isCapturing = false
81
+ // isCapturing: public read so BlurVibeViewApi31.draw() can check it
82
+ var isCapturing = false
88
83
  private set
89
84
  private var blurEnabled = true
90
85
  private var autoUpdate = true
91
86
  private var frameScheduled = false
92
- private var pixelCopyInFlight = false
93
87
 
94
88
  // ── Choreographer gate ────────────────────────────────────────────────────
95
89
 
@@ -117,8 +111,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
117
111
 
118
112
  init {
119
113
  setWillNotDraw(false)
120
- // outlineProvider = BACKGROUND: ReactViewBackgroundDrawable implements
121
- // getOutline() correctly for all RN borderRadius variants automatically.
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.
122
117
  outlineProvider = ViewOutlineProvider.BACKGROUND
123
118
  }
124
119
 
@@ -135,13 +130,15 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
135
130
  override fun onDetachedFromWindow() {
136
131
  safeRemovePreDrawListener()
137
132
  Choreographer.getInstance().removeFrameCallback(frameCallback)
138
- frameScheduled = false
139
- isCapturing = false
140
- pixelCopyInFlight = false
141
- blurRoot = null
142
- readyBitmap = null
133
+ frameScheduled = false
134
+ isCapturing = false
135
+ blurRoot = null
136
+ readyBitmap = null
143
137
  noiseBitmap?.recycle(); noiseBitmap = null
144
- workerHandler.post { releaseBitmapPool() }
138
+ workerHandler.post {
139
+ captureBitmap?.recycle(); captureBitmap = null
140
+ scaledBitmap?.recycle(); scaledBitmap = null
141
+ }
145
142
  super.onDetachedFromWindow()
146
143
  }
147
144
 
@@ -149,7 +146,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
149
146
  super.onSizeChanged(w, h, oldw, oldh)
150
147
  if (w > 0 && h > 0) {
151
148
  readyBitmap = null
152
- workerHandler.post { releaseBitmapPool() }
149
+ workerHandler.post {
150
+ captureBitmap?.recycle(); captureBitmap = null
151
+ scaledBitmap?.recycle(); scaledBitmap = null
152
+ }
153
153
  scheduleFrame()
154
154
  }
155
155
  }
@@ -162,10 +162,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
162
162
  }
163
163
  }
164
164
 
165
- // ── draw() — no-op during root capture ────────────────────────────────────
165
+ // ── draw() — skip self during root capture ────────────────────────────────
166
166
 
167
167
  override fun draw(canvas: Canvas) {
168
- if (isCapturing) return
168
+ if (isCapturing) return // prevents capturing own blur output
169
169
  super.draw(canvas)
170
170
  }
171
171
 
@@ -176,26 +176,25 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
176
176
  val w = width.toFloat(); if (w <= 0f) return
177
177
  val h = height.toFloat(); if (h <= 0f) return
178
178
 
179
+ // Show overlay as placeholder while first blur is loading
179
180
  val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
180
- // No blur ready yet — show overlay color as placeholder
181
181
  if (Color.alpha(overlayColor) > 0) {
182
182
  overlayPaint.color = overlayColor
183
183
  canvas.drawRect(0f, 0f, w, h, overlayPaint)
184
184
  }
185
- // Redraw border on top even when no blur ready
186
185
  background?.draw(canvas)
187
186
  return
188
187
  }
189
188
 
190
- // Step 1: progressive mask layer
191
- val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
189
+ // Step 1: save layer for progressive mask compositing
190
+ val saveCount = if (progressiveDirection != PROGRESSIVE_NONE)
192
191
  canvas.saveLayer(0f, 0f, w, h, null)
193
- } else -1
192
+ else -1
194
193
 
195
- // Step 2: blurred bitmap
194
+ // Step 2: blurred bitmap — fills entire view
196
195
  canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
197
196
 
198
- // Step 3: progressive alpha mask
197
+ // Step 3: progressive alpha mask (fades blur across view)
199
198
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
200
199
  buildProgressiveShader(w, h)?.let { shader ->
201
200
  maskPaint.shader = shader
@@ -216,197 +215,113 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
216
215
  noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
217
216
  canvas.drawRect(0f, 0f, w, h, noisePaint)
218
217
  }
218
+
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.
219
223
  background?.draw(canvas)
220
224
  }
221
225
 
222
- // ── Capture pipeline PixelCopy (API 31+) ────────────────────────────────
226
+ // ── Capture + blur pipeline ────────────────────────────────────────────────
223
227
 
224
228
  private fun captureAndBlur() {
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
- )
229
+ if (isCapturing) return
230
+ val root = blurRoot ?: return
231
+ val vw = width; if (vw <= 0) return
232
+ val vh = height; if (vh <= 0) return
240
233
 
241
234
  val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
242
235
  val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
243
236
 
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
-
237
+ // Window-relative offset (correct for split-screen, freeform, PiP)
309
238
  root.getLocationInWindow(rootLoc)
310
239
  getLocationInWindow(myLoc)
311
240
  val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
312
241
  val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
313
242
 
314
- val capture = reuseBitmap(pixelCopyBitmap, vw, vh).also { pixelCopyBitmap = it }
315
- val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
243
+ val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
244
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
316
245
 
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)
317
248
  isCapturing = true
318
249
  val c = Canvas(capture)
319
250
  c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
320
251
  c.translate(-offsetX, -offsetY)
321
- try { root.draw(c) } catch (_: Exception) { isCapturing = false; return }
252
+ try {
253
+ root.draw(c)
254
+ } catch (_: Exception) {
255
+ isCapturing = false
256
+ return
257
+ }
322
258
  isCapturing = false
323
259
 
260
+ // Blur on worker thread — never blocks main/RenderThread
324
261
  val captureRef = capture
262
+ val radius = blurRadiusFromAmount(blurAmount)
263
+
325
264
  workerHandler.post {
265
+ // Downsample
326
266
  Canvas(scaled).drawBitmap(
327
267
  captureRef,
328
268
  Rect(0, 0, captureRef.width, captureRef.height),
329
269
  Rect(0, 0, scaled.width, scaled.height),
330
270
  capturePaint
331
271
  )
332
- val radius = blurRadiusFromAmount(blurAmount)
272
+ // Multi-pass StackBlur (pure Kotlin, no deprecated APIs, all API levels)
333
273
  repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
274
+
275
+ // Atomic @Volatile swap — RenderThread always reads a complete bitmap
334
276
  readyBitmap = scaled
335
277
  mainHandler.post { invalidate() }
336
278
  }
337
279
  }
338
280
 
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).
281
+ // ── StackBlur ─────────────────────────────────────────────────────────────
282
+ // Mario Klingemann's algorithm — O(w×h) regardless of radius.
283
+ // No RenderScript, no deprecated APIs. Works on all Android versions.
345
284
 
346
285
  private fun stackBlur(bmp: Bitmap, radius: Int) {
347
286
  val r = radius.coerceIn(1, 254)
348
- val w = bmp.width
349
- val h = bmp.height
287
+ val w = bmp.width; val h = bmp.height
350
288
  val pixels = IntArray(w * h)
351
289
  bmp.getPixels(pixels, 0, w, 0, 0, w, h)
352
-
353
290
  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
291
+ val wm = w - 1; val hm = h - 1
292
+ val divSumSq = ((div + 1) shr 1).let { it * it }
359
293
  val dv = IntArray(256 * divSumSq) { it / divSumSq }
360
-
361
- var yi = 0
362
294
  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
-
295
+ val rStack = IntArray(div); val gStack = IntArray(div); val bStack = IntArray(div)
296
+ var yi = 0
369
297
  for (y in 0 until h) {
370
298
  var rSum = 0; var gSum = 0; var bSum = 0
371
299
  var rOut = 0; var gOut = 0; var bOut = 0
372
-
373
300
  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) {
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) {
379
304
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
380
305
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
381
306
  rOut += pr; gOut += pg; bOut += pb
382
307
  }
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
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
387
311
  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)
312
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
391
313
  }
392
-
393
314
  var si = r
394
315
  for (x in 0 until w) {
395
316
  pixels[yi + x] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
396
317
  rSum -= rOut; gSum -= gOut; bSum -= bOut
397
318
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
398
- var sip = si + divSum
399
- if (sip >= div) sip -= div
319
+ var sip = si + ds; if (sip >= div) sip -= div
400
320
  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
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
410
325
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
411
326
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
412
327
  bOut += bStack[sip] - (vp and 0xFF)
@@ -414,50 +329,38 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
414
329
  }
415
330
  yi += w
416
331
  }
417
-
418
- var xi = 0
419
332
  for (x in 0 until w) {
420
333
  var rSum = 0; var gSum = 0; var bSum = 0
421
334
  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) {
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) {
428
338
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
429
339
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
430
340
  rOut += pr; gOut += pg; bOut += pb
431
341
  }
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
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
436
345
  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)
346
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
438
347
  }
439
348
  var si = r
440
349
  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])
350
+ pixels[x + y * w] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
442
351
  rSum -= rOut; gSum -= gOut; bSum -= bOut
443
352
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
444
- var sip = si + divSum; if (sip >= div) sip -= div
353
+ var sip = si + ds; if (sip >= div) sip -= div
445
354
  pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
446
- rOut += pr; gOut += pg; bOut += pb
447
- rSum += rOut; gSum += gOut; bSum += bOut
355
+ rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
448
356
  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
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
455
359
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
456
360
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
457
361
  bOut += bStack[sip] - (vp and 0xFF)
458
362
  if (++si >= div) si = 0
459
363
  }
460
- xi++
461
364
  }
462
365
  bmp.setPixels(pixels, 0, w, 0, 0, w, h)
463
366
  }
@@ -472,7 +375,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
472
375
  PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
473
376
  PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
474
377
  PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,sc,ec,Shader.TileMode.CLAMP)
475
- 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)
476
379
  else -> null
477
380
  }
478
381
  }
@@ -564,22 +467,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
564
467
  private fun findBlurRoot(): ViewGroup? {
565
468
  var p = parent
566
469
  while (p != null) {
567
- 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
568
472
  p = (p as? View)?.parent
569
473
  }
570
474
  p = parent
571
475
  while (p != null) {
572
- 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
573
478
  p = (p as? View)?.parent
574
479
  }
575
480
  return rootView as? ViewGroup
576
481
  }
577
482
 
578
- private fun releaseBitmapPool() {
579
- pixelCopyBitmap?.recycle(); pixelCopyBitmap = null
580
- scaledBitmap?.recycle(); scaledBitmap = null
581
- }
582
-
583
483
  private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
584
484
  if (existing != null && !existing.isRecycled
585
485
  && existing.width == w && existing.height == h) return existing
@@ -589,7 +489,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
589
489
 
590
490
  private fun blurRadiusFromAmount(amount: Float): Float {
591
491
  val t = amount.coerceIn(0f, 100f) / 100f
592
- return (2f + t * 22f) // 2–24, StackBlur works well in this range per pass
492
+ return 2f + t * 22f // 2–24, with BLUR_ROUNDS=4 effective spread 4–48px
593
493
  }
594
494
 
595
495
  private fun parseHexColor(s: String): Int? {
@@ -617,8 +517,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
617
517
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
618
518
 
619
519
  companion object {
620
- private const val DOWNSAMPLE = 2f
621
- private const val BLUR_ROUNDS = 3
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
622
522
  const val PROGRESSIVE_NONE = 0
623
523
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
624
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.14",
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",