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

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,13 @@ 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
34
39
 
35
40
  companion object {
36
41
  private const val TAG = "ReactNativeBlurView"
37
42
  private const val MAX_BLUR_RADIUS = 100f
38
43
  private const val DEFAULT_BLUR_RADIUS = 10f
39
- private const val DEBUG = false // Set to true for debug builds
44
+ private const val DEBUG = false
40
45
 
41
46
  // Cross-platform blur amount constants
42
47
  private const val MIN_BLUR_AMOUNT = 0f
@@ -69,32 +74,149 @@ class ReactNativeBlurView : BlurViewGroup {
69
74
  }
70
75
 
71
76
  constructor(context: Context?) : super(context, null) {
72
- initializeBlur()
77
+ setupView()
73
78
  }
74
79
 
75
80
  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
76
- initializeBlur()
81
+ setupView()
77
82
  }
78
83
 
79
84
  /**
80
- * Initialize the blur view with default settings.
81
- * QmBlurView automatically handles blur rendering without needing setupWith() calls.
85
+ * Initial view setup in constructor - only sets up visual defaults.
86
+ * Blur initialization is deferred to onAttachedToWindow to ensure the
87
+ * view hierarchy is fully mounted, preventing flickering and wrong frame capture.
82
88
  */
83
- private fun initializeBlur() {
89
+ private fun setupView() {
90
+ super.setBackgroundColor(currentOverlayColor)
91
+ clipChildren = true
92
+ clipToOutline = true
93
+ super.setDownsampleFactor(6.0F)
94
+ }
95
+
96
+ /**
97
+ * Called when the view is attached to a window.
98
+ * After QmBlurView's onAttachedToWindow sets the decor view as blur root,
99
+ * we use reflection to redirect it to the nearest Screen ancestor.
100
+ * This scopes the blur capture to just the current screen, preventing
101
+ * navigation transition artifacts.
102
+ */
103
+ override fun onAttachedToWindow() {
104
+ super.onAttachedToWindow()
105
+
106
+ // Defer the blur root swap to next frame so the view tree is fully mounted
107
+ post {
108
+ swapBlurRootToScreenAncestor()
109
+ initializeBlur()
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Uses reflection to redirect QmBlurView's internal blur capture root
115
+ * from the activity decor view to the nearest react-native-screens Screen ancestor.
116
+ *
117
+ * Reflection path: BlurViewGroup.mBaseBlurViewGroup -> BaseBlurViewGroup.mDecorView
118
+ * Also moves the OnPreDrawListener from the old root to the new one.
119
+ */
120
+ private fun swapBlurRootToScreenAncestor() {
121
+ val newRoot = findOptimalBlurRoot() ?: return
122
+
84
123
  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)
124
+ // Step 1: Get BlurViewGroup's private mBaseBlurViewGroup field
125
+ val blurViewGroupClass = BlurViewGroup::class.java
126
+ val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
127
+ baseField.isAccessible = true
128
+ val baseBlurViewGroup = baseField.get(this) ?: return
129
+
130
+ // Step 2: Get BaseBlurViewGroup's private fields
131
+ val baseClass = BaseBlurViewGroup::class.java
132
+
133
+ val decorViewField = baseClass.getDeclaredField("mDecorView")
134
+ decorViewField.isAccessible = true
135
+ val oldDecorView = decorViewField.get(baseBlurViewGroup) as? View
136
+
137
+ val preDrawListenerField = baseClass.getDeclaredField("preDrawListener")
138
+ preDrawListenerField.isAccessible = true
139
+ val preDrawListener = preDrawListenerField.get(baseBlurViewGroup) as? ViewTreeObserver.OnPreDrawListener
140
+
141
+ if (preDrawListener != null && oldDecorView != null) {
142
+ // Step 3: Remove listener from old root's ViewTreeObserver
143
+ try {
144
+ oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
145
+ } catch (e: Exception) {
146
+ logDebug("Could not remove old pre-draw listener: ${e.message}")
147
+ }
148
+
149
+ // Step 4: Set new root as mDecorView
150
+ decorViewField.set(baseBlurViewGroup, newRoot)
151
+
152
+ // Step 5: Add listener to new root's ViewTreeObserver
153
+ newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
154
+
155
+ // Step 6: Update mDifferentRoot flag
156
+ val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
157
+ differentRootField.isAccessible = true
158
+ differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
90
159
 
91
- clipChildren = true
160
+ // Step 7: Force a redraw
161
+ val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
162
+ forceRedrawField.isAccessible = true
163
+ forceRedrawField.setBoolean(baseBlurViewGroup, true)
92
164
 
93
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
94
- super.setBackgroundColor(currentOverlayColor)
165
+ logDebug("Swapped blur root to: ${newRoot.javaClass.simpleName} (was: ${oldDecorView.javaClass.simpleName})")
95
166
  }
167
+ } catch (e: NoSuchFieldException) {
168
+ logWarning("Reflection failed - QmBlurView field not found: ${e.message}. Falling back to decor view.")
169
+ } catch (e: Exception) {
170
+ logWarning("Failed to swap blur root: ${e.message}. Falling back to decor view.")
171
+ }
172
+ }
96
173
 
174
+ /**
175
+ * Finds the optimal view to use as blur capture root.
176
+ * Priority: nearest react-native-screens Screen > android.R.id.content > parent
177
+ */
178
+ private fun findOptimalBlurRoot(): ViewGroup? {
179
+ return findNearestScreenAncestor() ?: getContentViewFallback()
180
+ }
181
+
182
+ /**
183
+ * Walks up the view hierarchy looking for react-native-screens Screen components
184
+ * using class name detection to avoid hard dependencies on react-native-screens.
185
+ */
186
+ private fun findNearestScreenAncestor(): ViewGroup? {
187
+ var currentParent = this.parent
188
+ while (currentParent != null) {
189
+ if (currentParent.javaClass.name == "com.swmansion.rnscreens.Screen") {
190
+ return currentParent as? ViewGroup
191
+ }
192
+ currentParent = currentParent.parent
193
+ }
194
+ return null
195
+ }
196
+
197
+ /**
198
+ * Falls back to android.R.id.content or the activity root view.
199
+ */
200
+ private fun getContentViewFallback(): ViewGroup? {
201
+ try {
202
+ val activity = context as? android.app.Activity
203
+ activity?.findViewById<ViewGroup>(android.R.id.content)?.let { return it }
204
+ } catch (e: Exception) {
205
+ logDebug("Could not access activity content view: ${e.message}")
206
+ }
207
+ return this.parent as? ViewGroup
208
+ }
209
+
210
+ /**
211
+ * Initialize the blur view with current settings.
212
+ * Called after the view is attached and the blur root has been swapped.
213
+ */
214
+ private fun initializeBlur() {
215
+ try {
216
+ super.setBlurRadius(currentBlurRadius)
217
+ super.setOverlayColor(currentOverlayColor)
97
218
  updateCornerRadius()
219
+ isBlurInitialized = true
98
220
 
99
221
  logDebug("QmBlurView initialized with blurRadius: $currentBlurRadius, overlayColor: $currentOverlayColor")
100
222
  } catch (e: Exception) {
@@ -104,7 +226,8 @@ class ReactNativeBlurView : BlurViewGroup {
104
226
 
105
227
  /**
106
228
  * Called when the view is detached from a window.
107
- * Performs cleanup to prevent memory leaks.
229
+ * Performs cleanup to prevent memory leaks and resets initialization state
230
+ * so blur is re-initialized on next attach (e.g. navigation transitions).
108
231
  */
109
232
  override fun onDetachedFromWindow() {
110
233
  super.onDetachedFromWindow()
@@ -116,6 +239,7 @@ class ReactNativeBlurView : BlurViewGroup {
116
239
  * Helps prevent memory leaks and ensures clean state.
117
240
  */
118
241
  fun cleanup() {
242
+ isBlurInitialized = false
119
243
  removeCallbacks(null)
120
244
  logDebug("View cleaned up")
121
245
  }
@@ -129,7 +253,6 @@ class ReactNativeBlurView : BlurViewGroup {
129
253
  logDebug("setBlurAmount: $amount -> $currentBlurRadius (mapped from 0-100 to 0-25 range)")
130
254
 
131
255
  try {
132
- // QmBlurView uses setBlurRadius() to set blur intensity
133
256
  super.setBlurRadius(currentBlurRadius)
134
257
  } catch (e: Exception) {
135
258
  logError("Failed to set blur radius: ${e.message}", e)
@@ -146,12 +269,8 @@ class ReactNativeBlurView : BlurViewGroup {
146
269
  logDebug("setBlurType: $type -> ${blurType.name}")
147
270
 
148
271
  try {
149
- // QmBlurView uses setOverlayColor() to set the tint/overlay color
272
+ super.setBackgroundColor(currentOverlayColor)
150
273
  super.setOverlayColor(currentOverlayColor)
151
-
152
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
153
- super.setBackgroundColor(currentOverlayColor)
154
- }
155
274
  } catch (e: Exception) {
156
275
  logError("Failed to set overlay color: ${e.message}", e)
157
276
  }
@@ -249,11 +368,8 @@ class ReactNativeBlurView : BlurViewGroup {
249
368
  "blur" -> {
250
369
  // Restore original blur overlay color
251
370
  try {
371
+ super.setBackgroundColor(currentOverlayColor)
252
372
  super.setOverlayColor(currentOverlayColor)
253
-
254
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
255
- super.setBackgroundColor(currentOverlayColor)
256
- }
257
373
  } catch (e: Exception) {
258
374
  logError("Failed to restore blur overlay: ${e.message}", e)
259
375
  }
@@ -286,16 +402,12 @@ class ReactNativeBlurView : BlurViewGroup {
286
402
  context.resources.displayMetrics
287
403
  )
288
404
 
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
- }
405
+ outlineProvider = object : ViewOutlineProvider() {
406
+ override fun getOutline(view: View, outline: Outline?) {
407
+ outline?.setRoundRect(0, 0, view.width, view.height, radiusInPixels)
295
408
  }
296
-
297
- clipToOutline = true
298
409
  }
410
+ clipToOutline = true
299
411
 
300
412
  super.setCornerRadius(radiusInPixels)
301
413
  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.0",
4
4
  "description": "React native modern blur view",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",