react-native-blur-vibe 0.1.9 → 0.1.11

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.
@@ -12,31 +12,28 @@ import androidx.core.graphics.toColorInt
12
12
  import com.facebook.react.views.view.ReactViewGroup
13
13
 
14
14
  /**
15
- * BlurVibeView — Android API 21–30 backdrop blur (zero external dependencies)
15
+ * BlurVibeView — Android API 21–30 backdrop blur.
16
16
  *
17
- * Replaces QmBlurView with LegacyBlurController a direct RenderScript
18
- * implementation using only the Android SDK. No third-party library needed.
17
+ * Delegates all blur work to LegacyBlurController.
18
+ * Extends ReactViewGroup handles all RN style props (borderRadius,
19
+ * opacity, transforms etc) natively via ReactViewGroup's own draw pipeline.
19
20
  *
20
- * Extends ReactViewGroup so it hosts RN children correctly (Yoga layout,
21
- * touch events, z-ordering all work natively).
22
- *
23
- * For API 31+, BlurVibeViewApi31 is used instead (RenderEffect GPU path).
21
+ * THE STATIC BLUR FIX:
22
+ * draw() is overridden to be a no-op when LegacyBlurController.isCapturing
23
+ * is true. This prevents root.draw() from painting our stale blur output
24
+ * into the capture bitmap. Without this, each frame captures the previous
25
+ * frame's blur output and the blur appears frozen/static.
24
26
  */
25
27
  class BlurVibeView(context: Context) : ReactViewGroup(context) {
26
28
 
27
- // ── State ──────────────────────────────────────────────────────────────────
28
-
29
29
  private var blurController: LegacyBlurController? = null
30
- private var pendingBlurAmount = 10f
31
- private var pendingOverlay = Color.TRANSPARENT
32
- private var cornerRadiusPx = 0f
33
-
34
- // ── Init ───────────────────────────────────────────────────────────────────
30
+ private var pendingBlurAmount = 10f
31
+ private var pendingOverlay = Color.TRANSPARENT
32
+ private var cornerRadiusPx = 0f
35
33
 
36
34
  init {
37
35
  setWillNotDraw(false)
38
36
  super.setBackgroundColor(Color.TRANSPARENT)
39
- clipToOutline = true
40
37
  }
41
38
 
42
39
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -61,10 +58,30 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
61
58
  blurController?.onSizeChanged()
62
59
  }
63
60
 
64
- // ── Draw ───────────────────────────────────────────────────────────────────
61
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
62
+ super.onWindowFocusChanged(hasWindowFocus)
63
+ if (hasWindowFocus) blurController?.reAttach()
64
+ }
65
+
66
+ // ── draw() — suppress self during root capture ────────────────────────────
67
+ //
68
+ // When LegacyBlurController is actively capturing (root.draw() in progress),
69
+ // skip drawing ourselves. This makes us invisible to the capture canvas so
70
+ // the capture bitmap contains ONLY the content behind us, not our own stale
71
+ // blur output. Without this, the blur appears static/frozen.
72
+
73
+ override fun draw(canvas: Canvas) {
74
+ if (blurController?.isCapturing == true) return
75
+ super.draw(canvas)
76
+ }
77
+
78
+ // ── onDraw ────────────────────────────────────────────────────────────────
65
79
 
66
80
  override fun onDraw(canvas: Canvas) {
81
+ // Draw the blurred background first
67
82
  blurController?.draw(canvas, width.toFloat(), height.toFloat())
83
+ // Let ReactViewGroup draw borders/radius/background on top natively
84
+ super.onDraw(canvas)
68
85
  }
69
86
 
70
87
  // ── Public setters ─────────────────────────────────────────────────────────
@@ -81,24 +98,28 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
81
98
  }
82
99
 
83
100
  fun setBlurRadius(factor: Int) {
84
- // No-op for now LegacyBlurController uses fixed DOWNSAMPLE_FACTOR = 4
85
- // Could expose downsampleFactor setter on controller if needed
101
+ // Exposed as a downsample override for power users not used internally
86
102
  }
87
103
 
88
104
  fun applyBorderRadius(radiusDp: Float) {
89
105
  cornerRadiusPx = TypedValue.applyDimension(
90
106
  TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
91
107
  )
92
- outlineProvider = object : ViewOutlineProvider() {
93
- override fun getOutline(view: View, outline: Outline) {
94
- outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
108
+ if (cornerRadiusPx > 0f) {
109
+ outlineProvider = object : ViewOutlineProvider() {
110
+ override fun getOutline(view: View, outline: Outline) {
111
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
112
+ }
95
113
  }
114
+ clipToOutline = true
115
+ } else {
116
+ outlineProvider = ViewOutlineProvider.BACKGROUND
117
+ clipToOutline = false
96
118
  }
97
- clipToOutline = cornerRadiusPx > 0f
98
119
  invalidate()
99
120
  }
100
121
 
101
- fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) { }
122
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) {}
102
123
 
103
124
  fun applyBlurEnabled(enabled: Boolean) {
104
125
  blurController?.enabled = enabled
@@ -111,15 +132,13 @@ class BlurVibeView(context: Context) : ReactViewGroup(context) {
111
132
 
112
133
  // ── Layout passthrough ─────────────────────────────────────────────────────
113
134
 
114
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
115
- // Yoga handles all layout
116
- }
135
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {}
117
136
 
118
137
  // ── Helpers ────────────────────────────────────────────────────────────────
119
138
 
120
139
  private fun mapBlurAmount(amount: Float): Float {
121
140
  val t = amount.coerceIn(0f, 100f) / 100f
122
- return t * t * 25f // quadratic curve, max 25 (RenderScript kernel limit)
141
+ return t * t * 25f
123
142
  }
124
143
 
125
144
  private fun findBlurRoot(): ViewGroup? {
@@ -3,6 +3,8 @@ package com.blurvibe
3
3
  import android.content.Context
4
4
  import android.graphics.Bitmap
5
5
  import android.graphics.BitmapShader
6
+ import android.graphics.BlendMode
7
+ import android.graphics.BlendModeColorFilter
6
8
  import android.graphics.Canvas
7
9
  import android.graphics.Color
8
10
  import android.graphics.LinearGradient
@@ -33,73 +35,78 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
33
35
 
34
36
  // ── Blur params ────────────────────────────────────────────────────────────
35
37
 
36
- private var blurRadiusX = DEFAULT_BLUR_RADIUS
37
- private var blurRadiusY = DEFAULT_BLUR_RADIUS
38
+ private var blurAmount = 10f
38
39
  private var overlayColor = Color.TRANSPARENT
39
40
  private var cornerRadiusPx = 0f
40
41
 
41
- // ── Progressive blur params ────────────────────────────────────────────────
42
+ // ── Progressive blur ──────────────────────────────────────────────────────
42
43
 
43
44
  private var progressiveDirection = PROGRESSIVE_NONE
44
45
  private var progressiveStartIntensity = 1f
45
46
  private var progressiveEndIntensity = 0f
46
47
 
47
- // ── Noise params ──────────────────────────────────────────────────────────
48
+ // ── Noise ─────────────────────────────────────────────────────────────────
48
49
 
49
50
  private var noiseFactor = 0.08f
50
51
  private var noiseBitmap: Bitmap? = null
51
52
  private val noisePaint = Paint()
52
53
 
53
- // ── RenderNodes ───────────────────────────────────────────────────────────
54
+ // ── Bitmap + RenderNode ────────────────────────────────────────────────────
55
+
56
+ private var internalBitmap: Bitmap? = null
57
+ private val renderNode = RenderNode("BlurVibeNode")
58
+
59
+ // ── Capture exclusion flag ────────────────────────────────────────────────
54
60
  //
55
- // contentNode: records root-view draw commands ("what's behind me")
56
- // blurNode: crops + translates contentNode to this view's position,
57
- // with RenderEffect blur applied
61
+ // THE FIX FOR STATIC BLUR:
58
62
  //
59
- // IMPORTANT NO setUseCompositingLayer(true) on either node.
60
- // Compositing layer on a re-recorded RenderNode causes GPU memory
61
- // thrashing and SIGSEGV on some API 31 drivers.
63
+ // root.draw(canvas) walks the entire view tree including THIS BlurView.
64
+ // When it reaches us during capture, our onDraw draws the PREVIOUS frame's
65
+ // blurred bitmap so the capture contains our own stale output, not just
66
+ // the content behind us. This makes the blur appear static because each
67
+ // frame captures the previous frame's blur output, not the live content.
62
68
  //
63
- // IMPORTANT NO LAYER_TYPE_HARDWARE on the view itself.
64
- // canvas.drawRenderNode() is only valid on a hardware-accelerated canvas
65
- // that is NOT itself a hardware layer mixing them causes SIGSEGV
66
- // in RenderThread (the exact crash we saw in logcat).
67
-
68
- private val contentNode = RenderNode("BlurVibeContent")
69
- private val blurNode = RenderNode("BlurVibeBlur")
70
-
71
- // ── Recording guard — prevents double-beginRecording crashes ─────────────
69
+ // Fix: set isCapturing = true before root.draw(), override draw() to be
70
+ // a no-op when isCapturing = true. root.draw() then skips us completely,
71
+ // capturing ONLY the content behind us. This is exactly how Dimezis
72
+ // BlurView solves the same problem.
72
73
  //
73
- // If captureRootIntoNode fires twice in the same frame (e.g. during
74
- // layout + invalidate), a second beginRecording() on an active recording
75
- // crashes the RenderThread. This flag gates it.
74
+ // This does NOT cause a flash because we are not changing visibility —
75
+ // we are only suppressing our own draw() during the off-screen capture.
76
+ // The view remains visible on screen; we just skip drawing into the
77
+ // off-screen capture canvas.
76
78
 
77
79
  private var isCapturing = false
78
80
 
79
- // ── Paint objects ──────────────────────────────────────────────────────────
81
+ // ── Draw paints ───────────────────────────────────────────────────────────
80
82
 
81
- private val overlayPaint = Paint(Paint.ANTI_ALIAS_FLAG)
82
- private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
83
+ private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
83
84
  xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
84
85
  }
86
+ private val noisePaintFinal = Paint()
85
87
 
86
88
  // ── Root view ─────────────────────────────────────────────────────────────
87
89
 
88
90
  private var blurRoot: ViewGroup? = null
91
+ private val myLocation = IntArray(2)
92
+ private val rootLocation = IntArray(2)
89
93
 
90
- // ── Choreographer gate ────────────────────────────────────────────────────
94
+ // ── State ─────────────────────────────────────────────────────────────────
91
95
 
96
+ private var blurEnabled = true
97
+ private var autoUpdate = true
92
98
  private var frameScheduled = false
99
+ private var initialized = false
100
+
101
+ // ── Choreographer gate ────────────────────────────────────────────────────
102
+
93
103
  private val frameCallback = Choreographer.FrameCallback {
94
104
  frameScheduled = false
95
- if (isAttachedToWindow) {
96
- captureRootIntoNode()
97
- invalidate()
98
- }
105
+ if (isAttachedToWindow && blurEnabled) updateBlur()
99
106
  }
100
107
 
101
108
  private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
102
- if (!frameScheduled) {
109
+ if (!frameScheduled && blurEnabled && autoUpdate) {
103
110
  frameScheduled = true
104
111
  Choreographer.getInstance().postFrameCallback(frameCallback)
105
112
  }
@@ -111,11 +118,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
111
118
  init {
112
119
  setWillNotDraw(false)
113
120
  super.setBackgroundColor(Color.TRANSPARENT)
114
- clipToOutline = true
115
- // DO NOT call setLayerType(LAYER_TYPE_HARDWARE) here —
116
- // it conflicts with canvas.drawRenderNode() and causes SIGSEGV in RenderThread.
117
- // The view uses the default layer type (LAYER_TYPE_NONE) so its canvas is
118
- // the hardware-accelerated display list canvas — which supports drawRenderNode.
119
121
  }
120
122
 
121
123
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -123,107 +125,146 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
123
125
  override fun onAttachedToWindow() {
124
126
  super.onAttachedToWindow()
125
127
  blurRoot = findBlurRoot()
126
- blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
128
+ safeAddPreDrawListener()
127
129
  generateNoiseBitmap()
130
+ if (measuredWidth > 0 && measuredHeight > 0) initBlur()
128
131
  }
129
132
 
130
133
  override fun onDetachedFromWindow() {
131
134
  blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
132
135
  Choreographer.getInstance().removeFrameCallback(frameCallback)
133
136
  frameScheduled = false
137
+ initialized = false
134
138
  isCapturing = false
135
139
  blurRoot = null
136
- noiseBitmap?.recycle()
137
- noiseBitmap = null
138
- // Discard RenderNode display lists to free GPU memory
139
- contentNode.discardDisplayList()
140
- blurNode.discardDisplayList()
140
+ noiseBitmap?.recycle(); noiseBitmap = null
141
+ internalBitmap?.recycle(); internalBitmap = null
142
+ renderNode.discardDisplayList()
141
143
  super.onDetachedFromWindow()
142
144
  }
143
145
 
144
146
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
145
147
  super.onSizeChanged(w, h, oldw, oldh)
146
- // Update blurNode bounds — contentNode bounds are set in captureRootIntoNode
147
148
  if (w > 0 && h > 0) {
148
- blurNode.setPosition(0, 0, w, h)
149
- applyBlurRenderEffect()
149
+ internalBitmap?.recycle(); internalBitmap = null
150
+ renderNode.discardDisplayList()
151
+ initialized = false
152
+ initBlur()
150
153
  }
151
154
  }
152
155
 
153
- // ── Capture pipeline ───────────────────────────────────────────────────────
156
+ // ── Multi-window safety ───────────────────────────────────────────────────
154
157
 
155
- private fun captureRootIntoNode() {
156
- if (isCapturing) return // guard against re-entrant / double recording crash
157
- val root = blurRoot ?: return
158
- val rw = root.width; if (rw <= 0) return
159
- val rh = root.height; if (rh <= 0) return
160
- val vw = width; if (vw <= 0) return
161
- val vh = height; if (vh <= 0) return
158
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
159
+ super.onWindowFocusChanged(hasWindowFocus)
160
+ if (hasWindowFocus && blurEnabled && autoUpdate) {
161
+ safeAddPreDrawListener()
162
+ scheduleFrame()
163
+ }
164
+ }
162
165
 
163
- isCapturing = true
164
- try {
165
- // Step 1: record root-view draw into contentNode
166
- contentNode.setPosition(0, 0, rw, rh)
167
- val contentCanvas = contentNode.beginRecording()
168
- try {
169
- contentCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
170
- root.draw(contentCanvas)
171
- } finally {
172
- contentNode.endRecording() // always end — even if draw() throws
173
- }
166
+ private fun safeAddPreDrawListener() {
167
+ val root = blurRoot ?: return
168
+ val vto = root.viewTreeObserver
169
+ vto.removeOnPreDrawListener(preDrawListener)
170
+ if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
171
+ }
174
172
 
175
- // Step 2: contentNode recording is FINISHED before we reference it
176
- // in blurNode. This is critical — drawing an actively-recording
177
- // RenderNode into another canvas is undefined behaviour.
178
- val myLoc = IntArray(2); getLocationInWindow(myLoc)
179
- val rootLoc = IntArray(2); root.getLocationInWindow(rootLoc)
180
- val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
181
- val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
182
-
183
- blurNode.setPosition(0, 0, vw, vh)
184
- applyBlurRenderEffect()
185
-
186
- val blurCanvas = blurNode.beginRecording()
187
- try {
188
- blurCanvas.translate(-offsetX, -offsetY)
189
- blurCanvas.drawRenderNode(contentNode) // safe: contentNode recording is done
190
- } finally {
191
- blurNode.endRecording()
192
- }
173
+ // ── Init blur ─────────────────────────────────────────────────────────────
193
174
 
194
- } finally {
195
- isCapturing = false
196
- }
175
+ private fun initBlur() {
176
+ val w = measuredWidth; if (w <= 0) return
177
+ val h = measuredHeight; if (h <= 0) return
178
+ internalBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
179
+ renderNode.setPosition(0, 0, w, h)
180
+ initialized = true
181
+ updateBlur()
197
182
  }
198
183
 
199
- private fun applyBlurRenderEffect() {
200
- if (blurRadiusX < 0.5f && blurRadiusY < 0.5f) {
201
- blurNode.setRenderEffect(null)
184
+ // ── Core: capture + blur + render ─────────────────────────────────────────
185
+
186
+ private fun updateBlur() {
187
+ if (!blurEnabled || !initialized) return
188
+ val root = blurRoot ?: return
189
+ val bitmap = internalBitmap ?: return
190
+ if (bitmap.isRecycled) return
191
+
192
+ // ① Compute this view's offset within the root (window coords — correct
193
+ // for all window modes: split-screen, freeform, PiP, DeX)
194
+ root.getLocationInWindow(rootLocation)
195
+ getLocationInWindow(myLocation)
196
+ val offsetX = (myLocation[0] - rootLocation[0]).toFloat()
197
+ val offsetY = (myLocation[1] - rootLocation[1]).toFloat()
198
+
199
+ // ② Capture root content EXCLUDING this view.
200
+ // isCapturing = true causes our draw() to be a no-op, so root.draw()
201
+ // skips us and captures only the content behind us.
202
+ isCapturing = true
203
+ val captureCanvas = Canvas(bitmap)
204
+ captureCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
205
+ captureCanvas.translate(-offsetX, -offsetY)
206
+ try {
207
+ root.draw(captureCanvas)
208
+ } catch (_: Exception) {
209
+ isCapturing = false
202
210
  return
203
211
  }
204
- blurNode.setRenderEffect(
205
- RenderEffect.createBlurEffect(blurRadiusX, blurRadiusY, Shader.TileMode.CLAMP)
212
+ isCapturing = false
213
+
214
+ // ③ Record bitmap into RenderNode.
215
+ // Drawing a BITMAP into RenderNode is stable on all OEM drivers.
216
+ // Drawing a RenderNode into another RenderNode's recording is NOT.
217
+ renderNode.setPosition(0, 0, bitmap.width, bitmap.height)
218
+ val nodeCanvas = renderNode.beginRecording()
219
+ nodeCanvas.drawBitmap(bitmap, 0f, 0f, null)
220
+ renderNode.endRecording()
221
+
222
+ // ④ Apply GPU blur + tint as a chained RenderEffect (single GPU pass)
223
+ val radius = blurRadiusFromAmount(blurAmount)
224
+ val blurEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.MIRROR)
225
+ renderNode.setRenderEffect(
226
+ if (Color.alpha(overlayColor) > 0) {
227
+ RenderEffect.createChainEffect(
228
+ RenderEffect.createColorFilterEffect(
229
+ BlendModeColorFilter(overlayColor, BlendMode.SRC_ATOP)
230
+ ),
231
+ blurEffect
232
+ )
233
+ } else blurEffect
206
234
  )
235
+
236
+ invalidate()
207
237
  }
208
238
 
209
- // ── Draw ───────────────────────────────────────────────────────────────────
239
+ // ── draw() override — no-op during capture ────────────────────────────────
240
+ //
241
+ // When isCapturing = true (root.draw() is in progress capturing background),
242
+ // suppress our own draw so we don't paint stale blur into the capture bitmap.
243
+ // This makes us invisible to root.draw() during capture only —
244
+ // NOT to the actual screen renderer.
245
+
246
+ override fun draw(canvas: Canvas) {
247
+ if (isCapturing) return // skip self during root capture
248
+ super.draw(canvas)
249
+ }
250
+
251
+ // ── onDraw ────────────────────────────────────────────────────────────────
210
252
 
211
253
  override fun onDraw(canvas: Canvas) {
254
+ if (!blurEnabled || !initialized) return
212
255
  val w = width.toFloat(); if (w <= 0f) return
213
256
  val h = height.toFloat(); if (h <= 0f) return
257
+ if (!renderNode.hasDisplayList()) return
214
258
 
215
- // Guard: only draw if blurNode has a valid recorded display list
216
- if (!blurNode.hasDisplayList()) return
217
-
218
- // Step 1: save layer for progressive mask compositing
259
+ // Progressive mask requires a saved layer so DST_IN mask composites correctly
219
260
  val saveCount = if (progressiveDirection != PROGRESSIVE_NONE) {
220
261
  canvas.saveLayer(0f, 0f, w, h, null)
221
262
  } else -1
222
263
 
223
- // Step 2: draw the blurred backdrop
224
- canvas.drawRenderNode(blurNode)
264
+ // Draw GPU-blurred + tinted result from RenderNode
265
+ canvas.drawRenderNode(renderNode)
225
266
 
226
- // Step 3: progressive alpha mask
267
+ // Progressive alpha mask fades blur across the view
227
268
  if (progressiveDirection != PROGRESSIVE_NONE && saveCount >= 0) {
228
269
  buildProgressiveShader(w, h)?.let { shader ->
229
270
  maskPaint.shader = shader
@@ -232,53 +273,43 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
232
273
  canvas.restoreToCount(saveCount)
233
274
  }
234
275
 
235
- // Step 4: overlay tint
236
- if (Color.alpha(overlayColor) > 0) {
237
- overlayPaint.color = overlayColor
238
- if (cornerRadiusPx > 0f) {
239
- canvas.drawRoundRect(RectF(0f, 0f, w, h), cornerRadiusPx, cornerRadiusPx, overlayPaint)
240
- } else {
241
- canvas.drawRect(0f, 0f, w, h, overlayPaint)
242
- }
276
+ // Noise grain overlay
277
+ noiseBitmap?.takeIf { !it.isRecycled && noiseFactor > 0f }?.let { noise ->
278
+ noisePaintFinal.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
279
+ noisePaintFinal.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
280
+ canvas.drawRect(0f, 0f, w, h, noisePaintFinal)
243
281
  }
244
282
 
245
- // Step 5: noise grain
246
- noiseBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
247
- if (noiseFactor > 0f) {
248
- noisePaint.alpha = (noiseFactor * 255f).toInt().coerceIn(0, 255)
249
- noisePaint.shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
250
- canvas.drawRect(0f, 0f, w, h, noisePaint)
251
- }
252
- }
283
+ // Let ReactViewGroup draw borders/background on top (handles borderRadius
284
+ // and all other RN style props natively — no conflict with our blur)
285
+ super.onDraw(canvas)
253
286
  }
254
287
 
255
288
  // ── Progressive shader ────────────────────────────────────────────────────
256
289
 
257
290
  private fun buildProgressiveShader(w: Float, h: Float): Shader? {
258
- val startColor = Color.argb((progressiveStartIntensity.coerceIn(0f,1f) * 255).toInt(), 0,0,0)
259
- val endColor = Color.argb((progressiveEndIntensity.coerceIn(0f,1f) * 255).toInt(), 0,0,0)
291
+ val sc = Color.argb((progressiveStartIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
292
+ val ec = Color.argb((progressiveEndIntensity.coerceIn(0f,1f)*255).toInt(),0,0,0)
260
293
  return when (progressiveDirection) {
261
- PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h, startColor,endColor, Shader.TileMode.CLAMP)
262
- PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f, startColor,endColor, Shader.TileMode.CLAMP)
263
- PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f, startColor,endColor, Shader.TileMode.CLAMP)
264
- PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f, startColor,endColor, Shader.TileMode.CLAMP)
265
- PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f, min(w,h)/2f, startColor,endColor, Shader.TileMode.CLAMP)
266
- else -> null
294
+ PROGRESSIVE_TOP_TO_BOTTOM -> LinearGradient(0f,0f,0f,h,sc,ec,Shader.TileMode.CLAMP)
295
+ PROGRESSIVE_BOTTOM_TO_TOP -> LinearGradient(0f,h,0f,0f,sc,ec,Shader.TileMode.CLAMP)
296
+ PROGRESSIVE_LEFT_TO_RIGHT -> LinearGradient(0f,0f,w,0f,sc,ec,Shader.TileMode.CLAMP)
297
+ PROGRESSIVE_RIGHT_TO_LEFT -> LinearGradient(w,0f,0f,0f,sc,ec,Shader.TileMode.CLAMP)
298
+ PROGRESSIVE_RADIAL -> RadialGradient(w/2f,h/2f,min(w,h)/2f,sc,ec,Shader.TileMode.CLAMP)
299
+ else -> null
267
300
  }
268
301
  }
269
302
 
270
- // ── Noise bitmap ──────────────────────────────────────────────────────────
303
+ // ── Noise ─────────────────────────────────────────────────────────────────
271
304
 
272
305
  private fun generateNoiseBitmap() {
273
306
  if (noiseBitmap?.isRecycled == false) return
274
307
  val size = 64
275
308
  val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
276
309
  val rng = Random(42)
277
- for (x in 0 until size) {
278
- for (y in 0 until size) {
279
- val v = rng.nextInt(256)
280
- bmp.setPixel(x, y, Color.argb(255, v, v, v))
281
- }
310
+ for (x in 0 until size) for (y in 0 until size) {
311
+ val v = rng.nextInt(256)
312
+ bmp.setPixel(x, y, Color.argb(255, v, v, v))
282
313
  }
283
314
  noiseBitmap = bmp
284
315
  }
@@ -286,75 +317,68 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
286
317
  // ── Public setters ─────────────────────────────────────────────────────────
287
318
 
288
319
  fun setBlurAmount(amount: Float) {
289
- val t = amount.coerceIn(0f, 100f) / 100f
290
- blurRadiusX = t * t * MAX_BLUR_RADIUS
291
- blurRadiusY = blurRadiusX
292
- applyBlurRenderEffect()
293
- scheduleFrame()
320
+ blurAmount = amount.coerceIn(0f, 100f); scheduleFrame()
294
321
  }
295
322
 
296
323
  fun setOverlayColor(colorString: String?) {
297
324
  overlayColor = parseHexColor(colorString ?: "transparent") ?: Color.TRANSPARENT
298
- invalidate()
325
+ scheduleFrame()
299
326
  }
300
327
 
328
+ // borderRadius from JS style prop — handled natively by ReactViewGroup.
329
+ // applyBorderRadius is called by our @ReactProp "borderRadius" binding.
330
+ // We additionally set clipToOutline so the blur content is clipped correctly.
301
331
  fun applyBorderRadius(radiusDp: Float) {
302
332
  cornerRadiusPx = TypedValue.applyDimension(
303
333
  TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
304
334
  )
305
- outlineProvider = object : ViewOutlineProvider() {
306
- override fun getOutline(view: View, outline: Outline) {
307
- outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
335
+ if (cornerRadiusPx > 0f) {
336
+ outlineProvider = object : ViewOutlineProvider() {
337
+ override fun getOutline(view: View, outline: Outline) {
338
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx)
339
+ }
308
340
  }
341
+ clipToOutline = true
342
+ } else {
343
+ outlineProvider = ViewOutlineProvider.BACKGROUND
344
+ clipToOutline = false
309
345
  }
310
- clipToOutline = cornerRadiusPx > 0f
311
346
  invalidate()
312
347
  }
313
348
 
314
- fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") color: String?) { }
349
+ fun setReducedTransparencyFallbackColor(@Suppress("UNUSED_PARAMETER") c: String?) {}
315
350
 
316
- fun setProgressiveBlurDirection(direction: String?) {
317
- progressiveDirection = when (direction) {
351
+ fun setProgressiveBlurDirection(d: String?) {
352
+ progressiveDirection = when (d) {
318
353
  "topToBottom" -> PROGRESSIVE_TOP_TO_BOTTOM
319
354
  "bottomToTop" -> PROGRESSIVE_BOTTOM_TO_TOP
320
355
  "leftToRight" -> PROGRESSIVE_LEFT_TO_RIGHT
321
356
  "rightToLeft" -> PROGRESSIVE_RIGHT_TO_LEFT
322
357
  "radial" -> PROGRESSIVE_RADIAL
323
358
  else -> PROGRESSIVE_NONE
324
- }
325
- invalidate()
359
+ }; invalidate()
326
360
  }
327
361
 
328
- fun setProgressiveStartIntensity(intensity: Float) {
329
- progressiveStartIntensity = intensity.coerceIn(0f, 1f); invalidate()
330
- }
331
-
332
- fun setProgressiveEndIntensity(intensity: Float) {
333
- progressiveEndIntensity = intensity.coerceIn(0f, 1f); invalidate()
334
- }
335
-
336
- fun setNoiseFactor(factor: Float) {
337
- noiseFactor = factor.coerceIn(0f, 1f); invalidate()
338
- }
362
+ fun setProgressiveStartIntensity(v: Float) { progressiveStartIntensity = v.coerceIn(0f,1f); invalidate() }
363
+ fun setProgressiveEndIntensity(v: Float) { progressiveEndIntensity = v.coerceIn(0f,1f); invalidate() }
364
+ fun setNoiseFactor(v: Float) { noiseFactor = v.coerceIn(0f,1f); invalidate() }
339
365
 
340
366
  fun applyBlurEnabled(enabled: Boolean) {
341
- if (!enabled) {
367
+ blurEnabled = enabled
368
+ if (enabled) { safeAddPreDrawListener(); scheduleFrame() }
369
+ else {
342
370
  blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
343
371
  Choreographer.getInstance().removeFrameCallback(frameCallback)
344
372
  frameScheduled = false
345
- blurNode.discardDisplayList()
346
- contentNode.discardDisplayList()
373
+ renderNode.discardDisplayList()
347
374
  invalidate()
348
- } else {
349
- blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
350
- scheduleFrame()
351
375
  }
352
376
  }
353
377
 
354
- fun setAutoUpdate(autoUpdate: Boolean) {
355
- if (autoUpdate) {
356
- blurRoot?.viewTreeObserver?.addOnPreDrawListener(preDrawListener)
357
- } else {
378
+ fun setAutoUpdate(update: Boolean) {
379
+ autoUpdate = update
380
+ if (update) safeAddPreDrawListener()
381
+ else {
358
382
  blurRoot?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
359
383
  Choreographer.getInstance().removeFrameCallback(frameCallback)
360
384
  frameScheduled = false
@@ -364,12 +388,15 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
364
388
  // ── Helpers ────────────────────────────────────────────────────────────────
365
389
 
366
390
  private fun scheduleFrame() {
367
- if (!frameScheduled) {
391
+ if (!frameScheduled && blurEnabled) {
368
392
  frameScheduled = true
369
393
  Choreographer.getInstance().postFrameCallback(frameCallback)
370
394
  }
371
395
  }
372
396
 
397
+ private fun blurRadiusFromAmount(amount: Float): Float =
398
+ ((amount / 100f).let { it * it } * 25f).coerceIn(1f, 25f)
399
+
373
400
  private fun findBlurRoot(): ViewGroup? {
374
401
  var p = parent
375
402
  while (p != null) {
@@ -406,16 +433,14 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
406
433
  } catch (_: NumberFormatException) { null }
407
434
  }
408
435
 
409
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { }
436
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
410
437
 
411
438
  companion object {
412
- private const val MAX_BLUR_RADIUS = 25f
413
- private const val DEFAULT_BLUR_RADIUS = 2.5f
414
- const val PROGRESSIVE_NONE = 0
415
- const val PROGRESSIVE_TOP_TO_BOTTOM = 1
416
- const val PROGRESSIVE_BOTTOM_TO_TOP = 2
417
- const val PROGRESSIVE_LEFT_TO_RIGHT = 3
418
- const val PROGRESSIVE_RIGHT_TO_LEFT = 4
419
- const val PROGRESSIVE_RADIAL = 5
439
+ const val PROGRESSIVE_NONE = 0
440
+ const val PROGRESSIVE_TOP_TO_BOTTOM = 1
441
+ const val PROGRESSIVE_BOTTOM_TO_TOP = 2
442
+ const val PROGRESSIVE_LEFT_TO_RIGHT = 3
443
+ const val PROGRESSIVE_RIGHT_TO_LEFT = 4
444
+ const val PROGRESSIVE_RADIAL = 5
420
445
  }
421
446
  }
@@ -6,7 +6,6 @@ import android.graphics.Color
6
6
  import android.graphics.Paint
7
7
  import android.graphics.PorterDuff
8
8
  import android.graphics.Rect
9
- import android.os.Build
10
9
  import android.renderscript.Allocation
11
10
  import android.renderscript.Element
12
11
  import android.renderscript.RenderScript
@@ -19,41 +18,30 @@ import android.view.ViewTreeObserver
19
18
  /**
20
19
  * LegacyBlurController — zero-dependency backdrop blur for Android API 21–30.
21
20
  *
22
- * Replaces QmBlurView with a direct RenderScript implementation.
23
- * RenderScript is part of the Android SDK — no external library needed.
21
+ * Uses Android SDK RenderScript (ScriptIntrinsicBlur) no external library.
24
22
  *
25
- * Pipeline per vsync:
26
- * preDrawListener (sets dirty flag only)
27
- * Choreographer.FrameCallback (once per vsync)
28
- * rootView.draw() into captureBitmap (main thread)
29
- * downsample into scaledBitmap
30
- * → ScriptIntrinsicBlur.forEach() (RenderScript, GPU-accelerated)
31
- * → view.invalidate()
32
- * → onDraw: drawBitmap(scaledBitmap) + overlay tint
33
- *
34
- * Key optimisations vs naive implementation:
35
- * - Choreographer gate: max 1 capture per vsync regardless of invalidation count
36
- * - Bitmap pool: captureBitmap + scaledBitmap reused each frame (zero GC)
37
- * - RenderScript Allocation pool: inputAlloc + outputAlloc reused (zero GC)
38
- * - Blur rounds = 2: two passes for smooth spread without pixelation
39
- * - Downsample factor = 4: captures at 1/16 resolution, blur hides pixel detail
23
+ * THE STATIC BLUR FIX:
24
+ * Before root.draw(), BlurVibeView.draw() is made a no-op via isCapturing flag.
25
+ * This means root.draw() skips the BlurView during capture, so we capture
26
+ * ONLY the content behind us — not our own previous blur output.
27
+ * Without this, each frame captures the previous blur blur appears static.
40
28
  */
41
- @Suppress("DEPRECATION") // RenderScript deprecated in API 31 — we only use this on API < 31
29
+ @Suppress("DEPRECATION")
42
30
  internal class LegacyBlurController(
43
- private val view: View,
31
+ private val view: BlurVibeView,
44
32
  private val rootView: ViewGroup
45
33
  ) {
46
34
 
47
35
  companion object {
48
- private const val DOWNSAMPLE_FACTOR = 4f // capture at 1/4 linear resolution (1/16 pixels)
49
- private const val BLUR_RADIUS = 8f // RenderScript Gaussian radius (1–25)
50
- private const val BLUR_ROUNDS = 2 // passes — 2 gives smooth spread
36
+ private const val DOWNSAMPLE_FACTOR = 4f
37
+ private const val BLUR_RADIUS = 8f
38
+ private const val BLUR_ROUNDS = 2
51
39
  }
52
40
 
53
41
  // ── Bitmap pool ────────────────────────────────────────────────────────────
54
42
 
55
- private var captureBitmap: Bitmap? = null // full-res root capture
56
- private var scaledBitmap: Bitmap? = null // downsampled before blur
43
+ private var captureBitmap: Bitmap? = null
44
+ private var scaledBitmap: Bitmap? = null
57
45
  private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
58
46
  private val drawPaint = Paint(Paint.FILTER_BITMAP_FLAG)
59
47
 
@@ -66,19 +54,23 @@ internal class LegacyBlurController(
66
54
 
67
55
  // ── State ──────────────────────────────────────────────────────────────────
68
56
 
69
- var overlayColor: Int = Color.TRANSPARENT
57
+ var overlayColor: Int = Color.TRANSPARENT
70
58
  var blurRadius: Float = BLUR_RADIUS
71
59
  var enabled: Boolean = true
72
60
  set(value) { field = value; if (!value) invalidatePool() }
73
61
  var autoUpdate: Boolean = true
74
62
  set(value) {
75
63
  field = value
76
- if (value) rootView.viewTreeObserver.addOnPreDrawListener(preDrawListener)
64
+ if (value) safeAddPreDrawListener()
77
65
  else rootView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
78
66
  }
79
67
 
68
+ // isCapturing: set true before root.draw() so BlurVibeView.draw() is a no-op
69
+ // preventing stale self-capture. Accessed by BlurVibeView.draw().
70
+ var isCapturing = false
71
+ private set
72
+
80
73
  private var frameScheduled = false
81
- private var isCapturing = false
82
74
 
83
75
  // ── Choreographer gate ────────────────────────────────────────────────────
84
76
 
@@ -88,7 +80,7 @@ internal class LegacyBlurController(
88
80
  }
89
81
 
90
82
  private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
91
- if (!frameScheduled && enabled) {
83
+ if (!frameScheduled && enabled && autoUpdate) {
92
84
  frameScheduled = true
93
85
  Choreographer.getInstance().postFrameCallback(frameCallback)
94
86
  }
@@ -99,7 +91,7 @@ internal class LegacyBlurController(
99
91
 
100
92
  init {
101
93
  initRenderScript()
102
- rootView.viewTreeObserver.addOnPreDrawListener(preDrawListener)
94
+ safeAddPreDrawListener()
103
95
  }
104
96
 
105
97
  private fun initRenderScript() {
@@ -112,7 +104,6 @@ internal class LegacyBlurController(
112
104
  // ── Capture + blur ─────────────────────────────────────────────────────────
113
105
 
114
106
  private fun captureAndBlur() {
115
- if (isCapturing) return
116
107
  val rw = rootView.width; if (rw <= 0) return
117
108
  val rh = rootView.height; if (rh <= 0) return
118
109
  val vw = view.width; if (vw <= 0) return
@@ -121,85 +112,88 @@ internal class LegacyBlurController(
121
112
  val sw = (vw / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
122
113
  val sh = (vh / DOWNSAMPLE_FACTOR).toInt().coerceAtLeast(1)
123
114
 
115
+ val myLoc = IntArray(2); view.getLocationInWindow(myLoc)
116
+ val rootLoc = IntArray(2); rootView.getLocationInWindow(rootLoc)
117
+ val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
118
+ val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
119
+
120
+ val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
121
+ val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
122
+
123
+ // Set isCapturing BEFORE root.draw() so BlurVibeView.draw() is skipped
124
124
  isCapturing = true
125
+ val c = Canvas(capture)
126
+ c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
127
+ c.translate(-offsetX, -offsetY)
125
128
  try {
126
- // ① Compute offset of view within root
127
- val myLoc = IntArray(2); view.getLocationInWindow(myLoc)
128
- val rootLoc = IntArray(2); rootView.getLocationInWindow(rootLoc)
129
- val offsetX = myLoc[0] - rootLoc[0]
130
- val offsetY = myLoc[1] - rootLoc[1]
131
-
132
- // ② Allocate bitmaps (reuse if size matches)
133
- val capture = reuseBitmap(captureBitmap, vw, vh).also { captureBitmap = it }
134
- val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
135
-
136
- // ③ Capture just the region behind this view from root
137
- val c = Canvas(capture)
138
- c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
139
- c.translate(-offsetX.toFloat(), -offsetY.toFloat())
140
129
  rootView.draw(c)
141
-
142
- // ④ Downsample
143
- val sc = Canvas(scaled)
144
- sc.drawBitmap(capture,
145
- Rect(0, 0, capture.width, capture.height),
146
- Rect(0, 0, scaled.width, scaled.height),
147
- capturePaint)
148
-
149
- // ⑤ Blur (2 rounds for smooth spread)
150
- repeat(BLUR_ROUNDS) { blurBitmap(scaled) }
151
-
152
- // ⑥ Trigger redraw with new bitmap
153
- view.invalidate()
154
-
155
130
  } catch (_: Exception) {
156
- } finally {
157
131
  isCapturing = false
132
+ return
158
133
  }
134
+ isCapturing = false
135
+
136
+ // Downsample
137
+ Canvas(scaled).drawBitmap(
138
+ capture,
139
+ Rect(0, 0, capture.width, capture.height),
140
+ Rect(0, 0, scaled.width, scaled.height),
141
+ capturePaint
142
+ )
143
+
144
+ // Blur (2 rounds)
145
+ repeat(BLUR_ROUNDS) { blurBitmap(scaled) }
146
+
147
+ view.invalidate()
159
148
  }
160
149
 
161
150
  private fun blurBitmap(bitmap: Bitmap) {
162
- val rs = this.rs ?: return softwareBlur(bitmap)
163
- val sc = this.blurScript ?: return softwareBlur(bitmap)
151
+ val r = rs ?: return softwareBlur(bitmap)
152
+ val sc = blurScript ?: return softwareBlur(bitmap)
164
153
  try {
165
- val inA = reuseAlloc(inputAlloc, bitmap, rs).also { inputAlloc = it }
166
- val outA = reuseAlloc(outputAlloc, bitmap, rs).also { outputAlloc = it }
167
- inA.copyFrom(bitmap)
154
+ val iA = reuseAlloc(inputAlloc, bitmap, r).also { inputAlloc = it }
155
+ val oA = reuseAlloc(outputAlloc, bitmap, r).also { outputAlloc = it }
156
+ iA.copyFrom(bitmap)
168
157
  sc.setRadius(blurRadius.coerceIn(1f, 25f))
169
- sc.setInput(inA)
170
- sc.forEach(outA)
171
- outA.copyTo(bitmap)
172
- } catch (_: Exception) {
173
- softwareBlur(bitmap)
174
- }
158
+ sc.setInput(iA)
159
+ sc.forEach(oA)
160
+ oA.copyTo(bitmap)
161
+ } catch (_: Exception) { softwareBlur(bitmap) }
175
162
  }
176
163
 
177
164
  private fun softwareBlur(bitmap: Bitmap) {
178
- // Pure software Gaussian fallback (slower but always works)
179
165
  val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
180
166
  maskFilter = android.graphics.BlurMaskFilter(blurRadius, android.graphics.BlurMaskFilter.Blur.NORMAL)
181
167
  }
182
168
  Canvas(bitmap).drawBitmap(bitmap, 0f, 0f, paint)
183
169
  }
184
170
 
185
- // ── Draw — called from BlurVibeView.onDraw() ──────────────────────────────
171
+ // ── Draw ─────────────────────────────────────────────────────────────────
186
172
 
187
173
  fun draw(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
188
174
  scaledBitmap?.takeIf { !it.isRecycled }?.let { bmp ->
189
175
  canvas.drawBitmap(bmp, null,
190
176
  android.graphics.RectF(0f, 0f, viewWidth, viewHeight), drawPaint)
191
177
  }
192
- if (Color.alpha(overlayColor) > 0) {
193
- canvas.drawColor(overlayColor)
194
- }
178
+ if (Color.alpha(overlayColor) > 0) canvas.drawColor(overlayColor)
195
179
  }
196
180
 
197
- // ── Lifecycle ─────────────────────────────────────────────────────────────
181
+ // ── Multi-window ──────────────────────────────────────────────────────────
198
182
 
199
- fun onSizeChanged() {
200
- invalidatePool()
183
+ fun reAttach() {
184
+ if (enabled && autoUpdate) safeAddPreDrawListener()
201
185
  }
202
186
 
187
+ private fun safeAddPreDrawListener() {
188
+ val vto = rootView.viewTreeObserver
189
+ vto.removeOnPreDrawListener(preDrawListener)
190
+ if (vto.isAlive) vto.addOnPreDrawListener(preDrawListener)
191
+ }
192
+
193
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
194
+
195
+ fun onSizeChanged() { invalidatePool() }
196
+
203
197
  private fun invalidatePool() {
204
198
  captureBitmap?.recycle(); captureBitmap = null
205
199
  scaledBitmap?.recycle(); scaledBitmap = null
@@ -218,7 +212,7 @@ internal class LegacyBlurController(
218
212
  scaledBitmap?.recycle()
219
213
  }
220
214
 
221
- // ── Bitmap / Allocation helpers ───────────────────────────────────────────
215
+ // ── Helpers ───────────────────────────────────────────────────────────────
222
216
 
223
217
  private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
224
218
  if (existing != null && !existing.isRecycled
@@ -228,9 +222,8 @@ internal class LegacyBlurController(
228
222
  }
229
223
 
230
224
  private fun reuseAlloc(existing: Allocation?, src: Bitmap, rs: RenderScript): Allocation {
231
- if (existing != null
232
- && existing.type.x == src.width
233
- && existing.type.y == src.height) return existing
225
+ if (existing != null && existing.type.x == src.width && existing.type.y == src.height)
226
+ return existing
234
227
  existing?.destroy()
235
228
  return Allocation.createFromBitmap(rs, src,
236
229
  Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-blur-vibe",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",