react-native-blur-vibe 0.1.6 → 0.1.7
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/README.md +374 -181
- package/android/src/main/java/com/blurvibe/BlurVibeView.kt +7 -5
- package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
- package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +70 -17
- package/ios/BlurVibeView.swift +28 -27
- package/ios/BlurVibeViewManager.m +9 -9
- package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
- package/ios/Views/ProgressiveBlurView.swift +255 -0
- package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
- package/lib/commonjs/BlurView.js +34 -7
- package/lib/commonjs/BlurView.js.map +1 -1
- package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
- package/lib/module/BlurView.js +34 -7
- package/lib/module/BlurView.js.map +1 -1
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
- package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
- package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/types.d.ts +236 -18
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
- package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/BlurView.d.ts +27 -8
- package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
- package/lib/typescript/module/src/types.d.ts +236 -18
- package/lib/typescript/module/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/BlurVibeViewNativeComponent.ts +10 -16
- package/src/BlurView.tsx +34 -7
- package/src/types.ts +267 -18
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
package com.blurvibe
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapShader
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.LinearGradient
|
|
9
|
+
import android.graphics.Outline
|
|
10
|
+
import android.graphics.Paint
|
|
11
|
+
import android.graphics.PorterDuff
|
|
12
|
+
import android.graphics.PorterDuffXfermode
|
|
13
|
+
import android.graphics.RadialGradient
|
|
14
|
+
import android.graphics.RectF
|
|
15
|
+
import android.graphics.RenderEffect
|
|
16
|
+
import android.graphics.RenderNode
|
|
17
|
+
import android.graphics.Shader
|
|
18
|
+
import android.os.Build
|
|
19
|
+
import android.util.TypedValue
|
|
20
|
+
import android.view.Choreographer
|
|
21
|
+
import android.view.View
|
|
22
|
+
import android.view.ViewGroup
|
|
23
|
+
import android.view.ViewOutlineProvider
|
|
24
|
+
import android.view.ViewTreeObserver
|
|
25
|
+
import androidx.annotation.RequiresApi
|
|
26
|
+
import androidx.core.graphics.toColorInt
|
|
27
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
28
|
+
import kotlin.math.min
|
|
29
|
+
import kotlin.random.Random
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* BlurVibeViewApi31 — GPU backdrop blur for Android API 31+
|
|
33
|
+
*
|
|
34
|
+
* Features:
|
|
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)
|
|
41
|
+
*
|
|
42
|
+
* Progressive blur technique (from Haze docs):
|
|
43
|
+
* Uses a mask approach — a LinearGradient/RadialGradient is drawn as an
|
|
44
|
+
* alpha mask over the blur output using PorterDuff.DST_IN.
|
|
45
|
+
* This fades the blur from full-strength to zero across the view.
|
|
46
|
+
* Per Haze docs: "masks are much faster with negligible performance cost"
|
|
47
|
+
* vs true per-pixel radius variation which costs ~25% more on API 33+.
|
|
48
|
+
*
|
|
49
|
+
* Noise texture:
|
|
50
|
+
* Haze uses noise at 15% opacity by default for tactility.
|
|
51
|
+
* We generate a small tileable noise bitmap once and draw it with low alpha.
|
|
52
|
+
*/
|
|
53
|
+
@RequiresApi(Build.VERSION_CODES.S)
|
|
54
|
+
class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
55
|
+
|
|
56
|
+
// ── Blur params ────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
private var blurRadiusX = DEFAULT_BLUR_RADIUS
|
|
59
|
+
private var blurRadiusY = DEFAULT_BLUR_RADIUS
|
|
60
|
+
private var overlayColor = Color.TRANSPARENT
|
|
61
|
+
private var cornerRadiusPx = 0f
|
|
62
|
+
|
|
63
|
+
// ── Progressive blur params ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
private var progressiveDirection = PROGRESSIVE_NONE
|
|
66
|
+
private var progressiveStartIntensity = 1f // 0.0–1.0, full blur at start
|
|
67
|
+
private var progressiveEndIntensity = 0f // 0.0–1.0, no blur at end
|
|
68
|
+
|
|
69
|
+
// ── Noise params ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
private var noiseFactor = 0.08f // Haze default is 0.15 — we use 0.08 as default (subtler)
|
|
72
|
+
private var noiseBitmap: Bitmap? = null
|
|
73
|
+
private val noisePaint = Paint().apply { alpha = (noiseFactor * 255).toInt() }
|
|
74
|
+
|
|
75
|
+
// ── RenderNodes ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Records the root-view content — "what's behind me" */
|
|
78
|
+
private val contentNode = RenderNode("BlurVibeContent").apply {
|
|
79
|
+
setUseCompositingLayer(true, null) // caches as GPU texture — repeated reads are free
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Holds contentNode cropped to this view's position, with RenderEffect blur applied */
|
|
83
|
+
private val blurNode = RenderNode("BlurVibeBlur")
|
|
84
|
+
|
|
85
|
+
// ── Paint objects (reused, no per-frame allocation) ───────────────────────
|
|
86
|
+
|
|
87
|
+
private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
88
|
+
private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
89
|
+
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
|
|
90
|
+
}
|
|
91
|
+
private val clearPaint = Paint().apply {
|
|
92
|
+
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Root view ─────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
private var blurRoot: ViewGroup? = null
|
|
98
|
+
|
|
99
|
+
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
private var frameScheduled = false
|
|
102
|
+
private val frameCallback = Choreographer.FrameCallback {
|
|
103
|
+
frameScheduled = false
|
|
104
|
+
if (isAttachedToWindow) {
|
|
105
|
+
captureRootIntoNode()
|
|
106
|
+
invalidate()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
110
|
+
if (!frameScheduled) {
|
|
111
|
+
frameScheduled = true
|
|
112
|
+
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
113
|
+
}
|
|
114
|
+
true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
init {
|
|
120
|
+
setWillNotDraw(false)
|
|
121
|
+
super.setBackgroundColor(Color.TRANSPARENT)
|
|
122
|
+
clipToOutline = true
|
|
123
|
+
// Enable hardware layer so onDraw() runs on GPU
|
|
124
|
+
setLayerType(LAYER_TYPE_HARDWARE, null)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
override fun onAttachedToWindow() {
|
|
130
|
+
super.onAttachedToWindow()
|
|
131
|
+
blurRoot = findBlurRoot()
|
|
132
|
+
blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
|
|
133
|
+
generateNoiseBitmap()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun onDetachedFromWindow() {
|
|
137
|
+
blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
138
|
+
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
139
|
+
frameScheduled = false
|
|
140
|
+
blurRoot = null
|
|
141
|
+
noiseBitmap?.recycle()
|
|
142
|
+
noiseBitmap = null
|
|
143
|
+
super.onDetachedFromWindow()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
147
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
148
|
+
blurNode.setPosition(0, 0, w, h)
|
|
149
|
+
applyBlurRenderEffect()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Capture ────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
private fun captureRootIntoNode() {
|
|
155
|
+
val root = blurRoot ?: return
|
|
156
|
+
if (root.width <= 0 || root.height <= 0) return
|
|
157
|
+
|
|
158
|
+
contentNode.setPosition(0, 0, root.width, root.height)
|
|
159
|
+
|
|
160
|
+
val canvas = contentNode.beginRecording()
|
|
161
|
+
try {
|
|
162
|
+
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
163
|
+
root.draw(canvas)
|
|
164
|
+
} finally {
|
|
165
|
+
contentNode.endRecording()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
rebuildBlurNode()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fun rebuildBlurNode() {
|
|
172
|
+
val root = blurRoot ?: return
|
|
173
|
+
if (width <= 0 || height <= 0) return
|
|
174
|
+
|
|
175
|
+
val myLoc = IntArray(2); getLocationInWindow(myLoc)
|
|
176
|
+
val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
|
|
177
|
+
val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
|
|
178
|
+
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
179
|
+
|
|
180
|
+
blurNode.setPosition(0, 0, width, height)
|
|
181
|
+
applyBlurRenderEffect()
|
|
182
|
+
|
|
183
|
+
val canvas = blurNode.beginRecording()
|
|
184
|
+
try {
|
|
185
|
+
canvas.translate(-offsetX, -offsetY)
|
|
186
|
+
canvas.drawRenderNode(contentNode)
|
|
187
|
+
} finally {
|
|
188
|
+
blurNode.endRecording()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun applyBlurRenderEffect() {
|
|
193
|
+
if (blurRadiusX < 0.5f && blurRadiusY < 0.5f) {
|
|
194
|
+
blurNode.setRenderEffect(null)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
blurNode.setRenderEffect(
|
|
198
|
+
RenderEffect.createBlurEffect(blurRadiusX, blurRadiusY, Shader.TileMode.CLAMP)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
override fun onDraw(canvas: Canvas) {
|
|
205
|
+
val w = width.toFloat()
|
|
206
|
+
val h = height.toFloat()
|
|
207
|
+
if (w <= 0f || h <= 0f) return
|
|
208
|
+
|
|
209
|
+
if (!blurNode.hasDisplayList()) return
|
|
210
|
+
|
|
211
|
+
// ── Step 1: Save layer so we can apply mask on top of blur ────────────────
|
|
212
|
+
// saveLayer lets us composite blur + progressive mask as a unit
|
|
213
|
+
val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
|
|
214
|
+
canvas.saveLayer(0f, 0f, w, h, null)
|
|
215
|
+
} else {
|
|
216
|
+
-1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Step 2: Draw blurred backdrop ─────────────────────────────────────────
|
|
220
|
+
canvas.drawRenderNode(blurNode)
|
|
221
|
+
|
|
222
|
+
// ── Step 3: Progressive mask (alpha gradient fades the blur) ──────────────
|
|
223
|
+
if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
|
|
224
|
+
val shader = buildProgressiveShader(w, h)
|
|
225
|
+
if (shader != null) {
|
|
226
|
+
maskPaint.shader = shader
|
|
227
|
+
canvas.drawRect(0f, 0f, w, h, maskPaint)
|
|
228
|
+
}
|
|
229
|
+
canvas.restoreToCount(saveCount)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Step 4: Overlay tint ──────────────────────────────────────────────────
|
|
233
|
+
if (Color.alpha(overlayColor) > 0) {
|
|
234
|
+
overlayPaint.color = overlayColor
|
|
235
|
+
if (cornerRadiusPx > 0f) {
|
|
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
|
|
247
|
+
canvas.drawRect(0f, 0f, w, h, noisePaint)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Progressive shader builder ────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
private fun buildProgressiveShader(w: Float, h: Float): Shader? {
|
|
254
|
+
// Map intensity values to alpha: 1.0 = fully opaque (full blur), 0.0 = fully transparent (no blur)
|
|
255
|
+
val startAlpha = progressiveStartIntensity.coerceIn(0f, 1f)
|
|
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
|
+
|
|
260
|
+
return when (progressiveDirection) {
|
|
261
|
+
PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(
|
|
262
|
+
0f, 0f, 0f, h, startColor, endColor, Shader.TileMode.CLAMP
|
|
263
|
+
)
|
|
264
|
+
PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(
|
|
265
|
+
0f, h, 0f, 0f, startColor, endColor, Shader.TileMode.CLAMP
|
|
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
|
+
)
|
|
279
|
+
else -> null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Noise generation ─────────────────────────────────────────────────────
|
|
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.
|
|
288
|
+
|
|
289
|
+
private fun generateNoiseBitmap() {
|
|
290
|
+
if (noiseBitmap != null && !noiseBitmap!!.isRecycled) return
|
|
291
|
+
val size = 64
|
|
292
|
+
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
293
|
+
val rng = Random(42) // fixed seed = deterministic noise, no shimmer on re-render
|
|
294
|
+
for (x in 0 until size) {
|
|
295
|
+
for (y in 0 until size) {
|
|
296
|
+
val v = rng.nextInt(256)
|
|
297
|
+
bmp.setPixel(x, y, Color.argb(255, v, v, v))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
noiseBitmap = bmp
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Public setters ─────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
fun setBlurAmount(amount: Float) {
|
|
306
|
+
val t = amount.coerceIn(0f, 100f) / 100f
|
|
307
|
+
val radius = t * t * MAX_BLUR_RADIUS // quadratic — matches CSS backdrop-blur feel
|
|
308
|
+
blurRadiusX = radius
|
|
309
|
+
blurRadiusY = radius
|
|
310
|
+
applyBlurRenderEffect()
|
|
311
|
+
scheduleFrame()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fun setOverlayColor(colorString: String?) {
|
|
315
|
+
overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
316
|
+
invalidate()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun applyBorderRadius(radiusDp: Float) {
|
|
320
|
+
cornerRadiusPx = TypedValue.applyDimension(
|
|
321
|
+
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
322
|
+
)
|
|
323
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
324
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
325
|
+
outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
clipToOutline = cornerRadiusPx > 0f
|
|
329
|
+
invalidate()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
|
|
333
|
+
// iOS-only — no-op on Android
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Progressive blur direction.
|
|
338
|
+
* @param direction one of: "none", "topToBottom", "bottomToTop",
|
|
339
|
+
* "leftToRight", "rightToLeft", "radial"
|
|
340
|
+
*/
|
|
341
|
+
fun setProgressiveBlurDirection(direction: String?) {
|
|
342
|
+
progressiveDirection = when (direction) {
|
|
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()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Progressive blur start intensity (0.0 = no blur, 1.0 = full blur).
|
|
355
|
+
* This is the intensity at the START of the gradient direction.
|
|
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
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Progressive blur end intensity (0.0 = no blur, 1.0 = full blur).
|
|
365
|
+
* This is the intensity at the END of the gradient direction.
|
|
366
|
+
* Default 0.0 — fades to no blur at bottom/right/edge.
|
|
367
|
+
*/
|
|
368
|
+
fun setProgressiveEndIntensity(intensity: Float) {
|
|
369
|
+
progressiveEndIntensity = intensity.coerceIn(0f, 1f)
|
|
370
|
+
invalidate()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Noise factor — grain overlay strength for frosted-glass tactility.
|
|
375
|
+
* 0.0 = no noise, 1.0 = full noise. Default 0.08 (8%).
|
|
376
|
+
* Haze's default is 0.15. Set 0 to disable.
|
|
377
|
+
*/
|
|
378
|
+
fun setNoiseFactor(factor: Float) {
|
|
379
|
+
noiseFactor = factor.coerceIn(0f, 1f)
|
|
380
|
+
invalidate()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
private fun scheduleFrame() {
|
|
386
|
+
if (!frameScheduled) {
|
|
387
|
+
frameScheduled = true
|
|
388
|
+
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private fun findBlurRoot(): ViewGroup? {
|
|
393
|
+
var p = parent
|
|
394
|
+
while (p != null) {
|
|
395
|
+
if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
|
|
396
|
+
return p as? ViewGroup
|
|
397
|
+
p = (p as? View)?.parent
|
|
398
|
+
}
|
|
399
|
+
p = parent
|
|
400
|
+
while (p != null) {
|
|
401
|
+
if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
|
|
402
|
+
return p as? ViewGroup
|
|
403
|
+
p = (p as? View)?.parent
|
|
404
|
+
}
|
|
405
|
+
return rootView as? ViewGroup
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private fun parseHexColor(s: String): Int? {
|
|
409
|
+
val t = s.trim()
|
|
410
|
+
if (t.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
|
|
411
|
+
if (!t.startsWith("#")) return try { t.toColorInt() } catch (_: Exception) { null }
|
|
412
|
+
val hex = t.removePrefix("#")
|
|
413
|
+
return try {
|
|
414
|
+
when (hex.length) {
|
|
415
|
+
3 -> Color.argb(255,
|
|
416
|
+
hex[0].toString().repeat(2).toInt(16),
|
|
417
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
418
|
+
hex[2].toString().repeat(2).toInt(16))
|
|
419
|
+
6 -> Color.argb(255,
|
|
420
|
+
hex.substring(0, 2).toInt(16),
|
|
421
|
+
hex.substring(2, 4).toInt(16),
|
|
422
|
+
hex.substring(4, 6).toInt(16))
|
|
423
|
+
8 -> Color.argb(
|
|
424
|
+
hex.substring(6, 8).toInt(16),
|
|
425
|
+
hex.substring(0, 2).toInt(16),
|
|
426
|
+
hex.substring(2, 4).toInt(16),
|
|
427
|
+
hex.substring(4, 6).toInt(16))
|
|
428
|
+
else -> null
|
|
429
|
+
}
|
|
430
|
+
} catch (_: NumberFormatException) { null }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
434
|
+
// Yoga handles all layout
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
companion object {
|
|
438
|
+
private const val MAX_BLUR_RADIUS = 25f
|
|
439
|
+
private const val DEFAULT_BLUR_RADIUS = 2.5f
|
|
440
|
+
|
|
441
|
+
const val PROGRESSIVE_NONE = 0
|
|
442
|
+
const val PROGRESSIVE_TOP_TO_BOTTOM = 1
|
|
443
|
+
const val PROGRESSIVE_BOTTOM_TO_TOP = 2
|
|
444
|
+
const val PROGRESSIVE_LEFT_TO_RIGHT = 3
|
|
445
|
+
const val PROGRESSIVE_RIGHT_TO_LEFT = 4
|
|
446
|
+
const val PROGRESSIVE_RADIAL = 5
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.blurvibe
|
|
2
2
|
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import android.view.ViewGroup
|
|
3
5
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
4
6
|
import com.facebook.react.uimanager.ViewGroupManager
|
|
5
7
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
@@ -7,45 +9,96 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
7
9
|
/**
|
|
8
10
|
* BlurVibeViewManager
|
|
9
11
|
*
|
|
10
|
-
* ViewGroupManager
|
|
11
|
-
*
|
|
12
|
+
* Extends ViewGroupManager<ViewGroup> so that both BlurVibeView (which extends
|
|
13
|
+
* BlurViewGroup, not ReactViewGroup) and BlurVibeViewApi31 (which extends
|
|
14
|
+
* ReactViewGroup) satisfy the type bound.
|
|
15
|
+
*
|
|
16
|
+
* @ReactProp handlers receive ViewGroup and smart-cast via `when`.
|
|
17
|
+
*
|
|
18
|
+
* Naming rules to avoid supertype collisions on the VIEW classes:
|
|
19
|
+
* Manager method → View method called
|
|
20
|
+
* setBlurBorderRadius → applyBorderRadius (ReactViewGroup has setBorderRadius)
|
|
21
|
+
* setBlurRadiusProp → setBlurRadius (unique name on BlurVibeView)
|
|
22
|
+
* setOverlayColorProp → setOverlayColor (unique — not in ReactViewGroup)
|
|
23
|
+
* setBlurTypeProp → no-op
|
|
12
24
|
*/
|
|
13
|
-
class BlurVibeViewManager : ViewGroupManager<
|
|
25
|
+
class BlurVibeViewManager : ViewGroupManager<ViewGroup>() {
|
|
14
26
|
|
|
15
27
|
override fun getName() = "BlurVibeView"
|
|
16
28
|
|
|
17
|
-
override fun createViewInstance(context: ThemedReactContext) =
|
|
29
|
+
override fun createViewInstance(context: ThemedReactContext): ViewGroup =
|
|
30
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) BlurVibeViewApi31(context)
|
|
31
|
+
else BlurVibeView(context)
|
|
32
|
+
|
|
33
|
+
// ── Core props ─────────────────────────────────────────────────────────────
|
|
18
34
|
|
|
19
35
|
@ReactProp(name = "blurAmount", defaultFloat = 10f)
|
|
20
|
-
fun setBlurAmount(view:
|
|
21
|
-
view
|
|
36
|
+
fun setBlurAmount(view: ViewGroup, amount: Float) {
|
|
37
|
+
when (view) {
|
|
38
|
+
is BlurVibeViewApi31 -> view.setBlurAmount(amount)
|
|
39
|
+
is BlurVibeView -> view.setBlurAmount(amount)
|
|
40
|
+
}
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
@ReactProp(name = "blurType")
|
|
25
|
-
fun
|
|
26
|
-
//
|
|
44
|
+
fun setBlurTypeProp(view: ViewGroup, @Suppress("UNUSED_PARAMETER") type: String?) {
|
|
45
|
+
// iOS UIBlurEffectStyle only — no-op on Android
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
@ReactProp(name = "overlayColor")
|
|
30
|
-
fun
|
|
31
|
-
view
|
|
49
|
+
fun setOverlayColorProp(view: ViewGroup, color: String?) {
|
|
50
|
+
when (view) {
|
|
51
|
+
is BlurVibeViewApi31 -> view.setOverlayColor(color)
|
|
52
|
+
is BlurVibeView -> view.setOverlayColor(color)
|
|
53
|
+
}
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
@ReactProp(name = "reducedTransparencyFallbackColor")
|
|
35
|
-
fun setReducedTransparencyFallbackColor(view:
|
|
36
|
-
view
|
|
57
|
+
fun setReducedTransparencyFallbackColor(view: ViewGroup, color: String?) {
|
|
58
|
+
when (view) {
|
|
59
|
+
is BlurVibeViewApi31 -> view.setReducedTransparencyFallbackColor(color)
|
|
60
|
+
is BlurVibeView -> view.setReducedTransparencyFallbackColor(color)
|
|
61
|
+
}
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
@ReactProp(name = "blurRadius", defaultInt = 4)
|
|
40
|
-
fun
|
|
41
|
-
|
|
65
|
+
fun setBlurRadiusProp(view: ViewGroup, radius: Int) {
|
|
66
|
+
// API < 31 only — QmBlurView downsample factor
|
|
67
|
+
// API 31+ uses full-res RenderNode, downsample irrelevant
|
|
68
|
+
if (view is BlurVibeView) view.setBlurRadius(radius)
|
|
42
69
|
}
|
|
43
70
|
|
|
44
71
|
@ReactProp(name = "borderRadius", defaultFloat = 0f)
|
|
45
|
-
fun setBlurBorderRadius(view:
|
|
46
|
-
view
|
|
72
|
+
fun setBlurBorderRadius(view: ViewGroup, radius: Float) {
|
|
73
|
+
when (view) {
|
|
74
|
+
is BlurVibeViewApi31 -> view.applyBorderRadius(radius) // renamed — avoids ReactViewGroup.setBorderRadius
|
|
75
|
+
is BlurVibeView -> view.setBorderRadius(radius)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Progressive blur props (API 31+ only) ──────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@ReactProp(name = "progressiveBlurDirection")
|
|
82
|
+
fun setProgressiveBlurDirection(view: ViewGroup, direction: String?) {
|
|
83
|
+
if (view is BlurVibeViewApi31) view.setProgressiveBlurDirection(direction)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@ReactProp(name = "progressiveStartIntensity", defaultFloat = 1f)
|
|
87
|
+
fun setProgressiveStartIntensity(view: ViewGroup, intensity: Float) {
|
|
88
|
+
if (view is BlurVibeViewApi31) view.setProgressiveStartIntensity(intensity)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactProp(name = "progressiveEndIntensity", defaultFloat = 0f)
|
|
92
|
+
fun setProgressiveEndIntensity(view: ViewGroup, intensity: Float) {
|
|
93
|
+
if (view is BlurVibeViewApi31) view.setProgressiveEndIntensity(intensity)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Noise prop (API 31+ only) ──────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
@ReactProp(name = "noiseFactor", defaultFloat = 0.08f)
|
|
99
|
+
fun setNoiseFactorProp(view: ViewGroup, factor: Float) {
|
|
100
|
+
if (view is BlurVibeViewApi31) view.setNoiseFactor(factor)
|
|
47
101
|
}
|
|
48
102
|
|
|
49
|
-
// React Native's Yoga handles child layout — return false
|
|
50
103
|
override fun needsCustomLayoutForChildren(): Boolean = false
|
|
51
104
|
}
|