react-native-blur-vibe 0.1.7 → 0.1.9
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 +95 -193
- package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +158 -185
- package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +77 -28
- package/android/src/main/java/com/blurvibe/LegacyBlurController.kt +238 -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
package/android/build.gradle
CHANGED
|
@@ -66,10 +66,10 @@ android {
|
|
|
66
66
|
repositories {
|
|
67
67
|
google()
|
|
68
68
|
mavenCentral()
|
|
69
|
-
maven { url 'https://jitpack.io' }
|
|
69
|
+
//maven { url 'https://jitpack.io' }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
dependencies {
|
|
73
73
|
implementation "com.facebook.react:react-android"
|
|
74
|
-
implementation 'com.qmdeve.blurview:core:1.1.4'
|
|
74
|
+
//implementation 'com.qmdeve.blurview:core:1.1.4'
|
|
75
75
|
}
|
|
@@ -1,258 +1,160 @@
|
|
|
1
1
|
package com.blurvibe
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
4
5
|
import android.graphics.Color
|
|
5
6
|
import android.graphics.Outline
|
|
6
7
|
import android.util.TypedValue
|
|
7
8
|
import android.view.View
|
|
8
9
|
import android.view.ViewGroup
|
|
9
10
|
import android.view.ViewOutlineProvider
|
|
10
|
-
import android.view.ViewTreeObserver
|
|
11
11
|
import androidx.core.graphics.toColorInt
|
|
12
|
-
import com.
|
|
13
|
-
import com.qmdeve.blurview.widget.BlurViewGroup
|
|
12
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
|
-
* BlurVibeView — Android backdrop blur
|
|
15
|
+
* BlurVibeView — Android API 21–30 backdrop blur (zero external dependencies)
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
17
|
+
* Replaces QmBlurView with LegacyBlurController — a direct RenderScript
|
|
18
|
+
* implementation using only the Android SDK. No third-party library needed.
|
|
24
19
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* preventing full-screen blur and navigation transition artifacts.
|
|
20
|
+
* Extends ReactViewGroup so it hosts RN children correctly (Yoga layout,
|
|
21
|
+
* touch events, z-ordering all work natively).
|
|
28
22
|
*
|
|
29
|
-
*
|
|
23
|
+
* For API 31+, BlurVibeViewApi31 is used instead (RenderEffect GPU path).
|
|
30
24
|
*/
|
|
31
|
-
class BlurVibeView(context: Context) :
|
|
25
|
+
class BlurVibeView(context: Context) : ReactViewGroup(context) {
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
private var currentOverlayColor = Color.TRANSPARENT
|
|
35
|
-
private var currentCornerRadius = 0f
|
|
36
|
-
private var isBlurInitialized = false
|
|
27
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
private const val MAX_BLUR_RADIUS = 25f // QmBlurView Gaussian kernel designed for 0-25
|
|
29
|
+
private var blurController: LegacyBlurController? = null
|
|
30
|
+
private var pendingBlurAmount = 10f
|
|
31
|
+
private var pendingOverlay = Color.TRANSPARENT
|
|
32
|
+
private var cornerRadiusPx = 0f
|
|
43
33
|
|
|
44
|
-
|
|
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
|
-
}
|
|
34
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
52
35
|
|
|
53
36
|
init {
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
setWillNotDraw(false)
|
|
38
|
+
super.setBackgroundColor(Color.TRANSPARENT)
|
|
56
39
|
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
|
|
59
40
|
}
|
|
60
41
|
|
|
42
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
61
44
|
override fun onAttachedToWindow() {
|
|
62
45
|
super.onAttachedToWindow()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
val root = findBlurRoot() ?: return
|
|
47
|
+
blurController = LegacyBlurController(this, root).also {
|
|
48
|
+
it.blurRadius = mapBlurAmount(pendingBlurAmount)
|
|
49
|
+
it.overlayColor = pendingOverlay
|
|
50
|
+
}
|
|
66
51
|
}
|
|
67
52
|
|
|
68
53
|
override fun onDetachedFromWindow() {
|
|
54
|
+
blurController?.destroy()
|
|
55
|
+
blurController = null
|
|
69
56
|
super.onDetachedFromWindow()
|
|
70
|
-
isBlurInitialized = false
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
frameScheduled = false
|
|
77
|
-
try { invalidate() } catch (_: Exception) {}
|
|
59
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
60
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
61
|
+
blurController?.onSizeChanged()
|
|
78
62
|
}
|
|
79
63
|
|
|
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)
|
|
64
|
+
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
108
65
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
121
|
-
|
|
122
|
-
// Add gated listener to new root (NOT the original raw listener)
|
|
123
|
-
newRoot.viewTreeObserver.addOnPreDrawListener(gatedListener)
|
|
66
|
+
override fun onDraw(canvas: Canvas) {
|
|
67
|
+
blurController?.draw(canvas, width.toFloat(), height.toFloat())
|
|
68
|
+
}
|
|
124
69
|
|
|
125
|
-
|
|
126
|
-
val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
|
|
127
|
-
differentRootField.isAccessible = true
|
|
128
|
-
differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
|
|
70
|
+
// ── Public setters ─────────────────────────────────────────────────────────
|
|
129
71
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
}
|
|
72
|
+
fun setBlurAmount(amount: Float) {
|
|
73
|
+
pendingBlurAmount = amount
|
|
74
|
+
blurController?.blurRadius = mapBlurAmount(amount)
|
|
139
75
|
}
|
|
140
76
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return current as? ViewGroup
|
|
146
|
-
}
|
|
147
|
-
current = current.parent
|
|
148
|
-
}
|
|
149
|
-
return null
|
|
77
|
+
fun setOverlayColor(colorString: String?) {
|
|
78
|
+
pendingOverlay = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
|
|
79
|
+
blurController?.overlayColor = pendingOverlay
|
|
80
|
+
invalidate()
|
|
150
81
|
}
|
|
151
82
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (current.javaClass.name == "com.facebook.react.ReactRootView") {
|
|
156
|
-
return current as? ViewGroup
|
|
157
|
-
}
|
|
158
|
-
current = current.parent
|
|
159
|
-
}
|
|
160
|
-
return null
|
|
83
|
+
fun setBlurRadius(factor: Int) {
|
|
84
|
+
// No-op for now — LegacyBlurController uses fixed DOWNSAMPLE_FACTOR = 4
|
|
85
|
+
// Could expose downsampleFactor setter on controller if needed
|
|
161
86
|
}
|
|
162
87
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Ignore — view may not be fully attached yet
|
|
88
|
+
fun applyBorderRadius(radiusDp: Float) {
|
|
89
|
+
cornerRadiusPx = TypedValue.applyDimension(
|
|
90
|
+
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
91
|
+
)
|
|
92
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
93
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
94
|
+
outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
|
|
95
|
+
}
|
|
172
96
|
}
|
|
97
|
+
clipToOutline = cornerRadiusPx > 0f
|
|
98
|
+
invalidate()
|
|
173
99
|
}
|
|
174
100
|
|
|
175
|
-
|
|
101
|
+
fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) { }
|
|
176
102
|
|
|
177
|
-
fun
|
|
178
|
-
|
|
179
|
-
|
|
103
|
+
fun applyBlurEnabled(enabled: Boolean) {
|
|
104
|
+
blurController?.enabled = enabled
|
|
105
|
+
if (!enabled) invalidate()
|
|
180
106
|
}
|
|
181
107
|
|
|
182
|
-
fun
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
super.setBackgroundColor(currentOverlayColor)
|
|
186
|
-
super.setOverlayColor(currentOverlayColor)
|
|
187
|
-
} catch (e: Exception) {}
|
|
108
|
+
fun setAutoUpdate(autoUpdate: Boolean) {
|
|
109
|
+
blurController?.autoUpdate = autoUpdate
|
|
188
110
|
}
|
|
189
111
|
|
|
190
|
-
|
|
191
|
-
// Stored for future use — QmBlurView handles accessibility fallback internally
|
|
192
|
-
}
|
|
112
|
+
// ── Layout passthrough ─────────────────────────────────────────────────────
|
|
193
113
|
|
|
194
|
-
fun
|
|
195
|
-
//
|
|
196
|
-
val downsample = radius.coerceIn(1, 8).toFloat()
|
|
197
|
-
try { super.setDownsampleFactor(downsample) } catch (e: Exception) {}
|
|
114
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
115
|
+
// Yoga handles all layout
|
|
198
116
|
}
|
|
199
117
|
|
|
200
|
-
|
|
201
|
-
currentCornerRadius = radius
|
|
202
|
-
updateCornerRadius()
|
|
203
|
-
}
|
|
118
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
204
119
|
|
|
205
|
-
private fun
|
|
206
|
-
val
|
|
207
|
-
|
|
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)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
clipToOutline = true
|
|
217
|
-
try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
|
|
120
|
+
private fun mapBlurAmount(amount: Float): Float {
|
|
121
|
+
val t = amount.coerceIn(0f, 100f) / 100f
|
|
122
|
+
return t * t * 25f // quadratic curve, max 25 (RenderScript kernel limit)
|
|
218
123
|
}
|
|
219
124
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
125
|
+
private fun findBlurRoot(): ViewGroup? {
|
|
126
|
+
var p = parent
|
|
127
|
+
while (p != null) {
|
|
128
|
+
if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
|
|
129
|
+
p = (p as? View)?.parent
|
|
130
|
+
}
|
|
131
|
+
p = parent
|
|
132
|
+
while (p != null) {
|
|
133
|
+
if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
|
|
134
|
+
p = (p as? View)?.parent
|
|
135
|
+
}
|
|
136
|
+
return rootView as? ViewGroup
|
|
223
137
|
}
|
|
224
138
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!s.startsWith("#")) {
|
|
231
|
-
return try { s.toColorInt() } catch (e: Exception) { null }
|
|
232
|
-
}
|
|
233
|
-
val hex = s.removePrefix("#")
|
|
139
|
+
private fun parseHexColor(s: String): Int? {
|
|
140
|
+
val t = s.trim()
|
|
141
|
+
if (t.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
|
|
142
|
+
if (!t.startsWith("#")) return try { t.toColorInt() } catch (_: Exception) { null }
|
|
143
|
+
val hex = t.removePrefix("#")
|
|
234
144
|
return try {
|
|
235
145
|
when (hex.length) {
|
|
236
|
-
3 -> Color.argb(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
)
|
|
146
|
+
3 -> Color.argb(255, hex[0].toString().repeat(2).toInt(16),
|
|
147
|
+
hex[1].toString().repeat(2).toInt(16),
|
|
148
|
+
hex[2].toString().repeat(2).toInt(16))
|
|
149
|
+
6 -> Color.argb(255, hex.substring(0,2).toInt(16),
|
|
150
|
+
hex.substring(2,4).toInt(16),
|
|
151
|
+
hex.substring(4,6).toInt(16))
|
|
152
|
+
8 -> Color.argb(hex.substring(6,8).toInt(16),
|
|
153
|
+
hex.substring(0,2).toInt(16),
|
|
154
|
+
hex.substring(2,4).toInt(16),
|
|
155
|
+
hex.substring(4,6).toInt(16))
|
|
254
156
|
else -> null
|
|
255
157
|
}
|
|
256
|
-
} catch (
|
|
158
|
+
} catch (_: NumberFormatException) { null }
|
|
257
159
|
}
|
|
258
160
|
}
|