react-native-blur-vibe 0.1.9 → 0.1.10
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.
|
@@ -61,6 +61,13 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
|
61
61
|
blurController?.onSizeChanged()
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
65
|
+
super.onWindowFocusChanged(hasWindowFocus)
|
|
66
|
+
// Re-attach to the current ViewTreeObserver after split-screen / PiP transition.
|
|
67
|
+
// Android may have killed and replaced the old observer during the mode switch.
|
|
68
|
+
if (hasWindowFocus) blurController?.reAttach()
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
65
72
|
|
|
66
73
|
override fun onDraw(canvas: Canvas) {
|
|
@@ -3,6 +3,8 @@ package com.blurvibe
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import android.graphics.BitmapShader
|
|
6
|
+
import android.graphics.BlendMode
|
|
7
|
+
import android.graphics.BlendModeColorFilter
|
|
6
8
|
import android.graphics.Canvas
|
|
7
9
|
import android.graphics.Color
|
|
8
10
|
import android.graphics.LinearGradient
|
|
@@ -11,6 +13,7 @@ import android.graphics.Paint
|
|
|
11
13
|
import android.graphics.PorterDuff
|
|
12
14
|
import android.graphics.PorterDuffXfermode
|
|
13
15
|
import android.graphics.RadialGradient
|
|
16
|
+
import android.graphics.Rect
|
|
14
17
|
import android.graphics.RectF
|
|
15
18
|
import android.graphics.RenderEffect
|
|
16
19
|
import android.graphics.RenderNode
|
|
@@ -25,81 +28,96 @@ import android.view.ViewTreeObserver
|
|
|
25
28
|
import androidx.annotation.RequiresApi
|
|
26
29
|
import androidx.core.graphics.toColorInt
|
|
27
30
|
import com.facebook.react.views.view.ReactViewGroup
|
|
31
|
+
import kotlin.math.max
|
|
28
32
|
import kotlin.math.min
|
|
29
33
|
import kotlin.random.Random
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* BlurVibeViewApi31 — Backdrop blur for Android API 31+
|
|
37
|
+
*
|
|
38
|
+
* Pipeline (adapted from ModernBlurView's RenderEffectBlur approach):
|
|
39
|
+
*
|
|
40
|
+
* 1. rootView.draw(canvas) → internalBitmap (bitmap capture, main thread)
|
|
41
|
+
* 2. renderNode.beginRecording()
|
|
42
|
+
* canvas.drawBitmap(internalBitmap) (bitmap → RenderNode — safe on all OEMs)
|
|
43
|
+
* renderNode.endRecording()
|
|
44
|
+
* 3. renderNode.setRenderEffect(
|
|
45
|
+
* createChainEffect(tintEffect, blurEffect) (GPU blur + tint in one pass)
|
|
46
|
+
* )
|
|
47
|
+
* 4. onDraw: canvas.drawRenderNode(renderNode) (draws GPU result to screen)
|
|
48
|
+
* + progressive mask + noise
|
|
49
|
+
*
|
|
50
|
+
* KEY INSIGHT from ModernBlurView:
|
|
51
|
+
* Drawing a flat BITMAP into a RenderNode, then drawRenderNode() is stable
|
|
52
|
+
* on all OEM devices (Oppo/OnePlus/Xiaomi/Samsung).
|
|
53
|
+
* Drawing a RenderNode INSIDE another RenderNode's recording crashes
|
|
54
|
+
* on OEM-patched GPU drivers. We don't do that here.
|
|
55
|
+
*
|
|
56
|
+
* Choreographer gate: max 1 capture per vsync.
|
|
57
|
+
* Bitmap pool + RenderNode reuse: zero GC per frame.
|
|
58
|
+
*/
|
|
31
59
|
@RequiresApi(Build.VERSION_CODES.S)
|
|
32
60
|
class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
33
61
|
|
|
34
62
|
// ── Blur params ────────────────────────────────────────────────────────────
|
|
35
63
|
|
|
36
|
-
private var
|
|
37
|
-
private var
|
|
38
|
-
private var overlayColor = Color.TRANSPARENT
|
|
64
|
+
private var blurAmount = 10f
|
|
65
|
+
private var overlayColor = Color.TRANSPARENT
|
|
39
66
|
private var cornerRadiusPx = 0f
|
|
40
67
|
|
|
41
|
-
// ── Progressive blur
|
|
68
|
+
// ── Progressive blur ──────────────────────────────────────────────────────
|
|
42
69
|
|
|
43
70
|
private var progressiveDirection = PROGRESSIVE_NONE
|
|
44
71
|
private var progressiveStartIntensity = 1f
|
|
45
72
|
private var progressiveEndIntensity = 0f
|
|
46
73
|
|
|
47
|
-
// ── Noise
|
|
74
|
+
// ── Noise ─────────────────────────────────────────────────────────────────
|
|
48
75
|
|
|
49
76
|
private var noiseFactor = 0.08f
|
|
50
77
|
private var noiseBitmap: Bitmap? = null
|
|
51
78
|
private val noisePaint = Paint()
|
|
52
79
|
|
|
53
|
-
// ──
|
|
80
|
+
// ── Bitmap + RenderNode (ModernBlurView pattern) ───────────────────────────
|
|
54
81
|
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// with RenderEffect blur applied
|
|
82
|
+
// internalBitmap: captured root pixels at view resolution
|
|
83
|
+
// renderNode: holds bitmap + RenderEffect (GPU blur + tint chain)
|
|
58
84
|
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// thrashing and SIGSEGV on some API 31 drivers.
|
|
62
|
-
//
|
|
63
|
-
// IMPORTANT — NO LAYER_TYPE_HARDWARE on the view itself.
|
|
64
|
-
// canvas.drawRenderNode() is only valid on a hardware-accelerated canvas
|
|
65
|
-
// that is NOT itself a hardware layer — mixing them causes SIGSEGV
|
|
66
|
-
// in RenderThread (the exact crash we saw in logcat).
|
|
67
|
-
|
|
68
|
-
private val contentNode = RenderNode("BlurVibeContent")
|
|
69
|
-
private val blurNode = RenderNode("BlurVibeBlur")
|
|
70
|
-
|
|
71
|
-
// ── Recording guard — prevents double-beginRecording crashes ─────────────
|
|
72
|
-
//
|
|
73
|
-
// If captureRootIntoNode fires twice in the same frame (e.g. during
|
|
74
|
-
// layout + invalidate), a second beginRecording() on an active recording
|
|
75
|
-
// crashes the RenderThread. This flag gates it.
|
|
85
|
+
// The renderNode is reused every frame — only its content (bitmap) and
|
|
86
|
+
// effect (radius/tint) are updated, not recreated.
|
|
76
87
|
|
|
77
|
-
private var
|
|
88
|
+
private var internalBitmap: Bitmap? = null
|
|
89
|
+
private val renderNode = RenderNode("BlurVibeNode")
|
|
78
90
|
|
|
79
|
-
// ──
|
|
91
|
+
// ── Draw paint ────────────────────────────────────────────────────────────
|
|
80
92
|
|
|
81
|
-
private val
|
|
93
|
+
private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
82
94
|
private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
83
95
|
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
|
|
84
96
|
}
|
|
97
|
+
private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
85
98
|
|
|
86
99
|
// ── Root view ─────────────────────────────────────────────────────────────
|
|
87
100
|
|
|
88
101
|
private var blurRoot: ViewGroup? = null
|
|
102
|
+
private val rootLocation = IntArray(2)
|
|
103
|
+
private val blurViewLocation = IntArray(2)
|
|
89
104
|
|
|
90
|
-
// ──
|
|
105
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
91
106
|
|
|
107
|
+
private var blurEnabled = true
|
|
108
|
+
private var autoUpdate = true
|
|
92
109
|
private var frameScheduled = false
|
|
110
|
+
private var initialized = false
|
|
111
|
+
|
|
112
|
+
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
113
|
+
|
|
93
114
|
private val frameCallback = Choreographer.FrameCallback {
|
|
94
115
|
frameScheduled = false
|
|
95
|
-
if (isAttachedToWindow)
|
|
96
|
-
captureRootIntoNode()
|
|
97
|
-
invalidate()
|
|
98
|
-
}
|
|
116
|
+
if (isAttachedToWindow && blurEnabled) updateBlur()
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
102
|
-
if (!frameScheduled) {
|
|
120
|
+
if (!frameScheduled && blurEnabled && autoUpdate) {
|
|
103
121
|
frameScheduled = true
|
|
104
122
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
105
123
|
}
|
|
@@ -112,10 +130,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
112
130
|
setWillNotDraw(false)
|
|
113
131
|
super.setBackgroundColor(Color.TRANSPARENT)
|
|
114
132
|
clipToOutline = true
|
|
115
|
-
// DO NOT call setLayerType(LAYER_TYPE_HARDWARE) here —
|
|
116
|
-
// it conflicts with canvas.drawRenderNode() and causes SIGSEGV in RenderThread.
|
|
117
|
-
// The view uses the default layer type (LAYER_TYPE_NONE) so its canvas is
|
|
118
|
-
// the hardware-accelerated display list canvas — which supports drawRenderNode.
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -123,107 +137,163 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
123
137
|
override fun onAttachedToWindow() {
|
|
124
138
|
super.onAttachedToWindow()
|
|
125
139
|
blurRoot = findBlurRoot()
|
|
126
|
-
|
|
140
|
+
safeAddPreDrawListener()
|
|
127
141
|
generateNoiseBitmap()
|
|
142
|
+
if (measuredWidth > 0 && measuredHeight > 0) initBlur()
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
override fun onDetachedFromWindow() {
|
|
131
146
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
132
147
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
133
148
|
frameScheduled = false
|
|
134
|
-
|
|
149
|
+
initialized = false
|
|
135
150
|
blurRoot = null
|
|
136
|
-
noiseBitmap?.recycle()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
contentNode.discardDisplayList()
|
|
140
|
-
blurNode.discardDisplayList()
|
|
151
|
+
noiseBitmap?.recycle(); noiseBitmap = null
|
|
152
|
+
internalBitmap?.recycle(); internalBitmap = null
|
|
153
|
+
renderNode.discardDisplayList()
|
|
141
154
|
super.onDetachedFromWindow()
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
145
158
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
146
|
-
// Update blurNode bounds — contentNode bounds are set in captureRootIntoNode
|
|
147
159
|
if (w > 0 && h > 0) {
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
internalBitmap?.recycle(); internalBitmap = null
|
|
161
|
+
initBlur()
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
|
|
153
|
-
// ──
|
|
165
|
+
// ── Multi-window / split-screen / PiP safety ──────────────────────────────
|
|
166
|
+
//
|
|
167
|
+
// Android can "kill" a ViewTreeObserver when the window enters/exits
|
|
168
|
+
// split-screen, PiP, or freeform mode — creating a new one silently.
|
|
169
|
+
// If we hold a reference to the old (dead) observer our preDrawListener
|
|
170
|
+
// stops firing and blur freezes. We fix this by:
|
|
171
|
+
// 1. Always re-attaching via the CURRENT observer (not a cached one)
|
|
172
|
+
// 2. Checking isAlive() before adding — safe even if called redundantly
|
|
173
|
+
// 3. Re-attaching on window focus gain (fires after every mode transition)
|
|
174
|
+
|
|
175
|
+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
176
|
+
super.onWindowFocusChanged(hasWindowFocus)
|
|
177
|
+
if (hasWindowFocus && blurEnabled && autoUpdate) {
|
|
178
|
+
// Re-attach listener to the current (possibly new) ViewTreeObserver
|
|
179
|
+
safeAddPreDrawListener()
|
|
180
|
+
scheduleFrame()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
154
183
|
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Add preDrawListener to rootView's CURRENT ViewTreeObserver.
|
|
186
|
+
* Removes from any stale observer first, then attaches to the live one.
|
|
187
|
+
* Safe to call multiple times — isAlive() prevents double-attachment.
|
|
188
|
+
*/
|
|
189
|
+
private fun safeAddPreDrawListener() {
|
|
157
190
|
val root = blurRoot ?: return
|
|
158
|
-
val
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
val vto = root.viewTreeObserver
|
|
192
|
+
// Remove first (no-op if not attached) then re-add to current observer
|
|
193
|
+
vto.removeOnPreDrawListener(preDrawListener)
|
|
194
|
+
if (vto.isAlive) {
|
|
195
|
+
vto.addOnPreDrawListener(preDrawListener)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
162
198
|
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
// Step 1: record root-view draw into contentNode
|
|
166
|
-
contentNode.setPosition(0, 0, rw, rh)
|
|
167
|
-
val contentCanvas = contentNode.beginRecording()
|
|
168
|
-
try {
|
|
169
|
-
contentCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
170
|
-
root.draw(contentCanvas)
|
|
171
|
-
} finally {
|
|
172
|
-
contentNode.endRecording() // always end — even if draw() throws
|
|
173
|
-
}
|
|
199
|
+
// ── Blur init ─────────────────────────────────────────────────────────────
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
val myLoc = IntArray(2); getLocationInWindow(myLoc)
|
|
179
|
-
val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
|
|
180
|
-
val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
|
|
181
|
-
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
182
|
-
|
|
183
|
-
blurNode.setPosition(0, 0, vw, vh)
|
|
184
|
-
applyBlurRenderEffect()
|
|
185
|
-
|
|
186
|
-
val blurCanvas = blurNode.beginRecording()
|
|
187
|
-
try {
|
|
188
|
-
blurCanvas.translate(-offsetX, -offsetY)
|
|
189
|
-
blurCanvas.drawRenderNode(contentNode) // safe: contentNode recording is done
|
|
190
|
-
} finally {
|
|
191
|
-
blurNode.endRecording()
|
|
192
|
-
}
|
|
201
|
+
private fun initBlur() {
|
|
202
|
+
val w = measuredWidth; if (w <= 0) return
|
|
203
|
+
val h = measuredHeight; if (h <= 0) return
|
|
193
204
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
internalBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
206
|
+
renderNode.setPosition(0, 0, w, h)
|
|
207
|
+
initialized = true
|
|
208
|
+
setWillNotDraw(false)
|
|
209
|
+
updateBlur()
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
// ── Core blur update (ModernBlurView pattern) ─────────────────────────────
|
|
213
|
+
|
|
214
|
+
private fun updateBlur() {
|
|
215
|
+
if (!blurEnabled || !initialized) return
|
|
216
|
+
val root = blurRoot ?: return
|
|
217
|
+
val bitmap = internalBitmap ?: return
|
|
218
|
+
|
|
219
|
+
// ① Capture root into internalBitmap (same as ModernBlurView's approach)
|
|
220
|
+
// Translate canvas so we capture exactly the region behind this view
|
|
221
|
+
// getLocationInWindow — correct for ALL Android versions and ALL window modes
|
|
222
|
+
// (split-screen, freeform, PiP, DeX).
|
|
223
|
+
// rootView.draw() uses window-relative coordinates, so we must also use
|
|
224
|
+
// window-relative positions for the offset — not screen-absolute.
|
|
225
|
+
// getLocationOnScreen is WRONG in split-screen (Android 7+) because the
|
|
226
|
+
// app window doesn't start at screen (0,0) in that mode.
|
|
227
|
+
root.getLocationInWindow(rootLocation)
|
|
228
|
+
getLocationInWindow(blurViewLocation)
|
|
229
|
+
|
|
230
|
+
val scaleW = width.toFloat() / bitmap.width.toFloat()
|
|
231
|
+
val scaleH = height.toFloat() / bitmap.height.toFloat()
|
|
232
|
+
val left = (blurViewLocation[0] - rootLocation[0])
|
|
233
|
+
val top = (blurViewLocation[1] - rootLocation[1])
|
|
234
|
+
|
|
235
|
+
val captureCanvas = Canvas(bitmap)
|
|
236
|
+
captureCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
237
|
+
captureCanvas.translate(-left / scaleW, -top / scaleH)
|
|
238
|
+
captureCanvas.scale(1f / scaleW, 1f / scaleH)
|
|
239
|
+
try {
|
|
240
|
+
root.draw(captureCanvas)
|
|
241
|
+
} catch (_: Exception) { return }
|
|
242
|
+
|
|
243
|
+
// ② Record bitmap into RenderNode (ModernBlurView key insight:
|
|
244
|
+
// bitmap → RenderNode is stable; RenderNode → RenderNode is not)
|
|
245
|
+
if (renderNode.width != bitmap.width || renderNode.height != bitmap.height) {
|
|
246
|
+
renderNode.setPosition(0, 0, bitmap.width, bitmap.height)
|
|
203
247
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
248
|
+
val nodeCanvas = renderNode.beginRecording()
|
|
249
|
+
nodeCanvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
250
|
+
renderNode.endRecording()
|
|
251
|
+
|
|
252
|
+
// ③ Build chained RenderEffect: blur first, then tint on top (one GPU pass)
|
|
253
|
+
val radius = blurRadiusFromAmount(blurAmount)
|
|
254
|
+
val blurEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.MIRROR)
|
|
255
|
+
|
|
256
|
+
val finalEffect = if (Color.alpha(overlayColor) > 0) {
|
|
257
|
+
// Chain: blur → tint in single GPU pass (ModernBlurView's chained approach)
|
|
258
|
+
val tintEffect = RenderEffect.createColorFilterEffect(
|
|
259
|
+
BlendModeColorFilter(overlayColor, BlendMode.SRC_ATOP)
|
|
260
|
+
)
|
|
261
|
+
RenderEffect.createChainEffect(tintEffect, blurEffect)
|
|
262
|
+
} else {
|
|
263
|
+
blurEffect
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderNode.setRenderEffect(finalEffect)
|
|
267
|
+
|
|
268
|
+
// ④ Trigger redraw — onDraw will drawRenderNode (GPU-rendered result)
|
|
269
|
+
invalidate()
|
|
207
270
|
}
|
|
208
271
|
|
|
209
272
|
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
210
273
|
|
|
211
274
|
override fun onDraw(canvas: Canvas) {
|
|
275
|
+
if (!blurEnabled || !initialized) return
|
|
212
276
|
val w = width.toFloat(); if (w <= 0f) return
|
|
213
277
|
val h = height.toFloat(); if (h <= 0f) return
|
|
214
|
-
|
|
215
|
-
// Guard: only draw if blurNode has a valid recorded display list
|
|
216
|
-
if (!blurNode.hasDisplayList()) return
|
|
278
|
+
if (!renderNode.hasDisplayList()) return
|
|
217
279
|
|
|
218
280
|
// Step 1: save layer for progressive mask compositing
|
|
219
281
|
val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
|
|
220
282
|
canvas.saveLayer(0f, 0f, w, h, null)
|
|
221
283
|
} else -1
|
|
222
284
|
|
|
223
|
-
// Step 2: draw
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
285
|
+
// Step 2: draw GPU-blurred + tinted result
|
|
286
|
+
// Scale from bitmap resolution back to view resolution
|
|
287
|
+
val bitmapW = internalBitmap?.width?.toFloat() ?: w
|
|
288
|
+
val bitmapH = internalBitmap?.height?.toFloat() ?: h
|
|
289
|
+
val scaleX = w / bitmapW
|
|
290
|
+
val scaleY = h / bitmapH
|
|
291
|
+
canvas.save()
|
|
292
|
+
canvas.scale(scaleX, scaleY)
|
|
293
|
+
canvas.drawRenderNode(renderNode)
|
|
294
|
+
canvas.restore()
|
|
295
|
+
|
|
296
|
+
// Step 3: progressive alpha mask fades the blur
|
|
227
297
|
if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
|
|
228
298
|
buildProgressiveShader(w, h)?.let { shader ->
|
|
229
299
|
maskPaint.shader = shader
|
|
@@ -232,38 +302,26 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
232
302
|
canvas.restoreToCount(saveCount)
|
|
233
303
|
}
|
|
234
304
|
|
|
235
|
-
// Step 4: overlay
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} else {
|
|
241
|
-
canvas.drawRect(0f, 0f, w, h, overlayPaint)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Step 5: noise grain
|
|
246
|
-
noiseBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
|
|
247
|
-
if (noiseFactor > 0f) {
|
|
248
|
-
noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
|
|
249
|
-
noisePaint.shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
250
|
-
canvas.drawRect(0f, 0f, w, h, noisePaint)
|
|
251
|
-
}
|
|
305
|
+
// Step 4: noise grain overlay
|
|
306
|
+
noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
|
|
307
|
+
noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
|
|
308
|
+
noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
309
|
+
canvas.drawRect(0f, 0f, w, h, noisePaint)
|
|
252
310
|
}
|
|
253
311
|
}
|
|
254
312
|
|
|
255
313
|
// ── Progressive shader ────────────────────────────────────────────────────
|
|
256
314
|
|
|
257
315
|
private fun buildProgressiveShader(w: Float, h: Float): Shader? {
|
|
258
|
-
val
|
|
259
|
-
val
|
|
316
|
+
val sc = Color.argb((progressiveStartIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
|
|
317
|
+
val ec = Color.argb((progressiveEndIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
|
|
260
318
|
return when (progressiveDirection) {
|
|
261
|
-
PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h,
|
|
262
|
-
PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,
|
|
263
|
-
PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,
|
|
264
|
-
PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,
|
|
265
|
-
PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,
|
|
266
|
-
else
|
|
319
|
+
PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h,sc,ec,Shader.TileMode.CLAMP)
|
|
320
|
+
PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
|
|
321
|
+
PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
|
|
322
|
+
PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,sc,ec,Shader.TileMode.CLAMP)
|
|
323
|
+
PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,min(w,h)/2f,sc,ec,Shader.TileMode.CLAMP)
|
|
324
|
+
else -> null
|
|
267
325
|
}
|
|
268
326
|
}
|
|
269
327
|
|
|
@@ -274,11 +332,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
274
332
|
val size = 64
|
|
275
333
|
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
276
334
|
val rng = Random(42)
|
|
277
|
-
for (x in 0 until size) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
bmp.setPixel(x, y, Color.argb(255, v, v, v))
|
|
281
|
-
}
|
|
335
|
+
for (x in 0 until size) for (y in 0 until size) {
|
|
336
|
+
val v = rng.nextInt(256)
|
|
337
|
+
bmp.setPixel(x, y, Color.argb(255, v, v, v))
|
|
282
338
|
}
|
|
283
339
|
noiseBitmap = bmp
|
|
284
340
|
}
|
|
@@ -286,16 +342,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
286
342
|
// ── Public setters ─────────────────────────────────────────────────────────
|
|
287
343
|
|
|
288
344
|
fun setBlurAmount(amount: Float) {
|
|
289
|
-
|
|
290
|
-
blurRadiusX = t * t * MAX_BLUR_RADIUS
|
|
291
|
-
blurRadiusY = blurRadiusX
|
|
292
|
-
applyBlurRenderEffect()
|
|
345
|
+
blurAmount = amount.coerceIn(0f, 100f)
|
|
293
346
|
scheduleFrame()
|
|
294
347
|
}
|
|
295
348
|
|
|
296
349
|
fun setOverlayColor(colorString: String?) {
|
|
297
350
|
overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
298
|
-
|
|
351
|
+
scheduleFrame()
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
fun applyBorderRadius(radiusDp: Float) {
|
|
@@ -311,50 +364,41 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
311
364
|
invalidate()
|
|
312
365
|
}
|
|
313
366
|
|
|
314
|
-
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER")
|
|
367
|
+
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") c: String?) {}
|
|
315
368
|
|
|
316
|
-
fun setProgressiveBlurDirection(
|
|
317
|
-
progressiveDirection = when (
|
|
369
|
+
fun setProgressiveBlurDirection(d: String?) {
|
|
370
|
+
progressiveDirection = when (d) {
|
|
318
371
|
"topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
|
|
319
372
|
"bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
|
|
320
373
|
"leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
|
|
321
374
|
"rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
|
|
322
375
|
"radial" -> PROGRESSIVE_RADIAL
|
|
323
376
|
else -> PROGRESSIVE_NONE
|
|
324
|
-
}
|
|
325
|
-
invalidate()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
fun setProgressiveStartIntensity(intensity: Float) {
|
|
329
|
-
progressiveStartIntensity = intensity.coerceIn(0f, 1f); invalidate()
|
|
377
|
+
}; invalidate()
|
|
330
378
|
}
|
|
331
379
|
|
|
332
|
-
fun
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
fun setNoiseFactor(factor: Float) {
|
|
337
|
-
noiseFactor = factor.coerceIn(0f, 1f); invalidate()
|
|
338
|
-
}
|
|
380
|
+
fun setProgressiveStartIntensity(v: Float) { progressiveStartIntensity = v.coerceIn(0f,1f); invalidate() }
|
|
381
|
+
fun setProgressiveEndIntensity(v: Float) { progressiveEndIntensity = v.coerceIn(0f,1f); invalidate() }
|
|
382
|
+
fun setNoiseFactor(v: Float) { noiseFactor = v.coerceIn(0f,1f); invalidate() }
|
|
339
383
|
|
|
340
384
|
fun applyBlurEnabled(enabled: Boolean) {
|
|
341
|
-
|
|
385
|
+
blurEnabled = enabled
|
|
386
|
+
if (enabled) {
|
|
387
|
+
safeAddPreDrawListener()
|
|
388
|
+
scheduleFrame()
|
|
389
|
+
} else {
|
|
342
390
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
343
391
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
344
392
|
frameScheduled = false
|
|
345
|
-
|
|
346
|
-
contentNode.discardDisplayList()
|
|
393
|
+
renderNode.discardDisplayList()
|
|
347
394
|
invalidate()
|
|
348
|
-
} else {
|
|
349
|
-
blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
|
|
350
|
-
scheduleFrame()
|
|
351
395
|
}
|
|
352
396
|
}
|
|
353
397
|
|
|
354
|
-
fun setAutoUpdate(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
398
|
+
fun setAutoUpdate(update: Boolean) {
|
|
399
|
+
autoUpdate = update
|
|
400
|
+
if (update) safeAddPreDrawListener()
|
|
401
|
+
else {
|
|
358
402
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
359
403
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
360
404
|
frameScheduled = false
|
|
@@ -364,12 +408,17 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
364
408
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
365
409
|
|
|
366
410
|
private fun scheduleFrame() {
|
|
367
|
-
if (!frameScheduled) {
|
|
411
|
+
if (!frameScheduled && blurEnabled) {
|
|
368
412
|
frameScheduled = true
|
|
369
413
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
370
414
|
}
|
|
371
415
|
}
|
|
372
416
|
|
|
417
|
+
private fun blurRadiusFromAmount(amount: Float): Float {
|
|
418
|
+
val t = amount / 100f
|
|
419
|
+
return (t * t * 25f).coerceIn(1f, 25f)
|
|
420
|
+
}
|
|
421
|
+
|
|
373
422
|
private fun findBlurRoot(): ViewGroup? {
|
|
374
423
|
var p = parent
|
|
375
424
|
while (p != null) {
|
|
@@ -391,12 +440,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
391
440
|
val hex = t.removePrefix("#")
|
|
392
441
|
return try {
|
|
393
442
|
when (hex.length) {
|
|
394
|
-
3 -> Color.argb(255,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
6 -> Color.argb(255,
|
|
398
|
-
|
|
399
|
-
|
|
443
|
+
3 -> Color.argb(255,hex[0].toString().repeat(2).toInt(16),
|
|
444
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
445
|
+
hex[2].toString().repeat(2).toInt(16))
|
|
446
|
+
6 -> Color.argb(255,hex.substring(0,2).toInt(16),
|
|
447
|
+
hex.substring(2,4).toInt(16),
|
|
448
|
+
hex.substring(4,6).toInt(16))
|
|
400
449
|
8 -> Color.argb(hex.substring(6,8).toInt(16),
|
|
401
450
|
hex.substring(0,2).toInt(16),
|
|
402
451
|
hex.substring(2,4).toInt(16),
|
|
@@ -406,16 +455,14 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
406
455
|
} catch (_: NumberFormatException) { null }
|
|
407
456
|
}
|
|
408
457
|
|
|
409
|
-
override fun onLayout(changed: Boolean,
|
|
458
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
|
|
410
459
|
|
|
411
460
|
companion object {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const val
|
|
415
|
-
const val
|
|
416
|
-
const val
|
|
417
|
-
const val
|
|
418
|
-
const val PROGRESSIVE_RIGHT_TO_LEFT = 4
|
|
419
|
-
const val PROGRESSIVE_RADIAL = 5
|
|
461
|
+
const val PROGRESSIVE_NONE = 0
|
|
462
|
+
const val PROGRESSIVE_TOP_TO_BOTTOM = 1
|
|
463
|
+
const val PROGRESSIVE_BOTTOM_TO_TOP = 2
|
|
464
|
+
const val PROGRESSIVE_LEFT_TO_RIGHT = 3
|
|
465
|
+
const val PROGRESSIVE_RIGHT_TO_LEFT = 4
|
|
466
|
+
const val PROGRESSIVE_RADIAL = 5
|
|
420
467
|
}
|
|
421
468
|
}
|
|
@@ -73,7 +73,7 @@ internal class LegacyBlurController(
|
|
|
73
73
|
var autoUpdate: Boolean = true
|
|
74
74
|
set(value) {
|
|
75
75
|
field = value
|
|
76
|
-
if (value)
|
|
76
|
+
if (value) safeAddPreDrawListener()
|
|
77
77
|
else rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -99,7 +99,7 @@ internal class LegacyBlurController(
|
|
|
99
99
|
|
|
100
100
|
init {
|
|
101
101
|
initRenderScript()
|
|
102
|
-
|
|
102
|
+
safeAddPreDrawListener()
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
private fun initRenderScript() {
|
|
@@ -207,6 +207,26 @@ internal class LegacyBlurController(
|
|
|
207
207
|
outputAlloc?.destroy(); outputAlloc = null
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// ── Multi-window / split-screen / PiP safety ──────────────────────────────
|
|
211
|
+
//
|
|
212
|
+
// Called by BlurVibeView.onWindowFocusChanged(hasFocus=true).
|
|
213
|
+
// Re-attaches the preDrawListener to the rootView's current
|
|
214
|
+
// (possibly newly created) ViewTreeObserver after a window mode transition.
|
|
215
|
+
|
|
216
|
+
fun reAttach() {
|
|
217
|
+
if (enabled && autoUpdate) {
|
|
218
|
+
safeAddPreDrawListener()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private fun safeAddPreDrawListener() {
|
|
223
|
+
val vto = rootView.viewTreeObserver
|
|
224
|
+
vto.removeOnPreDrawListener(preDrawListener) // no-op if not attached
|
|
225
|
+
if (vto.isAlive) {
|
|
226
|
+
vto.addOnPreDrawListener(preDrawListener)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
210
230
|
fun destroy() {
|
|
211
231
|
rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
212
232
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
package/package.json
CHANGED