@sbaiahmed1/react-native-blur 4.5.4 → 4.5.5-beta.1

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.
@@ -3,15 +3,15 @@ package com.sbaiahmed1.reactnativeblur
3
3
  import android.content.Context
4
4
  import android.graphics.Color
5
5
  import android.graphics.Outline
6
- import android.os.Build
7
6
  import android.util.AttributeSet
8
7
  import android.util.Log
9
8
  import android.util.TypedValue
10
9
  import android.view.View
11
10
  import android.view.ViewGroup
12
11
  import android.view.ViewOutlineProvider
13
- import androidx.annotation.RequiresApi
12
+ import android.view.ViewTreeObserver
14
13
  import com.qmdeve.blurview.widget.BlurViewGroup
14
+ import com.qmdeve.blurview.base.BaseBlurViewGroup
15
15
  import androidx.core.graphics.toColorInt
16
16
 
17
17
  import android.view.View.MeasureSpec
@@ -22,6 +22,10 @@ import android.view.View.MeasureSpec
22
22
  *
23
23
  * QmBlurView is a high-performance blur library that uses native blur algorithms
24
24
  * implemented with underlying Native calls for optimal performance.
25
+ *
26
+ * Uses reflection to redirect the blur capture root from the activity decor view
27
+ * to the nearest react-native-screens Screen ancestor, preventing flickering and
28
+ * wrong frame capture during navigation transitions.
25
29
  */
26
30
  class ReactNativeBlurView : BlurViewGroup {
27
31
  private var currentBlurRadius = DEFAULT_BLUR_RADIUS
@@ -31,12 +35,14 @@ class ReactNativeBlurView : BlurViewGroup {
31
35
  private var glassOpacity: Float = 1.0f
32
36
  private var viewType: String = "blur"
33
37
  private var glassType: String = "clear"
38
+ private var isBlurInitialized: Boolean = false
39
+ private var initRunnable: Runnable? = null
34
40
 
35
41
  companion object {
36
42
  private const val TAG = "ReactNativeBlurView"
37
43
  private const val MAX_BLUR_RADIUS = 100f
38
44
  private const val DEFAULT_BLUR_RADIUS = 10f
39
- private const val DEBUG = false // Set to true for debug builds
45
+ private const val DEBUG = false
40
46
 
41
47
  // Cross-platform blur amount constants
42
48
  private const val MIN_BLUR_AMOUNT = 0f
@@ -69,32 +75,166 @@ class ReactNativeBlurView : BlurViewGroup {
69
75
  }
70
76
 
71
77
  constructor(context: Context?) : super(context, null) {
72
- initializeBlur()
78
+ setupView()
73
79
  }
74
80
 
75
81
  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
76
- initializeBlur()
82
+ setupView()
77
83
  }
78
84
 
79
85
  /**
80
- * Initialize the blur view with default settings.
81
- * QmBlurView automatically handles blur rendering without needing setupWith() calls.
86
+ * Initial view setup in constructor - only sets up visual defaults.
87
+ * Blur initialization is deferred to onAttachedToWindow to ensure the
88
+ * view hierarchy is fully mounted, preventing flickering and wrong frame capture.
82
89
  */
83
- private fun initializeBlur() {
90
+ private fun setupView() {
91
+ super.setBackgroundColor(currentOverlayColor)
92
+ clipChildren = true
93
+ clipToOutline = true
94
+ super.setDownsampleFactor(6.0F)
95
+ }
96
+
97
+ /**
98
+ * Called when the view is attached to a window.
99
+ * After QmBlurView's onAttachedToWindow sets the decor view as blur root,
100
+ * we use reflection to redirect it to the nearest Screen ancestor.
101
+ * This scopes the blur capture to just the current screen, preventing
102
+ * navigation transition artifacts.
103
+ */
104
+ override fun onAttachedToWindow() {
105
+ super.onAttachedToWindow()
106
+
107
+ if (isBlurInitialized) return
108
+
109
+ // Defer the blur root swap to next frame so the view tree is fully mounted
110
+ val runnable = Runnable {
111
+ initRunnable = null
112
+ if (isBlurInitialized) return@Runnable
113
+ swapBlurRootToScreenAncestor()
114
+ initializeBlur()
115
+ }
116
+ initRunnable = runnable
117
+ post(runnable)
118
+ }
119
+
120
+ /**
121
+ * Uses reflection to redirect QmBlurView's internal blur capture root
122
+ * from the activity decor view to the nearest react-native-screens Screen ancestor.
123
+ *
124
+ * Reflection path: BlurViewGroup.mBaseBlurViewGroup -> BaseBlurViewGroup.mDecorView
125
+ * Also moves the OnPreDrawListener from the old root to the new one.
126
+ */
127
+ private fun swapBlurRootToScreenAncestor() {
128
+ // Pinned to QmBlurView 1.1.4 – depends on: mBaseBlurViewGroup, mDecorView, preDrawListener, mDifferentRoot, mForceRedraw
129
+ val newRoot = findOptimalBlurRoot() ?: return
130
+
84
131
  try {
85
- // Set initial blur properties using QmBlurView's API
86
- // setBlurRadius takes Float, setOverlayColor takes Int, setCornerRadius takes Float (in dp)
87
- super.setBlurRadius(currentBlurRadius)
88
- super.setOverlayColor(currentOverlayColor)
89
- super.setDownsampleFactor(6.0F)
132
+ // Step 1: Get BlurViewGroup's private mBaseBlurViewGroup field
133
+ val blurViewGroupClass = BlurViewGroup::class.java
134
+ val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
135
+ baseField.isAccessible = true
136
+ val baseBlurViewGroup = baseField.get(this) ?: return
137
+
138
+ // Step 2: Get BaseBlurViewGroup's private fields
139
+ val baseClass = BaseBlurViewGroup::class.java
140
+
141
+ val decorViewField = baseClass.getDeclaredField("mDecorView")
142
+ decorViewField.isAccessible = true
143
+ val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
144
+
145
+ val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
146
+ preDrawListenerField.isAccessible = true
147
+ val preDrawListener = preDrawListenerField.get(baseBlurViewGroup) as? ViewTreeObserver.OnPreDrawListener
148
+
149
+ if (oldDecorView == null) {
150
+ logWarning("swapBlurRootToScreenAncestor: oldDecorView is null, skipping swap – falling back to decor view")
151
+ }
152
+ if (preDrawListener == null) {
153
+ logWarning("swapBlurRootToScreenAncestor: preDrawListener is null, skipping swap – falling back to decor view")
154
+ }
155
+
156
+ if (preDrawListener != null && oldDecorView != null) {
157
+ // Step 3: Remove listener from old root's ViewTreeObserver
158
+ try {
159
+ oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
160
+ } catch (e: Exception) {
161
+ logDebug("Could not remove old pre-draw listener: ${e.message}")
162
+ }
163
+
164
+ // Step 4: Set new root as mDecorView
165
+ decorViewField.set(baseBlurViewGroup, newRoot)
166
+
167
+ // Step 5: Add listener to new root's ViewTreeObserver
168
+ newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
169
+
170
+ // Step 6: Update mDifferentRoot flag
171
+ val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
172
+ differentRootField.isAccessible = true
173
+ differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
174
+
175
+ // Step 7: Force a redraw
176
+ val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
177
+ forceRedrawField.isAccessible = true
178
+ forceRedrawField.setBoolean(baseBlurViewGroup, true)
90
179
 
91
- clipChildren = true
180
+ logDebug("Swapped blur root to: ${newRoot.javaClass.simpleName} (was: ${oldDecorView.javaClass.simpleName})")
181
+ }
182
+ } catch (e: NoSuchFieldException) {
183
+ logWarning("Reflection failed - QmBlurView field not found: ${e.message}. Falling back to decor view.")
184
+ } catch (e: Exception) {
185
+ logWarning("Failed to swap blur root: ${e.message}. Falling back to decor view.")
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Finds the optimal view to use as blur capture root.
191
+ * Priority: nearest react-native-screens Screen > android.R.id.content > parent
192
+ */
193
+ private fun findOptimalBlurRoot(): ViewGroup? {
194
+ return findNearestScreenAncestor() ?: getContentViewFallback()
195
+ }
92
196
 
93
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
94
- super.setBackgroundColor(currentOverlayColor)
197
+ /**
198
+ * Walks up the view hierarchy looking for react-native-screens Screen components
199
+ * using class name detection to avoid hard dependencies on react-native-screens.
200
+ */
201
+ private fun findNearestScreenAncestor(): ViewGroup? {
202
+ var currentParent = this.parent
203
+ while (currentParent != null) {
204
+ if (currentParent.javaClass.name == "com.swmansion.rnscreens.Screen") {
205
+ return currentParent as? ViewGroup
95
206
  }
207
+ currentParent = currentParent.parent
208
+ }
209
+ return null
210
+ }
211
+
212
+ /**
213
+ * Falls back to android.R.id.content or the activity root view.
214
+ */
215
+ private fun getContentViewFallback(): ViewGroup? {
216
+ try {
217
+ val activity = context as? android.app.Activity
218
+ activity?.findViewById<ViewGroup>(android.R.id.content)?.let { return it }
219
+ } catch (e: Exception) {
220
+ logDebug("Could not access activity content view: ${e.message}")
221
+ }
222
+ return this.parent as? ViewGroup
223
+ }
224
+
225
+ /**
226
+ * Initialize the blur view with current settings.
227
+ * Called after the view is attached and the blur root has been swapped.
228
+ * Guarded by isBlurInitialized to prevent duplicate setup.
229
+ */
230
+ private fun initializeBlur() {
231
+ if (isBlurInitialized) return
96
232
 
233
+ try {
234
+ super.setBlurRadius(currentBlurRadius)
235
+ super.setOverlayColor(currentOverlayColor)
97
236
  updateCornerRadius()
237
+ isBlurInitialized = true
98
238
 
99
239
  logDebug("QmBlurView initialized with blurRadius: $currentBlurRadius, overlayColor: $currentOverlayColor")
100
240
  } catch (e: Exception) {
@@ -104,7 +244,8 @@ class ReactNativeBlurView : BlurViewGroup {
104
244
 
105
245
  /**
106
246
  * Called when the view is detached from a window.
107
- * Performs cleanup to prevent memory leaks.
247
+ * Performs cleanup to prevent memory leaks and resets initialization state
248
+ * so blur is re-initialized on next attach (e.g. navigation transitions).
108
249
  */
109
250
  override fun onDetachedFromWindow() {
110
251
  super.onDetachedFromWindow()
@@ -116,7 +257,9 @@ class ReactNativeBlurView : BlurViewGroup {
116
257
  * Helps prevent memory leaks and ensures clean state.
117
258
  */
118
259
  fun cleanup() {
119
- removeCallbacks(null)
260
+ isBlurInitialized = false
261
+ initRunnable?.let { removeCallbacks(it) }
262
+ initRunnable = null
120
263
  logDebug("View cleaned up")
121
264
  }
122
265
 
@@ -129,7 +272,6 @@ class ReactNativeBlurView : BlurViewGroup {
129
272
  logDebug("setBlurAmount: $amount -> $currentBlurRadius (mapped from 0-100 to 0-25 range)")
130
273
 
131
274
  try {
132
- // QmBlurView uses setBlurRadius() to set blur intensity
133
275
  super.setBlurRadius(currentBlurRadius)
134
276
  } catch (e: Exception) {
135
277
  logError("Failed to set blur radius: ${e.message}", e)
@@ -146,12 +288,8 @@ class ReactNativeBlurView : BlurViewGroup {
146
288
  logDebug("setBlurType: $type -> ${blurType.name}")
147
289
 
148
290
  try {
149
- // QmBlurView uses setOverlayColor() to set the tint/overlay color
291
+ super.setBackgroundColor(currentOverlayColor)
150
292
  super.setOverlayColor(currentOverlayColor)
151
-
152
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
153
- super.setBackgroundColor(currentOverlayColor)
154
- }
155
293
  } catch (e: Exception) {
156
294
  logError("Failed to set overlay color: ${e.message}", e)
157
295
  }
@@ -249,11 +387,8 @@ class ReactNativeBlurView : BlurViewGroup {
249
387
  "blur" -> {
250
388
  // Restore original blur overlay color
251
389
  try {
390
+ super.setBackgroundColor(currentOverlayColor)
252
391
  super.setOverlayColor(currentOverlayColor)
253
-
254
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
255
- super.setBackgroundColor(currentOverlayColor)
256
- }
257
392
  } catch (e: Exception) {
258
393
  logError("Failed to restore blur overlay: ${e.message}", e)
259
394
  }
@@ -286,16 +421,12 @@ class ReactNativeBlurView : BlurViewGroup {
286
421
  context.resources.displayMetrics
287
422
  )
288
423
 
289
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
290
- rootView.outlineProvider = object : ViewOutlineProvider() {
291
- @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
292
- override fun getOutline(view: View, outline: Outline?) {
293
- outline?.setRoundRect(0, 0, view.width, view.height, radiusInPixels)
294
- }
424
+ outlineProvider = object : ViewOutlineProvider() {
425
+ override fun getOutline(view: View, outline: Outline?) {
426
+ outline?.setRoundRect(0, 0, view.width, view.height, radiusInPixels)
295
427
  }
296
-
297
- clipToOutline = true
298
428
  }
429
+ clipToOutline = true
299
430
 
300
431
  super.setCornerRadius(radiusInPixels)
301
432
  logDebug("Updated corner radius: ${currentCornerRadius}dp -> ${radiusInPixels}px")
@@ -10,10 +10,11 @@ import android.graphics.PorterDuffXfermode
10
10
  import android.graphics.Shader
11
11
  import android.util.AttributeSet
12
12
  import android.util.Log
13
+ import android.view.View
14
+ import android.view.ViewGroup
13
15
  import android.widget.FrameLayout
14
16
  import android.view.View.MeasureSpec
15
17
  import com.qmdeve.blurview.widget.BlurView
16
- import androidx.core.graphics.toColorInt
17
18
  import kotlin.math.max
18
19
 
19
20
  /**
@@ -33,12 +34,13 @@ class ReactNativeProgressiveBlurView : FrameLayout {
33
34
  private var currentDirection = "topToBottom"
34
35
  private var currentStartOffset = 0.0f
35
36
  private var hasExplicitBackground: Boolean = false
37
+ private var isBlurInitialized: Boolean = false
36
38
 
37
39
  companion object {
38
40
  private const val TAG = "ReactNativeProgressiveBlur"
39
41
  private const val MAX_BLUR_RADIUS = 100f
40
42
  private const val DEFAULT_BLUR_RADIUS = 10f
41
- private const val DEBUG = true
43
+ private const val DEBUG = false
42
44
 
43
45
  // Cross-platform blur amount constants
44
46
  private const val MIN_BLUR_AMOUNT = 0f
@@ -72,36 +74,69 @@ class ReactNativeProgressiveBlurView : FrameLayout {
72
74
  }
73
75
 
74
76
  constructor(context: Context) : super(context) {
75
- initializeProgressiveBlur()
77
+ setupView()
76
78
  }
77
79
 
78
80
  constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
79
- initializeProgressiveBlur()
81
+ setupView()
80
82
  }
81
83
 
82
84
  /**
83
- * Initialize the progressive blur view with blur + gradient approach.
85
+ * Initial view setup in constructor - only sets up visual defaults and gradient paint.
86
+ * Blur child creation is deferred to onAttachedToWindow.
84
87
  */
85
- private fun initializeProgressiveBlur() {
88
+ private fun setupView() {
89
+ // Set up the gradient paint
90
+ gradientPaint.style = Paint.Style.FILL
91
+ setWillNotDraw(false)
92
+
93
+ // Set transparent background for the container
94
+ super.setBackgroundColor(Color.TRANSPARENT)
95
+ }
96
+
97
+ /**
98
+ * Called when the view is attached to a window.
99
+ * Defers blur initialization to the next frame to ensure the view tree is ready.
100
+ */
101
+ override fun onAttachedToWindow() {
102
+ super.onAttachedToWindow()
103
+
104
+ if (!isBlurInitialized) {
105
+ post {
106
+ initializeBlurChild()
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Initialize the internal blur view child after the view tree is ready.
113
+ * Also swaps the blur capture root to the nearest Screen ancestor.
114
+ */
115
+ private fun initializeBlurChild() {
116
+ if (isBlurInitialized) return
117
+
86
118
  try {
87
- // Create and add the blur view as a child
88
- blurView = BlurView(context, null).apply {
89
- layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
119
+ if (blurView == null) {
120
+ blurView = BlurView(context, null).apply {
121
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
122
+ setDownsampleFactor(6.0F)
123
+ blurRounds = 3
124
+ }
125
+ addView(blurView)
126
+ }
127
+
128
+ blurView?.apply {
90
129
  setBlurRadius(currentBlurRadius)
91
- setDownsampleFactor(6.0F)
92
- blurRounds = 3
93
130
  overlayColor = currentOverlayColor
94
131
  setBackgroundColor(currentOverlayColor)
95
132
  }
96
- addView(blurView)
97
133
 
98
- // Set up the gradient paint
99
- gradientPaint.style = Paint.Style.FILL
100
- setWillNotDraw(false) // Enable onDraw for gradient overlay
101
-
102
- // Set transparent background for the container
103
- super.setBackgroundColor(Color.TRANSPARENT)
134
+ // Swap blur root after BlurView is attached (deferred to let it attach first)
135
+ blurView?.post {
136
+ swapBlurRootToScreenAncestor()
137
+ }
104
138
 
139
+ isBlurInitialized = true
105
140
  logDebug("Initialized progressive blur with blur + gradient approach")
106
141
  updateGradient()
107
142
 
@@ -110,6 +145,79 @@ class ReactNativeProgressiveBlurView : FrameLayout {
110
145
  }
111
146
  }
112
147
 
148
+ /**
149
+ * Redirects the internal BlurView's blur capture root from the activity decor view
150
+ * to the nearest react-native-screens Screen ancestor.
151
+ *
152
+ * BaseBlurView has public mDecorView and preDrawListener fields, so no reflection needed.
153
+ */
154
+ private fun swapBlurRootToScreenAncestor() {
155
+ val bv = blurView ?: return
156
+ val newRoot = findOptimalBlurRoot() ?: return
157
+
158
+ try {
159
+ val oldDecorView = bv.mDecorView
160
+ val listener = bv.preDrawListener
161
+
162
+ if (oldDecorView != null && listener != null) {
163
+ // Remove listener from old root
164
+ try {
165
+ oldDecorView.viewTreeObserver.removeOnPreDrawListener(listener)
166
+ } catch (e: Exception) {
167
+ logDebug("Could not remove old pre-draw listener: ${e.message}")
168
+ }
169
+
170
+ // Set new root
171
+ bv.mDecorView = newRoot
172
+
173
+ // Add listener to new root
174
+ newRoot.viewTreeObserver.addOnPreDrawListener(listener)
175
+
176
+ // Update mDifferentRoot flag
177
+ bv.mDifferentRoot = newRoot.rootView != bv.rootView
178
+
179
+ logDebug("Progressive blur: swapped root to ${newRoot.javaClass.simpleName}")
180
+ }
181
+ } catch (e: Exception) {
182
+ logWarning("Failed to swap progressive blur root: ${e.message}")
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Finds the optimal view to use as blur capture root.
188
+ * Priority: nearest react-native-screens Screen > android.R.id.content > parent
189
+ */
190
+ private fun findOptimalBlurRoot(): ViewGroup? {
191
+ return findNearestScreenAncestor() ?: getContentViewFallback()
192
+ }
193
+
194
+ /**
195
+ * Walks up the view hierarchy looking for react-native-screens Screen components.
196
+ */
197
+ private fun findNearestScreenAncestor(): ViewGroup? {
198
+ var currentParent = this.parent
199
+ while (currentParent != null) {
200
+ if (currentParent.javaClass.name == "com.swmansion.rnscreens.Screen") {
201
+ return currentParent as? ViewGroup
202
+ }
203
+ currentParent = currentParent.parent
204
+ }
205
+ return null
206
+ }
207
+
208
+ /**
209
+ * Falls back to android.R.id.content or the activity root view.
210
+ */
211
+ private fun getContentViewFallback(): ViewGroup? {
212
+ try {
213
+ val activity = context as? android.app.Activity
214
+ activity?.findViewById<ViewGroup>(android.R.id.content)?.let { return it }
215
+ } catch (e: Exception) {
216
+ logDebug("Could not access activity content view: ${e.message}")
217
+ }
218
+ return this.parent as? ViewGroup
219
+ }
220
+
113
221
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
114
222
  val width = MeasureSpec.getSize(widthMeasureSpec)
115
223
  val height = MeasureSpec.getSize(heightMeasureSpec)
@@ -252,11 +360,12 @@ class ReactNativeProgressiveBlurView : FrameLayout {
252
360
 
253
361
  /**
254
362
  * Cleanup method to prevent memory leaks.
363
+ * Resets initialization state so blur is re-initialized on next attach.
255
364
  */
256
365
  fun cleanup() {
257
366
  hasExplicitBackground = false
367
+ isBlurInitialized = false
258
368
  removeCallbacks(null)
259
- blurView = null
260
369
  logDebug("View cleaned up")
261
370
  }
262
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sbaiahmed1/react-native-blur",
3
- "version": "4.5.4",
3
+ "version": "4.5.5-beta.1",
4
4
  "description": "React native modern blur view",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",