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