From 2c7b0a50b570fa180a6ec5409f0359657935360d Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Mon, 25 May 2026 17:43:32 +0100 Subject: [PATCH 1/3] fix: optimize blur animation handling for full and no blur states --- ios/Views/BlurEffectView.swift | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ios/Views/BlurEffectView.swift b/ios/Views/BlurEffectView.swift index 4641e04..44a0279 100644 --- a/ios/Views/BlurEffectView.swift +++ b/ios/Views/BlurEffectView.swift @@ -21,26 +21,29 @@ class BlurEffectView: UIVisualEffectView { } func updateBlur(style: UIBlurEffect.Style, intensity: Double) { - // Skip expensive animator recreation when nothing changed. - // During FlashList recycling, updateUIView fires on every layout pass - // even when props are identical, causing jank (issue #100). guard style != self.blurStyle || intensity != self.intensity else { return } self.blurStyle = style self.intensity = intensity - setupBlur() - } - override func didMoveToWindow() { - super.didMoveToWindow() - guard window != nil else { return } - // UIKit resumes paused CAAnimations when a view re-joins a window - // (e.g. after modal dismiss + re-present). If the animation plays - // toward its end state the blur drifts to full intensity. Re-pause - // and re-set the fraction here to lock it back to our intended value. - // pausesOnCompletion = true (set in setupBlur) ensures the animator - // stays .active even if it reaches fraction 1.0, so this is always safe. - animator?.pauseAnimation() - animator?.fractionComplete = intensity + if intensity == 1.0 { + // Fast path: full blur, skip animator entirely + animator?.stopAnimation(true) + animator = nil + effect = UIBlurEffect(style: style) + } else if intensity == 0.0 { + // Fast path: no blur + animator?.stopAnimation(true) + animator = nil + effect = nil + } else { + // Reuse existing animator if possible, only recreate if style changed + if let existing = animator, + existing.state == .active || existing.state == .inactive { + existing.fractionComplete = intensity + } else { + setupBlur() + } + } } private func setupBlur() { @@ -55,9 +58,6 @@ class BlurEffectView: UIVisualEffectView { newAnimator.addAnimations { [weak self] in self?.effect = UIBlurEffect(style: self?.blurStyle ?? .systemMaterial) } - // pausesOnCompletion: if UIKit ever resumes and runs this to the end, - // the animator stays .active (paused at 1.0) instead of going .inactive. - // This guarantees didMoveToWindow can always call pauseAnimation() safely. newAnimator.pausesOnCompletion = true newAnimator.startAnimation() newAnimator.pauseAnimation() From 261c6ec99d6b7962a83b4c88d0b7eb5d297a9617 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Thu, 28 May 2026 19:08:12 +0100 Subject: [PATCH 2/3] fix: streamline blur type handling by removing dark mode checks and optimizing enum values --- .../sbaiahmed1/reactnativeblur/BlurType.kt | 41 ++--- .../reactnativeblur/ReactNativeBlurView.kt | 140 +----------------- .../ReactNativeProgressiveBlurView.kt | 14 +- example/package.json | 2 +- ios/Helpers/BlurStyleHelpers.swift | 17 +++ ios/Views/AdvancedBlurView.swift | 13 +- ios/Views/BasicColoredView.swift | 20 +-- ios/Views/LiquidGlassContainerView.swift | 11 +- ios/Views/ProgressiveBlurView.swift | 11 +- ios/Views/VariableBlurView.swift | 11 -- ios/Views/VibrancyEffectView.swift | 15 +- 11 files changed, 60 insertions(+), 235 deletions(-) diff --git a/android/src/main/java/com/sbaiahmed1/reactnativeblur/BlurType.kt b/android/src/main/java/com/sbaiahmed1/reactnativeblur/BlurType.kt index 0540113..c9e70a3 100644 --- a/android/src/main/java/com/sbaiahmed1/reactnativeblur/BlurType.kt +++ b/android/src/main/java/com/sbaiahmed1/reactnativeblur/BlurType.kt @@ -1,64 +1,55 @@ package com.sbaiahmed1.reactnativeblur -import android.content.res.Configuration import android.graphics.Color -/** - * Enum representing different blur types with their corresponding overlay colors. - * Maps iOS blur types to Android overlay colors to approximate the visual appearance. - */ enum class BlurType(val overlayColor: Int) { XLIGHT(Color.argb(140, 240, 240, 240)), LIGHT(Color.argb(42, 255, 255, 255)), DARK(Color.argb(120, 26, 22, 22)), EXTRA_DARK(Color.argb(160, 35, 35, 35)), - REGULAR_LIGHT(Color.argb(35, 255, 255, 255)), - REGULAR_DARK(Color.argb(35, 28, 28, 30)), - PROMINENT_LIGHT(Color.argb(140, 240, 240, 240)), - PROMINENT_DARK(Color.argb(140, 28, 28, 30)), + REGULAR(Color.argb(35, 255, 255, 255)), + PROMINENT(Color.argb(140, 240, 240, 240)), + SYSTEM_ULTRA_THIN_MATERIAL(Color.argb(75, 240, 240, 240)), SYSTEM_ULTRA_THIN_MATERIAL_LIGHT(Color.argb(75, 240, 240, 240)), SYSTEM_ULTRA_THIN_MATERIAL_DARK(Color.argb(65, 40, 40, 40)), + SYSTEM_THIN_MATERIAL(Color.argb(102, 240, 240, 240)), SYSTEM_THIN_MATERIAL_LIGHT(Color.argb(102, 240, 240, 240)), SYSTEM_THIN_MATERIAL_DARK(Color.argb(102, 35, 35, 35)), + SYSTEM_MATERIAL(Color.argb(140, 245, 245, 245)), SYSTEM_MATERIAL_LIGHT(Color.argb(140, 245, 245, 245)), SYSTEM_MATERIAL_DARK(Color.argb(215, 65, 60, 60)), + SYSTEM_THICK_MATERIAL(Color.argb(210, 248, 248, 248)), SYSTEM_THICK_MATERIAL_LIGHT(Color.argb(210, 248, 248, 248)), SYSTEM_THICK_MATERIAL_DARK(Color.argb(160, 35, 35, 35)), + SYSTEM_CHROME_MATERIAL(Color.argb(165, 248, 248, 248)), SYSTEM_CHROME_MATERIAL_LIGHT(Color.argb(165, 248, 248, 248)), SYSTEM_CHROME_MATERIAL_DARK(Color.argb(100, 32, 32, 32)); companion object { - /** - * Get BlurType from string, with fallback to LIGHT for unknown types. - * Uses the provided configuration to determine if dark mode is active for - * appropriate defaults. - */ - fun fromString(type: String, configuration: Configuration): BlurType { - val isDarkMode = (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - + fun fromString(type: String): BlurType { return when (type.lowercase()) { "xlight" -> XLIGHT "light" -> LIGHT "dark" -> DARK "extradark" -> EXTRA_DARK - "regular" -> if (isDarkMode) REGULAR_DARK else REGULAR_LIGHT - "prominent" -> if (isDarkMode) PROMINENT_DARK else PROMINENT_LIGHT - "systemultrathinmaterial" -> if (isDarkMode) SYSTEM_ULTRA_THIN_MATERIAL_DARK else SYSTEM_ULTRA_THIN_MATERIAL_LIGHT + "regular" -> REGULAR + "prominent" -> PROMINENT + "systemultrathinmaterial" -> SYSTEM_ULTRA_THIN_MATERIAL "systemultrathinmateriallight" -> SYSTEM_ULTRA_THIN_MATERIAL_LIGHT "systemultrathinmaterialdark" -> SYSTEM_ULTRA_THIN_MATERIAL_DARK - "systemthinmaterial" -> if (isDarkMode) SYSTEM_THIN_MATERIAL_DARK else SYSTEM_THIN_MATERIAL_LIGHT + "systemthinmaterial" -> SYSTEM_THIN_MATERIAL "systemthinmateriallight" -> SYSTEM_THIN_MATERIAL_LIGHT "systemthinmaterialdark" -> SYSTEM_THIN_MATERIAL_DARK - "systemmaterial" -> if (isDarkMode) SYSTEM_MATERIAL_DARK else SYSTEM_MATERIAL_LIGHT + "systemmaterial" -> SYSTEM_MATERIAL "systemmateriallight" -> SYSTEM_MATERIAL_LIGHT "systemmaterialdark" -> SYSTEM_MATERIAL_DARK - "systemthickmaterial" -> if (isDarkMode) SYSTEM_THICK_MATERIAL_DARK else SYSTEM_THICK_MATERIAL_LIGHT + "systemthickmaterial" -> SYSTEM_THICK_MATERIAL "systemthickmateriallight" -> SYSTEM_THICK_MATERIAL_LIGHT "systemthickmaterialdark" -> SYSTEM_THICK_MATERIAL_DARK - "systemchromematerial" -> if (isDarkMode) SYSTEM_CHROME_MATERIAL_DARK else SYSTEM_CHROME_MATERIAL_LIGHT + "systemchromematerial" -> SYSTEM_CHROME_MATERIAL "systemchromemateriallight" -> SYSTEM_CHROME_MATERIAL_LIGHT "systemchromematerialdark" -> SYSTEM_CHROME_MATERIAL_DARK - else -> XLIGHT // default fallback + else -> XLIGHT } } } diff --git a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt index 8c48145..c9a8fe7 100644 --- a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt +++ b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt @@ -1,7 +1,6 @@ package com.sbaiahmed1.reactnativeblur import android.content.Context -import android.content.res.Configuration import android.graphics.Color import android.graphics.Outline import android.graphics.Path @@ -54,7 +53,6 @@ class ReactNativeBlurView : BlurViewGroup { private const val DEFAULT_BLUR_ROUNDS = 5 private const val DEBUG = false - // Cross-platform blur amount constants private const val MIN_BLUR_AMOUNT = 0f private const val MAX_BLUR_AMOUNT = 100f @@ -72,12 +70,6 @@ class ReactNativeBlurView : BlurViewGroup { Log.e(TAG, message, throwable) } - /** - * Maps blur amount (0-100) to Android blur radius (0-25). - * This ensures cross-platform consistency while respecting Android's limitations. - * @param amount The blur amount from 0-100 - * @return The corresponding blur radius from 0-25 - */ private fun mapBlurAmountToRadius(amount: Float): Float { val clampedAmount = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT) return (clampedAmount / MAX_BLUR_AMOUNT) * MAX_BLUR_RADIUS @@ -92,11 +84,6 @@ class ReactNativeBlurView : BlurViewGroup { setupView() } - /** - * Initial view setup in constructor - only sets up visual defaults. - * Blur initialization is deferred to onAttachedToWindow to ensure the - * view hierarchy is fully mounted, preventing flickering and wrong frame capture. - */ private fun setupView() { super.setBackgroundColor(currentOverlayColor) clipChildren = true @@ -105,45 +92,24 @@ class ReactNativeBlurView : BlurViewGroup { super.setDownsampleFactor(6.0F) } - /** - * Called when the view is attached to a window. - * After QmBlurView's onAttachedToWindow sets the decor view as blur root, - * we use reflection to redirect it to the nearest Screen ancestor. - * This scopes the blur capture to just the current screen, preventing - * navigation transition artifacts. - */ override fun onAttachedToWindow() { super.onAttachedToWindow() if (isBlurInitialized) return - // Immediately try to swap blur root and initialize. - // We avoid posting a runnable to prevent the 1-second delay artifact. - // If the parent hierarchy is not ready yet (unlikely in onAttachedToWindow), - // we could fall back to post, but for now we prioritize immediate execution. swapBlurRootToScreenAncestor() initializeBlur() } - /** - * Uses reflection to redirect QmBlurView's internal blur capture root - * from the activity decor view to the nearest react-native-screens Screen ancestor. - * - * Reflection path: BlurViewGroup.mBaseBlurViewGroup -> BaseBlurViewGroup.mDecorView - * Also moves the OnPreDrawListener from the old root to the new one. - */ private fun swapBlurRootToScreenAncestor() { - // Pinned to QmBlurView 1.1.4 – depends on: mBaseBlurViewGroup, mDecorView, preDrawListener, mDifferentRoot, mForceRedraw val newRoot = findOptimalBlurRoot() ?: return try { - // Step 1: Get BlurViewGroup's private mBaseBlurViewGroup field val blurViewGroupClass = BlurViewGroup::class.java val baseField = blurViewGroupClass.getDeclaredField("mBaseBlurViewGroup") baseField.isAccessible = true val baseBlurViewGroup = baseField.get(this) ?: return - // Step 2: Get BaseBlurViewGroup's private fields val baseClass = BaseBlurViewGroup::class.java val decorViewField = baseClass.getDeclaredField("mDecorView") @@ -162,25 +128,20 @@ class ReactNativeBlurView : BlurViewGroup { } if (preDrawListener != null && oldDecorView != null) { - // Step 3: Remove listener from old root's ViewTreeObserver try { oldDecorView.viewTreeObserver.removeOnPreDrawListener(preDrawListener) } catch (e: Exception) { logDebug("Could not remove old pre-draw listener: ${e.message}") } - // Step 4: Set new root as mDecorView decorViewField.set(baseBlurViewGroup, newRoot) - // Step 5: Add listener to new root's ViewTreeObserver newRoot.viewTreeObserver.addOnPreDrawListener(preDrawListener) - // Step 6: Update mDifferentRoot flag val differentRootField = baseClass.getDeclaredField("mDifferentRoot") differentRootField.isAccessible = true differentRootField.setBoolean(baseBlurViewGroup, newRoot.rootView != this.rootView) - // Step 7: Force a redraw val forceRedrawField = baseClass.getDeclaredField("mForceRedraw") forceRedrawField.isAccessible = true forceRedrawField.setBoolean(baseBlurViewGroup, true) @@ -194,27 +155,10 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Finds the optimal view to use as blur capture root. - * - * Priority: - * 1. Nearest react-native-screens Screen ancestor — scopes blur to the current - * screen and prevents capturing navigation transition artifacts. - * 2. Nearest ReactRootView ancestor — scopes blur to the React Native root when - * the component is not inside a Screen (e.g. plain View hierarchies). Without - * this fallback, QmBlurView defaults to the activity decor view and blurs the - * entire screen instead of just the component area (issue #89). - * 3. null — returned for modals, which intentionally need to blur content from - * the main activity window (decor view is correct there). - */ private fun findOptimalBlurRoot(): ViewGroup? { return findNearestScreenAncestor() ?: findNearestReactRootView() } - /** - * Walks up the view hierarchy looking for react-native-screens Screen components - * using class name detection to avoid hard dependencies on react-native-screens. - */ private fun findNearestScreenAncestor(): ViewGroup? { var currentParent = this.parent while (currentParent != null) { @@ -226,11 +170,6 @@ class ReactNativeBlurView : BlurViewGroup { return null } - /** - * Walks up the view hierarchy looking for the React Native root view. - * Used as a fallback when no Screen ancestor exists, to scope the blur - * capture to the RN root rather than the full activity decor view. - */ private fun findNearestReactRootView(): ViewGroup? { var currentParent = this.parent while (currentParent != null) { @@ -242,11 +181,6 @@ class ReactNativeBlurView : BlurViewGroup { return null } - /** - * Initialize the blur view with current settings. - * Called after the view is attached and the blur root has been swapped. - * Guarded by isBlurInitialized to prevent duplicate setup. - */ private fun initializeBlur() { if (isBlurInitialized) return @@ -262,20 +196,11 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Called when the view is detached from a window. - * Performs cleanup to prevent memory leaks and resets initialization state - * so blur is re-initialized on next attach (e.g. navigation transitions). - */ override fun onDetachedFromWindow() { super.onDetachedFromWindow() cleanup() } - /** - * Cleanup method to reset state. - * Helps prevent memory leaks and ensures clean state. - */ fun cleanup() { isBlurInitialized = false initRunnable?.let { removeCallbacks(it) } @@ -283,10 +208,6 @@ class ReactNativeBlurView : BlurViewGroup { logDebug("View cleaned up") } - /** - * Set the blur amount with cross-platform mapping. - * @param amount The blur amount value (0-100), will be mapped to Android's 0-25 radius range - */ fun setBlurAmount(amount: Float) { currentBlurRadius = mapBlurAmountToRadius(amount) logDebug("setBlurAmount: $amount -> $currentBlurRadius (mapped from 0-100 to 0-25 range)") @@ -298,10 +219,6 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Set the number of blur rounds. - * @param rounds The number of blur rounds (1-15) - */ fun setRounds(rounds: Int) { val blurRounds = rounds.coerceIn(1, 15) currentBlurRounds = blurRounds @@ -320,7 +237,7 @@ class ReactNativeBlurView : BlurViewGroup { */ fun setBlurType(type: String) { currentBlurType = type - val blurType = BlurType.fromString(type, resources.configuration) + val blurType = BlurType.fromString(type) currentOverlayColor = blurType.overlayColor logDebug("setBlurType: $type -> ${blurType.name}") @@ -334,9 +251,6 @@ class ReactNativeBlurView : BlurViewGroup { /** * Set the glass tint color for liquid glass effect. - * @param color The color string in hex format (e.g., "#FF0000") or null to clear - */ - fun setGlassTintColor(color: String?) { color?.let { try { glassTintColor = it.toColorInt() @@ -353,58 +267,37 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Set the glass opacity for liquid glass effect. - * @param opacity The opacity value (0.0 to 1.0) - */ fun setGlassOpacity(opacity: Float) { glassOpacity = opacity.coerceIn(0.0f, 1.0f) logDebug("setGlassOpacity: $opacity") updateGlassEffect() } - /** - * Set the view type (blur or liquidGlass). - * @param type The view type string - */ fun setType(type: String) { viewType = type logDebug("setType: $type") updateViewType() } - /** - * Set the view type (blur or liquidGlass). - * @param isInteractive The view type string - */ fun setIsInteractive(isInteractive: Boolean) { logDebug("setType: $isInteractive") } - /** - * Set the glass type for liquid glass effect. - * @param type The glass type string - */ fun setGlassType(type: String) { glassType = type logDebug("setGlassType: $type") updateGlassEffect() } - /** - * Update the glass effect based on current glass properties. - */ private fun updateGlassEffect() { if (viewType == "liquidGlass") { try { - // Apply glass tint with opacity val glassColor = Color.argb( (glassOpacity * 255).toInt(), Color.red(glassTintColor), Color.green(glassTintColor), Color.blue(glassTintColor) ) - // Use QmBlurView's setOverlayColor method super.setOverlayColor(glassColor) logDebug("Applied glass effect: color=$glassColor, opacity=$glassOpacity") } catch (e: Exception) { @@ -413,16 +306,12 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Update the view type and apply appropriate effects. - */ private fun updateViewType() { when (viewType) { "liquidGlass" -> { updateGlassEffect() } "blur" -> { - // Restore original blur overlay color try { super.setBackgroundColor(currentOverlayColor) super.setOverlayColor(currentOverlayColor) @@ -433,11 +322,6 @@ class ReactNativeBlurView : BlurViewGroup { } } - /** - * Set the border radius from React Native StyleSheet. - * React Native provides values in logical pixels (dp), which we convert for the native view. - * @param radius The border radius value in dp - */ fun setBorderRadius(radius: Float) { borderRadius = radius logDebug("setBorderRadius: $radius dp") @@ -468,11 +352,6 @@ class ReactNativeBlurView : BlurViewGroup { updateCornerRadius() } - /** - * Convert pixels to density-independent pixels and update the corner radius. - * QmBlurView's setCornerRadius expects values in pixels, and React Native already - * provides values in dp, so we need to convert from dp to pixels. - */ private fun convertDpToPx(dp: Float): Float { val displayMetrics = context.resources.displayMetrics return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics) @@ -561,22 +440,5 @@ class ReactNativeBlurView : BlurViewGroup { */ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { // No-op: Layout is handled by React Native's UIManager. - // We override this to prevent the superclass (BlurViewGroup/FrameLayout) from - // re-positioning children based on its own logic (e.g. gravity), which would - // conflict with React Native's layout. - } - - /** - * Handle configuration changes, such as dark mode or orientation changes. - * This ensures the blur view updates its overlay color based on the new - * configuration. - */ - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - if (viewType == "blur") { - // Re-apply blur type to update overlay color based on new configuration - setBlurType(currentBlurType) - } } } diff --git a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt index dff40cb..0f078ca 100644 --- a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt +++ b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt @@ -1,7 +1,6 @@ package com.sbaiahmed1.reactnativeblur import android.content.Context -import android.content.res.Configuration import android.graphics.Canvas import android.graphics.Color import android.graphics.LinearGradient @@ -397,17 +396,6 @@ class ReactNativeProgressiveBlurView : FrameLayout { cleanup() } - /** - * Handle configuration changes, such as dark mode or orientation changes. - * This ensures the blur view updates its overlay color based on the new - * configuration. - */ - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - setBlurType(currentBlurType) - } - /** * Cleanup method to prevent memory leaks. * Resets initialization state so blur is re-initialized on next attach. @@ -526,7 +514,7 @@ class ReactNativeProgressiveBlurView : FrameLayout { */ fun setBlurType(type: String) { currentBlurType = type - val blurType = BlurType.fromString(type, resources.configuration) + val blurType = BlurType.fromString(type) currentOverlayColor = blurType.overlayColor logDebug("setBlurType: $type -> ${blurType.name} -> ${Integer.toHexString(currentOverlayColor)}") diff --git a/example/package.json b/example/package.json index f184dff..2131fb0 100644 --- a/example/package.json +++ b/example/package.json @@ -7,7 +7,7 @@ "start": "expo start", "android": "expo run:android", "prebuild:ios": "expo prebuild --platform ios --clean", - "ios": "expo run:ios", + "ios": "expo run:ios --device", "web": "expo start --web", "lint": "expo lint", "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", diff --git a/ios/Helpers/BlurStyleHelpers.swift b/ios/Helpers/BlurStyleHelpers.swift index f355016..4c5aa2e 100644 --- a/ios/Helpers/BlurStyleHelpers.swift +++ b/ios/Helpers/BlurStyleHelpers.swift @@ -55,6 +55,23 @@ func blurStyleFromString(_ styleString: String) -> UIBlurEffect.Style { } } +/// Determines the fixed interface style for a blur type to prevent system adaptation. +/// Returns nil for ambiguous styles that should inherit from the system. +func interfaceStyleForBlurType(_ styleString: String) -> UIUserInterfaceStyle? { + switch styleString { + case "xlight", "light", + "systemUltraThinMaterialLight", "systemThinMaterialLight", + "systemMaterialLight", "systemThickMaterialLight", "systemChromeMaterialLight": + return .light + case "dark", "extraDark", + "systemUltraThinMaterialDark", "systemThinMaterialDark", + "systemMaterialDark", "systemThickMaterialDark", "systemChromeMaterialDark": + return .dark + default: + return nil + } +} + /// Maps string glass type names to Glass effect values (iOS 26.0+) #if compiler(>=6.2) @available(iOS 26.0, *) diff --git a/ios/Views/AdvancedBlurView.swift b/ios/Views/AdvancedBlurView.swift index 5e080af..cafc0f6 100644 --- a/ios/Views/AdvancedBlurView.swift +++ b/ios/Views/AdvancedBlurView.swift @@ -49,7 +49,6 @@ import UIKit } private func setupHostingController() { - // Completely remove old hosting controller if let oldHosting = hostingController { oldHosting.view.removeFromSuperview() oldHosting.removeFromParent() @@ -68,8 +67,10 @@ import UIKit hosting.view.backgroundColor = .clear hosting.view.translatesAutoresizingMaskIntoConstraints = false - // Insert at index 0 to ensure it stays behind any potential subviews (though usually this view has no children) - // This fixes the z-ordering bug where blur covers content + let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified + overrideUserInterfaceStyle = interfaceStyle + hosting.overrideUserInterfaceStyle = interfaceStyle + if !subviews.isEmpty { insertSubview(hosting.view, at: 0) } else { @@ -87,9 +88,11 @@ import UIKit } private func updateView() { + let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified + overrideUserInterfaceStyle = interfaceStyle + if let hosting = hostingController { - // Update the existing controller's root view to avoid expensive recreation - // This fixes performance bottlenecks and state synchronization issues + hosting.overrideUserInterfaceStyle = interfaceStyle let blurStyle = blurStyleFromString(blurTypeString) let swiftUIView = BasicColoredView( blurAmount: blurAmount, diff --git a/ios/Views/BasicColoredView.swift b/ios/Views/BasicColoredView.swift index e4dc118..fe0220f 100644 --- a/ios/Views/BasicColoredView.swift +++ b/ios/Views/BasicColoredView.swift @@ -3,17 +3,12 @@ import SwiftUI import UIKit -// MARK: - SwiftUI View Component for Blur - struct BasicColoredView: View { let blurAmount: Double let blurStyle: UIBlurEffect.Style - let reducedTransparencyFallbackColor: UIColor let blurIntensity: Double let ignoreSafeArea: Bool - let isReducedTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled - init(blurAmount: Double, blurStyle: UIBlurEffect.Style, ignoreSafeArea: Bool, @@ -21,28 +16,15 @@ struct BasicColoredView: View { self.blurAmount = blurAmount self.blurStyle = blurStyle self.ignoreSafeArea = ignoreSafeArea - self.reducedTransparencyFallbackColor = reducedTransparencyFallbackColor self.blurIntensity = mapBlurAmountToIntensity(blurAmount) } var body: some View { - content + regularBlurView .ignoresSafeArea(ignoreSafeArea ? .all : []) } - private var content: some View { - if isReducedTransparencyEnabled { - AnyView( - Rectangle() - .fill(Color(reducedTransparencyFallbackColor)) - ) - } else { - AnyView(regularBlurView) - } - } - private var regularBlurView: some View { - // Use proper blur intensity control for regular blur Rectangle() .fill(Color(.clear)) .background(Blur(style: blurStyle, intensity: blurIntensity)) diff --git a/ios/Views/LiquidGlassContainerView.swift b/ios/Views/LiquidGlassContainerView.swift index d37ac4b..99652a8 100644 --- a/ios/Views/LiquidGlassContainerView.swift +++ b/ios/Views/LiquidGlassContainerView.swift @@ -108,14 +108,12 @@ import UIKit } private func updateFallback() { - // If reduce transparency is enabled, show solid color if UIAccessibility.isReduceTransparencyEnabled { backgroundColor = reducedTransparencyFallbackColor glassEffectView?.effect = nil } else { backgroundColor = .clear - - // Map glass types to blur styles for fallback + let style: UIBlurEffect.Style switch glassType { case "regular": @@ -125,14 +123,13 @@ import UIKit default: style = .regular } - + let effect = UIBlurEffect(style: style) glassEffectView?.effect = effect - - // Clear any background color on content view + glassEffectView?.contentView.backgroundColor = .clear } - + layer.cornerRadius = allBorderRadius glassEffectView?.layer.cornerRadius = allBorderRadius glassEffectView?.layer.masksToBounds = true diff --git a/ios/Views/ProgressiveBlurView.swift b/ios/Views/ProgressiveBlurView.swift index b237bdb..88986e1 100644 --- a/ios/Views/ProgressiveBlurView.swift +++ b/ios/Views/ProgressiveBlurView.swift @@ -49,7 +49,6 @@ import UIKit } private func setupView() { - // Remove old view if exists variableBlurView?.removeFromSuperview() let blurStyle = blurStyleFromString(blurTypeString) @@ -74,7 +73,10 @@ import UIKit self.variableBlurView = variableBlur - // Handle reduced transparency + let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified + overrideUserInterfaceStyle = interfaceStyle + variableBlur.overrideUserInterfaceStyle = interfaceStyle + if UIAccessibility.isReduceTransparencyEnabled { variableBlur.isHidden = true backgroundColor = reducedTransparencyFallbackColor @@ -100,7 +102,10 @@ import UIKit blurStyle: blurStyle ) - // Handle reduced transparency + let interfaceStyle = interfaceStyleForBlurType(blurTypeString) ?? .unspecified + overrideUserInterfaceStyle = interfaceStyle + variableBlurView.overrideUserInterfaceStyle = interfaceStyle + if UIAccessibility.isReduceTransparencyEnabled { variableBlurView.isHidden = true backgroundColor = reducedTransparencyFallbackColor diff --git a/ios/Views/VariableBlurView.swift b/ios/Views/VariableBlurView.swift index 85fc302..07f287d 100644 --- a/ios/Views/VariableBlurView.swift +++ b/ios/Views/VariableBlurView.swift @@ -113,17 +113,6 @@ open class VariableBlurView: UIVisualEffectView { } } - open override func traitCollectionDidChange( - _ previousTraitCollection: UITraitCollection? - ) { - super.traitCollectionDidChange(previousTraitCollection) - // Re-setup blur if needed when trait collection changes - if let previousTraitCollection = previousTraitCollection, - traitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle { - setupVariableBlur() - } - } - private func makeGradientImage( width: CGFloat = 100, height: CGFloat = 100, diff --git a/ios/Views/VibrancyEffectView.swift b/ios/Views/VibrancyEffectView.swift index 82cd72d..2f87708 100644 --- a/ios/Views/VibrancyEffectView.swift +++ b/ios/Views/VibrancyEffectView.swift @@ -53,14 +53,15 @@ import UIKit } private func updateEffect() { - // Clean up existing animator + let interfaceStyle = interfaceStyleForBlurType(blurType) ?? .unspecified + overrideUserInterfaceStyle = interfaceStyle + if let animator = blurAnimator { animator.stopAnimation(true) animator.finishAnimation(at: .current) } blurAnimator = nil - // Reset effects blurEffectView.effect = nil vibrancyEffectView.effect = nil @@ -68,28 +69,18 @@ import UIKit let blurEffect = UIBlurEffect(style: style) let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect) - // Set effects directly first to ensure they are visible - // Animating them from nil often causes issues with UIVibrancyEffect blurEffectView.effect = blurEffect vibrancyEffectView.effect = vibrancyEffect - // Create animator to adjust intensity blurAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [weak self] in self?.blurEffectView.effect = nil self?.vibrancyEffectView.effect = nil } - // Convert blurAmount (0-100) to intensity (0.0-1.0) - // We reverse the logic: - // fractionComplete = 0.0 -> effects are fully applied (start state) - // fractionComplete = 1.0 -> effects are removed (end state) - // So to get desired intensity X, we set fractionComplete to (1 - X) let intensity = min(max(blurAmount / 100.0, 0.0), 1.0) blurAnimator?.fractionComplete = 1.0 - intensity - // Stop the animation at the current state DispatchQueue.main.async { [weak self, weak blurAnimator] in - // Only stop the animator if it's still the current one guard let self = self, let currentAnimator = self.blurAnimator, currentAnimator === blurAnimator else { return } currentAnimator.stopAnimation(true) From e151f8de1660e61ba8c91565ba4671efe5385b1b Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Thu, 28 May 2026 20:35:01 +0100 Subject: [PATCH 3/3] fix: enhance blur effect handling by adding glass tint color support and optimizing animator logic --- .../sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt | 3 +++ ios/Views/BlurEffectView.swift | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt index c9a8fe7..5c9e362 100644 --- a/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt +++ b/android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt @@ -251,6 +251,9 @@ class ReactNativeBlurView : BlurViewGroup { /** * Set the glass tint color for liquid glass effect. + * @param color The color string in hex format (e.g., "#FF0000") or null to clear + */ + fun setGlassTintColor(color: String?) { color?.let { try { glassTintColor = it.toColorInt() diff --git a/ios/Views/BlurEffectView.swift b/ios/Views/BlurEffectView.swift index 44a0279..b8d3be1 100644 --- a/ios/Views/BlurEffectView.swift +++ b/ios/Views/BlurEffectView.swift @@ -9,6 +9,7 @@ class BlurEffectView: UIVisualEffectView { private var animator: UIViewPropertyAnimator? private var blurStyle: UIBlurEffect.Style = .systemMaterial private var intensity: Double = 1.0 + private var currentEffectStyle: UIBlurEffect.Style? override init(effect: UIVisualEffect?) { super.init(effect: effect) @@ -26,19 +27,19 @@ class BlurEffectView: UIVisualEffectView { self.intensity = intensity if intensity == 1.0 { - // Fast path: full blur, skip animator entirely animator?.stopAnimation(true) animator = nil + currentEffectStyle = style effect = UIBlurEffect(style: style) } else if intensity == 0.0 { - // Fast path: no blur animator?.stopAnimation(true) animator = nil + currentEffectStyle = nil effect = nil } else { - // Reuse existing animator if possible, only recreate if style changed if let existing = animator, - existing.state == .active || existing.state == .inactive { + (existing.state == .active || existing.state == .inactive), + currentEffectStyle == style { existing.fractionComplete = intensity } else { setupBlur() @@ -53,6 +54,7 @@ class BlurEffectView: UIVisualEffectView { animator = nil effect = nil + currentEffectStyle = blurStyle let newAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) newAnimator.addAnimations { [weak self] in