react-native-blur-vibe 0.1.3 → 0.1.5

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.
@@ -66,10 +66,8 @@ android {
66
66
  repositories {
67
67
  google()
68
68
  mavenCentral()
69
- maven { url 'https://jitpack.io' }
70
69
  }
71
70
 
72
71
  dependencies {
73
72
  implementation "com.facebook.react:react-android"
74
- implementation 'com.qmdeve.blurview:core:1.1.4'
75
73
  }
@@ -0,0 +1,230 @@
1
+ package com.blurvibe
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.os.Build
8
+ import android.os.Handler
9
+ import android.os.HandlerThread
10
+ import android.os.Looper
11
+ import android.renderscript.Allocation
12
+ import android.renderscript.Element
13
+ import android.renderscript.RenderScript
14
+ import android.renderscript.ScriptIntrinsicBlur
15
+ import android.view.Choreographer
16
+ import android.view.ViewGroup
17
+ import android.view.ViewTreeObserver
18
+ import java.util.concurrent.CopyOnWriteArraySet
19
+
20
+ /**
21
+ * BlurCaptureCoordinator
22
+ *
23
+ * Singleton per root-view. Owns ONE preDrawListener, ONE bitmap capture, ONE blur pass
24
+ * per vsync — shared across ALL BlurVibeViews that point at the same root.
25
+ *
26
+ * Cost: O(1) per frame regardless of how many BlurVibeViews are mounted.
27
+ * Compare to naive per-view: O(N) per frame.
28
+ *
29
+ * Thread model:
30
+ * rootView.draw() → main thread (Android requires this)
31
+ * RenderScript blur → workerThread (non-blocking)
32
+ * onBlurReady() → main thread via mainHandler.post()
33
+ */
34
+ internal class BlurCaptureCoordinator private constructor(
35
+ private val rootView: ViewGroup
36
+ ) {
37
+
38
+ // registered BlurVibeViews — thread-safe, iterated on main thread
39
+ private val clients = CopyOnWriteArraySet<BlurVibeView>()
40
+
41
+ // bitmap pool — allocated once, reused every frame (zero GC)
42
+ private var captureBitmap: Bitmap? = null
43
+ private var scaledBitmap: Bitmap? = null
44
+
45
+ private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
46
+
47
+ // worker thread for blur (keeps main thread free)
48
+ private val workerThread = HandlerThread("BlurVibeWorker-${System.identityHashCode(rootView)}")
49
+ .also { it.start() }
50
+ private val workerHandler = Handler(workerThread.looper)
51
+ private val mainHandler = Handler(Looper.getMainLooper())
52
+
53
+ // RenderScript state (API < 31 only, created lazily on workerThread)
54
+ private var rs: RenderScript? = null
55
+ private var blurScript: ScriptIntrinsicBlur? = null
56
+ private var inputAlloc: Allocation? = null
57
+ private var outputAlloc: Allocation? = null
58
+
59
+ // blur params
60
+ var blurRadius: Float = 8f
61
+ set(value) { field = value.coerceIn(1f, 25f) }
62
+ var downsampleFactor: Float = DOWNSAMPLE_FACTOR
63
+
64
+ // frame gate — at most one capture queued at a time
65
+ private var frameScheduled = false
66
+ private val frameCallback = Choreographer.FrameCallback {
67
+ frameScheduled = false
68
+ captureAndBlur()
69
+ }
70
+
71
+ // ONE preDrawListener for the entire coordinator
72
+ private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
73
+ if (!frameScheduled) {
74
+ frameScheduled = true
75
+ Choreographer.getInstance().postFrameCallback(frameCallback)
76
+ }
77
+ true // never block the draw pass
78
+ }
79
+
80
+ // ── Init / destroy ────────────────────────────────────────────────────────
81
+
82
+ init {
83
+ rootView.viewTreeObserver.addOnPreDrawListener(preDrawListener)
84
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
85
+ workerHandler.post { initRenderScript() }
86
+ }
87
+ }
88
+
89
+ private fun destroy() {
90
+ rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
91
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
92
+ cache.remove(rootView)
93
+ workerHandler.post {
94
+ inputAlloc?.destroy(); inputAlloc = null
95
+ outputAlloc?.destroy(); outputAlloc = null
96
+ blurScript?.destroy(); blurScript = null
97
+ rs?.destroy(); rs = null
98
+ captureBitmap?.recycle(); captureBitmap = null
99
+ scaledBitmap?.recycle(); scaledBitmap = null
100
+ workerThread.quitSafely()
101
+ }
102
+ }
103
+
104
+ // ── Registration ──────────────────────────────────────────────────────────
105
+
106
+ fun register(view: BlurVibeView) {
107
+ clients.add(view)
108
+ // deliver cached result immediately so view doesn't flash blank
109
+ scaledBitmap?.takeIf { !it.isRecycled }?.let { view.onBlurReady(it) }
110
+ }
111
+
112
+ fun unregister(view: BlurVibeView) {
113
+ clients.remove(view)
114
+ if (clients.isEmpty()) destroy()
115
+ }
116
+
117
+ // ── Capture + blur pipeline ───────────────────────────────────────────────
118
+
119
+ private fun captureAndBlur() {
120
+ if (clients.isEmpty()) return
121
+ val w = rootView.width; if (w <= 0) return
122
+ val h = rootView.height; if (h <= 0) return
123
+
124
+ val factor = downsampleFactor
125
+ val scaledW = (w / factor).toInt().coerceAtLeast(1)
126
+ val scaledH = (h / factor).toInt().coerceAtLeast(1)
127
+
128
+ // allocate / reuse bitmaps
129
+ val capture = reuseBitmap(captureBitmap, w, h).also { captureBitmap = it }
130
+ val scaled = reuseBitmap(scaledBitmap, scaledW, scaledH).also { scaledBitmap = it }
131
+
132
+ // ① capture on main thread (required by Android)
133
+ try {
134
+ val c = Canvas(capture)
135
+ c.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR)
136
+ rootView.draw(c)
137
+ } catch (_: Exception) { return }
138
+
139
+ val radius = blurRadius
140
+ val captureRef = capture
141
+ val scaledRef = scaled
142
+
143
+ // ② blur on worker thread
144
+ workerHandler.post {
145
+ // downsample
146
+ Canvas(scaledRef).drawBitmap(
147
+ captureRef,
148
+ android.graphics.Rect(0, 0, captureRef.width, captureRef.height),
149
+ android.graphics.Rect(0, 0, scaledRef.width, scaledRef.height),
150
+ capturePaint
151
+ )
152
+
153
+ // blur
154
+ val blurred = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
155
+ blurSoftware(scaledRef, radius) // BlurMaskFilter — fast enough at small size
156
+ } else {
157
+ blurRenderScript(scaledRef, radius) ?: blurSoftware(scaledRef, radius)
158
+ }
159
+
160
+ // ③ deliver to all clients on main thread
161
+ mainHandler.post {
162
+ clients.forEach { it.onBlurReady(blurred) }
163
+ }
164
+ }
165
+ }
166
+
167
+ // ── Blur implementations ──────────────────────────────────────────────────
168
+
169
+ private fun initRenderScript() {
170
+ try {
171
+ rs = RenderScript.create(rootView.context)
172
+ blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
173
+ } catch (_: Exception) {}
174
+ }
175
+
176
+ private fun blurRenderScript(src: Bitmap, radius: Float): Bitmap? {
177
+ val r = this.rs ?: return null
178
+ val sc = this.blurScript ?: return null
179
+ return try {
180
+ val inA = reuseAlloc(inputAlloc, src, r).also { inputAlloc = it }
181
+ val outA = reuseAlloc(outputAlloc, src, r).also { outputAlloc = it }
182
+ inA.copyFrom(src)
183
+ sc.setRadius(radius)
184
+ sc.setInput(inA)
185
+ sc.forEach(outA)
186
+ val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
187
+ outA.copyTo(out)
188
+ out
189
+ } catch (_: Exception) { null }
190
+ }
191
+
192
+ private fun blurSoftware(src: Bitmap, radius: Float): Bitmap {
193
+ val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
194
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
195
+ maskFilter = android.graphics.BlurMaskFilter(radius, android.graphics.BlurMaskFilter.Blur.NORMAL)
196
+ }
197
+ Canvas(out).drawBitmap(src, 0f, 0f, paint)
198
+ return out
199
+ }
200
+
201
+ // ── Bitmap / Allocation helpers ───────────────────────────────────────────
202
+
203
+ private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
204
+ if (existing != null && !existing.isRecycled
205
+ && existing.width == w && existing.height == h) return existing
206
+ existing?.recycle()
207
+ return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
208
+ }
209
+
210
+ private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
211
+ if (existing != null
212
+ && existing.type.x == src.width
213
+ && existing.type.y == src.height) return existing
214
+ existing?.destroy()
215
+ return Allocation.createFromBitmap(rs, src,
216
+ Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
217
+ }
218
+
219
+ // ── Singleton cache ───────────────────────────────────────────────────────
220
+
221
+ companion object {
222
+ /** Global downsample factor. Higher = faster + softer. Range 2–16. */
223
+ var DOWNSAMPLE_FACTOR: Float = 8f
224
+
225
+ private val cache = HashMap<ViewGroup, BlurCaptureCoordinator>()
226
+
227
+ fun forRoot(rootView: ViewGroup): BlurCaptureCoordinator =
228
+ cache.getOrPut(rootView) { BlurCaptureCoordinator(rootView) }
229
+ }
230
+ }
@@ -1,240 +1,230 @@
1
1
  package com.blurvibe
2
2
 
3
3
  import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
4
6
  import android.graphics.Color
5
7
  import android.graphics.Outline
8
+ import android.graphics.Paint
9
+ import android.graphics.Rect
10
+ import android.graphics.RectF
6
11
  import android.util.TypedValue
7
12
  import android.view.View
8
13
  import android.view.ViewGroup
9
14
  import android.view.ViewOutlineProvider
10
- import android.view.ViewTreeObserver
11
15
  import androidx.core.graphics.toColorInt
12
- import com.qmdeve.blurview.base.BaseBlurViewGroup
13
- import com.qmdeve.blurview.widget.BlurViewGroup
16
+ import com.facebook.react.views.view.ReactViewGroup
14
17
 
15
18
  /**
16
- * BlurVibeView — Android backdrop blur implementation
19
+ * BlurVibeView — CSS backdrop-filter: blur() for React Native / Android
17
20
  *
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
21
+ * Extends ReactViewGroup so it can host React Native children correctly
22
+ * (Yoga layout, touch events, z-ordering all work out of the box).
24
23
  *
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.
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.
28
27
  *
29
- * Credit: approach adapted from sbaiahmed1/react-native-blur
28
+ * Each view clips the shared blurred bitmap to its own screen-space rect in
29
+ * onDraw(), then draws the overlay color on top.
30
30
  */
31
- class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
31
+ class BlurVibeView(context: Context) : ReactViewGroup(context) {
32
32
 
33
- private var currentBlurRadius = DEFAULT_BLUR_RADIUS
34
- private var currentOverlayColor = Color.TRANSPARENT
35
- private var currentCornerRadius = 0f
36
- private var isBlurInitialized = false
33
+ // ── State ──────────────────────────────────────────────────────────────────
37
34
 
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
- }
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 ───────────────────────────────────────────────────────────────────
50
52
 
51
53
  init {
52
- super.setBackgroundColor(currentOverlayColor)
53
- clipChildren = true
54
+ setWillNotDraw(false)
55
+ super.setBackgroundColor(Color.TRANSPARENT)
54
56
  clipToOutline = true
55
- blurRounds = 5
56
- super.setDownsampleFactor(6.0f)
57
57
  }
58
58
 
59
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
60
+
59
61
  override fun onAttachedToWindow() {
60
62
  super.onAttachedToWindow()
61
- if (isBlurInitialized) return
62
- swapBlurRootToOptimalAncestor()
63
- initializeBlur()
63
+ attachToCoordinator()
64
64
  }
65
65
 
66
66
  override fun onDetachedFromWindow() {
67
+ coordinator?.unregister(this)
68
+ coordinator = null
67
69
  super.onDetachedFromWindow()
68
- isBlurInitialized = false
69
70
  }
70
71
 
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
72
+ // ── Coordinator attachment ─────────────────────────────────────────────────
87
73
 
88
- val decorViewField = baseClass.getDeclaredField("mDecorView")
89
- decorViewField.isAccessible = true
90
- val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
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)
82
+ }
91
83
 
92
- val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
93
- preDrawListenerField.isAccessible = true
94
- val preDrawListener = preDrawListenerField.get(baseBlurViewGroup)
95
- as? ViewTreeObserver.OnPreDrawListener
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
+ }
96
89
 
97
- if (oldDecorView != null && preDrawListener != null) {
98
- // Remove listener from old root
99
- oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
90
+ // ── Drawing ────────────────────────────────────────────────────────────────
100
91
 
101
- // Set new root
102
- decorViewField.set(baseBlurViewGroup, newRoot)
92
+ override fun onDraw(canvas: Canvas) {
93
+ val bitmap = latestBitmap?.takeIf { !it.isRecycled } ?: return
94
+ val root = findBlurRoot() ?: return
103
95
 
104
- // Add listener to new root
105
- newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
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)
106
99
 
107
- // Update mDifferentRoot flag
108
- val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
109
- differentRootField.isAccessible = true
110
- differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
100
+ val l = myLoc[0] - rootLoc[0]
101
+ val t = myLoc[1] - rootLoc[1]
111
102
 
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
120
- }
121
- }
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())
122
112
 
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
130
- }
131
- return null
132
- }
113
+ if (!srcRect.isEmpty) canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint)
133
114
 
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
115
+ if (Color.alpha(overlayColor) > 0) {
116
+ overlayPaint.color = overlayColor
117
+ canvas.drawRect(dstRect, overlayPaint)
154
118
  }
155
119
  }
156
120
 
157
- // MARK: - Public setters
121
+ // ── Public setters (ViewManager → UI thread) ──────────────────────────────
158
122
 
159
123
  fun setBlurAmount(amount: Float) {
160
- currentBlurRadius = mapBlurAmountToRadius(amount)
161
- try { super.setBlurRadius(currentBlurRadius) } catch (e: Exception) {}
124
+ blurRadius = (amount.coerceIn(0f, 100f) / 100f) * 25f
125
+ coordinator?.blurRadius = blurRadius
162
126
  }
163
127
 
164
- fun setOverlayColor(colorString: String?) {
165
- currentOverlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
166
- try {
167
- super.setBackgroundColor(currentOverlayColor)
168
- super.setOverlayColor(currentOverlayColor)
169
- } catch (e: Exception) {}
128
+ fun applyOverlayColor(colorString: String?) {
129
+ overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
130
+ invalidate()
170
131
  }
171
132
 
172
- fun setReducedTransparencyFallbackColor(colorString: String?) {
173
- // Stored for future use QmBlurView handles accessibility fallback internally
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()
174
141
  }
175
142
 
176
- fun setBlurRadius(radius: Int) {
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) {}
143
+ fun applyBorderRadius(radiusDp: Float) {
144
+ cornerRadiusPx = TypedValue.applyDimension(
145
+ TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
146
+ )
147
+ updateOutline()
180
148
  }
181
149
 
182
- fun setBorderRadius(radius: Float) {
183
- currentCornerRadius = radius
184
- updateCornerRadius()
150
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
151
+ // iOS-only concept — no-op on Android
185
152
  }
186
153
 
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)
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
+ }
196
162
  }
163
+ clipToOutline = true
164
+ } else {
165
+ outlineProvider = ViewOutlineProvider.BACKGROUND
166
+ clipToOutline = false
197
167
  }
198
- clipToOutline = true
199
- try { super.setCornerRadius(radiusPx) } catch (e: Exception) {}
168
+ invalidate()
200
169
  }
201
170
 
202
- // React Native handles layout prevent superclass from interfering
171
+ // ── React Native layout passthrough ───────────────────────────────────────
172
+
203
173
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
204
- // No-op: layout handled by React Native's Yoga engine
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).
205
176
  }
206
177
 
207
- // MARK: - Color parser
208
- // Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
209
- private fun parseHexColor(colorString: String): Int? {
210
- val s = colorString.trim()
211
- if (s.equals("transparent", ignoreCase = true)) return Color.TRANSPARENT
212
- if (!s.startsWith("#")) {
213
- return try { s.toColorInt() } catch (e: Exception) { null }
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
214
189
  }
215
- val hex = s.removePrefix("#")
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("#")
216
207
  return try {
217
208
  when (hex.length) {
218
- 3 -> Color.argb(
219
- 255,
220
- hex[0].toString().repeat(2).toInt(16),
221
- hex[1].toString().repeat(2).toInt(16),
222
- hex[2].toString().repeat(2).toInt(16)
223
- )
224
- 6 -> Color.argb(
225
- 255,
226
- hex.substring(0, 2).toInt(16),
227
- hex.substring(2, 4).toInt(16),
228
- hex.substring(4, 6).toInt(16)
229
- )
230
- 8 -> Color.argb(
231
- hex.substring(6, 8).toInt(16), // AA is last in #RRGGBBAA
232
- hex.substring(0, 2).toInt(16),
233
- hex.substring(2, 4).toInt(16),
234
- hex.substring(4, 6).toInt(16)
235
- )
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
222
  else -> null
237
223
  }
238
- } catch (e: NumberFormatException) { null }
224
+ } catch (_: NumberFormatException) { null }
225
+ }
226
+
227
+ companion object {
228
+ private const val DEFAULT_BLUR_RADIUS = 2.5f // blurAmount=10 → 2.5
239
229
  }
240
230
  }
@@ -7,8 +7,15 @@ import com.facebook.react.uimanager.annotations.ReactProp
7
7
  /**
8
8
  * BlurVibeViewManager
9
9
  *
10
- * ViewGroupManager BlurVibeView (which extends BlurViewGroup/FrameLayout)
11
- * hosts React children, so we must use ViewGroupManager, not SimpleViewManager.
10
+ * All @ReactProp handler names are intentionally distinct from BaseViewManager /
11
+ * ReactViewGroup / View supertype methods to avoid "hides member" compile errors:
12
+ *
13
+ * setBlurAmount — unique, not in any supertype
14
+ * setBlurTypeProp — avoids any "setBlurType" conflict
15
+ * setOverlayColorProp — avoids BaseViewManager.setBackgroundColor etc.
16
+ * setReducedTransparencyFallbackColor — unique
17
+ * setBlurRadiusProp — avoids View.setRadius / BlurView.setBlurRadius
18
+ * setBlurBorderRadius — avoids BaseViewManager.setBorderRadius
12
19
  */
13
20
  class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
14
21
 
@@ -17,34 +24,29 @@ class BlurVibeViewManager : ViewGroupManager<BlurVibeView>() {
17
24
  override fun createViewInstance(context: ThemedReactContext) = BlurVibeView(context)
18
25
 
19
26
  @ReactProp(name = "blurAmount", defaultFloat = 10f)
20
- fun setBlurAmount(view: BlurVibeView, amount: Float) {
27
+ fun setBlurAmount(view: BlurVibeView, amount: Float) =
21
28
  view.setBlurAmount(amount)
22
- }
23
29
 
24
30
  @ReactProp(name = "blurType")
25
- fun setBlurType(view: BlurVibeView, type: String?) {
26
- // No-op on Android — blurType maps to iOS UIBlurEffectStyle only
31
+ fun setBlurTypeProp(view: BlurVibeView, @Suppress("UNUSED_PARAMETER") type: String?) {
32
+ // iOS UIBlurEffectStyle — no-op on Android
27
33
  }
28
34
 
29
35
  @ReactProp(name = "overlayColor")
30
- fun setOverlayColor(view: BlurVibeView, color: String?) {
31
- view.setOverlayColor(color)
32
- }
36
+ fun setOverlayColorProp(view: BlurVibeView, color: String?) =
37
+ view.applyOverlayColor(color)
33
38
 
34
39
  @ReactProp(name = "reducedTransparencyFallbackColor")
35
- fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) {
40
+ fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) =
36
41
  view.setReducedTransparencyFallbackColor(color)
37
- }
38
42
 
39
43
  @ReactProp(name = "blurRadius", defaultInt = 4)
40
- fun setBlurRadius(view: BlurVibeView, radius: Int) {
41
- view.setBlurRadius(radius)
42
- }
44
+ fun setBlurRadiusProp(view: BlurVibeView, radius: Int) =
45
+ view.applyBlurRadius(radius)
43
46
 
44
- override fun onDropViewInstance(view: BlurVibeView) {
45
- super.onDropViewInstance(view)
46
- }
47
+ @ReactProp(name = "borderRadius", defaultFloat = 0f)
48
+ fun setBlurBorderRadius(view: BlurVibeView, radius: Float) =
49
+ view.applyBorderRadius(radius)
47
50
 
48
- // React Native's Yoga handles child layout — return false
49
51
  override fun needsCustomLayoutForChildren(): Boolean = false
50
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",