@sbaiahmed1/react-native-blur 4.6.2 → 4.6.3-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.
@@ -74,7 +74,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
74
74
  dependencies {
75
75
  implementation "com.facebook.react:react-android"
76
76
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
- implementation 'com.qmdeve.blurview:core:1.1.4'
77
+ implementation 'com.github.qmdeve:qmblurview:v1.1.5'
78
78
  }
79
79
 
80
80
  react {
@@ -1,64 +1,55 @@
1
1
  package com.sbaiahmed1.reactnativeblur
2
2
 
3
- import android.content.res.Configuration
4
3
  import android.graphics.Color
5
4
 
6
- /**
7
- * Enum representing different blur types with their corresponding overlay colors.
8
- * Maps iOS blur types to Android overlay colors to approximate the visual appearance.
9
- */
10
5
  enum class BlurType(val overlayColor: Int) {
11
6
  XLIGHT(Color.argb(140, 240, 240, 240)),
12
7
  LIGHT(Color.argb(42, 255, 255, 255)),
13
8
  DARK(Color.argb(120, 26, 22, 22)),
14
9
  EXTRA_DARK(Color.argb(160, 35, 35, 35)),
15
- REGULAR_LIGHT(Color.argb(35, 255, 255, 255)),
16
- REGULAR_DARK(Color.argb(35, 28, 28, 30)),
17
- PROMINENT_LIGHT(Color.argb(140, 240, 240, 240)),
18
- PROMINENT_DARK(Color.argb(140, 28, 28, 30)),
10
+ REGULAR(Color.argb(35, 255, 255, 255)),
11
+ PROMINENT(Color.argb(140, 240, 240, 240)),
12
+ SYSTEM_ULTRA_THIN_MATERIAL(Color.argb(75, 240, 240, 240)),
19
13
  SYSTEM_ULTRA_THIN_MATERIAL_LIGHT(Color.argb(75, 240, 240, 240)),
20
14
  SYSTEM_ULTRA_THIN_MATERIAL_DARK(Color.argb(65, 40, 40, 40)),
15
+ SYSTEM_THIN_MATERIAL(Color.argb(102, 240, 240, 240)),
21
16
  SYSTEM_THIN_MATERIAL_LIGHT(Color.argb(102, 240, 240, 240)),
22
17
  SYSTEM_THIN_MATERIAL_DARK(Color.argb(102, 35, 35, 35)),
18
+ SYSTEM_MATERIAL(Color.argb(140, 245, 245, 245)),
23
19
  SYSTEM_MATERIAL_LIGHT(Color.argb(140, 245, 245, 245)),
24
20
  SYSTEM_MATERIAL_DARK(Color.argb(215, 65, 60, 60)),
21
+ SYSTEM_THICK_MATERIAL(Color.argb(210, 248, 248, 248)),
25
22
  SYSTEM_THICK_MATERIAL_LIGHT(Color.argb(210, 248, 248, 248)),
26
23
  SYSTEM_THICK_MATERIAL_DARK(Color.argb(160, 35, 35, 35)),
24
+ SYSTEM_CHROME_MATERIAL(Color.argb(165, 248, 248, 248)),
27
25
  SYSTEM_CHROME_MATERIAL_LIGHT(Color.argb(165, 248, 248, 248)),
28
26
  SYSTEM_CHROME_MATERIAL_DARK(Color.argb(100, 32, 32, 32));
29
27
 
30
28
  companion object {
31
- /**
32
- * Get BlurType from string, with fallback to LIGHT for unknown types.
33
- * Uses the provided configuration to determine if dark mode is active for
34
- * appropriate defaults.
35
- */
36
- fun fromString(type: String, configuration: Configuration): BlurType {
37
- val isDarkMode = (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
38
-
29
+ fun fromString(type: String): BlurType {
39
30
  return when (type.lowercase()) {
40
31
  "xlight" -> XLIGHT
41
32
  "light" -> LIGHT
42
33
  "dark" -> DARK
43
34
  "extradark" -> EXTRA_DARK
44
- "regular" -> if (isDarkMode) REGULAR_DARK else REGULAR_LIGHT
45
- "prominent" -> if (isDarkMode) PROMINENT_DARK else PROMINENT_LIGHT
46
- "systemultrathinmaterial" -> if (isDarkMode) SYSTEM_ULTRA_THIN_MATERIAL_DARK else SYSTEM_ULTRA_THIN_MATERIAL_LIGHT
35
+ "regular" -> REGULAR
36
+ "prominent" -> PROMINENT
37
+ "systemultrathinmaterial" -> SYSTEM_ULTRA_THIN_MATERIAL
47
38
  "systemultrathinmateriallight" -> SYSTEM_ULTRA_THIN_MATERIAL_LIGHT
48
39
  "systemultrathinmaterialdark" -> SYSTEM_ULTRA_THIN_MATERIAL_DARK
49
- "systemthinmaterial" -> if (isDarkMode) SYSTEM_THIN_MATERIAL_DARK else SYSTEM_THIN_MATERIAL_LIGHT
40
+ "systemthinmaterial" -> SYSTEM_THIN_MATERIAL
50
41
  "systemthinmateriallight" -> SYSTEM_THIN_MATERIAL_LIGHT
51
42
  "systemthinmaterialdark" -> SYSTEM_THIN_MATERIAL_DARK
52
- "systemmaterial" -> if (isDarkMode) SYSTEM_MATERIAL_DARK else SYSTEM_MATERIAL_LIGHT
43
+ "systemmaterial" -> SYSTEM_MATERIAL
53
44
  "systemmateriallight" -> SYSTEM_MATERIAL_LIGHT
54
45
  "systemmaterialdark" -> SYSTEM_MATERIAL_DARK
55
- "systemthickmaterial" -> if (isDarkMode) SYSTEM_THICK_MATERIAL_DARK else SYSTEM_THICK_MATERIAL_LIGHT
46
+ "systemthickmaterial" -> SYSTEM_THICK_MATERIAL
56
47
  "systemthickmateriallight" -> SYSTEM_THICK_MATERIAL_LIGHT
57
48
  "systemthickmaterialdark" -> SYSTEM_THICK_MATERIAL_DARK
58
- "systemchromematerial" -> if (isDarkMode) SYSTEM_CHROME_MATERIAL_DARK else SYSTEM_CHROME_MATERIAL_LIGHT
49
+ "systemchromematerial" -> SYSTEM_CHROME_MATERIAL
59
50
  "systemchromemateriallight" -> SYSTEM_CHROME_MATERIAL_LIGHT
60
51
  "systemchromematerialdark" -> SYSTEM_CHROME_MATERIAL_DARK
61
- else -> XLIGHT // default fallback
52
+ else -> XLIGHT
62
53
  }
63
54
  }
64
55
  }
@@ -1,9 +1,10 @@
1
1
  package com.sbaiahmed1.reactnativeblur
2
2
 
3
3
  import android.content.Context
4
- import android.content.res.Configuration
5
4
  import android.graphics.Color
6
5
  import android.graphics.Outline
6
+ import android.graphics.Path
7
+ import android.os.Build
7
8
  import android.util.AttributeSet
8
9
  import android.util.Log
9
10
  import android.util.TypedValue
@@ -32,7 +33,11 @@ class ReactNativeBlurView : BlurViewGroup {
32
33
  private var currentBlurRadius = DEFAULT_BLUR_RADIUS
33
34
  private var currentOverlayColor = Color.TRANSPARENT
34
35
  private var currentBlurRounds = DEFAULT_BLUR_ROUNDS
35
- private var currentCornerRadius = 0f
36
+ private var borderRadius = 0f
37
+ private var borderTopLeftRadius = -1f
38
+ private var borderTopRightRadius = -1f
39
+ private var borderBottomLeftRadius = -1f
40
+ private var borderBottomRightRadius = -1f
36
41
  private var glassTintColor: Int = Color.TRANSPARENT
37
42
  private var glassOpacity: Float = 1.0f
38
43
  private var viewType: String = "blur"
@@ -48,7 +53,6 @@ class ReactNativeBlurView : BlurViewGroup {
48
53
  private const val DEFAULT_BLUR_ROUNDS = 5
49
54
  private const val DEBUG = false
50
55
 
51
- // Cross-platform blur amount constants
52
56
  private const val MIN_BLUR_AMOUNT = 0f
53
57
  private const val MAX_BLUR_AMOUNT = 100f
54
58
 
@@ -66,12 +70,6 @@ class ReactNativeBlurView : BlurViewGroup {
66
70
  Log.e(TAG, message, throwable)
67
71
  }
68
72
 
69
- /**
70
- * Maps blur amount (0-100) to Android blur radius (0-25).
71
- * This ensures cross-platform consistency while respecting Android's limitations.
72
- * @param amount The blur amount from 0-100
73
- * @return The corresponding blur radius from 0-25
74
- */
75
73
  private fun mapBlurAmountToRadius(amount: Float): Float {
76
74
  val clampedAmount = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT)
77
75
  return (clampedAmount / MAX_BLUR_AMOUNT) * MAX_BLUR_RADIUS
@@ -86,11 +84,6 @@ class ReactNativeBlurView : BlurViewGroup {
86
84
  setupView()
87
85
  }
88
86
 
89
- /**
90
- * Initial view setup in constructor - only sets up visual defaults.
91
- * Blur initialization is deferred to onAttachedToWindow to ensure the
92
- * view hierarchy is fully mounted, preventing flickering and wrong frame capture.
93
- */
94
87
  private fun setupView() {
95
88
  super.setBackgroundColor(currentOverlayColor)
96
89
  clipChildren = true
@@ -99,45 +92,24 @@ class ReactNativeBlurView : BlurViewGroup {
99
92
  super.setDownsampleFactor(6.0F)
100
93
  }
101
94
 
102
- /**
103
- * Called when the view is attached to a window.
104
- * After QmBlurView's onAttachedToWindow sets the decor view as blur root,
105
- * we use reflection to redirect it to the nearest Screen ancestor.
106
- * This scopes the blur capture to just the current screen, preventing
107
- * navigation transition artifacts.
108
- */
109
95
  override fun onAttachedToWindow() {
110
96
  super.onAttachedToWindow()
111
97
 
112
98
  if (isBlurInitialized) return
113
99
 
114
- // Immediately try to swap blur root and initialize.
115
- // We avoid posting a runnable to prevent the 1-second delay artifact.
116
- // If the parent hierarchy is not ready yet (unlikely in onAttachedToWindow),
117
- // we could fall back to post, but for now we prioritize immediate execution.
118
100
  swapBlurRootToScreenAncestor()
119
101
  initializeBlur()
120
102
  }
121
103
 
122
- /**
123
- * Uses reflection to redirect QmBlurView's internal blur capture root
124
- * from the activity decor view to the nearest react-native-screens Screen ancestor.
125
- *
126
- * Reflection path: BlurViewGroup.mBaseBlurViewGroup -> BaseBlurViewGroup.mDecorView
127
- * Also moves the OnPreDrawListener from the old root to the new one.
128
- */
129
104
  private fun swapBlurRootToScreenAncestor() {
130
- // Pinned to QmBlurView 1.1.4 – depends on: mBaseBlurViewGroup, mDecorView, preDrawListener, mDifferentRoot, mForceRedraw
131
105
  val newRoot = findOptimalBlurRoot() ?: return
132
106
 
133
107
  try {
134
- // Step 1: Get BlurViewGroup's private mBaseBlurViewGroup field
135
108
  val blurViewGroupClass = BlurViewGroup::class.java
136
109
  val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup")
137
110
  baseField.isAccessible = true
138
111
  val baseBlurViewGroup = baseField.get(this) ?: return
139
112
 
140
- // Step 2: Get BaseBlurViewGroup's private fields
141
113
  val baseClass = BaseBlurViewGroup::class.java
142
114
 
143
115
  val decorViewField = baseClass.getDeclaredField("mDecorView")
@@ -156,25 +128,20 @@ class ReactNativeBlurView : BlurViewGroup {
156
128
  }
157
129
 
158
130
  if (preDrawListener != null && oldDecorView != null) {
159
- // Step 3: Remove listener from old root's ViewTreeObserver
160
131
  try {
161
132
  oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
162
133
  } catch (e: Exception) {
163
134
  logDebug("Could not remove old pre-draw listener: ${e.message}")
164
135
  }
165
136
 
166
- // Step 4: Set new root as mDecorView
167
137
  decorViewField.set(baseBlurViewGroup, newRoot)
168
138
 
169
- // Step 5: Add listener to new root's ViewTreeObserver
170
139
  newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener)
171
140
 
172
- // Step 6: Update mDifferentRoot flag
173
141
  val differentRootField = baseClass.getDeclaredField("mDifferentRoot")
174
142
  differentRootField.isAccessible = true
175
143
  differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView)
176
144
 
177
- // Step 7: Force a redraw
178
145
  val forceRedrawField = baseClass.getDeclaredField("mForceRedraw")
179
146
  forceRedrawField.isAccessible = true
180
147
  forceRedrawField.setBoolean(baseBlurViewGroup, true)
@@ -188,27 +155,10 @@ class ReactNativeBlurView : BlurViewGroup {
188
155
  }
189
156
  }
190
157
 
191
- /**
192
- * Finds the optimal view to use as blur capture root.
193
- *
194
- * Priority:
195
- * 1. Nearest react-native-screens Screen ancestor — scopes blur to the current
196
- * screen and prevents capturing navigation transition artifacts.
197
- * 2. Nearest ReactRootView ancestor — scopes blur to the React Native root when
198
- * the component is not inside a Screen (e.g. plain View hierarchies). Without
199
- * this fallback, QmBlurView defaults to the activity decor view and blurs the
200
- * entire screen instead of just the component area (issue #89).
201
- * 3. null — returned for modals, which intentionally need to blur content from
202
- * the main activity window (decor view is correct there).
203
- */
204
158
  private fun findOptimalBlurRoot(): ViewGroup? {
205
159
  return findNearestScreenAncestor() ?: findNearestReactRootView()
206
160
  }
207
161
 
208
- /**
209
- * Walks up the view hierarchy looking for react-native-screens Screen components
210
- * using class name detection to avoid hard dependencies on react-native-screens.
211
- */
212
162
  private fun findNearestScreenAncestor(): ViewGroup? {
213
163
  var currentParent = this.parent
214
164
  while (currentParent != null) {
@@ -220,11 +170,6 @@ class ReactNativeBlurView : BlurViewGroup {
220
170
  return null
221
171
  }
222
172
 
223
- /**
224
- * Walks up the view hierarchy looking for the React Native root view.
225
- * Used as a fallback when no Screen ancestor exists, to scope the blur
226
- * capture to the RN root rather than the full activity decor view.
227
- */
228
173
  private fun findNearestReactRootView(): ViewGroup? {
229
174
  var currentParent = this.parent
230
175
  while (currentParent != null) {
@@ -236,11 +181,6 @@ class ReactNativeBlurView : BlurViewGroup {
236
181
  return null
237
182
  }
238
183
 
239
- /**
240
- * Initialize the blur view with current settings.
241
- * Called after the view is attached and the blur root has been swapped.
242
- * Guarded by isBlurInitialized to prevent duplicate setup.
243
- */
244
184
  private fun initializeBlur() {
245
185
  if (isBlurInitialized) return
246
186
 
@@ -256,20 +196,11 @@ class ReactNativeBlurView : BlurViewGroup {
256
196
  }
257
197
  }
258
198
 
259
- /**
260
- * Called when the view is detached from a window.
261
- * Performs cleanup to prevent memory leaks and resets initialization state
262
- * so blur is re-initialized on next attach (e.g. navigation transitions).
263
- */
264
199
  override fun onDetachedFromWindow() {
265
200
  super.onDetachedFromWindow()
266
201
  cleanup()
267
202
  }
268
203
 
269
- /**
270
- * Cleanup method to reset state.
271
- * Helps prevent memory leaks and ensures clean state.
272
- */
273
204
  fun cleanup() {
274
205
  isBlurInitialized = false
275
206
  initRunnable?.let { removeCallbacks(it) }
@@ -277,10 +208,6 @@ class ReactNativeBlurView : BlurViewGroup {
277
208
  logDebug("View cleaned up")
278
209
  }
279
210
 
280
- /**
281
- * Set the blur amount with cross-platform mapping.
282
- * @param amount The blur amount value (0-100), will be mapped to Android's 0-25 radius range
283
- */
284
211
  fun setBlurAmount(amount: Float) {
285
212
  currentBlurRadius = mapBlurAmountToRadius(amount)
286
213
  logDebug("setBlurAmount: $amount -> $currentBlurRadius (mapped from 0-100 to 0-25 range)")
@@ -292,10 +219,6 @@ class ReactNativeBlurView : BlurViewGroup {
292
219
  }
293
220
  }
294
221
 
295
- /**
296
- * Set the number of blur rounds.
297
- * @param rounds The number of blur rounds (1-15)
298
- */
299
222
  fun setRounds(rounds: Int) {
300
223
  val blurRounds = rounds.coerceIn(1, 15)
301
224
  currentBlurRounds = blurRounds
@@ -314,7 +237,7 @@ class ReactNativeBlurView : BlurViewGroup {
314
237
  */
315
238
  fun setBlurType(type: String) {
316
239
  currentBlurType = type
317
- val blurType = BlurType.fromString(type, resources.configuration)
240
+ val blurType = BlurType.fromString(type)
318
241
  currentOverlayColor = blurType.overlayColor
319
242
  logDebug("setBlurType: $type -> ${blurType.name}")
320
243
 
@@ -347,58 +270,37 @@ class ReactNativeBlurView : BlurViewGroup {
347
270
  }
348
271
  }
349
272
 
350
- /**
351
- * Set the glass opacity for liquid glass effect.
352
- * @param opacity The opacity value (0.0 to 1.0)
353
- */
354
273
  fun setGlassOpacity(opacity: Float) {
355
274
  glassOpacity = opacity.coerceIn(0.0f, 1.0f)
356
275
  logDebug("setGlassOpacity: $opacity")
357
276
  updateGlassEffect()
358
277
  }
359
278
 
360
- /**
361
- * Set the view type (blur or liquidGlass).
362
- * @param type The view type string
363
- */
364
279
  fun setType(type: String) {
365
280
  viewType = type
366
281
  logDebug("setType: $type")
367
282
  updateViewType()
368
283
  }
369
284
 
370
- /**
371
- * Set the view type (blur or liquidGlass).
372
- * @param isInteractive The view type string
373
- */
374
285
  fun setIsInteractive(isInteractive: Boolean) {
375
286
  logDebug("setType: $isInteractive")
376
287
  }
377
288
 
378
- /**
379
- * Set the glass type for liquid glass effect.
380
- * @param type The glass type string
381
- */
382
289
  fun setGlassType(type: String) {
383
290
  glassType = type
384
291
  logDebug("setGlassType: $type")
385
292
  updateGlassEffect()
386
293
  }
387
294
 
388
- /**
389
- * Update the glass effect based on current glass properties.
390
- */
391
295
  private fun updateGlassEffect() {
392
296
  if (viewType == "liquidGlass") {
393
297
  try {
394
- // Apply glass tint with opacity
395
298
  val glassColor = Color.argb(
396
299
  (glassOpacity * 255).toInt(),
397
300
  Color.red(glassTintColor),
398
301
  Color.green(glassTintColor),
399
302
  Color.blue(glassTintColor)
400
303
  )
401
- // Use QmBlurView's setOverlayColor method
402
304
  super.setOverlayColor(glassColor)
403
305
  logDebug("Applied glass effect: color=$glassColor, opacity=$glassOpacity")
404
306
  } catch (e: Exception) {
@@ -407,16 +309,12 @@ class ReactNativeBlurView : BlurViewGroup {
407
309
  }
408
310
  }
409
311
 
410
- /**
411
- * Update the view type and apply appropriate effects.
412
- */
413
312
  private fun updateViewType() {
414
313
  when (viewType) {
415
314
  "liquidGlass" -> {
416
315
  updateGlassEffect()
417
316
  }
418
317
  "blur" -> {
419
- // Restore original blur overlay color
420
318
  try {
421
319
  super.setBackgroundColor(currentOverlayColor)
422
320
  super.setOverlayColor(currentOverlayColor)
@@ -427,40 +325,90 @@ class ReactNativeBlurView : BlurViewGroup {
427
325
  }
428
326
  }
429
327
 
430
- /**
431
- * Set the border radius from React Native StyleSheet.
432
- * React Native provides values in logical pixels (dp), which we convert for the native view.
433
- * @param radius The border radius value in dp
434
- */
435
328
  fun setBorderRadius(radius: Float) {
436
- currentCornerRadius = radius
329
+ borderRadius = radius
437
330
  logDebug("setBorderRadius: $radius dp")
438
331
  updateCornerRadius()
439
332
  }
440
333
 
441
- /**
442
- * Convert pixels to density-independent pixels and update the corner radius.
443
- * QmBlurView's setCornerRadius expects values in pixels, and React Native already
444
- * provides values in dp, so we need to convert from dp to pixels.
445
- */
334
+ fun setBorderTopLeftRadius(radius: Float) {
335
+ borderTopLeftRadius = radius
336
+ logDebug("setBorderTopLeftRadius: $radius dp")
337
+ updateCornerRadius()
338
+ }
339
+
340
+ fun setBorderTopRightRadius(radius: Float) {
341
+ borderTopRightRadius = radius
342
+ logDebug("setBorderTopRightRadius: $radius dp")
343
+ updateCornerRadius()
344
+ }
345
+
346
+ fun setBorderBottomLeftRadius(radius: Float) {
347
+ borderBottomLeftRadius = radius
348
+ logDebug("setBorderBottomLeftRadius: $radius dp")
349
+ updateCornerRadius()
350
+ }
351
+
352
+ fun setBorderBottomRightRadius(radius: Float) {
353
+ borderBottomRightRadius = radius
354
+ logDebug("setBorderBottomRightRadius: $radius dp")
355
+ updateCornerRadius()
356
+ }
357
+
358
+ private fun convertDpToPx(dp: Float): Float {
359
+ val displayMetrics = context.resources.displayMetrics
360
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics)
361
+ }
362
+
446
363
  private fun updateCornerRadius() {
447
364
  try {
448
- // Convert from dp (React Native) to pixels (Android)
449
- val radiusInPixels = TypedValue.applyDimension(
450
- TypedValue.COMPLEX_UNIT_DIP,
451
- currentCornerRadius,
452
- context.resources.displayMetrics
453
- )
454
-
455
- outlineProvider = object : ViewOutlineProvider() {
456
- override fun getOutline(view: View, outline: Outline?) {
457
- outline?.setRoundRect(0, 0, view.width, view.height, radiusInPixels)
365
+ val baseRadius = convertDpToPx(borderRadius)
366
+ val topLeft = if (borderTopLeftRadius > 0) convertDpToPx(borderTopLeftRadius) else baseRadius
367
+ val topRight = if (borderTopRightRadius > 0) convertDpToPx(borderTopRightRadius) else baseRadius
368
+ val bottomLeft = if (borderBottomLeftRadius > 0) convertDpToPx(borderBottomLeftRadius) else baseRadius
369
+ val bottomRight = if (borderBottomRightRadius > 0) convertDpToPx(borderBottomRightRadius) else baseRadius
370
+
371
+ super.setTopLeftCornerRadius(topLeft)
372
+ super.setTopRightCornerRadius(topRight)
373
+ super.setBottomLeftCornerRadius(bottomLeft)
374
+ super.setBottomRightCornerRadius(bottomRight)
375
+ super.setCornerRadius(baseRadius)
376
+
377
+ val isUniform = topLeft == topRight && topRight == bottomLeft && bottomLeft == bottomRight
378
+
379
+ if (isUniform) {
380
+ outlineProvider = object : ViewOutlineProvider() {
381
+ override fun getOutline(view: View, outline: Outline?) {
382
+ outline?.setRoundRect(0, 0, view.width, view.height, baseRadius)
383
+ }
384
+ }
385
+ } else {
386
+ outlineProvider = object : ViewOutlineProvider() {
387
+ override fun getOutline(view: View, outline: Outline?) {
388
+ val path = Path()
389
+ val radii = floatArrayOf(
390
+ topLeft,
391
+ topLeft,
392
+ topRight,
393
+ topRight,
394
+ bottomRight,
395
+ bottomRight,
396
+ bottomLeft,
397
+ bottomLeft
398
+ )
399
+ path.addRoundRect(0f, 0f, view.width.toFloat(), view.height.toFloat(), radii, Path.Direction.CW)
400
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
401
+ outline?.setPath(path)
402
+ } else {
403
+ @Suppress("DEPRECATION")
404
+ outline?.setConvexPath(path)
405
+ }
406
+ }
458
407
  }
459
408
  }
460
- clipToOutline = true
461
409
 
462
- super.setCornerRadius(radiusInPixels)
463
- logDebug("Updated corner radius: ${currentCornerRadius}dp -> ${radiusInPixels}px")
410
+ clipToOutline = true
411
+ logDebug("Updated corner radius: topLeft=$topLeft, topRight=$topRight, bottomLeft=$bottomLeft, bottomRight=$bottomRight (px)")
464
412
  } catch (e: Exception) {
465
413
  logError("Failed to update corner radius: ${e.message}", e)
466
414
  }
@@ -495,22 +443,5 @@ class ReactNativeBlurView : BlurViewGroup {
495
443
  */
496
444
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
497
445
  // No-op: Layout is handled by React Native's UIManager.
498
- // We override this to prevent the superclass (BlurViewGroup/FrameLayout) from
499
- // re-positioning children based on its own logic (e.g. gravity), which would
500
- // conflict with React Native's layout.
501
- }
502
-
503
- /**
504
- * Handle configuration changes, such as dark mode or orientation changes.
505
- * This ensures the blur view updates its overlay color based on the new
506
- * configuration.
507
- */
508
- override fun onConfigurationChanged(newConfig: Configuration) {
509
- super.onConfigurationChanged(newConfig)
510
-
511
- if (viewType == "blur") {
512
- // Re-apply blur type to update overlay color based on new configuration
513
- setBlurType(currentBlurType)
514
- }
515
446
  }
516
447
  }
@@ -49,6 +49,26 @@ class ReactNativeBlurViewManager : ViewGroupManager<ReactNativeBlurView>(),
49
49
  view?.setBorderRadius(borderRadius)
50
50
  }
51
51
 
52
+ @ReactProp(name = "borderTopLeftRadius")
53
+ override fun setBorderTopLeftRadius(view: ReactNativeBlurView?, borderTopLeftRadius: Float) {
54
+ view?.setBorderTopLeftRadius(borderTopLeftRadius)
55
+ }
56
+
57
+ @ReactProp(name = "borderTopRightRadius")
58
+ override fun setBorderTopRightRadius(view: ReactNativeBlurView?, borderTopRightRadius: Float) {
59
+ view?.setBorderTopRightRadius(borderTopRightRadius)
60
+ }
61
+
62
+ @ReactProp(name = "borderBottomLeftRadius")
63
+ override fun setBorderBottomLeftRadius(view: ReactNativeBlurView?, borderBottomLeftRadius: Float) {
64
+ view?.setBorderBottomLeftRadius(borderBottomLeftRadius)
65
+ }
66
+
67
+ @ReactProp(name = "borderBottomRightRadius")
68
+ override fun setBorderBottomRightRadius(view: ReactNativeBlurView?, borderBottomRightRadius: Float) {
69
+ view?.setBorderBottomRightRadius(borderBottomRightRadius)
70
+ }
71
+
52
72
  @ReactProp(name = "reducedTransparencyFallbackColor")
53
73
  override fun setReducedTransparencyFallbackColor(view: ReactNativeBlurView?, reducedTransparencyFallbackColor: String?) {
54
74
  // no-op
@@ -1,7 +1,6 @@
1
1
  package com.sbaiahmed1.reactnativeblur
2
2
 
3
3
  import android.content.Context
4
- import android.content.res.Configuration
5
4
  import android.graphics.Canvas
6
5
  import android.graphics.Color
7
6
  import android.graphics.LinearGradient
@@ -397,17 +396,6 @@ class ReactNativeProgressiveBlurView : FrameLayout {
397
396
  cleanup()
398
397
  }
399
398
 
400
- /**
401
- * Handle configuration changes, such as dark mode or orientation changes.
402
- * This ensures the blur view updates its overlay color based on the new
403
- * configuration.
404
- */
405
- override fun onConfigurationChanged(newConfig: Configuration) {
406
- super.onConfigurationChanged(newConfig)
407
-
408
- setBlurType(currentBlurType)
409
- }
410
-
411
399
  /**
412
400
  * Cleanup method to prevent memory leaks.
413
401
  * Resets initialization state so blur is re-initialized on next attach.
@@ -526,7 +514,7 @@ class ReactNativeProgressiveBlurView : FrameLayout {
526
514
  */
527
515
  fun setBlurType(type: String) {
528
516
  currentBlurType = type
529
- val blurType = BlurType.fromString(type, resources.configuration)
517
+ val blurType = BlurType.fromString(type)
530
518
  currentOverlayColor = blurType.overlayColor
531
519
  logDebug("setBlurType: $type -> ${blurType.name} -> ${Integer.toHexString(currentOverlayColor)}")
532
520
 
@@ -55,6 +55,23 @@ func blurStyleFromString(_ styleString: String) -> UIBlurEffect.Style {
55
55
  }
56
56
  }
57
57
 
58
+ /// Determines the fixed interface style for a blur type to prevent system adaptation.
59
+ /// Returns nil for ambiguous styles that should inherit from the system.
60
+ func interfaceStyleForBlurType(_ styleString: String) -> UIUserInterfaceStyle? {
61
+ switch styleString {
62
+ case "xlight", "light",
63
+ "systemUltraThinMaterialLight", "systemThinMaterialLight",
64
+ "systemMaterialLight", "systemThickMaterialLight", "systemChromeMaterialLight":
65
+ return .light
66
+ case "dark", "extraDark",
67
+ "systemUltraThinMaterialDark", "systemThinMaterialDark",
68
+ "systemMaterialDark", "systemThickMaterialDark", "systemChromeMaterialDark":
69
+ return .dark
70
+ default:
71
+ return nil
72
+ }
73
+ }
74
+
58
75
  /// Maps string glass type names to Glass effect values (iOS 26.0+)
59
76
  #if compiler(>=6.2)
60
77
  @available(iOS 26.0, *)
@@ -49,7 +49,6 @@ import UIKit
49
49
  }
50
50
 
51
51
  private func setupHostingController() {
52
- // Completely remove old hosting controller
53
52
  if let oldHosting = hostingController {
54
53
  oldHosting.view.removeFromSuperview()
55
54
  oldHosting.removeFromParent()
@@ -68,8 +67,10 @@ import UIKit
68
67
  hosting.view.backgroundColor = .clear
69
68
  hosting.view.translatesAutoresizingMaskIntoConstraints = false
70
69
 
71
- // Insert at index 0 to ensure it stays behind any potential subviews (though usually this view has no children)
72
- // This fixes the z-ordering bug where blur covers content
70
+ let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
71
+ overrideUserInterfaceStyle = interfaceStyle
72
+ hosting.overrideUserInterfaceStyle = interfaceStyle
73
+
73
74
  if !subviews.isEmpty {
74
75
  insertSubview(hosting.view, at: 0)
75
76
  } else {
@@ -87,9 +88,11 @@ import UIKit
87
88
  }
88
89
 
89
90
  private func updateView() {
91
+ let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
92
+ overrideUserInterfaceStyle = interfaceStyle
93
+
90
94
  if let hosting = hostingController {
91
- // Update the existing controller's root view to avoid expensive recreation
92
- // This fixes performance bottlenecks and state synchronization issues
95
+ hosting.overrideUserInterfaceStyle = interfaceStyle
93
96
  let blurStyle = blurStyleFromString(blurTypeString)
94
97
  let swiftUIView = BasicColoredView(
95
98
  blurAmount: blurAmount,
@@ -3,17 +3,12 @@
3
3
  import SwiftUI
4
4
  import UIKit
5
5
 
6
- // MARK: - SwiftUI View Component for Blur
7
-
8
6
  struct BasicColoredView: View {
9
7
  let blurAmount: Double
10
8
  let blurStyle: UIBlurEffect.Style
11
- let reducedTransparencyFallbackColor: UIColor
12
9
  let blurIntensity: Double
13
10
  let ignoreSafeArea: Bool
14
11
 
15
- let isReducedTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
16
-
17
12
  init(blurAmount: Double,
18
13
  blurStyle: UIBlurEffect.Style,
19
14
  ignoreSafeArea: Bool,
@@ -21,28 +16,15 @@ struct BasicColoredView: View {
21
16
  self.blurAmount = blurAmount
22
17
  self.blurStyle = blurStyle
23
18
  self.ignoreSafeArea = ignoreSafeArea
24
- self.reducedTransparencyFallbackColor = reducedTransparencyFallbackColor
25
19
  self.blurIntensity = mapBlurAmountToIntensity(blurAmount)
26
20
  }
27
21
 
28
22
  var body: some View {
29
- content
23
+ regularBlurView
30
24
  .ignoresSafeArea(ignoreSafeArea ? .all : [])
31
25
  }
32
26
 
33
- private var content: some View {
34
- if isReducedTransparencyEnabled {
35
- AnyView(
36
- Rectangle()
37
- .fill(Color(reducedTransparencyFallbackColor))
38
- )
39
- } else {
40
- AnyView(regularBlurView)
41
- }
42
- }
43
-
44
27
  private var regularBlurView: some View {
45
- // Use proper blur intensity control for regular blur
46
28
  Rectangle()
47
29
  .fill(Color(.clear))
48
30
  .background(Blur(style: blurStyle, intensity: blurIntensity))
@@ -9,6 +9,7 @@ class BlurEffectView: UIVisualEffectView {
9
9
  private var animator: UIViewPropertyAnimator?
10
10
  private var blurStyle: UIBlurEffect.Style = .systemMaterial
11
11
  private var intensity: Double = 1.0
12
+ private var currentEffectStyle: UIBlurEffect.Style?
12
13
 
13
14
  override init(effect: UIVisualEffect?) {
14
15
  super.init(effect: effect)
@@ -21,26 +22,29 @@ class BlurEffectView: UIVisualEffectView {
21
22
  }
22
23
 
23
24
  func updateBlur(style: UIBlurEffect.Style, intensity: Double) {
24
- // Skip expensive animator recreation when nothing changed.
25
- // During FlashList recycling, updateUIView fires on every layout pass
26
- // even when props are identical, causing jank (issue #100).
27
25
  guard style != self.blurStyle || intensity != self.intensity else { return }
28
26
  self.blurStyle = style
29
27
  self.intensity = intensity
30
- setupBlur()
31
- }
32
28
 
33
- override func didMoveToWindow() {
34
- super.didMoveToWindow()
35
- guard window != nil else { return }
36
- // UIKit resumes paused CAAnimations when a view re-joins a window
37
- // (e.g. after modal dismiss + re-present). If the animation plays
38
- // toward its end state the blur drifts to full intensity. Re-pause
39
- // and re-set the fraction here to lock it back to our intended value.
40
- // pausesOnCompletion = true (set in setupBlur) ensures the animator
41
- // stays .active even if it reaches fraction 1.0, so this is always safe.
42
- animator?.pauseAnimation()
43
- animator?.fractionComplete = intensity
29
+ if intensity == 1.0 {
30
+ animator?.stopAnimation(true)
31
+ animator = nil
32
+ currentEffectStyle = style
33
+ effect = UIBlurEffect(style: style)
34
+ } else if intensity == 0.0 {
35
+ animator?.stopAnimation(true)
36
+ animator = nil
37
+ currentEffectStyle = nil
38
+ effect = nil
39
+ } else {
40
+ if let existing = animator,
41
+ (existing.state == .active || existing.state == .inactive),
42
+ currentEffectStyle == style {
43
+ existing.fractionComplete = intensity
44
+ } else {
45
+ setupBlur()
46
+ }
47
+ }
44
48
  }
45
49
 
46
50
  private func setupBlur() {
@@ -50,14 +54,12 @@ class BlurEffectView: UIVisualEffectView {
50
54
  animator = nil
51
55
 
52
56
  effect = nil
57
+ currentEffectStyle = blurStyle
53
58
 
54
59
  let newAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear)
55
60
  newAnimator.addAnimations { [weak self] in
56
61
  self?.effect = UIBlurEffect(style: self?.blurStyle ?? .systemMaterial)
57
62
  }
58
- // pausesOnCompletion: if UIKit ever resumes and runs this to the end,
59
- // the animator stays .active (paused at 1.0) instead of going .inactive.
60
- // This guarantees didMoveToWindow can always call pauseAnimation() safely.
61
63
  newAnimator.pausesOnCompletion = true
62
64
  newAnimator.startAnimation()
63
65
  newAnimator.pauseAnimation()
@@ -108,14 +108,12 @@ import UIKit
108
108
  }
109
109
 
110
110
  private func updateFallback() {
111
- // If reduce transparency is enabled, show solid color
112
111
  if UIAccessibility.isReduceTransparencyEnabled {
113
112
  backgroundColor = reducedTransparencyFallbackColor
114
113
  glassEffectView?.effect = nil
115
114
  } else {
116
115
  backgroundColor = .clear
117
-
118
- // Map glass types to blur styles for fallback
116
+
119
117
  let style: UIBlurEffect.Style
120
118
  switch glassType {
121
119
  case "regular":
@@ -125,14 +123,13 @@ import UIKit
125
123
  default:
126
124
  style = .regular
127
125
  }
128
-
126
+
129
127
  let effect = UIBlurEffect(style: style)
130
128
  glassEffectView?.effect = effect
131
-
132
- // Clear any background color on content view
129
+
133
130
  glassEffectView?.contentView.backgroundColor = .clear
134
131
  }
135
-
132
+
136
133
  layer.cornerRadius = allBorderRadius
137
134
  glassEffectView?.layer.cornerRadius = allBorderRadius
138
135
  glassEffectView?.layer.masksToBounds = true
@@ -49,7 +49,6 @@ import UIKit
49
49
  }
50
50
 
51
51
  private func setupView() {
52
- // Remove old view if exists
53
52
  variableBlurView?.removeFromSuperview()
54
53
 
55
54
  let blurStyle = blurStyleFromString(blurTypeString)
@@ -74,7 +73,10 @@ import UIKit
74
73
 
75
74
  self.variableBlurView = variableBlur
76
75
 
77
- // Handle reduced transparency
76
+ let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
77
+ overrideUserInterfaceStyle = interfaceStyle
78
+ variableBlur.overrideUserInterfaceStyle = interfaceStyle
79
+
78
80
  if UIAccessibility.isReduceTransparencyEnabled {
79
81
  variableBlur.isHidden = true
80
82
  backgroundColor = reducedTransparencyFallbackColor
@@ -100,7 +102,10 @@ import UIKit
100
102
  blurStyle: blurStyle
101
103
  )
102
104
 
103
- // Handle reduced transparency
105
+ let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified
106
+ overrideUserInterfaceStyle = interfaceStyle
107
+ variableBlurView.overrideUserInterfaceStyle = interfaceStyle
108
+
104
109
  if UIAccessibility.isReduceTransparencyEnabled {
105
110
  variableBlurView.isHidden = true
106
111
  backgroundColor = reducedTransparencyFallbackColor
@@ -113,17 +113,6 @@ open class VariableBlurView: UIVisualEffectView {
113
113
  }
114
114
  }
115
115
 
116
- open override func traitCollectionDidChange(
117
- _ previousTraitCollection: UITraitCollection?
118
- ) {
119
- super.traitCollectionDidChange(previousTraitCollection)
120
- // Re-setup blur if needed when trait collection changes
121
- if let previousTraitCollection = previousTraitCollection,
122
- traitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle {
123
- setupVariableBlur()
124
- }
125
- }
126
-
127
116
  private func makeGradientImage(
128
117
  width: CGFloat = 100,
129
118
  height: CGFloat = 100,
@@ -53,14 +53,15 @@ import UIKit
53
53
  }
54
54
 
55
55
  private func updateEffect() {
56
- // Clean up existing animator
56
+ let interfaceStyle = interfaceStyleForBlurType(blurType) ?? .unspecified
57
+ overrideUserInterfaceStyle = interfaceStyle
58
+
57
59
  if let animator = blurAnimator {
58
60
  animator.stopAnimation(true)
59
61
  animator.finishAnimation(at: .current)
60
62
  }
61
63
  blurAnimator = nil
62
64
 
63
- // Reset effects
64
65
  blurEffectView.effect = nil
65
66
  vibrancyEffectView.effect = nil
66
67
 
@@ -68,28 +69,18 @@ import UIKit
68
69
  let blurEffect = UIBlurEffect(style: style)
69
70
  let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
70
71
 
71
- // Set effects directly first to ensure they are visible
72
- // Animating them from nil often causes issues with UIVibrancyEffect
73
72
  blurEffectView.effect = blurEffect
74
73
  vibrancyEffectView.effect = vibrancyEffect
75
74
 
76
- // Create animator to adjust intensity
77
75
  blurAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [weak self] in
78
76
  self?.blurEffectView.effect = nil
79
77
  self?.vibrancyEffectView.effect = nil
80
78
  }
81
79
 
82
- // Convert blurAmount (0-100) to intensity (0.0-1.0)
83
- // We reverse the logic:
84
- // fractionComplete = 0.0 -> effects are fully applied (start state)
85
- // fractionComplete = 1.0 -> effects are removed (end state)
86
- // So to get desired intensity X, we set fractionComplete to (1 - X)
87
80
  let intensity = min(max(blurAmount / 100.0, 0.0), 1.0)
88
81
  blurAnimator?.fractionComplete = 1.0 - intensity
89
82
 
90
- // Stop the animation at the current state
91
83
  DispatchQueue.main.async { [weak self, weak blurAnimator] in
92
- // Only stop the animator if it's still the current one
93
84
  guard let self = self, let currentAnimator = self.blurAnimator, currentAnimator === blurAnimator else { return }
94
85
 
95
86
  currentAnimator.stopAnimation(true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sbaiahmed1/react-native-blur",
3
- "version": "4.6.2",
3
+ "version": "4.6.3-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",