react-native-blur-vibe 0.1.10 → 0.1.11
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.
|
@@ -12,31 +12,28 @@ import androidx.core.graphics.toColorInt
|
|
|
12
12
|
import com.facebook.react.views.view.ReactViewGroup
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* BlurVibeView — Android API 21–30 backdrop blur
|
|
15
|
+
* BlurVibeView — Android API 21–30 backdrop blur.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Delegates all blur work to LegacyBlurController.
|
|
18
|
+
* Extends ReactViewGroup — handles all RN style props (borderRadius,
|
|
19
|
+
* opacity, transforms etc) natively via ReactViewGroup's own draw pipeline.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* THE STATIC BLUR FIX:
|
|
22
|
+
* draw() is overridden to be a no-op when LegacyBlurController.isCapturing
|
|
23
|
+
* is true. This prevents root.draw() from painting our stale blur output
|
|
24
|
+
* into the capture bitmap. Without this, each frame captures the previous
|
|
25
|
+
* frame's blur output and the blur appears frozen/static.
|
|
24
26
|
*/
|
|
25
27
|
class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
26
28
|
|
|
27
|
-
// ── State ──────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
29
|
private var blurController: LegacyBlurController? = null
|
|
30
|
-
private var pendingBlurAmount
|
|
31
|
-
private var pendingOverlay
|
|
32
|
-
private var cornerRadiusPx
|
|
33
|
-
|
|
34
|
-
// ── Init ───────────────────────────────────────────────────────────────────
|
|
30
|
+
private var pendingBlurAmount = 10f
|
|
31
|
+
private var pendingOverlay = Color.TRANSPARENT
|
|
32
|
+
private var cornerRadiusPx = 0f
|
|
35
33
|
|
|
36
34
|
init {
|
|
37
35
|
setWillNotDraw(false)
|
|
38
36
|
super.setBackgroundColor(Color.TRANSPARENT)
|
|
39
|
-
clipToOutline = true
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -63,15 +60,28 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
|
63
60
|
|
|
64
61
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
65
62
|
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
63
|
if (hasWindowFocus) blurController?.reAttach()
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
// ──
|
|
66
|
+
// ── draw() — suppress self during root capture ────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// When LegacyBlurController is actively capturing (root.draw() in progress),
|
|
69
|
+
// skip drawing ourselves. This makes us invisible to the capture canvas so
|
|
70
|
+
// the capture bitmap contains ONLY the content behind us, not our own stale
|
|
71
|
+
// blur output. Without this, the blur appears static/frozen.
|
|
72
|
+
|
|
73
|
+
override fun draw(canvas: Canvas) {
|
|
74
|
+
if (blurController?.isCapturing == true) return
|
|
75
|
+
super.draw(canvas)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── onDraw ────────────────────────────────────────────────────────────────
|
|
72
79
|
|
|
73
80
|
override fun onDraw(canvas: Canvas) {
|
|
81
|
+
// Draw the blurred background first
|
|
74
82
|
blurController?.draw(canvas, width.toFloat(), height.toFloat())
|
|
83
|
+
// Let ReactViewGroup draw borders/radius/background on top natively
|
|
84
|
+
super.onDraw(canvas)
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
// ── Public setters ─────────────────────────────────────────────────────────
|
|
@@ -88,24 +98,28 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
fun setBlurRadius(factor: Int) {
|
|
91
|
-
//
|
|
92
|
-
// Could expose downsampleFactor setter on controller if needed
|
|
101
|
+
// Exposed as a downsample override for power users — not used internally
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
fun applyBorderRadius(radiusDp: Float) {
|
|
96
105
|
cornerRadiusPx = TypedValue.applyDimension(
|
|
97
106
|
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
98
107
|
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
if (cornerRadiusPx > 0f) {
|
|
109
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
110
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
111
|
+
outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
|
|
112
|
+
}
|
|
102
113
|
}
|
|
114
|
+
clipToOutline = true
|
|
115
|
+
} else {
|
|
116
|
+
outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
117
|
+
clipToOutline = false
|
|
103
118
|
}
|
|
104
|
-
clipToOutline = cornerRadiusPx > 0f
|
|
105
119
|
invalidate()
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
|
|
122
|
+
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {}
|
|
109
123
|
|
|
110
124
|
fun applyBlurEnabled(enabled: Boolean) {
|
|
111
125
|
blurController?.enabled = enabled
|
|
@@ -118,15 +132,13 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
|
118
132
|
|
|
119
133
|
// ── Layout passthrough ─────────────────────────────────────────────────────
|
|
120
134
|
|
|
121
|
-
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
122
|
-
// Yoga handles all layout
|
|
123
|
-
}
|
|
135
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {}
|
|
124
136
|
|
|
125
137
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
126
138
|
|
|
127
139
|
private fun mapBlurAmount(amount: Float): Float {
|
|
128
140
|
val t = amount.coerceIn(0f, 100f) / 100f
|
|
129
|
-
return t * t * 25f
|
|
141
|
+
return t * t * 25f
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
private fun findBlurRoot(): ViewGroup? {
|
|
@@ -13,7 +13,6 @@ import android.graphics.Paint
|
|
|
13
13
|
import android.graphics.PorterDuff
|
|
14
14
|
import android.graphics.PorterDuffXfermode
|
|
15
15
|
import android.graphics.RadialGradient
|
|
16
|
-
import android.graphics.Rect
|
|
17
16
|
import android.graphics.RectF
|
|
18
17
|
import android.graphics.RenderEffect
|
|
19
18
|
import android.graphics.RenderNode
|
|
@@ -28,41 +27,16 @@ import android.view.ViewTreeObserver
|
|
|
28
27
|
import androidx.annotation.RequiresApi
|
|
29
28
|
import androidx.core.graphics.toColorInt
|
|
30
29
|
import com.facebook.react.views.view.ReactViewGroup
|
|
31
|
-
import kotlin.math.max
|
|
32
30
|
import kotlin.math.min
|
|
33
31
|
import kotlin.random.Random
|
|
34
32
|
|
|
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
|
-
*/
|
|
59
33
|
@RequiresApi(Build.VERSION_CODES.S)
|
|
60
34
|
class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
61
35
|
|
|
62
36
|
// ── Blur params ────────────────────────────────────────────────────────────
|
|
63
37
|
|
|
64
|
-
private var blurAmount
|
|
65
|
-
private var overlayColor
|
|
38
|
+
private var blurAmount = 10f
|
|
39
|
+
private var overlayColor = Color.TRANSPARENT
|
|
66
40
|
private var cornerRadiusPx = 0f
|
|
67
41
|
|
|
68
42
|
// ── Progressive blur ──────────────────────────────────────────────────────
|
|
@@ -77,30 +51,45 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
77
51
|
private var noiseBitmap: Bitmap? = null
|
|
78
52
|
private val noisePaint = Paint()
|
|
79
53
|
|
|
80
|
-
// ── Bitmap + RenderNode
|
|
81
|
-
//
|
|
82
|
-
// internalBitmap: captured root pixels at view resolution
|
|
83
|
-
// renderNode: holds bitmap + RenderEffect (GPU blur + tint chain)
|
|
84
|
-
//
|
|
85
|
-
// The renderNode is reused every frame — only its content (bitmap) and
|
|
86
|
-
// effect (radius/tint) are updated, not recreated.
|
|
54
|
+
// ── Bitmap + RenderNode ────────────────────────────────────────────────────
|
|
87
55
|
|
|
88
56
|
private var internalBitmap: Bitmap? = null
|
|
89
57
|
private val renderNode = RenderNode("BlurVibeNode")
|
|
90
58
|
|
|
91
|
-
// ──
|
|
59
|
+
// ── Capture exclusion flag ────────────────────────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// THE FIX FOR STATIC BLUR:
|
|
62
|
+
//
|
|
63
|
+
// root.draw(canvas) walks the entire view tree including THIS BlurView.
|
|
64
|
+
// When it reaches us during capture, our onDraw draws the PREVIOUS frame's
|
|
65
|
+
// blurred bitmap — so the capture contains our own stale output, not just
|
|
66
|
+
// the content behind us. This makes the blur appear static because each
|
|
67
|
+
// frame captures the previous frame's blur output, not the live content.
|
|
68
|
+
//
|
|
69
|
+
// Fix: set isCapturing = true before root.draw(), override draw() to be
|
|
70
|
+
// a no-op when isCapturing = true. root.draw() then skips us completely,
|
|
71
|
+
// capturing ONLY the content behind us. This is exactly how Dimezis
|
|
72
|
+
// BlurView solves the same problem.
|
|
73
|
+
//
|
|
74
|
+
// This does NOT cause a flash because we are not changing visibility —
|
|
75
|
+
// we are only suppressing our own draw() during the off-screen capture.
|
|
76
|
+
// The view remains visible on screen; we just skip drawing into the
|
|
77
|
+
// off-screen capture canvas.
|
|
78
|
+
|
|
79
|
+
private var isCapturing = false
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
// ── Draw paints ───────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
95
84
|
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
|
|
96
85
|
}
|
|
97
|
-
private val
|
|
86
|
+
private val noisePaintFinal = Paint()
|
|
98
87
|
|
|
99
88
|
// ── Root view ─────────────────────────────────────────────────────────────
|
|
100
89
|
|
|
101
90
|
private var blurRoot: ViewGroup? = null
|
|
102
|
-
private val
|
|
103
|
-
private val
|
|
91
|
+
private val myLocation = IntArray(2)
|
|
92
|
+
private val rootLocation = IntArray(2)
|
|
104
93
|
|
|
105
94
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
106
95
|
|
|
@@ -129,7 +118,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
129
118
|
init {
|
|
130
119
|
setWillNotDraw(false)
|
|
131
120
|
super.setBackgroundColor(Color.TRANSPARENT)
|
|
132
|
-
clipToOutline = true
|
|
133
121
|
}
|
|
134
122
|
|
|
135
123
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -147,8 +135,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
147
135
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
148
136
|
frameScheduled = false
|
|
149
137
|
initialized = false
|
|
138
|
+
isCapturing = false
|
|
150
139
|
blurRoot = null
|
|
151
|
-
noiseBitmap?.recycle();
|
|
140
|
+
noiseBitmap?.recycle(); noiseBitmap = null
|
|
152
141
|
internalBitmap?.recycle(); internalBitmap = null
|
|
153
142
|
renderNode.discardDisplayList()
|
|
154
143
|
super.onDetachedFromWindow()
|
|
@@ -158,118 +147,108 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
158
147
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
159
148
|
if (w > 0 && h > 0) {
|
|
160
149
|
internalBitmap?.recycle(); internalBitmap = null
|
|
150
|
+
renderNode.discardDisplayList()
|
|
151
|
+
initialized = false
|
|
161
152
|
initBlur()
|
|
162
153
|
}
|
|
163
154
|
}
|
|
164
155
|
|
|
165
|
-
// ── Multi-window
|
|
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)
|
|
156
|
+
// ── Multi-window safety ───────────────────────────────────────────────────
|
|
174
157
|
|
|
175
158
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
176
159
|
super.onWindowFocusChanged(hasWindowFocus)
|
|
177
160
|
if (hasWindowFocus && blurEnabled && autoUpdate) {
|
|
178
|
-
// Re-attach listener to the current (possibly new) ViewTreeObserver
|
|
179
161
|
safeAddPreDrawListener()
|
|
180
162
|
scheduleFrame()
|
|
181
163
|
}
|
|
182
164
|
}
|
|
183
165
|
|
|
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
166
|
private fun safeAddPreDrawListener() {
|
|
190
167
|
val root = blurRoot ?: return
|
|
191
168
|
val vto = root.viewTreeObserver
|
|
192
|
-
// Remove first (no-op if not attached) then re-add to current observer
|
|
193
169
|
vto.removeOnPreDrawListener(preDrawListener)
|
|
194
|
-
if (vto.isAlive)
|
|
195
|
-
vto.addOnPreDrawListener(preDrawListener)
|
|
196
|
-
}
|
|
170
|
+
if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
|
|
197
171
|
}
|
|
198
172
|
|
|
199
|
-
// ──
|
|
173
|
+
// ── Init blur ─────────────────────────────────────────────────────────────
|
|
200
174
|
|
|
201
175
|
private fun initBlur() {
|
|
202
176
|
val w = measuredWidth; if (w <= 0) return
|
|
203
177
|
val h = measuredHeight; if (h <= 0) return
|
|
204
|
-
|
|
205
178
|
internalBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
206
179
|
renderNode.setPosition(0, 0, w, h)
|
|
207
180
|
initialized = true
|
|
208
|
-
setWillNotDraw(false)
|
|
209
181
|
updateBlur()
|
|
210
182
|
}
|
|
211
183
|
|
|
212
|
-
// ── Core blur
|
|
184
|
+
// ── Core: capture + blur + render ─────────────────────────────────────────
|
|
213
185
|
|
|
214
186
|
private fun updateBlur() {
|
|
215
187
|
if (!blurEnabled || !initialized) return
|
|
216
|
-
val root = blurRoot
|
|
217
|
-
val bitmap = internalBitmap
|
|
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])
|
|
188
|
+
val root = blurRoot ?: return
|
|
189
|
+
val bitmap = internalBitmap ?: return
|
|
190
|
+
if (bitmap.isRecycled) return
|
|
234
191
|
|
|
192
|
+
// ① Compute this view's offset within the root (window coords — correct
|
|
193
|
+
// for all window modes: split-screen, freeform, PiP, DeX)
|
|
194
|
+
root.getLocationInWindow(rootLocation)
|
|
195
|
+
getLocationInWindow(myLocation)
|
|
196
|
+
val offsetX = (myLocation[0] - rootLocation[0]).toFloat()
|
|
197
|
+
val offsetY = (myLocation[1] - rootLocation[1]).toFloat()
|
|
198
|
+
|
|
199
|
+
// ② Capture root content EXCLUDING this view.
|
|
200
|
+
// isCapturing = true causes our draw() to be a no-op, so root.draw()
|
|
201
|
+
// skips us and captures only the content behind us.
|
|
202
|
+
isCapturing = true
|
|
235
203
|
val captureCanvas = Canvas(bitmap)
|
|
236
204
|
captureCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
237
|
-
captureCanvas.translate(-
|
|
238
|
-
captureCanvas.scale(1f / scaleW, 1f / scaleH)
|
|
205
|
+
captureCanvas.translate(-offsetX, -offsetY)
|
|
239
206
|
try {
|
|
240
207
|
root.draw(captureCanvas)
|
|
241
|
-
} catch (_: Exception) {
|
|
242
|
-
|
|
243
|
-
|
|
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)
|
|
208
|
+
} catch (_: Exception) {
|
|
209
|
+
isCapturing = false
|
|
210
|
+
return
|
|
247
211
|
}
|
|
212
|
+
isCapturing = false
|
|
213
|
+
|
|
214
|
+
// ③ Record bitmap into RenderNode.
|
|
215
|
+
// Drawing a BITMAP into RenderNode is stable on all OEM drivers.
|
|
216
|
+
// Drawing a RenderNode into another RenderNode's recording is NOT.
|
|
217
|
+
renderNode.setPosition(0, 0, bitmap.width, bitmap.height)
|
|
248
218
|
val nodeCanvas = renderNode.beginRecording()
|
|
249
219
|
nodeCanvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
250
220
|
renderNode.endRecording()
|
|
251
221
|
|
|
252
|
-
//
|
|
253
|
-
val radius
|
|
222
|
+
// ④ Apply GPU blur + tint as a chained RenderEffect (single GPU pass)
|
|
223
|
+
val radius = blurRadiusFromAmount(blurAmount)
|
|
254
224
|
val blurEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.MIRROR)
|
|
225
|
+
renderNode.setRenderEffect(
|
|
226
|
+
if (Color.alpha(overlayColor) > 0) {
|
|
227
|
+
RenderEffect.createChainEffect(
|
|
228
|
+
RenderEffect.createColorFilterEffect(
|
|
229
|
+
BlendModeColorFilter(overlayColor, BlendMode.SRC_ATOP)
|
|
230
|
+
),
|
|
231
|
+
blurEffect
|
|
232
|
+
)
|
|
233
|
+
} else blurEffect
|
|
234
|
+
)
|
|
255
235
|
|
|
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
236
|
invalidate()
|
|
270
237
|
}
|
|
271
238
|
|
|
272
|
-
// ──
|
|
239
|
+
// ── draw() override — no-op during capture ────────────────────────────────
|
|
240
|
+
//
|
|
241
|
+
// When isCapturing = true (root.draw() is in progress capturing background),
|
|
242
|
+
// suppress our own draw so we don't paint stale blur into the capture bitmap.
|
|
243
|
+
// This makes us invisible to root.draw() during capture only —
|
|
244
|
+
// NOT to the actual screen renderer.
|
|
245
|
+
|
|
246
|
+
override fun draw(canvas: Canvas) {
|
|
247
|
+
if (isCapturing) return // skip self during root capture
|
|
248
|
+
super.draw(canvas)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── onDraw ────────────────────────────────────────────────────────────────
|
|
273
252
|
|
|
274
253
|
override fun onDraw(canvas: Canvas) {
|
|
275
254
|
if (!blurEnabled || !initialized) return
|
|
@@ -277,23 +256,15 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
277
256
|
val h = height.toFloat(); if (h <= 0f) return
|
|
278
257
|
if (!renderNode.hasDisplayList()) return
|
|
279
258
|
|
|
280
|
-
//
|
|
259
|
+
// Progressive mask requires a saved layer so DST_IN mask composites correctly
|
|
281
260
|
val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
|
|
282
261
|
canvas.saveLayer(0f, 0f, w, h, null)
|
|
283
262
|
} else -1
|
|
284
263
|
|
|
285
|
-
//
|
|
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)
|
|
264
|
+
// Draw GPU-blurred + tinted result from RenderNode
|
|
293
265
|
canvas.drawRenderNode(renderNode)
|
|
294
|
-
canvas.restore()
|
|
295
266
|
|
|
296
|
-
//
|
|
267
|
+
// Progressive alpha mask — fades blur across the view
|
|
297
268
|
if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
|
|
298
269
|
buildProgressiveShader(w, h)?.let { shader ->
|
|
299
270
|
maskPaint.shader = shader
|
|
@@ -302,12 +273,16 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
302
273
|
canvas.restoreToCount(saveCount)
|
|
303
274
|
}
|
|
304
275
|
|
|
305
|
-
//
|
|
276
|
+
// Noise grain overlay
|
|
306
277
|
noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
canvas.drawRect(0f, 0f, w, h,
|
|
278
|
+
noisePaintFinal.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
|
|
279
|
+
noisePaintFinal.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
280
|
+
canvas.drawRect(0f, 0f, w, h, noisePaintFinal)
|
|
310
281
|
}
|
|
282
|
+
|
|
283
|
+
// Let ReactViewGroup draw borders/background on top (handles borderRadius
|
|
284
|
+
// and all other RN style props natively — no conflict with our blur)
|
|
285
|
+
super.onDraw(canvas)
|
|
311
286
|
}
|
|
312
287
|
|
|
313
288
|
// ── Progressive shader ────────────────────────────────────────────────────
|
|
@@ -325,7 +300,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
325
300
|
}
|
|
326
301
|
}
|
|
327
302
|
|
|
328
|
-
// ── Noise
|
|
303
|
+
// ── Noise ─────────────────────────────────────────────────────────────────
|
|
329
304
|
|
|
330
305
|
private fun generateNoiseBitmap() {
|
|
331
306
|
if (noiseBitmap?.isRecycled == false) return
|
|
@@ -342,8 +317,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
342
317
|
// ── Public setters ─────────────────────────────────────────────────────────
|
|
343
318
|
|
|
344
319
|
fun setBlurAmount(amount: Float) {
|
|
345
|
-
blurAmount = amount.coerceIn(0f, 100f)
|
|
346
|
-
scheduleFrame()
|
|
320
|
+
blurAmount = amount.coerceIn(0f, 100f); scheduleFrame()
|
|
347
321
|
}
|
|
348
322
|
|
|
349
323
|
fun setOverlayColor(colorString: String?) {
|
|
@@ -351,16 +325,24 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
351
325
|
scheduleFrame()
|
|
352
326
|
}
|
|
353
327
|
|
|
328
|
+
// borderRadius from JS style prop — handled natively by ReactViewGroup.
|
|
329
|
+
// applyBorderRadius is called by our @ReactProp "borderRadius" binding.
|
|
330
|
+
// We additionally set clipToOutline so the blur content is clipped correctly.
|
|
354
331
|
fun applyBorderRadius(radiusDp: Float) {
|
|
355
332
|
cornerRadiusPx = TypedValue.applyDimension(
|
|
356
333
|
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
357
334
|
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
335
|
+
if (cornerRadiusPx > 0f) {
|
|
336
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
337
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
338
|
+
outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
|
|
339
|
+
}
|
|
361
340
|
}
|
|
341
|
+
clipToOutline = true
|
|
342
|
+
} else {
|
|
343
|
+
outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
344
|
+
clipToOutline = false
|
|
362
345
|
}
|
|
363
|
-
clipToOutline = cornerRadiusPx > 0f
|
|
364
346
|
invalidate()
|
|
365
347
|
}
|
|
366
348
|
|
|
@@ -383,10 +365,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
383
365
|
|
|
384
366
|
fun applyBlurEnabled(enabled: Boolean) {
|
|
385
367
|
blurEnabled = enabled
|
|
386
|
-
if (enabled) {
|
|
387
|
-
|
|
388
|
-
scheduleFrame()
|
|
389
|
-
} else {
|
|
368
|
+
if (enabled) { safeAddPreDrawListener(); scheduleFrame() }
|
|
369
|
+
else {
|
|
390
370
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
391
371
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
392
372
|
frameScheduled = false
|
|
@@ -414,10 +394,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
414
394
|
}
|
|
415
395
|
}
|
|
416
396
|
|
|
417
|
-
private fun blurRadiusFromAmount(amount: Float): Float
|
|
418
|
-
|
|
419
|
-
return (t * t * 25f).coerceIn(1f, 25f)
|
|
420
|
-
}
|
|
397
|
+
private fun blurRadiusFromAmount(amount: Float): Float =
|
|
398
|
+
((amount / 100f).let { it * it } * 25f).coerceIn(1f, 25f)
|
|
421
399
|
|
|
422
400
|
private fun findBlurRoot(): ViewGroup? {
|
|
423
401
|
var p = parent
|
|
@@ -440,12 +418,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
440
418
|
val hex = t.removePrefix("#")
|
|
441
419
|
return try {
|
|
442
420
|
when (hex.length) {
|
|
443
|
-
3 -> Color.argb(255,hex[0].toString().repeat(2).toInt(16),
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
6 -> Color.argb(255,hex.substring(0,2).toInt(16),
|
|
447
|
-
|
|
448
|
-
|
|
421
|
+
3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
|
|
422
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
423
|
+
hex[2].toString().repeat(2).toInt(16))
|
|
424
|
+
6 -> Color.argb(255, hex.substring(0,2).toInt(16),
|
|
425
|
+
hex.substring(2,4).toInt(16),
|
|
426
|
+
hex.substring(4,6).toInt(16))
|
|
449
427
|
8 -> Color.argb(hex.substring(6,8).toInt(16),
|
|
450
428
|
hex.substring(0,2).toInt(16),
|
|
451
429
|
hex.substring(2,4).toInt(16),
|
|
@@ -6,7 +6,6 @@ import android.graphics.Color
|
|
|
6
6
|
import android.graphics.Paint
|
|
7
7
|
import android.graphics.PorterDuff
|
|
8
8
|
import android.graphics.Rect
|
|
9
|
-
import android.os.Build
|
|
10
9
|
import android.renderscript.Allocation
|
|
11
10
|
import android.renderscript.Element
|
|
12
11
|
import android.renderscript.RenderScript
|
|
@@ -19,41 +18,30 @@ import android.view.ViewTreeObserver
|
|
|
19
18
|
/**
|
|
20
19
|
* LegacyBlurController — zero-dependency backdrop blur for Android API 21–30.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
* RenderScript is part of the Android SDK — no external library needed.
|
|
21
|
+
* Uses Android SDK RenderScript (ScriptIntrinsicBlur) — no external library.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* → ScriptIntrinsicBlur.forEach() (RenderScript, GPU-accelerated)
|
|
31
|
-
* → view.invalidate()
|
|
32
|
-
* → onDraw: drawBitmap(scaledBitmap) + overlay tint
|
|
33
|
-
*
|
|
34
|
-
* Key optimisations vs naive implementation:
|
|
35
|
-
* - Choreographer gate: max 1 capture per vsync regardless of invalidation count
|
|
36
|
-
* - Bitmap pool: captureBitmap + scaledBitmap reused each frame (zero GC)
|
|
37
|
-
* - RenderScript Allocation pool: inputAlloc + outputAlloc reused (zero GC)
|
|
38
|
-
* - Blur rounds = 2: two passes for smooth spread without pixelation
|
|
39
|
-
* - Downsample factor = 4: captures at 1/16 resolution, blur hides pixel detail
|
|
23
|
+
* THE STATIC BLUR FIX:
|
|
24
|
+
* Before root.draw(), BlurVibeView.draw() is made a no-op via isCapturing flag.
|
|
25
|
+
* This means root.draw() skips the BlurView during capture, so we capture
|
|
26
|
+
* ONLY the content behind us — not our own previous blur output.
|
|
27
|
+
* Without this, each frame captures the previous blur → blur appears static.
|
|
40
28
|
*/
|
|
41
|
-
@Suppress("DEPRECATION")
|
|
29
|
+
@Suppress("DEPRECATION")
|
|
42
30
|
internal class LegacyBlurController(
|
|
43
|
-
private val view:
|
|
31
|
+
private val view: BlurVibeView,
|
|
44
32
|
private val rootView: ViewGroup
|
|
45
33
|
) {
|
|
46
34
|
|
|
47
35
|
companion object {
|
|
48
|
-
private const val DOWNSAMPLE_FACTOR = 4f
|
|
49
|
-
private const val BLUR_RADIUS = 8f
|
|
50
|
-
private const val BLUR_ROUNDS = 2
|
|
36
|
+
private const val DOWNSAMPLE_FACTOR = 4f
|
|
37
|
+
private const val BLUR_RADIUS = 8f
|
|
38
|
+
private const val BLUR_ROUNDS = 2
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
// ── Bitmap pool ────────────────────────────────────────────────────────────
|
|
54
42
|
|
|
55
|
-
private var captureBitmap: Bitmap? = null
|
|
56
|
-
private var scaledBitmap: Bitmap? = null
|
|
43
|
+
private var captureBitmap: Bitmap? = null
|
|
44
|
+
private var scaledBitmap: Bitmap? = null
|
|
57
45
|
private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
58
46
|
private val drawPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
59
47
|
|
|
@@ -66,7 +54,7 @@ internal class LegacyBlurController(
|
|
|
66
54
|
|
|
67
55
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
68
56
|
|
|
69
|
-
var overlayColor: Int
|
|
57
|
+
var overlayColor: Int = Color.TRANSPARENT
|
|
70
58
|
var blurRadius: Float = BLUR_RADIUS
|
|
71
59
|
var enabled: Boolean = true
|
|
72
60
|
set(value) { field = value; if (!value) invalidatePool() }
|
|
@@ -77,8 +65,12 @@ internal class LegacyBlurController(
|
|
|
77
65
|
else rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
78
66
|
}
|
|
79
67
|
|
|
68
|
+
// isCapturing: set true before root.draw() so BlurVibeView.draw() is a no-op
|
|
69
|
+
// preventing stale self-capture. Accessed by BlurVibeView.draw().
|
|
70
|
+
var isCapturing = false
|
|
71
|
+
private set
|
|
72
|
+
|
|
80
73
|
private var frameScheduled = false
|
|
81
|
-
private var isCapturing = false
|
|
82
74
|
|
|
83
75
|
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
84
76
|
|
|
@@ -88,7 +80,7 @@ internal class LegacyBlurController(
|
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
91
|
-
if (!frameScheduled && enabled) {
|
|
83
|
+
if (!frameScheduled && enabled && autoUpdate) {
|
|
92
84
|
frameScheduled = true
|
|
93
85
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
94
86
|
}
|
|
@@ -112,7 +104,6 @@ internal class LegacyBlurController(
|
|
|
112
104
|
// ── Capture + blur ─────────────────────────────────────────────────────────
|
|
113
105
|
|
|
114
106
|
private fun captureAndBlur() {
|
|
115
|
-
if (isCapturing) return
|
|
116
107
|
val rw = rootView.width; if (rw <= 0) return
|
|
117
108
|
val rh = rootView.height; if (rh <= 0) return
|
|
118
109
|
val vw = view.width; if (vw <= 0) return
|
|
@@ -121,85 +112,88 @@ internal class LegacyBlurController(
|
|
|
121
112
|
val sw = (vw / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
|
|
122
113
|
val sh = (vh / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
|
|
123
114
|
|
|
115
|
+
val myLoc = IntArray(2); view.getLocationInWindow(myLoc)
|
|
116
|
+
val rootLoc = IntArray(2); rootView.getLocationInWindow(rootLoc)
|
|
117
|
+
val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
|
|
118
|
+
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
119
|
+
|
|
120
|
+
val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
|
|
121
|
+
val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
|
|
122
|
+
|
|
123
|
+
// Set isCapturing BEFORE root.draw() so BlurVibeView.draw() is skipped
|
|
124
124
|
isCapturing = true
|
|
125
|
+
val c = Canvas(capture)
|
|
126
|
+
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
127
|
+
c.translate(-offsetX, -offsetY)
|
|
125
128
|
try {
|
|
126
|
-
// ① Compute offset of view within root
|
|
127
|
-
val myLoc = IntArray(2); view.getLocationInWindow(myLoc)
|
|
128
|
-
val rootLoc = IntArray(2); rootView.getLocationInWindow(rootLoc)
|
|
129
|
-
val offsetX = myLoc[0] - rootLoc[0]
|
|
130
|
-
val offsetY = myLoc[1] - rootLoc[1]
|
|
131
|
-
|
|
132
|
-
// ② Allocate bitmaps (reuse if size matches)
|
|
133
|
-
val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
|
|
134
|
-
val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
|
|
135
|
-
|
|
136
|
-
// ③ Capture just the region behind this view from root
|
|
137
|
-
val c = Canvas(capture)
|
|
138
|
-
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
139
|
-
c.translate(-offsetX.toFloat(), -offsetY.toFloat())
|
|
140
129
|
rootView.draw(c)
|
|
141
|
-
|
|
142
|
-
// ④ Downsample
|
|
143
|
-
val sc = Canvas(scaled)
|
|
144
|
-
sc.drawBitmap(capture,
|
|
145
|
-
Rect(0, 0, capture.width, capture.height),
|
|
146
|
-
Rect(0, 0, scaled.width, scaled.height),
|
|
147
|
-
capturePaint)
|
|
148
|
-
|
|
149
|
-
// ⑤ Blur (2 rounds for smooth spread)
|
|
150
|
-
repeat(BLUR_ROUNDS) { blurBitmap(scaled) }
|
|
151
|
-
|
|
152
|
-
// ⑥ Trigger redraw with new bitmap
|
|
153
|
-
view.invalidate()
|
|
154
|
-
|
|
155
130
|
} catch (_: Exception) {
|
|
156
|
-
} finally {
|
|
157
131
|
isCapturing = false
|
|
132
|
+
return
|
|
158
133
|
}
|
|
134
|
+
isCapturing = false
|
|
135
|
+
|
|
136
|
+
// Downsample
|
|
137
|
+
Canvas(scaled).drawBitmap(
|
|
138
|
+
capture,
|
|
139
|
+
Rect(0, 0, capture.width, capture.height),
|
|
140
|
+
Rect(0, 0, scaled.width, scaled.height),
|
|
141
|
+
capturePaint
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Blur (2 rounds)
|
|
145
|
+
repeat(BLUR_ROUNDS) { blurBitmap(scaled) }
|
|
146
|
+
|
|
147
|
+
view.invalidate()
|
|
159
148
|
}
|
|
160
149
|
|
|
161
150
|
private fun blurBitmap(bitmap: Bitmap) {
|
|
162
|
-
val
|
|
163
|
-
val sc =
|
|
151
|
+
val r = rs ?: return softwareBlur(bitmap)
|
|
152
|
+
val sc = blurScript ?: return softwareBlur(bitmap)
|
|
164
153
|
try {
|
|
165
|
-
val
|
|
166
|
-
val
|
|
167
|
-
|
|
154
|
+
val iA = reuseAlloc(inputAlloc, bitmap, r).also { inputAlloc = it }
|
|
155
|
+
val oA = reuseAlloc(outputAlloc, bitmap, r).also { outputAlloc = it }
|
|
156
|
+
iA.copyFrom(bitmap)
|
|
168
157
|
sc.setRadius(blurRadius.coerceIn(1f, 25f))
|
|
169
|
-
sc.setInput(
|
|
170
|
-
sc.forEach(
|
|
171
|
-
|
|
172
|
-
} catch (_: Exception) {
|
|
173
|
-
softwareBlur(bitmap)
|
|
174
|
-
}
|
|
158
|
+
sc.setInput(iA)
|
|
159
|
+
sc.forEach(oA)
|
|
160
|
+
oA.copyTo(bitmap)
|
|
161
|
+
} catch (_: Exception) { softwareBlur(bitmap) }
|
|
175
162
|
}
|
|
176
163
|
|
|
177
164
|
private fun softwareBlur(bitmap: Bitmap) {
|
|
178
|
-
// Pure software Gaussian fallback (slower but always works)
|
|
179
165
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
180
166
|
maskFilter = android.graphics.BlurMaskFilter(blurRadius, android.graphics.BlurMaskFilter.Blur.NORMAL)
|
|
181
167
|
}
|
|
182
168
|
Canvas(bitmap).drawBitmap(bitmap, 0f, 0f, paint)
|
|
183
169
|
}
|
|
184
170
|
|
|
185
|
-
// ── Draw
|
|
171
|
+
// ── Draw ─────────────────────────────────────────────────────────────────
|
|
186
172
|
|
|
187
173
|
fun draw(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
|
|
188
174
|
scaledBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
|
|
189
175
|
canvas.drawBitmap(bmp, null,
|
|
190
176
|
android.graphics.RectF(0f, 0f, viewWidth, viewHeight), drawPaint)
|
|
191
177
|
}
|
|
192
|
-
if (Color.alpha(overlayColor) > 0)
|
|
193
|
-
canvas.drawColor(overlayColor)
|
|
194
|
-
}
|
|
178
|
+
if (Color.alpha(overlayColor) > 0) canvas.drawColor(overlayColor)
|
|
195
179
|
}
|
|
196
180
|
|
|
197
|
-
// ──
|
|
181
|
+
// ── Multi-window ──────────────────────────────────────────────────────────
|
|
198
182
|
|
|
199
|
-
fun
|
|
200
|
-
|
|
183
|
+
fun reAttach() {
|
|
184
|
+
if (enabled && autoUpdate) safeAddPreDrawListener()
|
|
201
185
|
}
|
|
202
186
|
|
|
187
|
+
private fun safeAddPreDrawListener() {
|
|
188
|
+
val vto = rootView.viewTreeObserver
|
|
189
|
+
vto.removeOnPreDrawListener(preDrawListener)
|
|
190
|
+
if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
fun onSizeChanged() { invalidatePool() }
|
|
196
|
+
|
|
203
197
|
private fun invalidatePool() {
|
|
204
198
|
captureBitmap?.recycle(); captureBitmap = null
|
|
205
199
|
scaledBitmap?.recycle(); scaledBitmap = null
|
|
@@ -207,26 +201,6 @@ internal class LegacyBlurController(
|
|
|
207
201
|
outputAlloc?.destroy(); outputAlloc = null
|
|
208
202
|
}
|
|
209
203
|
|
|
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
|
-
|
|
230
204
|
fun destroy() {
|
|
231
205
|
rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
232
206
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
@@ -238,7 +212,7 @@ internal class LegacyBlurController(
|
|
|
238
212
|
scaledBitmap?.recycle()
|
|
239
213
|
}
|
|
240
214
|
|
|
241
|
-
// ──
|
|
215
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
242
216
|
|
|
243
217
|
private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
|
|
244
218
|
if (existing != null && !existing.isRecycled
|
|
@@ -248,9 +222,8 @@ internal class LegacyBlurController(
|
|
|
248
222
|
}
|
|
249
223
|
|
|
250
224
|
private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
|
|
251
|
-
if (existing != null
|
|
252
|
-
|
|
253
|
-
&& existing.type.y == src.height) return existing
|
|
225
|
+
if (existing != null && existing.type.x == src.width && existing.type.y == src.height)
|
|
226
|
+
return existing
|
|
254
227
|
existing?.destroy()
|
|
255
228
|
return Allocation.createFromBitmap(rs, src,
|
|
256
229
|
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
|
package/package.json
CHANGED