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="
|
|
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
|
|
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
|
|
45
|
-
private var overlayColor
|
|
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
|
-
// ──
|
|
56
|
+
// ── Bitmap double-buffer ──────────────────────────────────────────────────
|
|
61
57
|
|
|
62
|
-
private var captureBitmap: Bitmap? = null
|
|
63
|
-
private var scaledBitmap: Bitmap? = null
|
|
64
|
-
@Volatile
|
|
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
|
|
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
|
-
// ──
|
|
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
|
|
91
|
-
private val rootLoc
|
|
76
|
+
private val myLoc = IntArray(2)
|
|
77
|
+
private val rootLoc = IntArray(2)
|
|
92
78
|
|
|
93
79
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
94
80
|
|
|
95
|
-
// isCapturing:
|
|
96
|
-
|
|
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
|
-
//
|
|
130
|
-
//
|
|
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
|
-
|
|
160
|
-
|
|
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 {
|
|
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() —
|
|
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
|
-
|
|
185
|
+
background?.draw(canvas)
|
|
207
186
|
return
|
|
208
187
|
}
|
|
209
188
|
|
|
210
|
-
// Step 1: progressive mask
|
|
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
|
-
|
|
192
|
+
else -1
|
|
214
193
|
|
|
215
|
-
// Step 2:
|
|
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:
|
|
241
|
-
|
|
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
|
|
250
|
-
val
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
266
|
+
Canvas(scaled).drawBitmap(
|
|
287
267
|
captureRef,
|
|
288
268
|
Rect(0, 0, captureRef.width, captureRef.height),
|
|
289
|
-
Rect(0, 0,
|
|
269
|
+
Rect(0, 0, scaled.width, scaled.height),
|
|
290
270
|
capturePaint
|
|
291
271
|
)
|
|
292
|
-
// Multi-pass
|
|
293
|
-
repeat(BLUR_ROUNDS) {
|
|
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
|
-
// ──
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
val
|
|
316
|
-
val
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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")
|
|
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")
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
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