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.
Files changed (31) hide show
  1. package/android/build.gradle +2 -2
  2. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +95 -193
  3. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +158 -185
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +77 -28
  5. package/android/src/main/java/com/blurvibe/LegacyBlurController.kt +238 -0
  6. package/ios/BlurVibeView.swift +2 -0
  7. package/ios/BlurVibeViewFabric.mm +112 -0
  8. package/ios/BlurVibeViewManager.m +12 -2
  9. package/ios/BlurVibeViewManager.swift +9 -9
  10. package/lib/commonjs/BlurVibeViewNativeComponent.ts +14 -25
  11. package/lib/commonjs/BlurView.js +9 -30
  12. package/lib/commonjs/BlurView.js.map +1 -1
  13. package/lib/module/BlurVibeViewNativeComponent.ts +14 -25
  14. package/lib/module/BlurView.js +9 -30
  15. package/lib/module/BlurView.js.map +1 -1
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +11 -9
  17. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/src/BlurView.d.ts +6 -31
  19. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/types.d.ts +26 -1
  21. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +11 -9
  23. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/module/src/BlurView.d.ts +6 -31
  25. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  26. package/lib/typescript/module/src/types.d.ts +26 -1
  27. package/lib/typescript/module/src/types.d.ts.map +1 -1
  28. package/package.json +11 -2
  29. package/src/BlurVibeViewNativeComponent.ts +14 -25
  30. package/src/BlurView.tsx +10 -33
  31. package/src/types.ts +30 -1
@@ -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.qmdeve.blurview.base.BaseBlurViewGroup
13
- import com.qmdeve.blurview.widget.BlurViewGroup
12
+ import com.facebook.react.views.view.ReactViewGroup
14
13
 
15
14
  /**
16
- * BlurVibeView — Android backdrop blur implementation
15
+ * BlurVibeView — Android API 21–30 backdrop blur (zero external dependencies)
17
16
  *
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
17
+ * Replaces QmBlurView with LegacyBlurController — a direct RenderScript
18
+ * implementation using only the Android SDK. No third-party library needed.
24
19
  *
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.
20
+ * Extends ReactViewGroup so it hosts RN children correctly (Yoga layout,
21
+ * touch events, z-ordering all work natively).
28
22
  *
29
- * Credit: approach adapted from sbaiahmed1/react-native-blur
23
+ * For API 31+, BlurVibeViewApi31 is used instead (RenderEffect GPU path).
30
24
  */
31
- class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
25
+ class BlurVibeView(context: Context) : ReactViewGroup(context) {
32
26
 
33
- private var currentBlurRadius = DEFAULT_BLUR_RADIUS
34
- private var currentOverlayColor = Color.TRANSPARENT
35
- private var currentCornerRadius = 0f
36
- private var isBlurInitialized = false
27
+ // ── State ──────────────────────────────────────────────────────────────────
37
28
 
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
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
- // 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
- }
34
+ // ── Init ───────────────────────────────────────────────────────────────────
52
35
 
53
36
  init {
54
- super.setBackgroundColor(currentOverlayColor)
55
- clipChildren = true
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
- if (isBlurInitialized) return
64
- swapBlurRootToOptimalAncestor()
65
- initializeBlur()
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
- private var frameScheduled = false
74
-
75
- private val frameCallback = android.view.Choreographer.FrameCallback {
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
- // 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
- }
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
- // Update mDifferentRoot flag
126
- val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
127
- differentRootField.isAccessible = true
128
- differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
70
+ // ── Public setters ─────────────────────────────────────────────────────────
129
71
 
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
- }
72
+ fun setBlurAmount(amount: Float) {
73
+ pendingBlurAmount = amount
74
+ blurController?.blurRadius = mapBlurAmount(amount)
139
75
  }
140
76
 
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
77
+ fun setOverlayColor(colorString: String?) {
78
+ pendingOverlay = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
79
+ blurController?.overlayColor = pendingOverlay
80
+ invalidate()
150
81
  }
151
82
 
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
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
- 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
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
- // MARK: - Public setters
101
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) { }
176
102
 
177
- fun setBlurAmount(amount: Float) {
178
- currentBlurRadius = mapBlurAmountToRadius(amount)
179
- try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
103
+ fun applyBlurEnabled(enabled: Boolean) {
104
+ blurController?.enabled = enabled
105
+ if (!enabled) invalidate()
180
106
  }
181
107
 
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) {}
108
+ fun setAutoUpdate(autoUpdate: Boolean) {
109
+ blurController?.autoUpdate = autoUpdate
188
110
  }
189
111
 
190
- fun setReducedTransparencyFallbackColor(colorString: String?) {
191
- // Stored for future use — QmBlurView handles accessibility fallback internally
192
- }
112
+ // ── Layout passthrough ─────────────────────────────────────────────────────
193
113
 
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) {}
114
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
115
+ // Yoga handles all layout
198
116
  }
199
117
 
200
- fun setBorderRadius(radius: Float) {
201
- currentCornerRadius = radius
202
- updateCornerRadius()
203
- }
118
+ // ── Helpers ────────────────────────────────────────────────────────────────
204
119
 
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)
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
- // React Native handles layout — prevent superclass from interfering
221
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
222
- // No-op: layout handled by React Native's Yoga engine
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
- // 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 }
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
- 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
- )
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 (e: NumberFormatException) { null }
158
+ } catch (_: NumberFormatException) { null }
257
159
  }
258
160
  }