react-native-blur-vibe 0.1.2 → 0.1.3

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.
@@ -63,6 +63,13 @@ android {
63
63
  }
64
64
  }
65
65
 
66
+ repositories {
67
+ google()
68
+ mavenCentral()
69
+ maven { url 'https://jitpack.io' }
70
+ }
71
+
66
72
  dependencies {
67
73
  implementation "com.facebook.react:react-android"
74
+ implementation 'com.qmdeve.blurview:core:1.1.4'
68
75
  }
@@ -1,162 +1,217 @@
1
1
  package com.blurvibe
2
2
 
3
- import android.annotation.SuppressLint
4
3
  import android.content.Context
5
- import android.graphics.Bitmap
6
- import android.graphics.Canvas
7
4
  import android.graphics.Color
8
- import android.graphics.RenderEffect
9
- import android.graphics.Shader
10
- import android.os.Build
11
- import android.renderscript.Allocation
12
- import android.renderscript.Element
13
- import android.renderscript.RenderScript
14
- import android.renderscript.ScriptIntrinsicBlur
5
+ import android.graphics.Outline
6
+ import android.util.TypedValue
15
7
  import android.view.View
16
8
  import android.view.ViewGroup
17
- import android.widget.FrameLayout
9
+ import android.view.ViewOutlineProvider
10
+ import android.view.ViewTreeObserver
11
+ import androidx.core.graphics.toColorInt
12
+ import com.qmdeve.blurview.base.BaseBlurViewGroup
13
+ import com.qmdeve.blurview.widget.BlurViewGroup
18
14
 
19
15
  /**
20
- * BlurVibeView
16
+ * BlurVibeView — Android backdrop blur implementation
21
17
  *
22
- * Extends FrameLayoutrequired because:
23
- * 1. We host children (overlay view + React children)
24
- * 2. ViewGroupManager (used in manager) requires a ViewGroup subclass
25
- * 3. SimpleViewManager cast to IViewGroupManager would crash
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
26
24
  *
27
- * Blur strategy:
28
- * API 31+ → RenderEffect (hardware accelerated, no bitmap)
29
- * API 24-30 RenderScript (bitmap-based, built into Android SDK)
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.
30
28
  *
31
- * Color props received as hex strings from JS, parsed manually.
32
- * Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
29
+ * Credit: approach adapted from sbaiahmed1/react-native-blur
33
30
  */
34
- @SuppressLint("NewApi")
35
- class BlurVibeView(context: Context) : FrameLayout(context) {
36
-
37
- private val overlayView = View(context)
38
- private var blurAmountValue: Float = 10f
39
- private var overlayColorValue: Int = Color.TRANSPARENT
40
- private var fallbackColorValue: Int = Color.parseColor("#F2F2F2")
41
- private var blurRadiusDownscale: Int = 4
31
+ class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
32
+
33
+ private var currentBlurRadius = DEFAULT_BLUR_RADIUS
34
+ private var currentOverlayColor = Color.TRANSPARENT
35
+ private var currentCornerRadius = 0f
36
+ private var isBlurInitialized = false
37
+
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 = 100f
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
48
+ }
49
+ }
42
50
 
43
51
  init {
44
- // overlayView fills entire frame, sits above blur, below React children
45
- overlayView.layoutParams = LayoutParams(
46
- LayoutParams.MATCH_PARENT,
47
- LayoutParams.MATCH_PARENT
48
- )
49
- overlayView.isClickable = false
50
- overlayView.isFocusable = false
51
- // Add overlay as first child — React children added later will be on top
52
- super.addView(overlayView, 0)
52
+ super.setBackgroundColor(currentOverlayColor)
53
+ clipChildren = true
54
+ clipToOutline = true
55
+ blurRounds = 5
56
+ super.setDownsampleFactor(6.0f)
57
+ }
58
+
59
+ override fun onAttachedToWindow() {
60
+ super.onAttachedToWindow()
61
+ if (isBlurInitialized) return
62
+ swapBlurRootToOptimalAncestor()
63
+ initializeBlur()
53
64
  }
54
65
 
55
- // MARK: - React child management
56
- // Must override to ensure React children go ABOVE our overlay view
66
+ override fun onDetachedFromWindow() {
67
+ super.onDetachedFromWindow()
68
+ isBlurInitialized = false
69
+ }
70
+
71
+ /**
72
+ * Uses reflection to redirect QmBlurView's internal blur capture root
73
+ * from the activity decor view to the nearest Screen or ReactRootView.
74
+ * This prevents the full-screen blur issue when BlurVibeView is used
75
+ * inside a ScrollView or with absolute positioning and zIndex.
76
+ */
77
+ private fun swapBlurRootToOptimalAncestor() {
78
+ val newRoot = findNearestScreenAncestor() ?: findNearestReactRootView() ?: return
79
+
80
+ try {
81
+ val blurViewGroupClass = BlurViewGroup::class.java
82
+ val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
83
+ baseField.isAccessible = true
84
+ val baseBlurViewGroup = baseField.get(this) ?: return
85
+
86
+ val baseClass = BaseBlurViewGroup::class.java
57
87
 
58
- override fun addView(child: View, index: Int) {
59
- if (child === overlayView) {
60
- super.addView(child, index)
61
- return
88
+ val decorViewField = baseClass.getDeclaredField("mDecorView")
89
+ decorViewField.isAccessible = true
90
+ val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
91
+
92
+ val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
93
+ preDrawListenerField.isAccessible = true
94
+ val preDrawListener = preDrawListenerField.get(baseBlurViewGroup)
95
+ as? ViewTreeObserver.OnPreDrawListener
96
+
97
+ if (oldDecorView != null && preDrawListener != null) {
98
+ // Remove listener from old root
99
+ oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
100
+
101
+ // Set new root
102
+ decorViewField.set(baseBlurViewGroup, newRoot)
103
+
104
+ // Add listener to new root
105
+ newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
106
+
107
+ // Update mDifferentRoot flag
108
+ val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
109
+ differentRootField.isAccessible = true
110
+ differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
111
+
112
+ // Force redraw
113
+ val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
114
+ forceRedrawField.isAccessible = true
115
+ forceRedrawField.setBoolean(baseBlurViewGroup, true)
116
+ }
117
+ } catch (e: Exception) {
118
+ // Reflection failed — QmBlurView internals changed
119
+ // Fall back gracefully to default decor view blur root
62
120
  }
63
- // React children always go on top of overlay
64
- super.addView(child, childCount)
65
121
  }
66
122
 
67
- override fun addView(child: View) {
68
- if (child === overlayView) {
69
- super.addView(child)
70
- return
123
+ private fun findNearestScreenAncestor(): ViewGroup? {
124
+ var current = parent
125
+ while (current != null) {
126
+ if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
127
+ return current as? ViewGroup
128
+ }
129
+ current = current.parent
71
130
  }
72
- super.addView(child, childCount)
131
+ return null
73
132
  }
74
133
 
75
- // MARK: - Setters (called by BlurVibeViewManager)
134
+ private fun findNearestReactRootView(): ViewGroup? {
135
+ var current = parent
136
+ while (current != null) {
137
+ if (current.javaClass.name == "com.facebook.react.ReactRootView") {
138
+ return current as? ViewGroup
139
+ }
140
+ current = current.parent
141
+ }
142
+ return null
143
+ }
144
+
145
+ private fun initializeBlur() {
146
+ if (isBlurInitialized) return
147
+ try {
148
+ super.setBlurRadius(currentBlurRadius)
149
+ super.setOverlayColor(currentOverlayColor)
150
+ updateCornerRadius()
151
+ isBlurInitialized = true
152
+ } catch (e: Exception) {
153
+ // Ignore — view may not be fully attached yet
154
+ }
155
+ }
156
+
157
+ // MARK: - Public setters
76
158
 
77
159
  fun setBlurAmount(amount: Float) {
78
- blurAmountValue = amount.coerceIn(0f, 100f)
79
- applyBlur()
160
+ currentBlurRadius = mapBlurAmountToRadius(amount)
161
+ try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
80
162
  }
81
163
 
82
164
  fun setOverlayColor(colorString: String?) {
83
- overlayColorValue = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
84
- overlayView.setBackgroundColor(overlayColorValue)
165
+ currentOverlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
166
+ try {
167
+ super.setBackgroundColor(currentOverlayColor)
168
+ super.setOverlayColor(currentOverlayColor)
169
+ } catch (e: Exception) {}
85
170
  }
86
171
 
87
172
  fun setReducedTransparencyFallbackColor(colorString: String?) {
88
- fallbackColorValue = parseHexColor(colorString ?: "#F2F2F2") ?: Color.parseColor("#F2F2F2")
173
+ // Stored for future use QmBlurView handles accessibility fallback internally
89
174
  }
90
175
 
91
176
  fun setBlurRadius(radius: Int) {
92
- blurRadiusDownscale = radius.coerceIn(1, 8)
93
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
94
- post { renderScriptBlur() }
95
- }
177
+ // blurRadius is the Android downscale factor — map to QmBlurView's downsample factor
178
+ val downsample = radius.coerceIn(1, 8).toFloat()
179
+ try { super.setDownsampleFactor(downsample) } catch (e: Exception) {}
96
180
  }
97
181
 
98
- // MARK: - Blur
99
-
100
- private fun applyBlur() {
101
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
102
- applyRenderEffect()
103
- } else {
104
- post { renderScriptBlur() }
105
- }
182
+ fun setBorderRadius(radius: Float) {
183
+ currentCornerRadius = radius
184
+ updateCornerRadius()
106
185
  }
107
186
 
108
- private fun applyRenderEffect() {
109
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
110
- val sigma = (blurAmountValue * 0.5f).coerceAtLeast(0.01f)
111
- setRenderEffect(
112
- RenderEffect.createBlurEffect(sigma, sigma, Shader.TileMode.MIRROR)
113
- )
114
- }
115
- }
116
-
117
- @Suppress("DEPRECATION")
118
- private fun renderScriptBlur() {
119
- val parentView = parent as? ViewGroup ?: return
120
- if (width <= 0 || height <= 0) return
121
- try {
122
- val scaledW = (width / blurRadiusDownscale).coerceAtLeast(1)
123
- val scaledH = (height / blurRadiusDownscale).coerceAtLeast(1)
124
- val bitmap = Bitmap.createBitmap(scaledW, scaledH, Bitmap.Config.ARGB_8888)
125
- val canvas = Canvas(bitmap)
126
- canvas.scale(1f / blurRadiusDownscale, 1f / blurRadiusDownscale)
127
- canvas.translate(-left.toFloat(), -top.toFloat())
128
- parentView.draw(canvas)
129
-
130
- val rs = RenderScript.create(context)
131
- val input = Allocation.createFromBitmap(rs, bitmap)
132
- val output = Allocation.createTyped(rs, input.type)
133
- val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
134
- val sigma = (blurAmountValue / 100f * 25f).coerceIn(1f, 25f)
135
- script.setRadius(sigma)
136
- script.setInput(input)
137
- script.forEach(output)
138
- output.copyTo(bitmap)
139
- rs.destroy()
140
-
141
- background = android.graphics.drawable.BitmapDrawable(resources, bitmap)
142
- } catch (e: Exception) {
143
- setBackgroundColor(fallbackColorValue)
187
+ private fun updateCornerRadius() {
188
+ val radiusPx = TypedValue.applyDimension(
189
+ TypedValue.COMPLEX_UNIT_DIP,
190
+ currentCornerRadius,
191
+ context.resources.displayMetrics
192
+ )
193
+ outlineProvider = object : ViewOutlineProvider() {
194
+ override fun getOutline(view: View, outline: Outline) {
195
+ outline.setRoundRect(0, 0, view.width, view.height, radiusPx)
196
+ }
144
197
  }
198
+ clipToOutline = true
199
+ try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
145
200
  }
146
201
 
147
- override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
148
- super.onLayout(changed, l, t, r, b)
149
- if (changed && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
150
- post { renderScriptBlur() }
151
- }
202
+ // React Native handles layout prevent superclass from interfering
203
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
204
+ // No-op: layout handled by React Native's Yoga engine
152
205
  }
153
206
 
154
- // MARK: - Hex color parser
207
+ // MARK: - Color parser
155
208
  // Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
156
209
  private fun parseHexColor(colorString: String): Int? {
157
210
  val s = colorString.trim()
158
211
  if (s.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
159
- if (!s.startsWith("#")) return null
212
+ if (!s.startsWith("#")) {
213
+ return try { s.toColorInt() } catch (e: Exception) { null }
214
+ }
160
215
  val hex = s.removePrefix("#")
161
216
  return try {
162
217
  when (hex.length) {
@@ -173,15 +228,13 @@ class BlurVibeView(context: Context) : FrameLayout(context) {
173
228
  hex.substring(4, 6).toInt(16)
174
229
  )
175
230
  8 -> Color.argb(
176
- hex.substring(6, 8).toInt(16), // alpha last in #RRGGBBAA
231
+ hex.substring(6, 8).toInt(16), // AA is last in #RRGGBBAA
177
232
  hex.substring(0, 2).toInt(16),
178
233
  hex.substring(2, 4).toInt(16),
179
234
  hex.substring(4, 6).toInt(16)
180
235
  )
181
236
  else -> null
182
237
  }
183
- } catch (e: NumberFormatException) {
184
- null
185
- }
238
+ } catch (e: NumberFormatException) { null }
186
239
  }
187
240
  }
@@ -7,10 +7,8 @@ import com.facebook.react.uimanager.annotations.ReactProp
7
7
  /**
8
8
  * BlurVibeViewManager
9
9
  *
10
- * Extends ViewGroupManager — NOT SimpleViewManager.
11
- * Reason: BlurVibeView hosts children (overlay + React children).
12
- * SimpleViewManager cast to IViewGroupManager crashes at runtime.
13
- * ViewGroupManager correctly implements IViewGroupManager interface.
10
+ * ViewGroupManager — BlurVibeView (which extends BlurViewGroup/FrameLayout)
11
+ * hosts React children, so we must use ViewGroupManager, not SimpleViewManager.
14
12
  */
15
13
  class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
16
14
 
@@ -18,34 +16,35 @@ class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
18
16
 
19
17
  override fun createViewInstance(context: ThemedReactContext) = BlurVibeView(context)
20
18
 
21
- // Float — matches TS NativeComponent Float
22
19
  @ReactProp(name = "blurAmount", defaultFloat = 10f)
23
20
  fun setBlurAmount(view: BlurVibeView, amount: Float) {
24
21
  view.setBlurAmount(amount)
25
22
  }
26
23
 
27
- // String — matches TS NativeComponent string (no-op on Android)
28
24
  @ReactProp(name = "blurType")
29
25
  fun setBlurType(view: BlurVibeView, type: String?) {
30
- // No-op — blurType maps to iOS UIBlurEffectStyle only
26
+ // No-op on Android — blurType maps to iOS UIBlurEffectStyle only
31
27
  }
32
28
 
33
- // String — matches TS NativeComponent string
34
- // Parsed as hex in BlurVibeView — no customType="Color" needed
35
29
  @ReactProp(name = "overlayColor")
36
30
  fun setOverlayColor(view: BlurVibeView, color: String?) {
37
31
  view.setOverlayColor(color)
38
32
  }
39
33
 
40
- // String — matches TS NativeComponent string
41
34
  @ReactProp(name = "reducedTransparencyFallbackColor")
42
35
  fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) {
43
36
  view.setReducedTransparencyFallbackColor(color)
44
37
  }
45
38
 
46
- // Int — matches TS NativeComponent Int32
47
39
  @ReactProp(name = "blurRadius", defaultInt = 4)
48
40
  fun setBlurRadius(view: BlurVibeView, radius: Int) {
49
41
  view.setBlurRadius(radius)
50
42
  }
43
+
44
+ override fun onDropViewInstance(view: BlurVibeView) {
45
+ super.onDropViewInstance(view)
46
+ }
47
+
48
+ // React Native's Yoga handles child layout — return false
49
+ override fun needsCustomLayoutForChildren(): Boolean = false
51
50
  }
@@ -1,92 +1,106 @@
1
+ // BlurVibeView.swift
2
+ // UIKit wrapper that hosts the SwiftUI blur view via UIHostingController.
3
+ // Approach mirrors sbaiahmed1/react-native-blur's AdvancedBlurView.
4
+
5
+ import SwiftUI
1
6
  import UIKit
2
7
 
3
8
  @objc(BlurVibeView)
4
9
  class BlurVibeView: UIView {
5
10
 
6
- // MARK: - Private Views
7
- private var blurEffectView: UIVisualEffectView?
8
- private let overlayView = UIView()
11
+ // MARK: - Private
9
12
 
10
- // MARK: - Props
13
+ private var hostingController: UIHostingController<BlurVibeSwiftUIView>?
11
14
 
12
- /// Blur intensity 0–100. Maps to UIBlurEffect intensity via animator.
13
- @objc var blurAmount: NSNumber = 10 { didSet { updateBlur() } }
15
+ // MARK: - Props
14
16
 
15
- /// iOS blur style maps to UIBlurEffectStyle
16
- @objc var blurType: NSString = "light" { didSet { updateBlur() } }
17
+ @objc var blurAmount: NSNumber = 10 { didSet { updateView() } }
18
+ @objc var blurType: NSString = "light" { didSet { updateView() } }
17
19
 
18
- /// Overlay color on top of blur. Works on iOS AND Android.
19
- /// Alpha controls blur visibility like CSS backdrop-filter + background-color.
20
- /// Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
21
- @objc var overlayColor: NSString = "transparent" { didSet { updateOverlay() } }
20
+ /// Hex overlay color on top of blur works on iOS AND Android.
21
+ /// "#00000000" = transparent (pure blur), "#00000080" = tinted blur
22
+ @objc var overlayColor: NSString = "transparent" { didSet { updateView() } }
22
23
 
23
- /// Fallback color when Reduce Transparency is enabled.
24
- @objc var reducedTransparencyFallbackColor: NSString = "#F2F2F2" { didSet { updateBlur() } }
24
+ @objc var reducedTransparencyFallbackColor: NSString = "#F2F2F2" { didSet { updateView() } }
25
25
 
26
- /// Android-only downscale factor. Accepted on iOS to avoid prop warning — no-op.
26
+ /// Android-only downscale factor accepted here as no-op to avoid prop warning
27
27
  @objc var blurRadius: NSNumber = 4
28
28
 
29
29
  // MARK: - Init
30
- override init(frame: CGRect) { super.init(frame: frame); commonInit() }
31
- required init?(coder: NSCoder) { super.init(coder: coder); commonInit() }
32
30
 
33
- private func commonInit() {
31
+ override init(frame: CGRect) {
32
+ super.init(frame: frame)
33
+ backgroundColor = .clear
34
+ }
35
+
36
+ required init?(coder: NSCoder) {
37
+ super.init(coder: coder)
34
38
  backgroundColor = .clear
35
- clipsToBounds = true
36
- overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
37
- overlayView.isUserInteractionEnabled = false
38
- updateBlur()
39
39
  }
40
40
 
41
41
  // MARK: - Layout
42
+
42
43
  override func layoutSubviews() {
43
44
  super.layoutSubviews()
44
- blurEffectView?.frame = bounds
45
- overlayView.frame = bounds
46
- bringSubviewToFront(overlayView)
47
- for subview in subviews where subview !== blurEffectView && subview !== overlayView {
48
- bringSubviewToFront(subview)
45
+ // Defer hosting controller setup until we have a valid frame
46
+ // Prevents issues with initial render in complex layouts (e.g. FlashList)
47
+ if hostingController == nil && bounds.width > 0 && bounds.height > 0 {
48
+ setupHostingController()
49
+ } else {
50
+ hostingController?.view.frame = bounds
49
51
  }
50
52
  }
51
53
 
52
- // MARK: - Blur
53
- private func updateBlur() {
54
- if UIAccessibility.isReduceTransparencyEnabled {
55
- blurEffectView?.removeFromSuperview()
56
- blurEffectView = nil
57
- backgroundColor = parseColor(reducedTransparencyFallbackColor as String)
58
- ?? UIColor(white: 0.95, alpha: 1)
59
- return
54
+ // MARK: - Hosting Controller
55
+
56
+ private func setupHostingController() {
57
+ // Remove existing hosting controller cleanly
58
+ if let old = hostingController {
59
+ old.view.removeFromSuperview()
60
+ old.removeFromParent()
60
61
  }
61
- backgroundColor = .clear
62
- let effect = UIBlurEffect(style: blurEffectStyle(for: blurType as String))
63
- if let existing = blurEffectView {
64
- existing.effect = effect
62
+ hostingController = nil
63
+
64
+ let swiftUIView = makeSwiftUIView()
65
+ let hosting = UIHostingController(rootView: swiftUIView)
66
+ hosting.view.backgroundColor = .clear
67
+ hosting.view.frame = bounds
68
+ hosting.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
69
+
70
+ // Insert at index 0 — stays behind React children
71
+ if !subviews.isEmpty {
72
+ insertSubview(hosting.view, at: 0)
65
73
  } else {
66
- let newBlurView = UIVisualEffectView(effect: effect)
67
- newBlurView.frame = bounds
68
- newBlurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
69
- blurEffectView = newBlurView
70
- insertSubview(newBlurView, at: 0)
71
- }
72
- if overlayView.superview == nil {
73
- insertSubview(overlayView, aboveSubview: blurEffectView!)
74
+ addSubview(hosting.view)
74
75
  }
75
- updateOverlay()
76
+
77
+ hostingController = hosting
76
78
  }
77
79
 
78
- private func updateOverlay() {
79
- let colorString = overlayColor as String
80
- if colorString.lowercased() == "transparent" {
81
- overlayView.backgroundColor = .clear
82
- } else {
83
- overlayView.backgroundColor = parseColor(colorString) ?? .clear
80
+ private func updateView() {
81
+ if let hosting = hostingController {
82
+ // Update root view without recreating the controller — avoids jank
83
+ hosting.rootView = makeSwiftUIView()
84
+ } else if bounds.width > 0 && bounds.height > 0 {
85
+ setupHostingController()
84
86
  }
85
87
  }
86
88
 
89
+ private func makeSwiftUIView() -> BlurVibeSwiftUIView {
90
+ return BlurVibeSwiftUIView(
91
+ blurAmount: Double(truncating: blurAmount),
92
+ blurStyle: blurStyleFromString(blurType as String),
93
+ overlayColor: parseColor(overlayColor as String) ?? .clear,
94
+ reducedTransparencyFallbackColor: parseColor(reducedTransparencyFallbackColor as String)
95
+ ?? UIColor(white: 0.95, alpha: 1)
96
+ )
97
+ }
98
+
87
99
  // MARK: - Blur Style Map
88
- private func blurEffectStyle(for type: String) -> UIBlurEffect.Style {
100
+
101
+ private func blurStyleFromString(_ type: String) -> UIBlurEffect.Style {
89
102
  switch type {
103
+ case "xlight": return .extraLight
90
104
  case "dark": return .dark
91
105
  case "extraLight": return .extraLight
92
106
  case "regular": return .regular
@@ -111,34 +125,61 @@ class BlurVibeView: UIView {
111
125
  }
112
126
 
113
127
  // MARK: - Color Parser
114
- // Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
128
+ // Supports: "transparent", named colors, "#RGB", "#RRGGBB", "#RRGGBBAA"
129
+
115
130
  private func parseColor(_ colorString: String) -> UIColor? {
131
+ let s = colorString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
132
+
133
+ // Named colors
134
+ let namedColors: [String: UIColor] = [
135
+ "transparent": .clear, "clear": .clear,
136
+ "white": .white, "black": .black,
137
+ "red": .red, "green": .green, "blue": .blue,
138
+ "gray": .gray, "grey": .gray,
139
+ ]
140
+ if let named = namedColors[s] { return named }
141
+
142
+ // Hex colors
116
143
  var hex = colorString.trimmingCharacters(in: .whitespacesAndNewlines)
117
144
  guard hex.hasPrefix("#") else { return nil }
118
145
  hex.removeFirst()
146
+
147
+ // Validate hex chars
148
+ let validHex = CharacterSet(charactersIn: "0123456789ABCDEFabcdef")
149
+ guard hex.unicodeScalars.allSatisfy({ validHex.contains($0) }) else { return nil }
150
+
119
151
  var rgbValue: UInt64 = 0
120
152
  Scanner(string: hex).scanHexInt64(&rgbValue)
153
+
121
154
  switch hex.count {
122
- case 3: // #RGB
123
- return UIColor(
124
- red: CGFloat((rgbValue & 0xF00) >> 8) / 15,
125
- green: CGFloat((rgbValue & 0x0F0) >> 4) / 15,
126
- blue: CGFloat( rgbValue & 0x00F ) / 15,
127
- alpha: 1)
155
+ case 3: // #RGB → expand
156
+ let r = (rgbValue & 0xF00) >> 8; let g = (rgbValue & 0x0F0) >> 4; let b = rgbValue & 0x00F
157
+ return UIColor(red: CGFloat(r | (r << 4)) / 255, green: CGFloat(g | (g << 4)) / 255,
158
+ blue: CGFloat(b | (b << 4)) / 255, alpha: 1)
159
+ case 4: // #RGBA expand
160
+ let r = (rgbValue & 0xF000) >> 12; let g = (rgbValue & 0x0F00) >> 8
161
+ let b = (rgbValue & 0x00F0) >> 4; let a = rgbValue & 0x000F
162
+ return UIColor(red: CGFloat(r | (r << 4)) / 255, green: CGFloat(g | (g << 4)) / 255,
163
+ blue: CGFloat(b | (b << 4)) / 255, alpha: CGFloat(a | (a << 4)) / 255)
128
164
  case 6: // #RRGGBB
129
- return UIColor(
130
- red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255,
131
- green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255,
132
- blue: CGFloat( rgbValue & 0x0000FF ) / 255,
133
- alpha: 1)
165
+ return UIColor(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255,
166
+ green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255,
167
+ blue: CGFloat(rgbValue & 0x0000FF) / 255, alpha: 1)
134
168
  case 8: // #RRGGBBAA
135
- return UIColor(
136
- red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255,
137
- green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255,
138
- blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255,
139
- alpha: CGFloat( rgbValue & 0x000000FF ) / 255)
169
+ return UIColor(red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255,
170
+ green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255,
171
+ blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255,
172
+ alpha: CGFloat(rgbValue & 0x000000FF) / 255)
140
173
  default:
141
174
  return nil
142
175
  }
143
176
  }
177
+
178
+ // MARK: - Cleanup
179
+
180
+ deinit {
181
+ hostingController?.view.removeFromSuperview()
182
+ hostingController?.removeFromParent()
183
+ hostingController = nil
184
+ }
144
185
  }
@@ -0,0 +1,85 @@
1
+ // BlurEffectView.swift
2
+ // UIVisualEffectView with UIViewPropertyAnimator for custom blur intensity.
3
+ // This is the ONLY correct way to achieve custom blur radius on iOS.
4
+ // UIBlurEffect itself ignores any custom intensity — the animator interpolates it.
5
+
6
+ import SwiftUI
7
+ import UIKit
8
+
9
+ // MARK: - UIKit blur with custom intensity
10
+
11
+ class BlurEffectView: UIVisualEffectView {
12
+ private var animator: UIViewPropertyAnimator?
13
+ private var blurStyle: UIBlurEffect.Style = .systemMaterial
14
+ private var intensity: Double = 1.0
15
+
16
+ override init(effect: UIVisualEffect?) {
17
+ super.init(effect: effect)
18
+ setupBlur()
19
+ }
20
+
21
+ required init?(coder: NSCoder) {
22
+ super.init(coder: coder)
23
+ setupBlur()
24
+ }
25
+
26
+ func updateBlur(style: UIBlurEffect.Style, intensity: Double) {
27
+ // Skip expensive animator recreation when nothing changed
28
+ guard style != self.blurStyle || intensity != self.intensity else { return }
29
+ self.blurStyle = style
30
+ self.intensity = intensity
31
+ setupBlur()
32
+ }
33
+
34
+ override func didMoveToWindow() {
35
+ super.didMoveToWindow()
36
+ guard window != nil else { return }
37
+ // UIKit resumes paused CAAnimations when view re-joins a window.
38
+ // Re-pause and re-lock the fraction to prevent blur drifting to full intensity.
39
+ animator?.pauseAnimation()
40
+ animator?.fractionComplete = intensity
41
+ }
42
+
43
+ private func setupBlur() {
44
+ if let existing = animator, existing.state == .active {
45
+ existing.stopAnimation(true)
46
+ }
47
+ animator = nil
48
+ effect = nil
49
+
50
+ let newAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear)
51
+ newAnimator.addAnimations { [weak self] in
52
+ self?.effect = UIBlurEffect(style: self?.blurStyle ?? .systemMaterial)
53
+ }
54
+ // pausesOnCompletion: keeps animator .active even at fraction 1.0
55
+ // so didMoveToWindow can always safely call pauseAnimation()
56
+ newAnimator.pausesOnCompletion = true
57
+ newAnimator.startAnimation()
58
+ newAnimator.pauseAnimation()
59
+ newAnimator.fractionComplete = intensity
60
+ animator = newAnimator
61
+ }
62
+
63
+ deinit {
64
+ if let animator = animator, animator.state == .active {
65
+ animator.stopAnimation(true)
66
+ }
67
+ }
68
+ }
69
+
70
+ // MARK: - SwiftUI wrapper for BlurEffectView
71
+
72
+ struct BlurVibeEffect: UIViewRepresentable {
73
+ var style: UIBlurEffect.Style = .systemMaterial
74
+ var intensity: Double = 1.0
75
+
76
+ func makeUIView(context: Context) -> BlurEffectView {
77
+ let view = BlurEffectView(effect: nil)
78
+ view.updateBlur(style: style, intensity: intensity)
79
+ return view
80
+ }
81
+
82
+ func updateUIView(_ uiView: BlurEffectView, context: Context) {
83
+ uiView.updateBlur(style: style, intensity: intensity)
84
+ }
85
+ }
@@ -0,0 +1,46 @@
1
+ // BlurVibeSwiftUIView.swift
2
+ // SwiftUI view that composes blur effect + overlay color.
3
+
4
+ import SwiftUI
5
+ import UIKit
6
+
7
+ struct BlurVibeSwiftUIView: View {
8
+ let blurAmount: Double
9
+ let blurStyle: UIBlurEffect.Style
10
+ let overlayColor: UIColor
11
+ let reducedTransparencyFallbackColor: UIColor
12
+
13
+ private let isReducedTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
14
+
15
+ // Map 0–100 blurAmount to 0.0–1.0 animator fraction
16
+ private var blurIntensity: Double {
17
+ (blurAmount / 100.0).clamped(to: 0.0...1.0)
18
+ }
19
+
20
+ var body: some View {
21
+ if isReducedTransparencyEnabled {
22
+ // Accessibility: Reduce Transparency is ON — show solid fallback color
23
+ Rectangle()
24
+ .fill(Color(reducedTransparencyFallbackColor))
25
+ } else {
26
+ ZStack {
27
+ // Layer 1: backdrop blur (what's behind this view gets blurred)
28
+ BlurVibeEffect(style: blurStyle, intensity: blurIntensity)
29
+
30
+ // Layer 2: overlay color with alpha on top of blur
31
+ // This is our overlayColor prop — same as CSS background-color with alpha
32
+ // "#00000000" = transparent = pure blur shows through
33
+ // "#00000080" = 50% black tint over blur
34
+ // "#000000FF" = fully opaque = blur hidden
35
+ Rectangle()
36
+ .fill(Color(overlayColor))
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ extension Comparable {
43
+ func clamped(to range: ClosedRange<Self>) -> Self {
44
+ min(max(self, range.lowerBound), range.upperBound)
45
+ }
46
+ }
@@ -9,13 +9,15 @@ Pod::Spec.new do |s|
9
9
  s.homepage = package["homepage"]
10
10
  s.license = package["license"]
11
11
  s.authors = package["author"]
12
- s.platforms = { :ios => "13.0" }
12
+ s.platforms = { :ios => "14.0" }
13
13
  s.source = { :git => "https://github.com/I-am-Pritam-20/react-native-blur-vibe.git", :tag => "#{s.version}" }
14
+
15
+ # Include all Swift and ObjC files in ios/ and ios/Views/
14
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+
15
18
  s.requires_arc = true
16
19
 
17
20
  s.dependency "React-Core"
18
21
 
19
- # New Architecture (Fabric) support
20
22
  install_modules_dependencies(s)
21
23
  end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "React Native package implementing Blur View in iOS and Android",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",