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
|
|
44
|
-
private var overlayColor
|
|
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
|
-
//
|
|
62
|
-
// scaledBitmap:
|
|
63
|
-
// readyBitmap:
|
|
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
|
|
66
|
-
private var scaledBitmap:
|
|
67
|
-
@Volatile private var readyBitmap:
|
|
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
|
|
135
|
+
// ── Root ──────────────────────────────────────────────────────────────────
|
|
80
136
|
|
|
81
|
-
private var blurRoot:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
121
|
-
//
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|
|
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
|
-
|
|
153
|
-
|
|
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() —
|
|
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
|
-
//
|
|
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
|
-
|
|
249
|
+
else -1
|
|
194
250
|
|
|
195
|
-
//
|
|
251
|
+
// Blurred bitmap
|
|
196
252
|
canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
|
|
197
253
|
|
|
198
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
// ──
|
|
281
|
+
// ── Capture — called in OnPreDrawListener (pre-draw, main thread) ─────────
|
|
300
282
|
|
|
301
|
-
private fun
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
// ──
|
|
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
|
|
348
|
-
val w
|
|
349
|
-
val h
|
|
350
|
-
val
|
|
351
|
-
bmp.getPixels(
|
|
352
|
-
|
|
353
|
-
val div
|
|
354
|
-
val wm
|
|
355
|
-
val hm
|
|
356
|
-
val
|
|
357
|
-
val
|
|
358
|
-
val
|
|
359
|
-
val
|
|
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
|
-
|
|
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
|
|
384
|
-
val
|
|
385
|
-
p
|
|
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 * (
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
|
|
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
|
|
433
|
-
if (i <= hm)
|
|
434
|
-
p
|
|
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 * (
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
450
|
-
|
|
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(
|
|
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
|
|
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);
|
|
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)
|
|
500
|
+
if (enabled) safeAddPreDrawListener()
|
|
532
501
|
else {
|
|
533
502
|
safeRemovePreDrawListener()
|
|
534
|
-
|
|
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")
|
|
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")
|
|
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
|
|
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
|
|
621
|
-
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
|
+
|
|
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.
|
|
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",
|