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.
Files changed (32) hide show
  1. package/README.md +374 -181
  2. package/android/build.gradle +2 -0
  3. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +189 -161
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
  5. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +74 -22
  6. package/ios/BlurVibeView.swift +28 -27
  7. package/ios/BlurVibeViewManager.m +9 -9
  8. package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
  9. package/ios/Views/ProgressiveBlurView.swift +255 -0
  10. package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
  11. package/lib/commonjs/BlurView.js +34 -7
  12. package/lib/commonjs/BlurView.js.map +1 -1
  13. package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
  14. package/lib/module/BlurView.js +34 -7
  15. package/lib/module/BlurView.js.map +1 -1
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
  17. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
  19. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/types.d.ts +236 -18
  21. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
  23. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  24. package/lib/typescript/module/src/BlurView.d.ts +27 -8
  25. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  26. package/lib/typescript/module/src/types.d.ts +236 -18
  27. package/lib/typescript/module/src/types.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/BlurVibeViewNativeComponent.ts +10 -16
  30. package/src/BlurView.tsx +34 -7
  31. package/src/types.ts +267 -18
  32. 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.facebook.react.views.view.ReactViewGroup
12
+ import com.qmdeve.blurview.base.BaseBlurViewGroup
13
+ import com.qmdeve.blurview.widget.BlurViewGroup
17
14
 
18
15
  /**
19
- * BlurVibeView — CSS backdrop-filter: blur() for React Native / Android
16
+ * BlurVibeView — Android backdrop blur implementation
20
17
  *
21
- * Extends ReactViewGroup so it can host React Native children correctly
22
- * (Yoga layout, touch events, z-ordering all work out of the box).
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
- * Blur is produced by BlurCaptureCoordinator a singleton per root view that
25
- * captures + blurs the root ONCE per vsync and shares the result to every
26
- * registered BlurVibeView. N blur views on screen = same cost as 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
- * Each view clips the shared blurred bitmap to its own screen-space rect in
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) : ReactViewGroup(context) {
31
+ class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
32
32
 
33
- // ── State ──────────────────────────────────────────────────────────────────
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
- private var blurRadius = DEFAULT_BLUR_RADIUS
36
- private var overlayColor = Color.TRANSPARENT
37
- private var cornerRadiusPx = 0f
38
-
39
- // ── Coordinator ───────────────────────────────────────────────────────────
40
-
41
- private var coordinator: BlurCaptureCoordinator? = null
42
-
43
- // ── Draw state (main thread only) ─────────────────────────────────────────
44
-
45
- @Volatile private var latestBitmap: Bitmap? = null
46
- private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG)
47
- private val overlayPaint = Paint()
48
- private val srcRect = Rect()
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
- setWillNotDraw(false)
55
- super.setBackgroundColor(Color.TRANSPARENT)
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
- attachToCoordinator()
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
- // ── Coordinator attachment ─────────────────────────────────────────────────
73
+ private var frameScheduled = false
73
74
 
74
- private fun attachToCoordinator() {
75
- coordinator?.unregister(this)
76
- val root = findBlurRoot() ?: return
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
- /** Called by coordinator on main thread when a new blurred bitmap is ready. */
85
- fun onBlurReady(bitmap: Bitmap) {
86
- latestBitmap = bitmap
87
- invalidate()
88
- }
89
-
90
- // ── Drawing ────────────────────────────────────────────────────────────────
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
- override fun onDraw(canvas: Canvas) {
93
- val bitmap = latestBitmap?.takeIf { !it.isRecycled } ?: return
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
- // compute this view's offset within the blur root
97
- val myLoc = IntArray(2); getLocationInWindow(myLoc)
98
- val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
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
- val l = myLoc[0] - rootLoc[0]
101
- val t = myLoc[1] - rootLoc[1]
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
- // the blurred bitmap is at 1/DOWNSAMPLE_FACTOR resolution — scale coords
104
- val f = BlurCaptureCoordinator.DOWNSAMPLE_FACTOR
105
- srcRect.set(
106
- (l / f).toInt().coerceAtLeast(0),
107
- (t / f).toInt().coerceAtLeast(0),
108
- ((l + width) / f).toInt().coerceAtMost(bitmap.width),
109
- ((t + height) / f).toInt().coerceAtMost(bitmap.height)
110
- )
111
- dstRect.set(0f, 0f, width.toFloat(), height.toFloat())
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
- if (!srcRect.isEmpty) canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint)
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
- if (Color.alpha(overlayColor) > 0) {
116
- overlayPaint.color = overlayColor
117
- canvas.drawRect(dstRect, overlayPaint)
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
- // ── Public setters (ViewManager → UI thread) ──────────────────────────────
175
+ // MARK: - Public setters
122
176
 
123
177
  fun setBlurAmount(amount: Float) {
124
- blurRadius = (amount.coerceIn(0f, 100f) / 100f) * 25f
125
- coordinator?.blurRadius = blurRadius
178
+ currentBlurRadius = mapBlurAmountToRadius(amount)
179
+ try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
126
180
  }
127
181
 
128
- fun applyOverlayColor(colorString: String?) {
129
- overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
130
- invalidate()
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
- * blurRadius prop: Android downsample factor (1–8).
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 applyBorderRadius(radiusDp: Float) {
144
- cornerRadiusPx = TypedValue.applyDimension(
145
- TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
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 setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
151
- // iOS-only concept — no-op on Android
200
+ fun setBorderRadius(radius: Float) {
201
+ currentCornerRadius = radius
202
+ updateCornerRadius()
152
203
  }
153
204
 
154
- // ── Corner radius / outline ────────────────────────────────────────────────
155
-
156
- private fun updateOutline() {
157
- if (cornerRadiusPx > 0f) {
158
- outlineProvider = object : ViewOutlineProvider() {
159
- override fun getOutline(view: View, outline: Outline) {
160
- outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
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
- invalidate()
216
+ clipToOutline = true
217
+ try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
169
218
  }
170
219
 
171
- // ── React Native layout passthrough ───────────────────────────────────────
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
- // Yoga owns layout calling super would run ReactViewGroup's layout which is correct,
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
- // ── Blur root finder ──────────────────────────────────────────────────────
179
- //
180
- // Priority: react-native-screens Screen → ReactRootView → window root
181
- // The root is what gets captured — use the narrowest stable ancestor.
182
-
183
- private fun findBlurRoot(): ViewGroup? {
184
- var p = parent
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
- p = parent
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 -> Color.argb(255,
210
- hex[0].toString().repeat(2).toInt(16),
211
- hex[1].toString().repeat(2).toInt(16),
212
- hex[2].toString().repeat(2).toInt(16))
213
- 6 -> Color.argb(255,
214
- hex.substring(0, 2).toInt(16),
215
- hex.substring(2, 4).toInt(16),
216
- hex.substring(4, 6).toInt(16))
217
- 8 -> Color.argb(
218
- hex.substring(6, 8).toInt(16), // alpha LAST in #RRGGBBAA
219
- hex.substring(0, 2).toInt(16),
220
- hex.substring(2, 4).toInt(16),
221
- hex.substring(4, 6).toInt(16))
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 (_: NumberFormatException) { null }
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
  }