react-native-blur-vibe 0.1.4 → 0.1.6
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.
|
@@ -3,10 +3,7 @@ package com.blurvibe
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Color
|
|
5
5
|
import android.graphics.Outline
|
|
6
|
-
import android.os.Handler
|
|
7
|
-
import android.os.Looper
|
|
8
6
|
import android.util.TypedValue
|
|
9
|
-
import android.view.Choreographer
|
|
10
7
|
import android.view.View
|
|
11
8
|
import android.view.ViewGroup
|
|
12
9
|
import android.view.ViewOutlineProvider
|
|
@@ -16,308 +13,244 @@ import com.qmdeve.blurview.base.BaseBlurViewGroup
|
|
|
16
13
|
import com.qmdeve.blurview.widget.BlurViewGroup
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
* BlurVibeView —
|
|
16
|
+
* BlurVibeView — Android backdrop blur implementation
|
|
20
17
|
*
|
|
21
|
-
*
|
|
18
|
+
* Extends QmBlurView's BlurViewGroup — a high-performance blur library
|
|
19
|
+
* that correctly implements CSS backdrop-filter: blur() semantics:
|
|
20
|
+
* - Blurs content BEHIND the view, not the view itself
|
|
21
|
+
* - Hardware accelerated via native blur algorithms
|
|
22
|
+
* - Handles scroll, animation, zIndex, absolute positioning correctly
|
|
23
|
+
* - Never causes draw loops or bitmap capture on the JS thread
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* to the human eye and costs 1/5 as much. Fixed: blurRounds = 1.
|
|
25
|
+
* Uses reflection to redirect the blur capture root from the activity
|
|
26
|
+
* decor view to the nearest ReactRootView or react-native-screens Screen,
|
|
27
|
+
* preventing full-screen blur and navigation transition artifacts.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
* mapBlurAmountToRadius() was returning up to 100.0. QmBlurView's Gaussian kernel
|
|
30
|
-
* at radius=100 iterates a ~200-wide kernel per-pixel every frame.
|
|
31
|
-
* Fixed: map blurAmount 0–100 → radius 0–25.
|
|
32
|
-
*
|
|
33
|
-
* 3. OnPreDrawListener fires every frame with no throttling
|
|
34
|
-
* The listener was doing full blur work synchronously inside the pre-draw callback,
|
|
35
|
-
* blocking the draw thread on every invalidation of every child in the tree.
|
|
36
|
-
* Fixed: listener only sets a dirty flag; actual blur work is deferred to a
|
|
37
|
-
* Choreographer.FrameCallback which fires at most once-per-vsync.
|
|
38
|
-
*
|
|
39
|
-
* 4. preDrawListener leaked on re-attach
|
|
40
|
-
* Each call to onAttachedToWindow re-added the listener without removing the old one,
|
|
41
|
-
* multiplying the per-frame cost every time a modal or navigator re-mounted the view.
|
|
42
|
-
* Fixed: detachPreDrawListener() called before every re-attach.
|
|
43
|
-
*
|
|
44
|
-
* ─── Performance profile after fixes ─────────────────────────────────────────
|
|
45
|
-
*
|
|
46
|
-
* • blur cost reduced ~40× (5 rounds → 1, radius 100 → 25, gated to 1/vsync)
|
|
47
|
-
* • zero JS thread work (Choreographer callback runs on UI thread only)
|
|
48
|
-
* • zero GC pressure (no bitmap allocations on hot path)
|
|
49
|
-
* • works with: Modal, ScrollView, FlatList, FlashList, ImageBackground,
|
|
50
|
-
* Reanimated (both JS and UI thread), react-navigation transitions
|
|
29
|
+
* Credit: approach adapted from sbaiahmed1/react-native-blur
|
|
51
30
|
*/
|
|
52
31
|
class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
private var pendingBlurRadius = DEFAULT_BLUR_RADIUS
|
|
33
|
+
private var currentBlurRadius = DEFAULT_BLUR_RADIUS
|
|
57
34
|
private var currentOverlayColor = Color.TRANSPARENT
|
|
58
35
|
private var currentCornerRadius = 0f
|
|
59
|
-
private var
|
|
60
|
-
|
|
61
|
-
// ── Choreographer frame gate ───────────────────────────────────────────────
|
|
62
|
-
//
|
|
63
|
-
// OnPreDrawListener sets pendingFrame = true and returns immediately (never
|
|
64
|
-
// blocks). Choreographer fires frameCallback at the next vsync boundary,
|
|
65
|
-
// which calls invalidate() → QmBlurView captures + blurs + draws exactly once.
|
|
66
|
-
// pendingFrame prevents multiple queued callbacks stacking up.
|
|
67
|
-
|
|
68
|
-
private var pendingFrame = false
|
|
69
|
-
|
|
70
|
-
private val frameCallback = Choreographer.FrameCallback {
|
|
71
|
-
pendingFrame = false
|
|
72
|
-
if (isAttachedToWindow) triggerBlurUpdate()
|
|
73
|
-
}
|
|
36
|
+
private var isBlurInitialized = false
|
|
74
37
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
38
|
+
companion object {
|
|
39
|
+
private const val DEFAULT_BLUR_RADIUS = 10f
|
|
40
|
+
private const val MIN_BLUR_AMOUNT = 0f
|
|
41
|
+
private const val MAX_BLUR_AMOUNT = 100f
|
|
42
|
+
private const val MAX_BLUR_RADIUS = 25f // QmBlurView Gaussian kernel designed for 0-25
|
|
43
|
+
|
|
44
|
+
// Maps 0–100 blurAmount to 0–25 QmBlurView radius range
|
|
45
|
+
private fun mapBlurAmountToRadius(amount: Float): Float {
|
|
46
|
+
val clamped = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT)
|
|
47
|
+
return (clamped / MAX_BLUR_AMOUNT) * MAX_BLUR_RADIUS
|
|
83
48
|
}
|
|
84
|
-
true // MUST return true — false would block the entire frame draw pass
|
|
85
49
|
}
|
|
86
50
|
|
|
87
|
-
// ── Init ──────────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
51
|
init {
|
|
90
|
-
super.setBackgroundColor(
|
|
52
|
+
super.setBackgroundColor(currentOverlayColor)
|
|
91
53
|
clipChildren = true
|
|
92
54
|
clipToOutline = true
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
// A single Gaussian pass on a downsampled bitmap is perceptually identical
|
|
96
|
-
// to 5 passes and costs exactly 1/5 as much GPU/CPU time.
|
|
97
|
-
blurRounds = 1
|
|
98
|
-
|
|
99
|
-
// Aggressive downsample: capture at 1/8 resolution before blurring.
|
|
100
|
-
// The blur kernel smooths away all pixel-level detail so 1/8 is sufficient.
|
|
101
|
-
// This reduces the bitmap size 64× and the blur kernel work proportionally.
|
|
102
|
-
super.setDownsampleFactor(8f)
|
|
55
|
+
blurRounds = 1 // was 5 — single pass is visually identical, 5x cheaper
|
|
56
|
+
super.setDownsampleFactor(8.0f) // was 6 — 1/64 pixel count, blur hides the difference
|
|
103
57
|
}
|
|
104
58
|
|
|
105
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
59
|
override fun onAttachedToWindow() {
|
|
108
60
|
super.onAttachedToWindow()
|
|
109
|
-
|
|
110
|
-
|
|
61
|
+
if (isBlurInitialized) return
|
|
62
|
+
swapBlurRootToOptimalAncestor()
|
|
63
|
+
initializeBlur()
|
|
111
64
|
}
|
|
112
65
|
|
|
113
66
|
override fun onDetachedFromWindow() {
|
|
114
|
-
detachPreDrawListener()
|
|
115
|
-
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
116
|
-
pendingFrame = false
|
|
117
|
-
isSetupDone = false
|
|
118
67
|
super.onDetachedFromWindow()
|
|
68
|
+
isBlurInitialized = false
|
|
119
69
|
}
|
|
120
70
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
private fun attachPreDrawListenerToOptimalRoot() {
|
|
124
|
-
detachPreDrawListener() // always detach first to prevent listener leaks
|
|
71
|
+
private var frameScheduled = false
|
|
125
72
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
?: return
|
|
130
|
-
|
|
131
|
-
attachedRoot = root
|
|
132
|
-
root.viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
133
|
-
redirectQmBlurCaptureRoot(root)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private fun detachPreDrawListener() {
|
|
137
|
-
attachedRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
|
|
138
|
-
attachedRoot = null
|
|
73
|
+
private val frameCallback = android.view.Choreographer.FrameCallback {
|
|
74
|
+
frameScheduled = false
|
|
75
|
+
try { invalidate() } catch (_: Exception) {}
|
|
139
76
|
}
|
|
140
77
|
|
|
141
78
|
/**
|
|
142
|
-
* Redirects QmBlurView's internal
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* We do NOT mirror QmBlurView's internal preDrawListener. We own the invalidation
|
|
147
|
-
* cycle via our own Choreographer-gated listener above.
|
|
79
|
+
* Redirects QmBlurView's internal preDrawListener from the old root to [newRoot].
|
|
80
|
+
* Also wraps it in a Choreographer gate so blur work fires at most ONCE per vsync,
|
|
81
|
+
* even when many views invalidate simultaneously (scroll, animation, etc).
|
|
148
82
|
*/
|
|
149
|
-
private fun
|
|
83
|
+
private fun swapBlurRootToOptimalAncestor() {
|
|
84
|
+
val newRoot = findNearestScreenAncestor() ?: findNearestReactRootView() ?: return
|
|
85
|
+
|
|
150
86
|
try {
|
|
151
|
-
val
|
|
87
|
+
val blurViewGroupClass = BlurViewGroup::class.java
|
|
88
|
+
val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
|
|
152
89
|
baseField.isAccessible = true
|
|
153
|
-
val
|
|
90
|
+
val baseBlurViewGroup = baseField.get(this) ?: return
|
|
154
91
|
|
|
155
92
|
val baseClass = BaseBlurViewGroup::class.java
|
|
156
93
|
|
|
157
|
-
val
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
val
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
94
|
+
val decorViewField = baseClass.getDeclaredField("mDecorView")
|
|
95
|
+
decorViewField.isAccessible = true
|
|
96
|
+
val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
|
|
97
|
+
|
|
98
|
+
val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
|
|
99
|
+
preDrawListenerField.isAccessible = true
|
|
100
|
+
val preDrawListener = preDrawListenerField.get(baseBlurViewGroup)
|
|
101
|
+
as? ViewTreeObserver.OnPreDrawListener
|
|
102
|
+
|
|
103
|
+
if (oldDecorView != null && preDrawListener != null) {
|
|
104
|
+
// Remove listener from old root
|
|
105
|
+
oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
106
|
+
|
|
107
|
+
// Set new root
|
|
108
|
+
decorViewField.set(baseBlurViewGroup, newRoot)
|
|
109
|
+
|
|
110
|
+
// Wrap in Choreographer gate: fires at most once per vsync regardless of
|
|
111
|
+
// how many child invalidations happen in the same frame
|
|
112
|
+
val gatedListener = ViewTreeObserver.OnPreDrawListener {
|
|
113
|
+
if (!frameScheduled) {
|
|
114
|
+
frameScheduled = true
|
|
115
|
+
android.view.Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
116
|
+
}
|
|
117
|
+
true // never block the draw pass
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add gated listener to new root (NOT the original raw listener)
|
|
121
|
+
newRoot.viewTreeObserver.addOnPreDrawListener(gatedListener)
|
|
122
|
+
|
|
123
|
+
// Update mDifferentRoot flag
|
|
124
|
+
val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
|
|
125
|
+
differentRootField.isAccessible = true
|
|
126
|
+
differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
|
|
127
|
+
|
|
128
|
+
// Force redraw
|
|
129
|
+
val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
|
|
130
|
+
forceRedrawField.isAccessible = true
|
|
131
|
+
forceRedrawField.setBoolean(baseBlurViewGroup, true)
|
|
132
|
+
}
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
// Reflection failed — QmBlurView internals changed
|
|
135
|
+
// Fall back gracefully to default decor view blur root
|
|
172
136
|
}
|
|
173
137
|
}
|
|
174
138
|
|
|
175
|
-
|
|
139
|
+
private fun findNearestScreenAncestor(): ViewGroup? {
|
|
140
|
+
var current = parent
|
|
141
|
+
while (current != null) {
|
|
142
|
+
if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
|
|
143
|
+
return current as? ViewGroup
|
|
144
|
+
}
|
|
145
|
+
current = current.parent
|
|
146
|
+
}
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
176
149
|
|
|
177
|
-
private fun
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
150
|
+
private fun findNearestReactRootView(): ViewGroup? {
|
|
151
|
+
var current = parent
|
|
152
|
+
while (current != null) {
|
|
153
|
+
if (current.javaClass.name == "com.facebook.react.ReactRootView") {
|
|
154
|
+
return current as? ViewGroup
|
|
155
|
+
}
|
|
156
|
+
current = current.parent
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
181
159
|
}
|
|
182
160
|
|
|
183
|
-
private fun
|
|
161
|
+
private fun initializeBlur() {
|
|
162
|
+
if (isBlurInitialized) return
|
|
184
163
|
try {
|
|
185
|
-
super.setBlurRadius(
|
|
164
|
+
super.setBlurRadius(currentBlurRadius)
|
|
186
165
|
super.setOverlayColor(currentOverlayColor)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
} catch (
|
|
190
|
-
//
|
|
166
|
+
updateCornerRadius()
|
|
167
|
+
isBlurInitialized = true
|
|
168
|
+
} catch (e: Exception) {
|
|
169
|
+
// Ignore — view may not be fully attached yet
|
|
191
170
|
}
|
|
192
171
|
}
|
|
193
172
|
|
|
194
|
-
//
|
|
173
|
+
// MARK: - Public setters
|
|
195
174
|
|
|
196
|
-
/**
|
|
197
|
-
* blurAmount: JS-facing 0–100.
|
|
198
|
-
* Mapped to 0–25 internally (QmBlurView Gaussian kernel's designed range).
|
|
199
|
-
* Values above 25 produce no visible increase in blur but cost more.
|
|
200
|
-
*/
|
|
201
175
|
fun setBlurAmount(amount: Float) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
try { super.setBlurRadius(pendingBlurRadius) } catch (_: Exception) {}
|
|
205
|
-
scheduleBlurFrame()
|
|
206
|
-
}
|
|
176
|
+
currentBlurRadius = mapBlurAmountToRadius(amount)
|
|
177
|
+
try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
|
|
207
178
|
}
|
|
208
179
|
|
|
209
180
|
fun setOverlayColor(colorString: String?) {
|
|
210
181
|
currentOverlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
} catch (_: Exception) {}
|
|
216
|
-
scheduleBlurFrame()
|
|
217
|
-
}
|
|
182
|
+
try {
|
|
183
|
+
super.setBackgroundColor(currentOverlayColor)
|
|
184
|
+
super.setOverlayColor(currentOverlayColor)
|
|
185
|
+
} catch (e: Exception) {}
|
|
218
186
|
}
|
|
219
187
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
try { super.setDownsampleFactor(factor.coerceIn(1, 8).toFloat()) } catch (_: Exception) {}
|
|
223
|
-
scheduleBlurFrame()
|
|
188
|
+
fun setReducedTransparencyFallbackColor(colorString: String?) {
|
|
189
|
+
// Stored for future use — QmBlurView handles accessibility fallback internally
|
|
224
190
|
}
|
|
225
191
|
|
|
226
|
-
fun
|
|
227
|
-
|
|
228
|
-
|
|
192
|
+
fun setBlurRadius(radius: Int) {
|
|
193
|
+
// blurRadius is the Android downscale factor — map to QmBlurView's downsample factor
|
|
194
|
+
val downsample = radius.coerceIn(1, 8).toFloat()
|
|
195
|
+
try { super.setDownsampleFactor(downsample) } catch (e: Exception) {}
|
|
229
196
|
}
|
|
230
197
|
|
|
231
|
-
fun
|
|
232
|
-
|
|
198
|
+
fun setBorderRadius(radius: Float) {
|
|
199
|
+
currentCornerRadius = radius
|
|
200
|
+
updateCornerRadius()
|
|
233
201
|
}
|
|
234
202
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
203
|
+
private fun updateCornerRadius() {
|
|
204
|
+
val radiusPx = TypedValue.applyDimension(
|
|
205
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
206
|
+
currentCornerRadius,
|
|
207
|
+
context.resources.displayMetrics
|
|
240
208
|
)
|
|
241
209
|
outlineProvider = object : ViewOutlineProvider() {
|
|
242
210
|
override fun getOutline(view: View, outline: Outline) {
|
|
243
|
-
outline.setRoundRect(0, 0, view.width, view.height,
|
|
211
|
+
outline.setRoundRect(0, 0, view.width, view.height, radiusPx)
|
|
244
212
|
}
|
|
245
213
|
}
|
|
246
|
-
clipToOutline =
|
|
247
|
-
try { super.setCornerRadius(
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
251
|
-
|
|
252
|
-
private fun scheduleBlurFrame() {
|
|
253
|
-
if (!pendingFrame) {
|
|
254
|
-
pendingFrame = true
|
|
255
|
-
Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private fun mapBlurAmount(amount: Float): Float =
|
|
260
|
-
(amount.coerceIn(0f, 100f) / 100f) * 25f
|
|
261
|
-
|
|
262
|
-
// ── Ancestor finders ──────────────────────────────────────────────────────
|
|
263
|
-
|
|
264
|
-
private fun findNearestScreenAncestor(): ViewGroup? {
|
|
265
|
-
var p = parent
|
|
266
|
-
while (p != null) {
|
|
267
|
-
if (p.javaClass.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
|
|
268
|
-
p = (p as? View)?.parent
|
|
269
|
-
}
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private fun findNearestReactRootView(): ViewGroup? {
|
|
274
|
-
var p = parent
|
|
275
|
-
while (p != null) {
|
|
276
|
-
if (p.javaClass.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
|
|
277
|
-
p = (p as? View)?.parent
|
|
278
|
-
}
|
|
279
|
-
return null
|
|
214
|
+
clipToOutline = true
|
|
215
|
+
try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
|
|
280
216
|
}
|
|
281
217
|
|
|
282
|
-
//
|
|
283
|
-
|
|
218
|
+
// React Native handles layout — prevent superclass from interfering
|
|
284
219
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
285
|
-
//
|
|
286
|
-
// FrameLayout logic to fight RN's layout system.
|
|
220
|
+
// No-op: layout handled by React Native's Yoga engine
|
|
287
221
|
}
|
|
288
222
|
|
|
289
|
-
//
|
|
290
|
-
// Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
223
|
+
// MARK: - Color parser
|
|
224
|
+
// Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
|
|
225
|
+
private fun parseHexColor(colorString: String): Int? {
|
|
226
|
+
val s = colorString.trim()
|
|
227
|
+
if (s.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
|
|
228
|
+
if (!s.startsWith("#")) {
|
|
229
|
+
return try { s.toColorInt() } catch (e: Exception) { null }
|
|
230
|
+
}
|
|
231
|
+
val hex = s.removePrefix("#")
|
|
297
232
|
return try {
|
|
298
233
|
when (hex.length) {
|
|
299
|
-
3 -> Color.argb(
|
|
234
|
+
3 -> Color.argb(
|
|
235
|
+
255,
|
|
300
236
|
hex[0].toString().repeat(2).toInt(16),
|
|
301
237
|
hex[1].toString().repeat(2).toInt(16),
|
|
302
|
-
hex[2].toString().repeat(2).toInt(16)
|
|
303
|
-
|
|
238
|
+
hex[2].toString().repeat(2).toInt(16)
|
|
239
|
+
)
|
|
240
|
+
6 -> Color.argb(
|
|
241
|
+
255,
|
|
304
242
|
hex.substring(0, 2).toInt(16),
|
|
305
243
|
hex.substring(2, 4).toInt(16),
|
|
306
|
-
hex.substring(4, 6).toInt(16)
|
|
244
|
+
hex.substring(4, 6).toInt(16)
|
|
245
|
+
)
|
|
307
246
|
8 -> Color.argb(
|
|
308
|
-
hex.substring(6, 8).toInt(16), //
|
|
247
|
+
hex.substring(6, 8).toInt(16), // AA is last in #RRGGBBAA
|
|
309
248
|
hex.substring(0, 2).toInt(16),
|
|
310
249
|
hex.substring(2, 4).toInt(16),
|
|
311
|
-
hex.substring(4, 6).toInt(16)
|
|
250
|
+
hex.substring(4, 6).toInt(16)
|
|
251
|
+
)
|
|
312
252
|
else -> null
|
|
313
253
|
}
|
|
314
|
-
} catch (
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ── Constants ─────────────────────────────────────────────────────────────
|
|
318
|
-
|
|
319
|
-
companion object {
|
|
320
|
-
// blurAmount=10 → radius 2.5 — a gentle, performant default
|
|
321
|
-
private const val DEFAULT_BLUR_RADIUS = 2.5f
|
|
254
|
+
} catch (e: NumberFormatException) { null }
|
|
322
255
|
}
|
|
323
256
|
}
|
|
@@ -7,8 +7,8 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
7
7
|
/**
|
|
8
8
|
* BlurVibeViewManager
|
|
9
9
|
*
|
|
10
|
-
* ViewGroupManager
|
|
11
|
-
* React children
|
|
10
|
+
* ViewGroupManager — BlurVibeView (which extends BlurViewGroup/FrameLayout)
|
|
11
|
+
* hosts React children, so we must use ViewGroupManager, not SimpleViewManager.
|
|
12
12
|
*/
|
|
13
13
|
class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
|
|
14
14
|
|
|
@@ -17,30 +17,35 @@ class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
|
|
|
17
17
|
override fun createViewInstance(context: ThemedReactContext) = BlurVibeView(context)
|
|
18
18
|
|
|
19
19
|
@ReactProp(name = "blurAmount", defaultFloat = 10f)
|
|
20
|
-
fun setBlurAmount(view: BlurVibeView, amount: Float)
|
|
20
|
+
fun setBlurAmount(view: BlurVibeView, amount: Float) {
|
|
21
|
+
view.setBlurAmount(amount)
|
|
22
|
+
}
|
|
21
23
|
|
|
22
24
|
@ReactProp(name = "blurType")
|
|
23
25
|
fun setBlurType(view: BlurVibeView, type: String?) {
|
|
24
|
-
// No-op on Android — blurType
|
|
26
|
+
// No-op on Android — blurType maps to iOS UIBlurEffectStyle only
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
@ReactProp(name = "overlayColor")
|
|
28
|
-
fun setOverlayColor(view: BlurVibeView, color: String?)
|
|
30
|
+
fun setOverlayColor(view: BlurVibeView, color: String?) {
|
|
31
|
+
view.setOverlayColor(color)
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
@ReactProp(name = "reducedTransparencyFallbackColor")
|
|
31
|
-
fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?)
|
|
35
|
+
fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) {
|
|
32
36
|
view.setReducedTransparencyFallbackColor(color)
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
@ReactProp(name = "blurRadius", defaultInt = 4)
|
|
35
|
-
fun setBlurRadius(view: BlurVibeView, radius: Int)
|
|
40
|
+
fun setBlurRadius(view: BlurVibeView, radius: Int) {
|
|
41
|
+
view.setBlurRadius(radius)
|
|
42
|
+
}
|
|
36
43
|
|
|
37
44
|
@ReactProp(name = "borderRadius", defaultFloat = 0f)
|
|
38
|
-
fun setBlurBorderRadius(view: BlurVibeView, radius: Float)
|
|
39
|
-
|
|
40
|
-
override fun onDropViewInstance(view: BlurVibeView) {
|
|
41
|
-
super.onDropViewInstance(view)
|
|
45
|
+
fun setBlurBorderRadius(view: BlurVibeView, radius: Float) {
|
|
46
|
+
view.setBorderRadius(radius)
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
// Yoga
|
|
49
|
+
// React Native's Yoga handles child layout — return false
|
|
45
50
|
override fun needsCustomLayoutForChildren(): Boolean = false
|
|
46
51
|
}
|