react-native-blur-vibe 0.1.8 → 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.
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/blurvibe/BlurVibeView.kt +101 -192
- package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +261 -241
- package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +77 -28
- package/android/src/main/java/com/blurvibe/LegacyBlurController.kt +258 -0
- package/ios/BlurVibeView.swift +2 -0
- package/ios/BlurVibeViewFabric.mm +112 -0
- package/ios/BlurVibeViewManager.m +12 -2
- package/ios/BlurVibeViewManager.swift +9 -9
- package/lib/commonjs/BlurVibeViewNativeComponent.ts +14 -25
- package/lib/commonjs/BlurView.js +9 -30
- package/lib/commonjs/BlurView.js.map +1 -1
- package/lib/module/BlurVibeViewNativeComponent.ts +14 -25
- package/lib/module/BlurView.js +9 -30
- package/lib/module/BlurView.js.map +1 -1
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +11 -9
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/BlurView.d.ts +6 -31
- package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/types.d.ts +26 -1
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +11 -9
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurView.d.ts +6 -31
- package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/module/src/types.d.ts +26 -1
- package/lib/typescript/module/src/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/src/BlurVibeViewNativeComponent.ts +14 -25
- package/src/BlurView.tsx +10 -33
- package/src/types.ts +30 -1
|
@@ -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,89 +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
|
|
|
31
35
|
/**
|
|
32
|
-
* BlurVibeViewApi31 —
|
|
36
|
+
* BlurVibeViewApi31 — Backdrop blur for Android API 31+
|
|
33
37
|
*
|
|
34
|
-
*
|
|
35
|
-
* • Dual-RenderNode blur (backdrop-filter CSS semantics)
|
|
36
|
-
* • Progressive / gradient blur (vertical, horizontal, radial)
|
|
37
|
-
* • Noise texture overlay (tactile frosted-glass feel, like Haze)
|
|
38
|
-
* • Overlay tint with full RGBA support
|
|
39
|
-
* • Corner radius with hardware clipping
|
|
40
|
-
* • Choreographer-gated updates (max 1 capture per vsync)
|
|
38
|
+
* Pipeline (adapted from ModernBlurView's RenderEffectBlur approach):
|
|
41
39
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
52
58
|
*/
|
|
53
59
|
@RequiresApi(Build.VERSION_CODES.S)
|
|
54
60
|
class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
55
61
|
|
|
56
62
|
// ── Blur params ────────────────────────────────────────────────────────────
|
|
57
63
|
|
|
58
|
-
private var
|
|
59
|
-
private var
|
|
60
|
-
private var overlayColor = Color.TRANSPARENT
|
|
64
|
+
private var blurAmount = 10f
|
|
65
|
+
private var overlayColor = Color.TRANSPARENT
|
|
61
66
|
private var cornerRadiusPx = 0f
|
|
62
67
|
|
|
63
|
-
// ── Progressive blur
|
|
68
|
+
// ── Progressive blur ──────────────────────────────────────────────────────
|
|
64
69
|
|
|
65
|
-
private var progressiveDirection
|
|
66
|
-
private var progressiveStartIntensity = 1f
|
|
67
|
-
private var progressiveEndIntensity = 0f
|
|
70
|
+
private var progressiveDirection = PROGRESSIVE_NONE
|
|
71
|
+
private var progressiveStartIntensity = 1f
|
|
72
|
+
private var progressiveEndIntensity = 0f
|
|
68
73
|
|
|
69
|
-
// ── Noise
|
|
74
|
+
// ── Noise ─────────────────────────────────────────────────────────────────
|
|
70
75
|
|
|
71
|
-
private var noiseFactor = 0.08f
|
|
76
|
+
private var noiseFactor = 0.08f
|
|
72
77
|
private var noiseBitmap: Bitmap? = null
|
|
73
|
-
private val noisePaint = Paint()
|
|
74
|
-
|
|
75
|
-
// ── RenderNodes ───────────────────────────────────────────────────────────
|
|
78
|
+
private val noisePaint = Paint()
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// ── Bitmap + RenderNode (ModernBlurView pattern) ───────────────────────────
|
|
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.
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
private val
|
|
88
|
+
private var internalBitmap: Bitmap? = null
|
|
89
|
+
private val renderNode = RenderNode("BlurVibeNode")
|
|
84
90
|
|
|
85
|
-
// ──
|
|
91
|
+
// ── Draw paint ────────────────────────────────────────────────────────────
|
|
86
92
|
|
|
87
|
-
private val
|
|
88
|
-
private val maskPaint
|
|
93
|
+
private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
94
|
+
private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
89
95
|
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
|
|
90
96
|
}
|
|
91
|
-
private val
|
|
92
|
-
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
93
|
-
}
|
|
97
|
+
private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
94
98
|
|
|
95
99
|
// ── Root view ─────────────────────────────────────────────────────────────
|
|
96
100
|
|
|
97
101
|
private var blurRoot: ViewGroup? = null
|
|
102
|
+
private val rootLocation = IntArray(2)
|
|
103
|
+
private val blurViewLocation = IntArray(2)
|
|
98
104
|
|
|
99
|
-
// ──
|
|
105
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
100
106
|
|
|
107
|
+
private var blurEnabled = true
|
|
108
|
+
private var autoUpdate = true
|
|
101
109
|
private var frameScheduled = false
|
|
110
|
+
private var initialized = false
|
|
111
|
+
|
|
112
|
+
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
113
|
+
|
|
102
114
|
private val frameCallback = Choreographer.FrameCallback {
|
|
103
115
|
frameScheduled = false
|
|
104
|
-
if (isAttachedToWindow)
|
|
105
|
-
captureRootIntoNode()
|
|
106
|
-
invalidate()
|
|
107
|
-
}
|
|
116
|
+
if (isAttachedToWindow && blurEnabled) updateBlur()
|
|
108
117
|
}
|
|
118
|
+
|
|
109
119
|
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
110
|
-
if (!frameScheduled) {
|
|
120
|
+
if (!frameScheduled && blurEnabled && autoUpdate) {
|
|
111
121
|
frameScheduled = true
|
|
112
122
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
113
123
|
}
|
|
@@ -120,8 +130,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
120
130
|
setWillNotDraw(false)
|
|
121
131
|
super.setBackgroundColor(Color.TRANSPARENT)
|
|
122
132
|
clipToOutline = true
|
|
123
|
-
// Enable hardware layer so onDraw() runs on GPU
|
|
124
|
-
setLayerType(LAYER_TYPE_HARDWARE, null)
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -129,173 +137,204 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
129
137
|
override fun onAttachedToWindow() {
|
|
130
138
|
super.onAttachedToWindow()
|
|
131
139
|
blurRoot = findBlurRoot()
|
|
132
|
-
|
|
140
|
+
safeAddPreDrawListener()
|
|
133
141
|
generateNoiseBitmap()
|
|
142
|
+
if (measuredWidth > 0 && measuredHeight > 0) initBlur()
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
override fun onDetachedFromWindow() {
|
|
137
146
|
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
138
147
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
139
148
|
frameScheduled = false
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
noiseBitmap = null
|
|
149
|
+
initialized = false
|
|
150
|
+
blurRoot = null
|
|
151
|
+
noiseBitmap?.recycle(); noiseBitmap = null
|
|
152
|
+
internalBitmap?.recycle(); internalBitmap = null
|
|
153
|
+
renderNode.discardDisplayList()
|
|
143
154
|
super.onDetachedFromWindow()
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
147
158
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
if (w > 0 && h > 0) {
|
|
160
|
+
internalBitmap?.recycle(); internalBitmap = null
|
|
161
|
+
initBlur()
|
|
162
|
+
}
|
|
150
163
|
}
|
|
151
164
|
|
|
152
|
-
// ──
|
|
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
|
+
}
|
|
153
183
|
|
|
154
|
-
|
|
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() {
|
|
155
190
|
val root = blurRoot ?: return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
163
|
-
root.draw(canvas)
|
|
164
|
-
} finally {
|
|
165
|
-
contentNode.endRecording()
|
|
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)
|
|
166
196
|
}
|
|
167
|
-
|
|
168
|
-
rebuildBlurNode()
|
|
169
197
|
}
|
|
170
198
|
|
|
171
|
-
|
|
172
|
-
val root = blurRoot ?: return
|
|
173
|
-
if (width <= 0 || height <= 0) return
|
|
199
|
+
// ── Blur init ─────────────────────────────────────────────────────────────
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
val
|
|
177
|
-
val
|
|
178
|
-
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
201
|
+
private fun initBlur() {
|
|
202
|
+
val w = measuredWidth; if (w <= 0) return
|
|
203
|
+
val h = measuredHeight; if (h <= 0) return
|
|
179
204
|
|
|
180
|
-
|
|
181
|
-
|
|
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()
|
|
210
|
+
}
|
|
182
211
|
|
|
183
|
-
|
|
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)
|
|
184
239
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} finally {
|
|
188
|
-
blurNode.endRecording()
|
|
189
|
-
}
|
|
190
|
-
}
|
|
240
|
+
root.draw(captureCanvas)
|
|
241
|
+
} catch (_: Exception) { return }
|
|
191
242
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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)
|
|
196
247
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
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()
|
|
200
270
|
}
|
|
201
271
|
|
|
202
272
|
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
203
273
|
|
|
204
274
|
override fun onDraw(canvas: Canvas) {
|
|
205
|
-
|
|
206
|
-
val
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!blurNode.hasDisplayList()) return
|
|
275
|
+
if (!blurEnabled || !initialized) return
|
|
276
|
+
val w = width.toFloat(); if (w <= 0f) return
|
|
277
|
+
val h = height.toFloat(); if (h <= 0f) return
|
|
278
|
+
if (!renderNode.hasDisplayList()) return
|
|
210
279
|
|
|
211
|
-
//
|
|
212
|
-
// saveLayer lets us composite blur + progressive mask as a unit
|
|
280
|
+
// Step 1: save layer for progressive mask compositing
|
|
213
281
|
val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
|
|
214
282
|
canvas.saveLayer(0f, 0f, w, h, null)
|
|
215
|
-
} else
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
283
|
+
} else -1
|
|
284
|
+
|
|
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
|
|
223
297
|
if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
|
|
224
|
-
|
|
225
|
-
if (shader != null) {
|
|
298
|
+
buildProgressiveShader(w, h)?.let { shader ->
|
|
226
299
|
maskPaint.shader = shader
|
|
227
300
|
canvas.drawRect(0f, 0f, w, h, maskPaint)
|
|
228
301
|
}
|
|
229
302
|
canvas.restoreToCount(saveCount)
|
|
230
303
|
}
|
|
231
304
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
canvas.drawRoundRect(RectF(0f, 0f, w, h), cornerRadiusPx, cornerRadiusPx, overlayPaint)
|
|
237
|
-
} else {
|
|
238
|
-
canvas.drawRect(0f, 0f, w, h, overlayPaint)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── Step 5: Noise texture (tactile frosted-glass feel) ────────────────────
|
|
243
|
-
if (noiseFactor > 0f && noiseBitmap != null && !noiseBitmap!!.isRecycled) {
|
|
244
|
-
noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
|
|
245
|
-
val noiseShader = BitmapShader(noiseBitmap!!, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
246
|
-
noisePaint.shader = noiseShader
|
|
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)
|
|
247
309
|
canvas.drawRect(0f, 0f, w, h, noisePaint)
|
|
248
310
|
}
|
|
249
311
|
}
|
|
250
312
|
|
|
251
|
-
// ── Progressive shader
|
|
313
|
+
// ── Progressive shader ────────────────────────────────────────────────────
|
|
252
314
|
|
|
253
315
|
private fun buildProgressiveShader(w: Float, h: Float): Shader? {
|
|
254
|
-
|
|
255
|
-
val
|
|
256
|
-
val endAlpha = progressiveEndIntensity.coerceIn(0f, 1f)
|
|
257
|
-
val startColor = Color.argb((startAlpha * 255).toInt(), 0, 0, 0)
|
|
258
|
-
val endColor = Color.argb((endAlpha * 255).toInt(), 0, 0, 0)
|
|
259
|
-
|
|
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(
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
267
|
-
PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(
|
|
268
|
-
0f, 0f, w, 0f, startColor, endColor, Shader.TileMode.CLAMP
|
|
269
|
-
)
|
|
270
|
-
PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(
|
|
271
|
-
w, 0f, 0f, 0f, startColor, endColor, Shader.TileMode.CLAMP
|
|
272
|
-
)
|
|
273
|
-
PROGRESSIVE_RADIAL -> RadialGradient(
|
|
274
|
-
w / 2f, h / 2f,
|
|
275
|
-
min(w, h) / 2f,
|
|
276
|
-
startColor, endColor,
|
|
277
|
-
Shader.TileMode.CLAMP
|
|
278
|
-
)
|
|
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)
|
|
279
324
|
else -> null
|
|
280
325
|
}
|
|
281
326
|
}
|
|
282
327
|
|
|
283
|
-
// ── Noise
|
|
284
|
-
//
|
|
285
|
-
// Generates a small (64×64) tileable noise bitmap once.
|
|
286
|
-
// Haze uses noise at 15% opacity for tactility — the fine grain
|
|
287
|
-
// breaks up the uniform blur and makes it look more like real frosted glass.
|
|
328
|
+
// ── Noise bitmap ──────────────────────────────────────────────────────────
|
|
288
329
|
|
|
289
330
|
private fun generateNoiseBitmap() {
|
|
290
|
-
if (noiseBitmap
|
|
331
|
+
if (noiseBitmap?.isRecycled == false) return
|
|
291
332
|
val size = 64
|
|
292
333
|
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
293
|
-
val rng = Random(42)
|
|
294
|
-
for (x in 0 until size) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
bmp.setPixel(x, y, Color.argb(255, v, v, v))
|
|
298
|
-
}
|
|
334
|
+
val rng = Random(42)
|
|
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))
|
|
299
338
|
}
|
|
300
339
|
noiseBitmap = bmp
|
|
301
340
|
}
|
|
@@ -303,17 +342,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
303
342
|
// ── Public setters ─────────────────────────────────────────────────────────
|
|
304
343
|
|
|
305
344
|
fun setBlurAmount(amount: Float) {
|
|
306
|
-
|
|
307
|
-
val radius = t * t * MAX_BLUR_RADIUS // quadratic — matches CSS backdrop-blur feel
|
|
308
|
-
blurRadiusX = radius
|
|
309
|
-
blurRadiusY = radius
|
|
310
|
-
applyBlurRenderEffect()
|
|
345
|
+
blurAmount = amount.coerceIn(0f, 100f)
|
|
311
346
|
scheduleFrame()
|
|
312
347
|
}
|
|
313
348
|
|
|
314
349
|
fun setOverlayColor(colorString: String?) {
|
|
315
350
|
overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
316
|
-
|
|
351
|
+
scheduleFrame()
|
|
317
352
|
}
|
|
318
353
|
|
|
319
354
|
fun applyBorderRadius(radiusDp: Float) {
|
|
@@ -329,77 +364,70 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
329
364
|
invalidate()
|
|
330
365
|
}
|
|
331
366
|
|
|
332
|
-
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER")
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
"topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
|
|
344
|
-
"bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
|
|
345
|
-
"leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
|
|
346
|
-
"rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
|
|
347
|
-
"radial" -> PROGRESSIVE_RADIAL
|
|
348
|
-
else -> PROGRESSIVE_NONE
|
|
349
|
-
}
|
|
350
|
-
invalidate()
|
|
367
|
+
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") c: String?) {}
|
|
368
|
+
|
|
369
|
+
fun setProgressiveBlurDirection(d: String?) {
|
|
370
|
+
progressiveDirection = when (d) {
|
|
371
|
+
"topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
|
|
372
|
+
"bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
|
|
373
|
+
"leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
|
|
374
|
+
"rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
|
|
375
|
+
"radial" -> PROGRESSIVE_RADIAL
|
|
376
|
+
else -> PROGRESSIVE_NONE
|
|
377
|
+
}; invalidate()
|
|
351
378
|
}
|
|
352
379
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
* Default 1.0 — full blur at top/left/center.
|
|
357
|
-
*/
|
|
358
|
-
fun setProgressiveStartIntensity(intensity: Float) {
|
|
359
|
-
progressiveStartIntensity = intensity.coerceIn(0f, 1f)
|
|
360
|
-
invalidate()
|
|
361
|
-
}
|
|
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() }
|
|
362
383
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
fun applyBlurEnabled(enabled: Boolean) {
|
|
385
|
+
blurEnabled = enabled
|
|
386
|
+
if (enabled) {
|
|
387
|
+
safeAddPreDrawListener()
|
|
388
|
+
scheduleFrame()
|
|
389
|
+
} else {
|
|
390
|
+
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
391
|
+
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
392
|
+
frameScheduled = false
|
|
393
|
+
renderNode.discardDisplayList()
|
|
394
|
+
invalidate()
|
|
395
|
+
}
|
|
371
396
|
}
|
|
372
397
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
fun setAutoUpdate(update: Boolean) {
|
|
399
|
+
autoUpdate = update
|
|
400
|
+
if (update) safeAddPreDrawListener()
|
|
401
|
+
else {
|
|
402
|
+
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
403
|
+
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
404
|
+
frameScheduled = false
|
|
405
|
+
}
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
384
409
|
|
|
385
410
|
private fun scheduleFrame() {
|
|
386
|
-
if (!frameScheduled) {
|
|
411
|
+
if (!frameScheduled && blurEnabled) {
|
|
387
412
|
frameScheduled = true
|
|
388
413
|
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
389
414
|
}
|
|
390
415
|
}
|
|
391
416
|
|
|
417
|
+
private fun blurRadiusFromAmount(amount: Float): Float {
|
|
418
|
+
val t = amount / 100f
|
|
419
|
+
return (t * t * 25f).coerceIn(1f, 25f)
|
|
420
|
+
}
|
|
421
|
+
|
|
392
422
|
private fun findBlurRoot(): ViewGroup? {
|
|
393
423
|
var p = parent
|
|
394
424
|
while (p != null) {
|
|
395
|
-
if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
|
|
396
|
-
return p as? ViewGroup
|
|
425
|
+
if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
|
|
397
426
|
p = (p as? View)?.parent
|
|
398
427
|
}
|
|
399
428
|
p = parent
|
|
400
429
|
while (p != null) {
|
|
401
|
-
if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
|
|
402
|
-
return p as? ViewGroup
|
|
430
|
+
if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
|
|
403
431
|
p = (p as? View)?.parent
|
|
404
432
|
}
|
|
405
433
|
return rootView as? ViewGroup
|
|
@@ -412,37 +440,29 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
412
440
|
val hex = t.removePrefix("#")
|
|
413
441
|
return try {
|
|
414
442
|
when (hex.length) {
|
|
415
|
-
3 -> Color.argb(255,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
hex.substring(0, 2).toInt(16),
|
|
426
|
-
hex.substring(2, 4).toInt(16),
|
|
427
|
-
hex.substring(4, 6).toInt(16))
|
|
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))
|
|
449
|
+
8 -> Color.argb(hex.substring(6,8).toInt(16),
|
|
450
|
+
hex.substring(0,2).toInt(16),
|
|
451
|
+
hex.substring(2,4).toInt(16),
|
|
452
|
+
hex.substring(4,6).toInt(16))
|
|
428
453
|
else -> null
|
|
429
454
|
}
|
|
430
455
|
} catch (_: NumberFormatException) { null }
|
|
431
456
|
}
|
|
432
457
|
|
|
433
|
-
override fun onLayout(changed: Boolean,
|
|
434
|
-
// Yoga handles all layout
|
|
435
|
-
}
|
|
458
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
|
|
436
459
|
|
|
437
460
|
companion object {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const val
|
|
442
|
-
const val
|
|
443
|
-
const val
|
|
444
|
-
const val PROGRESSIVE_LEFT_TO_RIGHT = 3
|
|
445
|
-
const val PROGRESSIVE_RIGHT_TO_LEFT = 4
|
|
446
|
-
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
|
|
447
467
|
}
|
|
448
468
|
}
|