react-native-blur-vibe 0.1.10 → 0.1.12
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
|
-
|
|
39
|
-
clipToOutline = true
|
|
36
|
+
// DO NOT call setBackgroundColor — see BlurVibeViewApi31 for explanation.
|
|
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,15 @@ 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 {
|
|
140
|
+
// Linear 0→1, 100→25 (RenderScript max kernel is 25)
|
|
141
|
+
// With 3 blur rounds this gives equivalent spread to Api31's 120px single-pass
|
|
128
142
|
val t = amount.coerceIn(0f, 100f) / 100f
|
|
129
|
-
return
|
|
143
|
+
return (1f + t * 24f) // 1–25 linear, rounds=3 gives wide spread
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
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
|
|
|
@@ -128,8 +117,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
128
117
|
|
|
129
118
|
init {
|
|
130
119
|
setWillNotDraw(false)
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
// DO NOT call setBackgroundColor here.
|
|
121
|
+
// ReactViewGroup manages its own ReactViewBackgroundDrawable which handles
|
|
122
|
+
// all RN style props: borderRadius, borderColor, borderWidth, opacity,
|
|
123
|
+
// backgroundColor, shadow, elevation etc.
|
|
124
|
+
// Calling super.setBackgroundColor() replaces that drawable with a plain
|
|
125
|
+
// ColorDrawable — destroying all style prop handling.
|
|
133
126
|
}
|
|
134
127
|
|
|
135
128
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -147,8 +140,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
147
140
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
148
141
|
frameScheduled = false
|
|
149
142
|
initialized = false
|
|
143
|
+
isCapturing = false
|
|
150
144
|
blurRoot = null
|
|
151
|
-
noiseBitmap?.recycle();
|
|
145
|
+
noiseBitmap?.recycle(); noiseBitmap = null
|
|
152
146
|
internalBitmap?.recycle(); internalBitmap = null
|
|
153
147
|
renderNode.discardDisplayList()
|
|
154
148
|
super.onDetachedFromWindow()
|
|
@@ -158,118 +152,114 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
158
152
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
159
153
|
if (w > 0 && h > 0) {
|
|
160
154
|
internalBitmap?.recycle(); internalBitmap = null
|
|
155
|
+
renderNode.discardDisplayList()
|
|
156
|
+
initialized = false
|
|
161
157
|
initBlur()
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
160
|
|
|
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)
|
|
161
|
+
// ── Multi-window safety ───────────────────────────────────────────────────
|
|
174
162
|
|
|
175
163
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
176
164
|
super.onWindowFocusChanged(hasWindowFocus)
|
|
177
165
|
if (hasWindowFocus && blurEnabled && autoUpdate) {
|
|
178
|
-
// Re-attach listener to the current (possibly new) ViewTreeObserver
|
|
179
166
|
safeAddPreDrawListener()
|
|
180
167
|
scheduleFrame()
|
|
181
168
|
}
|
|
182
169
|
}
|
|
183
170
|
|
|
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
171
|
private fun safeAddPreDrawListener() {
|
|
190
172
|
val root = blurRoot ?: return
|
|
191
173
|
val vto = root.viewTreeObserver
|
|
192
|
-
// Remove first (no-op if not attached) then re-add to current observer
|
|
193
174
|
vto.removeOnPreDrawListener(preDrawListener)
|
|
194
|
-
if (vto.isAlive)
|
|
195
|
-
vto.addOnPreDrawListener(preDrawListener)
|
|
196
|
-
}
|
|
175
|
+
if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
|
|
197
176
|
}
|
|
198
177
|
|
|
199
|
-
// ──
|
|
178
|
+
// ── Init blur ─────────────────────────────────────────────────────────────
|
|
200
179
|
|
|
201
180
|
private fun initBlur() {
|
|
202
181
|
val w = measuredWidth; if (w <= 0) return
|
|
203
182
|
val h = measuredHeight; if (h <= 0) return
|
|
204
|
-
|
|
205
183
|
internalBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
206
184
|
renderNode.setPosition(0, 0, w, h)
|
|
207
185
|
initialized = true
|
|
208
|
-
setWillNotDraw(false)
|
|
209
186
|
updateBlur()
|
|
210
187
|
}
|
|
211
188
|
|
|
212
|
-
// ── Core blur
|
|
189
|
+
// ── Core: capture + blur + render ─────────────────────────────────────────
|
|
213
190
|
|
|
214
191
|
private fun updateBlur() {
|
|
215
192
|
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])
|
|
193
|
+
val root = blurRoot ?: return
|
|
194
|
+
val bitmap = internalBitmap ?: return
|
|
195
|
+
if (bitmap.isRecycled) return
|
|
234
196
|
|
|
197
|
+
// ① Compute this view's offset within the root (window coords — correct
|
|
198
|
+
// for all window modes: split-screen, freeform, PiP, DeX)
|
|
199
|
+
root.getLocationInWindow(rootLocation)
|
|
200
|
+
getLocationInWindow(myLocation)
|
|
201
|
+
val offsetX = (myLocation[0] - rootLocation[0]).toFloat()
|
|
202
|
+
val offsetY = (myLocation[1] - rootLocation[1]).toFloat()
|
|
203
|
+
|
|
204
|
+
// ② Capture root content EXCLUDING this view.
|
|
205
|
+
// isCapturing = true causes our draw() to be a no-op, so root.draw()
|
|
206
|
+
// skips us and captures only the content behind us.
|
|
207
|
+
isCapturing = true
|
|
235
208
|
val captureCanvas = Canvas(bitmap)
|
|
236
209
|
captureCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
237
|
-
captureCanvas.translate(-
|
|
238
|
-
captureCanvas.scale(1f / scaleW, 1f / scaleH)
|
|
210
|
+
captureCanvas.translate(-offsetX, -offsetY)
|
|
239
211
|
try {
|
|
240
212
|
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)
|
|
213
|
+
} catch (_: Exception) {
|
|
214
|
+
isCapturing = false
|
|
215
|
+
return
|
|
247
216
|
}
|
|
217
|
+
isCapturing = false
|
|
218
|
+
|
|
219
|
+
// ③ Record bitmap into RenderNode.
|
|
220
|
+
// Drawing a BITMAP into RenderNode is stable on all OEM drivers.
|
|
221
|
+
// Drawing a RenderNode into another RenderNode's recording is NOT.
|
|
222
|
+
renderNode.setPosition(0, 0, bitmap.width, bitmap.height)
|
|
248
223
|
val nodeCanvas = renderNode.beginRecording()
|
|
249
224
|
nodeCanvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
250
225
|
renderNode.endRecording()
|
|
251
226
|
|
|
252
|
-
//
|
|
227
|
+
// ④ Apply GPU blur + tint as chained RenderEffects
|
|
228
|
+
// Double-pass blur: two Gaussian passes = wider spread kernel
|
|
229
|
+
// Equivalent to sqrt(2) wider sigma — gives frosted-glass light diffusion
|
|
230
|
+
// CLAMP tile mode: no edge reflection artifacts
|
|
253
231
|
val radius = blurRadiusFromAmount(blurAmount)
|
|
254
|
-
val
|
|
255
|
-
|
|
256
|
-
val
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
232
|
+
val pass1 = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
|
|
233
|
+
val pass2 = RenderEffect.createBlurEffect(radius * 0.5f, radius * 0.5f, Shader.TileMode.CLAMP)
|
|
234
|
+
val doubleBlur = RenderEffect.createChainEffect(pass2, pass1) // pass1 first, then pass2
|
|
235
|
+
|
|
236
|
+
renderNode.setRenderEffect(
|
|
237
|
+
if (Color.alpha(overlayColor) > 0) {
|
|
238
|
+
RenderEffect.createChainEffect(
|
|
239
|
+
RenderEffect.createColorFilterEffect(
|
|
240
|
+
BlendModeColorFilter(overlayColor, BlendMode.SRC_ATOP)
|
|
241
|
+
),
|
|
242
|
+
doubleBlur
|
|
243
|
+
)
|
|
244
|
+
} else doubleBlur
|
|
245
|
+
)
|
|
267
246
|
|
|
268
|
-
// ④ Trigger redraw — onDraw will drawRenderNode (GPU-rendered result)
|
|
269
247
|
invalidate()
|
|
270
248
|
}
|
|
271
249
|
|
|
272
|
-
// ──
|
|
250
|
+
// ── draw() override — no-op during capture ────────────────────────────────
|
|
251
|
+
//
|
|
252
|
+
// When isCapturing = true (root.draw() is in progress capturing background),
|
|
253
|
+
// suppress our own draw so we don't paint stale blur into the capture bitmap.
|
|
254
|
+
// This makes us invisible to root.draw() during capture only —
|
|
255
|
+
// NOT to the actual screen renderer.
|
|
256
|
+
|
|
257
|
+
override fun draw(canvas: Canvas) {
|
|
258
|
+
if (isCapturing) return // skip self during root capture
|
|
259
|
+
super.draw(canvas)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── onDraw ────────────────────────────────────────────────────────────────
|
|
273
263
|
|
|
274
264
|
override fun onDraw(canvas: Canvas) {
|
|
275
265
|
if (!blurEnabled || !initialized) return
|
|
@@ -277,23 +267,15 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
277
267
|
val h = height.toFloat(); if (h <= 0f) return
|
|
278
268
|
if (!renderNode.hasDisplayList()) return
|
|
279
269
|
|
|
280
|
-
//
|
|
270
|
+
// Progressive mask requires a saved layer so DST_IN mask composites correctly
|
|
281
271
|
val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
|
|
282
272
|
canvas.saveLayer(0f, 0f, w, h, null)
|
|
283
273
|
} else -1
|
|
284
274
|
|
|
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)
|
|
275
|
+
// Draw GPU-blurred + tinted result from RenderNode
|
|
293
276
|
canvas.drawRenderNode(renderNode)
|
|
294
|
-
canvas.restore()
|
|
295
277
|
|
|
296
|
-
//
|
|
278
|
+
// Progressive alpha mask — fades blur across the view
|
|
297
279
|
if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
|
|
298
280
|
buildProgressiveShader(w, h)?.let { shader ->
|
|
299
281
|
maskPaint.shader = shader
|
|
@@ -302,12 +284,16 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
302
284
|
canvas.restoreToCount(saveCount)
|
|
303
285
|
}
|
|
304
286
|
|
|
305
|
-
//
|
|
287
|
+
// Noise grain overlay
|
|
306
288
|
noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
canvas.drawRect(0f, 0f, w, h,
|
|
289
|
+
noisePaintFinal.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
|
|
290
|
+
noisePaintFinal.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
291
|
+
canvas.drawRect(0f, 0f, w, h, noisePaintFinal)
|
|
310
292
|
}
|
|
293
|
+
|
|
294
|
+
// Let ReactViewGroup draw borders/background on top (handles borderRadius
|
|
295
|
+
// and all other RN style props natively — no conflict with our blur)
|
|
296
|
+
super.onDraw(canvas)
|
|
311
297
|
}
|
|
312
298
|
|
|
313
299
|
// ── Progressive shader ────────────────────────────────────────────────────
|
|
@@ -325,7 +311,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
325
311
|
}
|
|
326
312
|
}
|
|
327
313
|
|
|
328
|
-
// ── Noise
|
|
314
|
+
// ── Noise ─────────────────────────────────────────────────────────────────
|
|
329
315
|
|
|
330
316
|
private fun generateNoiseBitmap() {
|
|
331
317
|
if (noiseBitmap?.isRecycled == false) return
|
|
@@ -342,8 +328,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
342
328
|
// ── Public setters ─────────────────────────────────────────────────────────
|
|
343
329
|
|
|
344
330
|
fun setBlurAmount(amount: Float) {
|
|
345
|
-
blurAmount = amount.coerceIn(0f, 100f)
|
|
346
|
-
scheduleFrame()
|
|
331
|
+
blurAmount = amount.coerceIn(0f, 100f); scheduleFrame()
|
|
347
332
|
}
|
|
348
333
|
|
|
349
334
|
fun setOverlayColor(colorString: String?) {
|
|
@@ -351,16 +336,24 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
351
336
|
scheduleFrame()
|
|
352
337
|
}
|
|
353
338
|
|
|
339
|
+
// borderRadius from JS style prop — handled natively by ReactViewGroup.
|
|
340
|
+
// applyBorderRadius is called by our @ReactProp "borderRadius" binding.
|
|
341
|
+
// We additionally set clipToOutline so the blur content is clipped correctly.
|
|
354
342
|
fun applyBorderRadius(radiusDp: Float) {
|
|
355
343
|
cornerRadiusPx = TypedValue.applyDimension(
|
|
356
344
|
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
357
345
|
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
346
|
+
if (cornerRadiusPx > 0f) {
|
|
347
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
348
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
349
|
+
outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
|
|
350
|
+
}
|
|
361
351
|
}
|
|
352
|
+
clipToOutline = true
|
|
353
|
+
} else {
|
|
354
|
+
outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
355
|
+
clipToOutline = false
|
|
362
356
|
}
|
|
363
|
-
clipToOutline = cornerRadiusPx > 0f
|
|
364
357
|
invalidate()
|
|
365
358
|
}
|
|
366
359
|
|
|
@@ -383,10 +376,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
383
376
|
|
|
384
377
|
fun applyBlurEnabled(enabled: Boolean) {
|
|
385
378
|
blurEnabled = enabled
|
|
386
|
-
if (enabled) {
|
|
387
|
-
|
|
388
|
-
scheduleFrame()
|
|
389
|
-
} else {
|
|
379
|
+
if (enabled) { safeAddPreDrawListener(); scheduleFrame() }
|
|
380
|
+
else {
|
|
390
381
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
391
382
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
392
383
|
frameScheduled = false
|
|
@@ -415,8 +406,14 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
415
406
|
}
|
|
416
407
|
|
|
417
408
|
private fun blurRadiusFromAmount(amount: Float): Float {
|
|
418
|
-
|
|
419
|
-
|
|
409
|
+
// Linear mapping: 0→1px, 10→13px, 25→31px, 50→61px, 75→91px, 100→120px
|
|
410
|
+
// These values match CSS backdrop-filter feel:
|
|
411
|
+
// blurAmount=10 ≈ backdrop-blur-sm (4px CSS = ~13px GPU after downsample)
|
|
412
|
+
// blurAmount=25 ≈ backdrop-blur-md (12px CSS ≈ 31px GPU)
|
|
413
|
+
// blurAmount=50 ≈ backdrop-blur-xl (24px CSS ≈ 61px GPU)
|
|
414
|
+
// blurAmount=100 ≈ backdrop-blur-3xl (64px CSS = fully frosted glass)
|
|
415
|
+
val t = amount.coerceIn(0f, 100f) / 100f
|
|
416
|
+
return (1f + t * 119f) // 1–120 linear
|
|
420
417
|
}
|
|
421
418
|
|
|
422
419
|
private fun findBlurRoot(): ViewGroup? {
|
|
@@ -440,12 +437,12 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
440
437
|
val hex = t.removePrefix("#")
|
|
441
438
|
return try {
|
|
442
439
|
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
|
-
|
|
440
|
+
3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
|
|
441
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
442
|
+
hex[2].toString().repeat(2).toInt(16))
|
|
443
|
+
6 -> Color.argb(255, hex.substring(0,2).toInt(16),
|
|
444
|
+
hex.substring(2,4).toInt(16),
|
|
445
|
+
hex.substring(4,6).toInt(16))
|
|
449
446
|
8 -> Color.argb(hex.substring(6,8).toInt(16),
|
|
450
447
|
hex.substring(0,2).toInt(16),
|
|
451
448
|
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,38 @@ 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
// DOWNSAMPLE_FACTOR = 2: capture at 1/4 pixels (less than before).
|
|
37
|
+
// Less downsampling = higher quality capture = crisper blur result.
|
|
38
|
+
// The blur hides pixel detail so 1/4 is the sweet spot.
|
|
39
|
+
private const val DOWNSAMPLE_FACTOR = 2f
|
|
40
|
+
|
|
41
|
+
// Default radius when blurAmount maps here.
|
|
42
|
+
// The actual radius per frame comes from view.blurRadius set by setBlurAmount().
|
|
43
|
+
private const val BLUR_RADIUS = 25f // max RenderScript kernel
|
|
44
|
+
|
|
45
|
+
// 3 rounds: more passes = wider spread = true frosted glass feel
|
|
46
|
+
private const val BLUR_ROUNDS = 3
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
// ── Bitmap pool ────────────────────────────────────────────────────────────
|
|
54
50
|
|
|
55
|
-
private var captureBitmap: Bitmap? = null
|
|
56
|
-
private var scaledBitmap: Bitmap? = null
|
|
51
|
+
private var captureBitmap: Bitmap? = null
|
|
52
|
+
private var scaledBitmap: Bitmap? = null
|
|
57
53
|
private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
58
54
|
private val drawPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
59
55
|
|
|
@@ -66,7 +62,7 @@ internal class LegacyBlurController(
|
|
|
66
62
|
|
|
67
63
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
68
64
|
|
|
69
|
-
var overlayColor: Int
|
|
65
|
+
var overlayColor: Int = Color.TRANSPARENT
|
|
70
66
|
var blurRadius: Float = BLUR_RADIUS
|
|
71
67
|
var enabled: Boolean = true
|
|
72
68
|
set(value) { field = value; if (!value) invalidatePool() }
|
|
@@ -77,8 +73,12 @@ internal class LegacyBlurController(
|
|
|
77
73
|
else rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
78
74
|
}
|
|
79
75
|
|
|
76
|
+
// isCapturing: set true before root.draw() so BlurVibeView.draw() is a no-op
|
|
77
|
+
// preventing stale self-capture. Accessed by BlurVibeView.draw().
|
|
78
|
+
var isCapturing = false
|
|
79
|
+
private set
|
|
80
|
+
|
|
80
81
|
private var frameScheduled = false
|
|
81
|
-
private var isCapturing = false
|
|
82
82
|
|
|
83
83
|
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
84
84
|
|
|
@@ -88,7 +88,7 @@ internal class LegacyBlurController(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
91
|
-
if (!frameScheduled && enabled) {
|
|
91
|
+
if (!frameScheduled && enabled && autoUpdate) {
|
|
92
92
|
frameScheduled = true
|
|
93
93
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
94
94
|
}
|
|
@@ -112,7 +112,6 @@ internal class LegacyBlurController(
|
|
|
112
112
|
// ── Capture + blur ─────────────────────────────────────────────────────────
|
|
113
113
|
|
|
114
114
|
private fun captureAndBlur() {
|
|
115
|
-
if (isCapturing) return
|
|
116
115
|
val rw = rootView.width; if (rw <= 0) return
|
|
117
116
|
val rh = rootView.height; if (rh <= 0) return
|
|
118
117
|
val vw = view.width; if (vw <= 0) return
|
|
@@ -121,85 +120,88 @@ internal class LegacyBlurController(
|
|
|
121
120
|
val sw = (vw / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
|
|
122
121
|
val sh = (vh / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
|
|
123
122
|
|
|
123
|
+
val myLoc = IntArray(2); view.getLocationInWindow(myLoc)
|
|
124
|
+
val rootLoc = IntArray(2); rootView.getLocationInWindow(rootLoc)
|
|
125
|
+
val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
|
|
126
|
+
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
127
|
+
|
|
128
|
+
val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
|
|
129
|
+
val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
|
|
130
|
+
|
|
131
|
+
// Set isCapturing BEFORE root.draw() so BlurVibeView.draw() is skipped
|
|
124
132
|
isCapturing = true
|
|
133
|
+
val c = Canvas(capture)
|
|
134
|
+
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
135
|
+
c.translate(-offsetX, -offsetY)
|
|
125
136
|
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
137
|
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
138
|
} catch (_: Exception) {
|
|
156
|
-
} finally {
|
|
157
139
|
isCapturing = false
|
|
140
|
+
return
|
|
158
141
|
}
|
|
142
|
+
isCapturing = false
|
|
143
|
+
|
|
144
|
+
// Downsample
|
|
145
|
+
Canvas(scaled).drawBitmap(
|
|
146
|
+
capture,
|
|
147
|
+
Rect(0, 0, capture.width, capture.height),
|
|
148
|
+
Rect(0, 0, scaled.width, scaled.height),
|
|
149
|
+
capturePaint
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Blur (2 rounds)
|
|
153
|
+
repeat(BLUR_ROUNDS) { blurBitmap(scaled) }
|
|
154
|
+
|
|
155
|
+
view.invalidate()
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
private fun blurBitmap(bitmap: Bitmap) {
|
|
162
|
-
val
|
|
163
|
-
val sc =
|
|
159
|
+
val r = rs ?: return softwareBlur(bitmap)
|
|
160
|
+
val sc = blurScript ?: return softwareBlur(bitmap)
|
|
164
161
|
try {
|
|
165
|
-
val
|
|
166
|
-
val
|
|
167
|
-
|
|
162
|
+
val iA = reuseAlloc(inputAlloc, bitmap, r).also { inputAlloc = it }
|
|
163
|
+
val oA = reuseAlloc(outputAlloc, bitmap, r).also { outputAlloc = it }
|
|
164
|
+
iA.copyFrom(bitmap)
|
|
168
165
|
sc.setRadius(blurRadius.coerceIn(1f, 25f))
|
|
169
|
-
sc.setInput(
|
|
170
|
-
sc.forEach(
|
|
171
|
-
|
|
172
|
-
} catch (_: Exception) {
|
|
173
|
-
softwareBlur(bitmap)
|
|
174
|
-
}
|
|
166
|
+
sc.setInput(iA)
|
|
167
|
+
sc.forEach(oA)
|
|
168
|
+
oA.copyTo(bitmap)
|
|
169
|
+
} catch (_: Exception) { softwareBlur(bitmap) }
|
|
175
170
|
}
|
|
176
171
|
|
|
177
172
|
private fun softwareBlur(bitmap: Bitmap) {
|
|
178
|
-
// Pure software Gaussian fallback (slower but always works)
|
|
179
173
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
180
174
|
maskFilter = android.graphics.BlurMaskFilter(blurRadius, android.graphics.BlurMaskFilter.Blur.NORMAL)
|
|
181
175
|
}
|
|
182
176
|
Canvas(bitmap).drawBitmap(bitmap, 0f, 0f, paint)
|
|
183
177
|
}
|
|
184
178
|
|
|
185
|
-
// ── Draw
|
|
179
|
+
// ── Draw ─────────────────────────────────────────────────────────────────
|
|
186
180
|
|
|
187
181
|
fun draw(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
|
|
188
182
|
scaledBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
|
|
189
183
|
canvas.drawBitmap(bmp, null,
|
|
190
184
|
android.graphics.RectF(0f, 0f, viewWidth, viewHeight), drawPaint)
|
|
191
185
|
}
|
|
192
|
-
if (Color.alpha(overlayColor) > 0)
|
|
193
|
-
canvas.drawColor(overlayColor)
|
|
194
|
-
}
|
|
186
|
+
if (Color.alpha(overlayColor) > 0) canvas.drawColor(overlayColor)
|
|
195
187
|
}
|
|
196
188
|
|
|
197
|
-
// ──
|
|
189
|
+
// ── Multi-window ──────────────────────────────────────────────────────────
|
|
198
190
|
|
|
199
|
-
fun
|
|
200
|
-
|
|
191
|
+
fun reAttach() {
|
|
192
|
+
if (enabled && autoUpdate) safeAddPreDrawListener()
|
|
201
193
|
}
|
|
202
194
|
|
|
195
|
+
private fun safeAddPreDrawListener() {
|
|
196
|
+
val vto = rootView.viewTreeObserver
|
|
197
|
+
vto.removeOnPreDrawListener(preDrawListener)
|
|
198
|
+
if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
fun onSizeChanged() { invalidatePool() }
|
|
204
|
+
|
|
203
205
|
private fun invalidatePool() {
|
|
204
206
|
captureBitmap?.recycle(); captureBitmap = null
|
|
205
207
|
scaledBitmap?.recycle(); scaledBitmap = null
|
|
@@ -207,26 +209,6 @@ internal class LegacyBlurController(
|
|
|
207
209
|
outputAlloc?.destroy(); outputAlloc = null
|
|
208
210
|
}
|
|
209
211
|
|
|
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
212
|
fun destroy() {
|
|
231
213
|
rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
232
214
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
@@ -238,7 +220,7 @@ internal class LegacyBlurController(
|
|
|
238
220
|
scaledBitmap?.recycle()
|
|
239
221
|
}
|
|
240
222
|
|
|
241
|
-
// ──
|
|
223
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
242
224
|
|
|
243
225
|
private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
|
|
244
226
|
if (existing != null && !existing.isRecycled
|
|
@@ -248,9 +230,8 @@ internal class LegacyBlurController(
|
|
|
248
230
|
}
|
|
249
231
|
|
|
250
232
|
private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
|
|
251
|
-
if (existing != null
|
|
252
|
-
|
|
253
|
-
&& existing.type.y == src.height) return existing
|
|
233
|
+
if (existing != null && existing.type.x == src.width && existing.type.y == src.height)
|
|
234
|
+
return existing
|
|
254
235
|
existing?.destroy()
|
|
255
236
|
return Allocation.createFromBitmap(rs, src,
|
|
256
237
|
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
|
package/package.json
CHANGED