react-native-blur-vibe 0.1.4 → 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,293 +1,203 @@
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
6
- import android.os.Handler
7
- import android.os.Looper
8
+ import android.graphics.Paint
9
+ import android.graphics.Rect
10
+ import android.graphics.RectF
8
11
  import android.util.TypedValue
9
- import android.view.Choreographer
10
12
  import android.view.View
11
13
  import android.view.ViewGroup
12
14
  import android.view.ViewOutlineProvider
13
- import android.view.ViewTreeObserver
14
15
  import androidx.core.graphics.toColorInt
15
- import com.qmdeve.blurview.base.BaseBlurViewGroup
16
- import com.qmdeve.blurview.widget.BlurViewGroup
16
+ import com.facebook.react.views.view.ReactViewGroup
17
17
 
18
18
  /**
19
- * BlurVibeView — Optimised Android backdrop-blur (QmBlurView / CSS backdrop-filter parity)
19
+ * BlurVibeView — CSS backdrop-filter: blur() for React Native / Android
20
20
  *
21
- * ─── What was killing performance (3 FPS) ────────────────────────────────────
21
+ * Extends ReactViewGroup so it can host React Native children correctly
22
+ * (Yoga layout, touch events, z-ordering all work out of the box).
22
23
  *
23
- * 1. blurRounds = 5
24
- * The single biggest killer. Each "round" is a full downsample Gaussian upsample
25
- * pipeline. At 60 fps that's 300 blur operations/second. One round looks identical
26
- * to the human eye and costs 1/5 as much. Fixed: blurRounds = 1.
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.
27
27
  *
28
- * 2. Blur radius mapped 0–100 instead of 0–25
29
- * mapBlurAmountToRadius() was returning up to 100.0. QmBlurView's Gaussian kernel
30
- * at radius=100 iterates a ~200-wide kernel per-pixel every frame.
31
- * Fixed: map blurAmount 0–100 → radius 0–25.
32
- *
33
- * 3. OnPreDrawListener fires every frame with no throttling
34
- * The listener was doing full blur work synchronously inside the pre-draw callback,
35
- * blocking the draw thread on every invalidation of every child in the tree.
36
- * Fixed: listener only sets a dirty flag; actual blur work is deferred to a
37
- * Choreographer.FrameCallback which fires at most once-per-vsync.
38
- *
39
- * 4. preDrawListener leaked on re-attach
40
- * Each call to onAttachedToWindow re-added the listener without removing the old one,
41
- * multiplying the per-frame cost every time a modal or navigator re-mounted the view.
42
- * Fixed: detachPreDrawListener() called before every re-attach.
43
- *
44
- * ─── Performance profile after fixes ─────────────────────────────────────────
45
- *
46
- * • blur cost reduced ~40× (5 rounds → 1, radius 100 → 25, gated to 1/vsync)
47
- * • zero JS thread work (Choreographer callback runs on UI thread only)
48
- * • zero GC pressure (no bitmap allocations on hot path)
49
- * • works with: Modal, ScrollView, FlatList, FlashList, ImageBackground,
50
- * Reanimated (both JS and UI thread), react-navigation transitions
28
+ * Each view clips the shared blurred bitmap to its own screen-space rect in
29
+ * onDraw(), then draws the overlay color on top.
51
30
  */
52
- class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
53
-
54
- // ── Blur state ─────────────────────────────────────────────────────────────
55
-
56
- private var pendingBlurRadius = DEFAULT_BLUR_RADIUS
57
- private var currentOverlayColor = Color.TRANSPARENT
58
- private var currentCornerRadius = 0f
59
- private var isSetupDone = false
31
+ class BlurVibeView(context: Context) : ReactViewGroup(context) {
60
32
 
61
- // ── Choreographer frame gate ───────────────────────────────────────────────
62
- //
63
- // OnPreDrawListener sets pendingFrame = true and returns immediately (never
64
- // blocks). Choreographer fires frameCallback at the next vsync boundary,
65
- // which calls invalidate() → QmBlurView captures + blurs + draws exactly once.
66
- // pendingFrame prevents multiple queued callbacks stacking up.
33
+ // ── State ──────────────────────────────────────────────────────────────────
67
34
 
68
- private var pendingFrame = false
35
+ private var blurRadius = DEFAULT_BLUR_RADIUS
36
+ private var overlayColor = Color.TRANSPARENT
37
+ private var cornerRadiusPx = 0f
69
38
 
70
- private val frameCallback = Choreographer.FrameCallback {
71
- pendingFrame = false
72
- if (isAttachedToWindow) triggerBlurUpdate()
73
- }
39
+ // ── Coordinator ───────────────────────────────────────────────────────────
74
40
 
75
- // ── PreDraw listener sets dirty flag only, does zero work ───────────────
41
+ private var coordinator: BlurCaptureCoordinator? = null
76
42
 
77
- private var attachedRoot: View? = null
43
+ // ── Draw state (main thread only) ─────────────────────────────────────────
78
44
 
79
- private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
80
- if (!pendingFrame) {
81
- pendingFrame = true
82
- Choreographer.getInstance().postFrameCallback(frameCallback)
83
- }
84
- true // MUST return true — false would block the entire frame draw pass
85
- }
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()
86
50
 
87
- // ── Init ──────────────────────────────────────────────────────────────────
51
+ // ── Init ───────────────────────────────────────────────────────────────────
88
52
 
89
53
  init {
54
+ setWillNotDraw(false)
90
55
  super.setBackgroundColor(Color.TRANSPARENT)
91
- clipChildren = true
92
56
  clipToOutline = true
93
-
94
- // THE critical fix #1: 1 round instead of 5.
95
- // A single Gaussian pass on a downsampled bitmap is perceptually identical
96
- // to 5 passes and costs exactly 1/5 as much GPU/CPU time.
97
- blurRounds = 1
98
-
99
- // Aggressive downsample: capture at 1/8 resolution before blurring.
100
- // The blur kernel smooths away all pixel-level detail so 1/8 is sufficient.
101
- // This reduces the bitmap size 64× and the blur kernel work proportionally.
102
- super.setDownsampleFactor(8f)
103
57
  }
104
58
 
105
- // ── Lifecycle ─────────────────────────────────────────────────────────────
59
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
106
60
 
107
61
  override fun onAttachedToWindow() {
108
62
  super.onAttachedToWindow()
109
- attachPreDrawListenerToOptimalRoot()
110
- if (!isSetupDone) applyPendingBlurConfig()
63
+ attachToCoordinator()
111
64
  }
112
65
 
113
66
  override fun onDetachedFromWindow() {
114
- detachPreDrawListener()
115
- Choreographer.getInstance().removeFrameCallback(frameCallback)
116
- pendingFrame = false
117
- isSetupDone = false
67
+ coordinator?.unregister(this)
68
+ coordinator = null
118
69
  super.onDetachedFromWindow()
119
70
  }
120
71
 
121
- // ── Root attachment ───────────────────────────────────────────────────────
72
+ // ── Coordinator attachment ─────────────────────────────────────────────────
122
73
 
123
- private fun attachPreDrawListenerToOptimalRoot() {
124
- detachPreDrawListener() // always detach first to prevent listener leaks
125
-
126
- val root: ViewGroup = findNearestScreenAncestor()
127
- ?: findNearestReactRootView()
128
- ?: (rootView as? ViewGroup)
129
- ?: return
130
-
131
- attachedRoot = root
132
- root.viewTreeObserver.addOnPreDrawListener(preDrawListener)
133
- redirectQmBlurCaptureRoot(root)
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)
134
82
  }
135
83
 
136
- private fun detachPreDrawListener() {
137
- attachedRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
138
- attachedRoot = null
84
+ /** Called by coordinator on main thread when a new blurred bitmap is ready. */
85
+ fun onBlurReady(bitmap: Bitmap) {
86
+ latestBitmap = bitmap
87
+ invalidate()
139
88
  }
140
89
 
141
- /**
142
- * Redirects QmBlurView's internal bitmap-capture root (mDecorView) to [newRoot]
143
- * via reflection. This scopes QmBlurView's capture to the chosen subtree instead
144
- * of the full activity decor view — smaller captures = faster blur.
145
- *
146
- * We do NOT mirror QmBlurView's internal preDrawListener. We own the invalidation
147
- * cycle via our own Choreographer-gated listener above.
148
- */
149
- private fun redirectQmBlurCaptureRoot(newRoot: ViewGroup) {
150
- try {
151
- val baseField = BlurViewGroup::class.java.getDeclaredField("mBaseBlurViewGroup")
152
- baseField.isAccessible = true
153
- val base = baseField.get(this) ?: return
154
-
155
- val baseClass = BaseBlurViewGroup::class.java
90
+ // ── Drawing ────────────────────────────────────────────────────────────────
156
91
 
157
- val decorField = baseClass.getDeclaredField("mDecorView")
158
- decorField.isAccessible = true
159
- decorField.set(base, newRoot)
92
+ override fun onDraw(canvas: Canvas) {
93
+ val bitmap = latestBitmap?.takeIf { !it.isRecycled } ?: return
94
+ val root = findBlurRoot() ?: return
160
95
 
161
- val diffRootField = baseClass.getDeclaredField("mDifferentRoot")
162
- diffRootField.isAccessible = true
163
- diffRootField.setBoolean(base, newRoot.rootView != this.rootView)
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)
164
99
 
165
- val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
166
- forceRedrawField.isAccessible = true
167
- forceRedrawField.setBoolean(base, true)
100
+ val l = myLoc[0] - rootLoc[0]
101
+ val t = myLoc[1] - rootLoc[1]
168
102
 
169
- } catch (_: Exception) {
170
- // Reflection failed (library updated internals).
171
- // Fall back gracefully — blur still works via the decor view.
172
- }
173
- }
174
-
175
- // ── Blur update (fires via Choreographer, once per vsync at most) ─────────
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())
176
112
 
177
- private fun triggerBlurUpdate() {
178
- try {
179
- if (!isSetupDone) applyPendingBlurConfig() else invalidate()
180
- } catch (_: Exception) {}
181
- }
113
+ if (!srcRect.isEmpty) canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint)
182
114
 
183
- private fun applyPendingBlurConfig() {
184
- try {
185
- super.setBlurRadius(pendingBlurRadius)
186
- super.setOverlayColor(currentOverlayColor)
187
- updateCornerRadiusInternal()
188
- isSetupDone = true
189
- } catch (_: Exception) {
190
- // Not fully attached yet — next Choreographer tick will retry
115
+ if (Color.alpha(overlayColor) > 0) {
116
+ overlayPaint.color = overlayColor
117
+ canvas.drawRect(dstRect, overlayPaint)
191
118
  }
192
119
  }
193
120
 
194
121
  // ── Public setters (ViewManager → UI thread) ──────────────────────────────
195
122
 
196
- /**
197
- * blurAmount: JS-facing 0–100.
198
- * Mapped to 0–25 internally (QmBlurView Gaussian kernel's designed range).
199
- * Values above 25 produce no visible increase in blur but cost more.
200
- */
201
123
  fun setBlurAmount(amount: Float) {
202
- pendingBlurRadius = mapBlurAmount(amount)
203
- if (isSetupDone) {
204
- try { super.setBlurRadius(pendingBlurRadius) } catch (_: Exception) {}
205
- scheduleBlurFrame()
206
- }
124
+ blurRadius = (amount.coerceIn(0f, 100f) / 100f) * 25f
125
+ coordinator?.blurRadius = blurRadius
207
126
  }
208
127
 
209
- fun setOverlayColor(colorString: String?) {
210
- currentOverlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
211
- if (isSetupDone) {
212
- try {
213
- super.setBackgroundColor(Color.TRANSPARENT)
214
- super.setOverlayColor(currentOverlayColor)
215
- } catch (_: Exception) {}
216
- scheduleBlurFrame()
217
- }
128
+ fun applyOverlayColor(colorString: String?) {
129
+ overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
130
+ invalidate()
218
131
  }
219
132
 
220
- /** downsample factor override (1–8). Higher = faster + softer. */
221
- fun setBlurRadius(factor: Int) {
222
- try { super.setDownsampleFactor(factor.coerceIn(1, 8).toFloat()) } catch (_: Exception) {}
223
- scheduleBlurFrame()
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()
224
141
  }
225
142
 
226
143
  fun applyBorderRadius(radiusDp: Float) {
227
- currentCornerRadius = radiusDp
228
- updateCornerRadiusInternal()
144
+ cornerRadiusPx = TypedValue.applyDimension(
145
+ TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
146
+ )
147
+ updateOutline()
229
148
  }
230
149
 
231
- fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") colorString: String?) {
232
- // ReservedQmBlurView handles its own reduced-transparency fallback
150
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {
151
+ // iOS-only concept no-op on Android
233
152
  }
234
153
 
235
- // ── Corner radius ─────────────────────────────────────────────────────────
154
+ // ── Corner radius / outline ────────────────────────────────────────────────
236
155
 
237
- private fun updateCornerRadiusInternal() {
238
- val px = TypedValue.applyDimension(
239
- TypedValue.COMPLEX_UNIT_DIP, currentCornerRadius, context.resources.displayMetrics
240
- )
241
- outlineProvider = object : ViewOutlineProvider() {
242
- override fun getOutline(view: View, outline: Outline) {
243
- outline.setRoundRect(0, 0, view.width, view.height, px)
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
+ }
244
162
  }
163
+ clipToOutline = true
164
+ } else {
165
+ outlineProvider = ViewOutlineProvider.BACKGROUND
166
+ clipToOutline = false
245
167
  }
246
- clipToOutline = currentCornerRadius > 0f
247
- try { super.setCornerRadius(px) } catch (_: Exception) {}
168
+ invalidate()
248
169
  }
249
170
 
250
- // ── Helpers ───────────────────────────────────────────────────────────────
171
+ // ── React Native layout passthrough ───────────────────────────────────────
251
172
 
252
- private fun scheduleBlurFrame() {
253
- if (!pendingFrame) {
254
- pendingFrame = true
255
- Choreographer.getInstance().postFrameCallback(frameCallback)
256
- }
173
+ 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).
257
176
  }
258
177
 
259
- private fun mapBlurAmount(amount: Float): Float =
260
- (amount.coerceIn(0f, 100f) / 100f) * 25f
261
-
262
- // ── Ancestor finders ──────────────────────────────────────────────────────
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.
263
182
 
264
- private fun findNearestScreenAncestor(): ViewGroup? {
183
+ private fun findBlurRoot(): ViewGroup? {
265
184
  var p = parent
266
185
  while (p != null) {
267
- if (p.javaClass.name == "com.swmansion.rnscreens.Screen") return p as? ViewGroup
186
+ if ((p as? View)?.javaClass?.name == "com.swmansion.rnscreens.Screen")
187
+ return p as? ViewGroup
268
188
  p = (p as? View)?.parent
269
189
  }
270
- return null
271
- }
272
-
273
- private fun findNearestReactRootView(): ViewGroup? {
274
- var p = parent
190
+ p = parent
275
191
  while (p != null) {
276
- if (p.javaClass.name == "com.facebook.react.ReactRootView") return p as? ViewGroup
192
+ if ((p as? View)?.javaClass?.name == "com.facebook.react.ReactRootView")
193
+ return p as? ViewGroup
277
194
  p = (p as? View)?.parent
278
195
  }
279
- return null
280
- }
281
-
282
- // ── React Native layout passthrough ───────────────────────────────────────
283
-
284
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
285
- // Yoga handles all child layout. Calling super here would cause QmBlurView's
286
- // FrameLayout logic to fight RN's layout system.
196
+ return rootView as? ViewGroup
287
197
  }
288
198
 
289
199
  // ── Color parser ──────────────────────────────────────────────────────────
290
- // Supports: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA", named colors
200
+ // Handles: "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA", named colors
291
201
 
292
202
  private fun parseHexColor(s: String): Int? {
293
203
  val t = s.trim()
@@ -296,28 +206,25 @@ class BlurVibeView(context: Context) : BlurViewGroup(context, null) {
296
206
  val hex = t.removePrefix("#")
297
207
  return try {
298
208
  when (hex.length) {
299
- 3 -> Color.argb(255,
300
- hex[0].toString().repeat(2).toInt(16),
301
- hex[1].toString().repeat(2).toInt(16),
302
- hex[2].toString().repeat(2).toInt(16))
303
- 6 -> Color.argb(255,
304
- hex.substring(0, 2).toInt(16),
305
- hex.substring(2, 4).toInt(16),
306
- hex.substring(4, 6).toInt(16))
307
- 8 -> Color.argb(
308
- hex.substring(6, 8).toInt(16), // alpha is LAST byte in #RRGGBBAA
309
- hex.substring(0, 2).toInt(16),
310
- hex.substring(2, 4).toInt(16),
311
- hex.substring(4, 6).toInt(16))
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))
312
222
  else -> null
313
223
  }
314
224
  } catch (_: NumberFormatException) { null }
315
225
  }
316
226
 
317
- // ── Constants ─────────────────────────────────────────────────────────────
318
-
319
227
  companion object {
320
- // blurAmount=10 → radius 2.5 — a gentle, performant default
321
- private const val DEFAULT_BLUR_RADIUS = 2.5f
228
+ private const val DEFAULT_BLUR_RADIUS = 2.5f // blurAmount=10 → 2.5
322
229
  }
323
230
  }
@@ -7,8 +7,15 @@ import com.facebook.react.uimanager.annotations.ReactProp
7
7
  /**
8
8
  * BlurVibeViewManager
9
9
  *
10
- * ViewGroupManager because BlurVibeView (→ BlurViewGroup FrameLayout) hosts
11
- * React children. needsCustomLayoutForChildren() = false lets Yoga own layout.
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,30 +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) = view.setBlurAmount(amount)
27
+ fun setBlurAmount(view: BlurVibeView, amount: Float) =
28
+ view.setBlurAmount(amount)
21
29
 
22
30
  @ReactProp(name = "blurType")
23
- fun setBlurType(view: BlurVibeView, type: String?) {
24
- // No-op on Android — blurType is an iOS UIBlurEffectStyle concept only
31
+ fun setBlurTypeProp(view: BlurVibeView, @Suppress("UNUSED_PARAMETER") type: String?) {
32
+ // iOS UIBlurEffectStyle — no-op on Android
25
33
  }
26
34
 
27
35
  @ReactProp(name = "overlayColor")
28
- fun setOverlayColor(view: BlurVibeView, color: String?) = view.setOverlayColor(color)
36
+ fun setOverlayColorProp(view: BlurVibeView, color: String?) =
37
+ view.applyOverlayColor(color)
29
38
 
30
39
  @ReactProp(name = "reducedTransparencyFallbackColor")
31
40
  fun setReducedTransparencyFallbackColor(view: BlurVibeView, color: String?) =
32
41
  view.setReducedTransparencyFallbackColor(color)
33
42
 
34
43
  @ReactProp(name = "blurRadius", defaultInt = 4)
35
- fun setBlurRadius(view: BlurVibeView, radius: Int) = view.setBlurRadius(radius)
44
+ fun setBlurRadiusProp(view: BlurVibeView, radius: Int) =
45
+ view.applyBlurRadius(radius)
36
46
 
37
47
  @ReactProp(name = "borderRadius", defaultFloat = 0f)
38
- fun setBlurBorderRadius(view: BlurVibeView, radius: Float) = view.applyBorderRadius(radius)
39
-
40
- override fun onDropViewInstance(view: BlurVibeView) {
41
- super.onDropViewInstance(view)
42
- }
48
+ fun setBlurBorderRadius(view: BlurVibeView, radius: Float) =
49
+ view.applyBorderRadius(radius)
43
50
 
44
- // Yoga drives all child layout — return false
45
51
  override fun needsCustomLayoutForChildren(): Boolean = false
46
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.4",
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",