react-native-blur-vibe 0.1.5 → 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/build.gradle +2 -0
- package/android/src/main/java/com/blurvibe/BlurVibeView.kt +189 -161
- package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
- package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +74 -22
- 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
- package/android/src/main/java/com/blurvibe/BlurCaptureCoordinator.kt +0 -230
|
@@ -1,230 +1,258 @@
|
|
|
1
1
|
package com.blurvibe
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.graphics.Bitmap
|
|
5
|
-
import android.graphics.Canvas
|
|
6
4
|
import android.graphics.Color
|
|
7
5
|
import android.graphics.Outline
|
|
8
|
-
import android.graphics.Paint
|
|
9
|
-
import android.graphics.Rect
|
|
10
|
-
import android.graphics.RectF
|
|
11
6
|
import android.util.TypedValue
|
|
12
7
|
import android.view.View
|
|
13
8
|
import android.view.ViewGroup
|
|
14
9
|
import android.view.ViewOutlineProvider
|
|
10
|
+
import android.view.ViewTreeObserver
|
|
15
11
|
import androidx.core.graphics.toColorInt
|
|
16
|
-
import com.
|
|
12
|
+
import com.qmdeve.blurview.base.BaseBlurViewGroup
|
|
13
|
+
import com.qmdeve.blurview.widget.BlurViewGroup
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
* BlurVibeView —
|
|
16
|
+
* BlurVibeView — Android backdrop blur implementation
|
|
20
17
|
*
|
|
21
|
-
* Extends
|
|
22
|
-
*
|
|
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
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
* onDraw(), then draws the overlay color on top.
|
|
29
|
+
* Credit: approach adapted from sbaiahmed1/react-native-blur
|
|
30
30
|
*/
|
|
31
|
-
class BlurVibeView(context: Context) :
|
|
31
|
+
class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
private var currentBlurRadius = DEFAULT_BLUR_RADIUS
|
|
34
|
+
private var currentOverlayColor = Color.TRANSPARENT
|
|
35
|
+
private var currentCornerRadius = 0f
|
|
36
|
+
private var isBlurInitialized = false
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
private val dstRect = RectF()
|
|
50
|
-
|
|
51
|
-
// ── Init ───────────────────────────────────────────────────────────────────
|
|
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
|
+
// Uses a squared curve so low values (0–30) stay subtle and mid-high values
|
|
46
|
+
// (50–100) produce the strong frosted-glass spread seen in CSS backdrop-blur-md/lg/xl.
|
|
47
|
+
private fun mapBlurAmountToRadius(amount: Float): Float {
|
|
48
|
+
val t = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT) / MAX_BLUR_AMOUNT // 0.0–1.0
|
|
49
|
+
return t * t * MAX_BLUR_RADIUS // quadratic: more spread at higher values
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
52
|
|
|
53
53
|
init {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
super.setBackgroundColor(currentOverlayColor)
|
|
55
|
+
clipChildren = true
|
|
56
56
|
clipToOutline = true
|
|
57
|
+
blurRounds = 2 // 2 passes = smooth Gaussian spread (frosted glass quality)
|
|
58
|
+
super.setDownsampleFactor(4.0f) // 1/4 res — eliminates pixelation, still fast
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
61
|
override fun onAttachedToWindow() {
|
|
62
62
|
super.onAttachedToWindow()
|
|
63
|
-
|
|
63
|
+
if (isBlurInitialized) return
|
|
64
|
+
swapBlurRootToOptimalAncestor()
|
|
65
|
+
initializeBlur()
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
override fun onDetachedFromWindow() {
|
|
67
|
-
coordinator?.unregister(this)
|
|
68
|
-
coordinator = null
|
|
69
69
|
super.onDetachedFromWindow()
|
|
70
|
+
isBlurInitialized = false
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
private var frameScheduled = false
|
|
73
74
|
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
val coord = BlurCaptureCoordinator.forRoot(root).also {
|
|
78
|
-
it.blurRadius = blurRadius
|
|
79
|
-
coordinator = it
|
|
80
|
-
}
|
|
81
|
-
coord.register(this)
|
|
75
|
+
private val frameCallback = android.view.Choreographer.FrameCallback {
|
|
76
|
+
frameScheduled = false
|
|
77
|
+
try { invalidate() } catch (_: Exception) {}
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Redirects QmBlurView's internal preDrawListener from the old root to [newRoot].
|
|
82
|
+
* Also wraps it in a Choreographer gate so blur work fires at most ONCE per vsync,
|
|
83
|
+
* even when many views invalidate simultaneously (scroll, animation, etc).
|
|
84
|
+
*/
|
|
85
|
+
private fun swapBlurRootToOptimalAncestor() {
|
|
86
|
+
val newRoot = findNearestScreenAncestor() ?: findNearestReactRootView() ?: return
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
val blurViewGroupClass = BlurViewGroup::class.java
|
|
90
|
+
val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
|
|
91
|
+
baseField.isAccessible = true
|
|
92
|
+
val baseBlurViewGroup = baseField.get(this) ?: return
|
|
93
|
+
|
|
94
|
+
val baseClass = BaseBlurViewGroup::class.java
|
|
95
|
+
|
|
96
|
+
val decorViewField = baseClass.getDeclaredField("mDecorView")
|
|
97
|
+
decorViewField.isAccessible = true
|
|
98
|
+
val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
|
|
99
|
+
|
|
100
|
+
val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
|
|
101
|
+
preDrawListenerField.isAccessible = true
|
|
102
|
+
val preDrawListener = preDrawListenerField.get(baseBlurViewGroup)
|
|
103
|
+
as? ViewTreeObserver.OnPreDrawListener
|
|
104
|
+
|
|
105
|
+
if (oldDecorView != null && preDrawListener != null) {
|
|
106
|
+
// Remove listener from old root
|
|
107
|
+
oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
108
|
+
|
|
109
|
+
// Set new root
|
|
110
|
+
decorViewField.set(baseBlurViewGroup, newRoot)
|
|
111
|
+
|
|
112
|
+
// Wrap in Choreographer gate: fires at most once per vsync regardless of
|
|
113
|
+
// how many child invalidations happen in the same frame
|
|
114
|
+
val gatedListener = ViewTreeObserver.OnPreDrawListener {
|
|
115
|
+
if (!frameScheduled) {
|
|
116
|
+
frameScheduled = true
|
|
117
|
+
android.view.Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
118
|
+
}
|
|
119
|
+
true // never block the draw pass
|
|
120
|
+
}
|
|
91
121
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
val root = findBlurRoot() ?: return
|
|
122
|
+
// Add gated listener to new root (NOT the original raw listener)
|
|
123
|
+
newRoot.viewTreeObserver.addOnPreDrawListener(gatedListener)
|
|
95
124
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
// Update mDifferentRoot flag
|
|
126
|
+
val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
|
|
127
|
+
differentRootField.isAccessible = true
|
|
128
|
+
differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
// Force redraw
|
|
131
|
+
val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
|
|
132
|
+
forceRedrawField.isAccessible = true
|
|
133
|
+
forceRedrawField.setBoolean(baseBlurViewGroup, true)
|
|
134
|
+
}
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
// Reflection failed — QmBlurView internals changed
|
|
137
|
+
// Fall back gracefully to default decor view blur root
|
|
138
|
+
}
|
|
139
|
+
}
|
|
102
140
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
private fun findNearestScreenAncestor(): ViewGroup? {
|
|
142
|
+
var current = parent
|
|
143
|
+
while (current != null) {
|
|
144
|
+
if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
|
|
145
|
+
return current as? ViewGroup
|
|
146
|
+
}
|
|
147
|
+
current = current.parent
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
112
151
|
|
|
113
|
-
|
|
152
|
+
private fun findNearestReactRootView(): ViewGroup? {
|
|
153
|
+
var current = parent
|
|
154
|
+
while (current != null) {
|
|
155
|
+
if (current.javaClass.name == "com.facebook.react.ReactRootView") {
|
|
156
|
+
return current as? ViewGroup
|
|
157
|
+
}
|
|
158
|
+
current = current.parent
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
private fun initializeBlur() {
|
|
164
|
+
if (isBlurInitialized) return
|
|
165
|
+
try {
|
|
166
|
+
super.setBlurRadius(currentBlurRadius)
|
|
167
|
+
super.setOverlayColor(currentOverlayColor)
|
|
168
|
+
updateCornerRadius()
|
|
169
|
+
isBlurInitialized = true
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
// Ignore — view may not be fully attached yet
|
|
118
172
|
}
|
|
119
173
|
}
|
|
120
174
|
|
|
121
|
-
//
|
|
175
|
+
// MARK: - Public setters
|
|
122
176
|
|
|
123
177
|
fun setBlurAmount(amount: Float) {
|
|
124
|
-
|
|
125
|
-
|
|
178
|
+
currentBlurRadius = mapBlurAmountToRadius(amount)
|
|
179
|
+
try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
|
|
126
180
|
}
|
|
127
181
|
|
|
128
|
-
fun
|
|
129
|
-
|
|
130
|
-
|
|
182
|
+
fun setOverlayColor(colorString: String?) {
|
|
183
|
+
currentOverlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
184
|
+
try {
|
|
185
|
+
super.setBackgroundColor(currentOverlayColor)
|
|
186
|
+
super.setOverlayColor(currentOverlayColor)
|
|
187
|
+
} catch (e: Exception) {}
|
|
131
188
|
}
|
|
132
189
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* Higher = faster + softer. Sets the global factor on the coordinator.
|
|
136
|
-
*/
|
|
137
|
-
fun applyBlurRadius(factor: Int) {
|
|
138
|
-
BlurCaptureCoordinator.DOWNSAMPLE_FACTOR = factor.coerceIn(2, 16).toFloat()
|
|
139
|
-
// re-attach so coordinator picks up new factor
|
|
140
|
-
attachToCoordinator()
|
|
190
|
+
fun setReducedTransparencyFallbackColor(colorString: String?) {
|
|
191
|
+
// Stored for future use — QmBlurView handles accessibility fallback internally
|
|
141
192
|
}
|
|
142
193
|
|
|
143
|
-
fun
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
updateOutline()
|
|
194
|
+
fun setBlurRadius(radius: Int) {
|
|
195
|
+
// blurRadius is the Android downscale factor — map to QmBlurView's downsample factor
|
|
196
|
+
val downsample = radius.coerceIn(1, 8).toFloat()
|
|
197
|
+
try { super.setDownsampleFactor(downsample) } catch (e: Exception) {}
|
|
148
198
|
}
|
|
149
199
|
|
|
150
|
-
fun
|
|
151
|
-
|
|
200
|
+
fun setBorderRadius(radius: Float) {
|
|
201
|
+
currentCornerRadius = radius
|
|
202
|
+
updateCornerRadius()
|
|
152
203
|
}
|
|
153
204
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
205
|
+
private fun updateCornerRadius() {
|
|
206
|
+
val radiusPx = TypedValue.applyDimension(
|
|
207
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
208
|
+
currentCornerRadius,
|
|
209
|
+
context.resources.displayMetrics
|
|
210
|
+
)
|
|
211
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
212
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
213
|
+
outline.setRoundRect(0, 0, view.width, view.height, radiusPx)
|
|
162
214
|
}
|
|
163
|
-
clipToOutline = true
|
|
164
|
-
} else {
|
|
165
|
-
outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
166
|
-
clipToOutline = false
|
|
167
215
|
}
|
|
168
|
-
|
|
216
|
+
clipToOutline = true
|
|
217
|
+
try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
|
|
169
218
|
}
|
|
170
219
|
|
|
171
|
-
//
|
|
172
|
-
|
|
220
|
+
// React Native handles layout — prevent superclass from interfering
|
|
173
221
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
174
|
-
//
|
|
175
|
-
// but we must NOT call FrameLayout super here (ReactViewGroup handles it internally).
|
|
222
|
+
// No-op: layout handled by React Native's Yoga engine
|
|
176
223
|
}
|
|
177
224
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
while (p != null) {
|
|
186
|
-
if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
|
|
187
|
-
return p as? ViewGroup
|
|
188
|
-
p = (p as? View)?.parent
|
|
225
|
+
// MARK: - Color parser
|
|
226
|
+
// Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
|
|
227
|
+
private fun parseHexColor(colorString: String): Int? {
|
|
228
|
+
val s = colorString.trim()
|
|
229
|
+
if (s.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
|
|
230
|
+
if (!s.startsWith("#")) {
|
|
231
|
+
return try { s.toColorInt() } catch (e: Exception) { null }
|
|
189
232
|
}
|
|
190
|
-
|
|
191
|
-
while (p != null) {
|
|
192
|
-
if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
|
|
193
|
-
return p as? ViewGroup
|
|
194
|
-
p = (p as? View)?.parent
|
|
195
|
-
}
|
|
196
|
-
return rootView as? ViewGroup
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ── Color parser ──────────────────────────────────────────────────────────
|
|
200
|
-
// Handles: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA", named colors
|
|
201
|
-
|
|
202
|
-
private fun parseHexColor(s: String): Int? {
|
|
203
|
-
val t = s.trim()
|
|
204
|
-
if (t.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
|
|
205
|
-
if (!t.startsWith("#")) return try { t.toColorInt() } catch (_: Exception) { null }
|
|
206
|
-
val hex = t.removePrefix("#")
|
|
233
|
+
val hex = s.removePrefix("#")
|
|
207
234
|
return try {
|
|
208
235
|
when (hex.length) {
|
|
209
|
-
3
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
3 -> Color.argb(
|
|
237
|
+
255,
|
|
238
|
+
hex[0].toString().repeat(2).toInt(16),
|
|
239
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
240
|
+
hex[2].toString().repeat(2).toInt(16)
|
|
241
|
+
)
|
|
242
|
+
6 -> Color.argb(
|
|
243
|
+
255,
|
|
244
|
+
hex.substring(0, 2).toInt(16),
|
|
245
|
+
hex.substring(2, 4).toInt(16),
|
|
246
|
+
hex.substring(4, 6).toInt(16)
|
|
247
|
+
)
|
|
248
|
+
8 -> Color.argb(
|
|
249
|
+
hex.substring(6, 8).toInt(16), // AA is last in #RRGGBBAA
|
|
250
|
+
hex.substring(0, 2).toInt(16),
|
|
251
|
+
hex.substring(2, 4).toInt(16),
|
|
252
|
+
hex.substring(4, 6).toInt(16)
|
|
253
|
+
)
|
|
222
254
|
else -> null
|
|
223
255
|
}
|
|
224
|
-
} catch (
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
companion object {
|
|
228
|
-
private const val DEFAULT_BLUR_RADIUS = 2.5f // blurAmount=10 → 2.5
|
|
256
|
+
} catch (e: NumberFormatException) { null }
|
|
229
257
|
}
|
|
230
258
|
}
|