react-native-blur-vibe 0.1.14 → 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.
@@ -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
@@ -19,9 +18,6 @@ import android.os.Handler
19
18
  import android.os.HandlerThread
20
19
  import android.os.Looper
21
20
  import android.util.TypedValue
22
- import android.view.Choreographer
23
- import android.view.PixelCopy
24
- import android.view.Surface
25
21
  import android.view.View
26
22
  import android.view.ViewGroup
27
23
  import android.view.ViewOutlineProvider
@@ -34,14 +30,71 @@ import kotlin.random.Random
34
30
 
35
31
  /**
36
32
  * BlurVibeViewApi31 — Backdrop blur for Android API 31+
37
- **/
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
+ */
38
91
  @RequiresApi(Build.VERSION_CODES.S)
39
92
  class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
40
93
 
41
94
  // ── Blur params ────────────────────────────────────────────────────────────
42
95
 
43
- private var blurAmount = 10f
44
- private var overlayColor = Color.TRANSPARENT
96
+ private var blurAmount = 10f
97
+ private var overlayColor = Color.TRANSPARENT
45
98
  private var cornerRadiusPx = 0f
46
99
 
47
100
  // ── Progressive blur ──────────────────────────────────────────────────────
@@ -58,13 +111,16 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
58
111
 
59
112
  // ── Bitmap double-buffer ──────────────────────────────────────────────────
60
113
  //
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()
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.
64
120
 
65
- private var pixelCopyBitmap: Bitmap? = null
66
- private var scaledBitmap: Bitmap? = null
67
- @Volatile private var readyBitmap: Bitmap? = null
121
+ private var captureBitmap: Bitmap? = null
122
+ private var scaledBitmap: Bitmap? = null
123
+ @Volatile private var readyBitmap: Bitmap? = null
68
124
 
69
125
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
70
126
  private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
@@ -76,34 +132,33 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
76
132
  private val workerHandler = Handler(workerThread.looper)
77
133
  private val mainHandler = Handler(Looper.getMainLooper())
78
134
 
79
- // ── Root / window ─────────────────────────────────────────────────────────
135
+ // ── Root ──────────────────────────────────────────────────────────────────
80
136
 
81
- private var blurRoot: ViewGroup? = null
137
+ private var blurRoot: ViewGroup? = null
82
138
  private val myLoc = IntArray(2)
83
139
  private val rootLoc = IntArray(2)
84
140
 
85
141
  // ── State ─────────────────────────────────────────────────────────────────
86
142
 
87
- var isCapturing = false
143
+ var isCapturing = false
88
144
  private set
89
- private var blurEnabled = true
90
- private var autoUpdate = true
91
- private var frameScheduled = false
92
- private var pixelCopyInFlight = false
93
145
 
94
- // ── 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
95
150
 
96
- private val frameCallback = Choreographer.FrameCallback {
97
- frameScheduled = false
98
- if (isAttachedToWindow && blurEnabled) captureAndBlur()
99
- }
151
+ // ── PreDraw listener fires PRE-DRAW, guaranteed before RenderThread ─────
100
152
 
101
153
  private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
102
- if (!frameScheduled && blurEnabled && autoUpdate) {
103
- frameScheduled = true
104
- 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
+ }
105
160
  }
106
- true
161
+ true // MUST return true — never block the draw pass
107
162
  }
108
163
 
109
164
  // ── Paint objects ─────────────────────────────────────────────────────────
@@ -117,8 +172,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
117
172
 
118
173
  init {
119
174
  setWillNotDraw(false)
120
- // outlineProvider = BACKGROUND: ReactViewBackgroundDrawable implements
121
- // getOutline() correctly for all RN borderRadius variants automatically.
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).
122
178
  outlineProvider = ViewOutlineProvider.BACKGROUND
123
179
  }
124
180
 
@@ -129,19 +185,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
129
185
  blurRoot = findBlurRoot()
130
186
  safeAddPreDrawListener()
131
187
  generateNoiseBitmap()
132
- scheduleFrame()
133
188
  }
134
189
 
135
190
  override fun onDetachedFromWindow() {
136
191
  safeRemovePreDrawListener()
137
- Choreographer.getInstance().removeFrameCallback(frameCallback)
138
- frameScheduled = false
139
- isCapturing = false
140
- pixelCopyInFlight = false
141
- blurRoot = null
142
- readyBitmap = null
192
+ isCapturing = false
193
+ workerBusy = false
194
+ blurRoot = null
195
+ readyBitmap = null
143
196
  noiseBitmap?.recycle(); noiseBitmap = null
144
- workerHandler.post { releaseBitmapPool() }
197
+ workerHandler.post {
198
+ captureBitmap?.recycle(); captureBitmap = null
199
+ scaledBitmap?.recycle(); scaledBitmap = null
200
+ }
145
201
  super.onDetachedFromWindow()
146
202
  }
147
203
 
@@ -149,8 +205,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
149
205
  super.onSizeChanged(w, h, oldw, oldh)
150
206
  if (w > 0 && h > 0) {
151
207
  readyBitmap = null
152
- workerHandler.post { releaseBitmapPool() }
153
- scheduleFrame()
208
+ workerBusy = false
209
+ workerHandler.post {
210
+ captureBitmap?.recycle(); captureBitmap = null
211
+ scaledBitmap?.recycle(); scaledBitmap = null
212
+ }
154
213
  }
155
214
  }
156
215
 
@@ -158,11 +217,10 @@ 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() — no-op during root capture ────────────────────────────────────
223
+ // ── draw() — skip self during capture ────────────────────────────────────
166
224
 
167
225
  override fun draw(canvas: Canvas) {
168
226
  if (isCapturing) return
@@ -177,25 +235,23 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
177
235
  val h = height.toFloat(); if (h <= 0f) return
178
236
 
179
237
  val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
180
- // No blur ready yet — show overlay color as placeholder
181
238
  if (Color.alpha(overlayColor) > 0) {
182
239
  overlayPaint.color = overlayColor
183
240
  canvas.drawRect(0f, 0f, w, h, overlayPaint)
184
241
  }
185
- // Redraw border on top even when no blur ready
186
242
  background?.draw(canvas)
187
243
  return
188
244
  }
189
245
 
190
- // Step 1: progressive mask layer
191
- val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
246
+ // Progressive mask layer
247
+ val saveCount = if (progressiveDirection != PROGRESSIVE_NONE)
192
248
  canvas.saveLayer(0f, 0f, w, h, null)
193
- } else -1
249
+ else -1
194
250
 
195
- // Step 2: blurred bitmap
251
+ // Blurred bitmap
196
252
  canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
197
253
 
198
- // Step 3: progressive alpha mask
254
+ // Progressive alpha mask
199
255
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
200
256
  buildProgressiveShader(w, h)?.let { shader ->
201
257
  maskPaint.shader = shader
@@ -204,209 +260,133 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
204
260
  canvas.restoreToCount(saveCount)
205
261
  }
206
262
 
207
- // Step 4: overlay tint
263
+ // Overlay tint
208
264
  if (Color.alpha(overlayColor) > 0) {
209
265
  overlayPaint.color = overlayColor
210
266
  canvas.drawRect(0f, 0f, w, h, overlayPaint)
211
267
  }
212
268
 
213
- // Step 5: noise grain
269
+ // Noise grain
214
270
  noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
215
271
  noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
216
272
  noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
217
273
  canvas.drawRect(0f, 0f, w, h, noisePaint)
218
274
  }
219
- background?.draw(canvas)
220
- }
221
-
222
- // ── Capture pipeline — PixelCopy (API 31+) ────────────────────────────────
223
-
224
- 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
- )
240
-
241
- val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
242
- val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
243
-
244
- val destBitmap = reuseBitmap(pixelCopyBitmap, vw, vh)
245
- .also { pixelCopyBitmap = it }
246
275
 
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
- )
276
+ // Redraw ReactViewBackgroundDrawable ON TOP of blur so borders/radius
277
+ // are visible above the blur layer, not hidden underneath it.
278
+ background?.draw(canvas)
297
279
  }
298
280
 
299
- // ── Fallback: root.draw() when PixelCopy unavailable ─────────────────────
281
+ // ── Capture — called in OnPreDrawListener (pre-draw, main thread) ─────────
300
282
 
301
- private fun captureWithRootDraw() {
302
- if (isCapturing) return
283
+ private fun captureNow() {
284
+ if (isCapturing || workerBusy) return
303
285
  val root = blurRoot ?: return
304
286
  val vw = width; if (vw <= 0) return
305
287
  val vh = height; if (vh <= 0) return
306
- val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
307
- val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
308
288
 
289
+ val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
290
+ val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
291
+
292
+ // Window-relative offset (correct for split-screen, freeform, PiP)
309
293
  root.getLocationInWindow(rootLoc)
310
294
  getLocationInWindow(myLoc)
311
295
  val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
312
296
  val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
313
297
 
314
- val capture = reuseBitmap(pixelCopyBitmap, vw, vh).also { pixelCopyBitmap = it }
315
- val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
298
+ val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
316
299
 
300
+ // isCapturing = true → draw() returns immediately → root.draw() skips us
301
+ // → capture contains ONLY the content behind us, not our own blur output
317
302
  isCapturing = true
318
303
  val c = Canvas(capture)
319
304
  c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
320
305
  c.translate(-offsetX, -offsetY)
321
- try { root.draw(c) } catch (_: Exception) { isCapturing = false; return }
306
+ try {
307
+ root.draw(c)
308
+ } catch (_: Exception) {
309
+ isCapturing = false
310
+ return
311
+ }
322
312
  isCapturing = false
323
313
 
314
+ // Hand off to worker thread — main thread returns immediately
315
+ workerBusy = true
324
316
  val captureRef = capture
317
+ val radius = blurRadiusFromAmount(blurAmount)
318
+
325
319
  workerHandler.post {
320
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
321
+
322
+ // Downsample
326
323
  Canvas(scaled).drawBitmap(
327
324
  captureRef,
328
325
  Rect(0, 0, captureRef.width, captureRef.height),
329
326
  Rect(0, 0, scaled.width, scaled.height),
330
327
  capturePaint
331
328
  )
332
- val radius = blurRadiusFromAmount(blurAmount)
333
- repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
329
+
330
+ // Multi-pass StackBlur — pure 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
334
335
  readyBitmap = scaled
336
+ workerBusy = false
337
+
335
338
  mainHandler.post { invalidate() }
336
339
  }
337
340
  }
338
341
 
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).
342
+ // ── StackBlur ─────────────────────────────────────────────────────────────
345
343
 
346
344
  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
-
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))
365
358
  val rStack = IntArray(div)
366
359
  val gStack = IntArray(div)
367
360
  val bStack = IntArray(div)
368
361
 
362
+ // Horizontal pass
363
+ var yi = 0
369
364
  for (y in 0 until h) {
370
365
  var rSum = 0; var gSum = 0; var bSum = 0
371
366
  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) {
367
+ var p = px[yi]; var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
368
+ for (i in 0 until ds) {
379
369
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
380
370
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
381
371
  rOut += pr; gOut += pg; bOut += pb
382
372
  }
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
373
+ for (i in 1 until ds) {
374
+ val xi = if (i <= wm) i else wm
375
+ p = px[yi + xi]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
387
376
  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)
377
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
391
378
  }
392
-
393
379
  var si = r
394
380
  for (x in 0 until w) {
395
- 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]
396
382
  rSum -= rOut; gSum -= gOut; bSum -= bOut
397
383
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
398
- var sip = si + divSum
399
- if (sip >= div) sip -= div
384
+ var sip = si + ds; if (sip >= div) sip -= div
400
385
  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
386
+ rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
387
+ vmin[x] = if (x + r < wm) x + r + 1 else wm
388
+ val sp = px[yi + vmin[x]]; val vp = px[yi + if (x > r) x - r else 0]
389
+ rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
410
390
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
411
391
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
412
392
  bOut += bStack[sip] - (vp and 0xFF)
@@ -415,64 +395,53 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
415
395
  yi += w
416
396
  }
417
397
 
418
- var xi = 0
398
+ // Vertical pass
419
399
  for (x in 0 until w) {
420
400
  var rSum = 0; var gSum = 0; var bSum = 0
421
401
  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) {
402
+ var p = px[x]; var pr = (p shr 16) and 0xFF; var pg = (p shr 8) and 0xFF; var pb = p and 0xFF
403
+ for (i in 0 until ds) {
428
404
  rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
429
405
  rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
430
406
  rOut += pr; gOut += pg; bOut += pb
431
407
  }
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
408
+ for (i in 1 until ds) {
409
+ val yi2 = if (i <= hm) i * w else hm * w
410
+ p = px[x + yi2]; pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
436
411
  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)
412
+ rSum += pr * (ds - i); gSum += pg * (ds - i); bSum += pb * (ds - i)
438
413
  }
439
414
  var si = r
440
415
  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])
416
+ px[x + y * w] = -0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum]
442
417
  rSum -= rOut; gSum -= gOut; bSum -= bOut
443
418
  rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
444
- var sip = si + divSum; if (sip >= div) sip -= div
419
+ var sip = si + ds; if (sip >= div) sip -= div
445
420
  pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
446
- rOut += pr; gOut += pg; bOut += pb
447
- rSum += rOut; gSum += gOut; bSum += bOut
421
+ rOut += pr; gOut += pg; bOut += pb; rSum += rOut; gSum += gOut; bSum += bOut
448
422
  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
423
+ val sp = px[x + vmin[y]]; val vp = px[x + if (y > r) (y - r) * w else 0]
424
+ rStack[sip] = (sp shr 16) and 0xFF; gStack[sip] = (sp shr 8) and 0xFF; bStack[sip] = sp and 0xFF
455
425
  rOut += rStack[sip] - ((vp shr 16) and 0xFF)
456
426
  gOut += gStack[sip] - ((vp shr 8) and 0xFF)
457
427
  bOut += bStack[sip] - (vp and 0xFF)
458
428
  if (++si >= div) si = 0
459
429
  }
460
- xi++
461
430
  }
462
- bmp.setPixels(pixels, 0, w, 0, 0, w, h)
431
+ bmp.setPixels(px, 0, w, 0, 0, w, h)
463
432
  }
464
433
 
465
434
  // ── Progressive shader ────────────────────────────────────────────────────
466
435
 
467
436
  private fun buildProgressiveShader(w: Float, h: Float): Shader? {
468
- val sc = Color.argb((progressiveStartIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
469
- 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)
470
439
  return when (progressiveDirection) {
471
- PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h,sc,ec,Shader.TileMode.CLAMP)
472
- PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
473
- PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
474
- 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)
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)
476
445
  else -> null
477
446
  }
478
447
  }
@@ -493,7 +462,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
493
462
  // ── Public setters ─────────────────────────────────────────────────────────
494
463
 
495
464
  fun setBlurAmount(amount: Float) {
496
- blurAmount = amount.coerceIn(0f, 100f); scheduleFrame()
465
+ blurAmount = amount.coerceIn(0f, 100f); invalidate()
497
466
  }
498
467
 
499
468
  fun setOverlayColor(colorString: String?) {
@@ -522,17 +491,16 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
522
491
  }; invalidate()
523
492
  }
524
493
 
525
- fun setProgressiveStartIntensity(v: Float) { progressiveStartIntensity = v.coerceIn(0f,1f); invalidate() }
526
- fun setProgressiveEndIntensity(v: Float) { progressiveEndIntensity = v.coerceIn(0f,1f); invalidate() }
527
- 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() }
528
497
 
529
498
  fun applyBlurEnabled(enabled: Boolean) {
530
499
  blurEnabled = enabled
531
- if (enabled) { safeAddPreDrawListener(); scheduleFrame() }
500
+ if (enabled) safeAddPreDrawListener()
532
501
  else {
533
502
  safeRemovePreDrawListener()
534
- Choreographer.getInstance().removeFrameCallback(frameCallback)
535
- frameScheduled = false; readyBitmap = null; invalidate()
503
+ workerBusy = false; readyBitmap = null; invalidate()
536
504
  }
537
505
  }
538
506
 
@@ -543,13 +511,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
543
511
 
544
512
  // ── Helpers ────────────────────────────────────────────────────────────────
545
513
 
546
- private fun scheduleFrame() {
547
- if (!frameScheduled && blurEnabled) {
548
- frameScheduled = true
549
- Choreographer.getInstance().postFrameCallback(frameCallback)
550
- }
551
- }
552
-
553
514
  private fun safeAddPreDrawListener() {
554
515
  val root = blurRoot ?: return
555
516
  val vto = root.viewTreeObserver
@@ -564,22 +525,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
564
525
  private fun findBlurRoot(): ViewGroup? {
565
526
  var p = parent
566
527
  while (p != null) {
567
- if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
528
+ if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
529
+ return p as? ViewGroup
568
530
  p = (p as? View)?.parent
569
531
  }
570
532
  p = parent
571
533
  while (p != null) {
572
- if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
534
+ if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
535
+ return p as? ViewGroup
573
536
  p = (p as? View)?.parent
574
537
  }
575
538
  return rootView as? ViewGroup
576
539
  }
577
540
 
578
- private fun releaseBitmapPool() {
579
- pixelCopyBitmap?.recycle(); pixelCopyBitmap = null
580
- scaledBitmap?.recycle(); scaledBitmap = null
581
- }
582
-
583
541
  private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
584
542
  if (existing != null && !existing.isRecycled
585
543
  && existing.width == w && existing.height == h) return existing
@@ -589,7 +547,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
589
547
 
590
548
  private fun blurRadiusFromAmount(amount: Float): Float {
591
549
  val t = amount.coerceIn(0f, 100f) / 100f
592
- return (2f + t * 22f) // 2–24, StackBlur works well in this range per pass
550
+ return 2f + t * 22f // 2–24px per pass, × BLUR_ROUNDS = wide spread
593
551
  }
594
552
 
595
553
  private fun parseHexColor(s: String): Int? {
@@ -602,13 +560,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
602
560
  3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
603
561
  hex[1].toString().repeat(2).toInt(16),
604
562
  hex[2].toString().repeat(2).toInt(16))
605
- 6 -> Color.argb(255, hex.substring(0,2).toInt(16),
606
- hex.substring(2,4).toInt(16),
607
- hex.substring(4,6).toInt(16))
608
- 8 -> Color.argb(hex.substring(6,8).toInt(16),
609
- hex.substring(0,2).toInt(16),
610
- hex.substring(2,4).toInt(16),
611
- 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))
612
570
  else -> null
613
571
  }
614
572
  } catch (_: NumberFormatException) { null }
@@ -617,8 +575,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
617
575
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
618
576
 
619
577
  companion object {
620
- private const val DOWNSAMPLE = 2f
621
- private const val BLUR_ROUNDS = 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
+
622
582
  const val PROGRESSIVE_NONE = 0
623
583
  const val PROGRESSIVE_TOP_TO_BOTTOM = 1
624
584
  const val PROGRESSIVE_BOTTOM_TO_TOP = 2
@@ -626,4 +586,4 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
626
586
  const val PROGRESSIVE_RIGHT_TO_LEFT = 4
627
587
  const val PROGRESSIVE_RADIAL = 5
628
588
  }
629
- }
589
+ }
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.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",