fix: resolve blur animator race, scroll lag, and Android full-screen blur#102
Conversation
…blur - iOS (#85, #101): remove DispatchQueue.main.async in BlurEffectView.setupBlur() and stop/finish the UIViewPropertyAnimator synchronously. The async dispatch left a one-frame window where the wrong intensity was visible, causing flickers on navigation and fallback-color flashes with 10+ instances on iOS 26.2. - iOS (#100): add early-exit guard in BlurEffectView.updateBlur() to skip animator teardown/recreation when style and intensity are unchanged. During FlashList recycling, updateUIView fires on every layout pass even with identical props, making this guard essential for smooth scrolling. - Android (#89): add findNearestReactRootView() fallback in ReactNativeBlurView and ReactNativeProgressiveBlurView. When no react-native-screens Screen ancestor exists, QmBlurView was falling back to the activity decor view as blur root, causing the entire screen to blur instead of just the component. The new fallback scopes capture to ReactRootView.
📝 WalkthroughWalkthroughAndroid: broadened blur root resolution to prefer a react-native-screens Screen, then fall back to the nearest ReactRootView before returning null. iOS: avoid redundant animator teardown/recreation by skipping setup when style/intensity unchanged and stabilize animator state when view moves windows. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
ios/Views/BlurEffectView.swift (1)
50-58: Consider applying the same synchronous fix toVibrancyEffectView.updateEffect()as a follow-up.
ios/Views/VibrancyEffectView.swift:55-98still dispatchesstopAnimation/finishAnimationasynchronously and uses an identity check (currentAnimator === blurAnimator) to guard against the race that exact change inBlurEffectView.setupBlur()was just designed to eliminate. If consumers ever mountVibrancyEffectViewin a FlashList/navigation-heavy scenario similar to the iOS 26.2 reproduction in#85, the same one-frame fallback-color flash is likely to surface there. Out of scope for this PR, but worth tracking — the fix pattern would be identical: drop the async wrapper and the identity check, and call stop/finish inline right after assigningfractionComplete.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/Views/BlurEffectView.swift` around lines 50 - 58, VibrancyEffectView.updateEffect() still uses an async wrapper and an identity guard (currentAnimator === blurAnimator) before calling stopAnimation/finishAnimation; replicate the BlurEffectView fix by removing the DispatchQueue.async wrapper and the identity check, and instead set the vibrancy animator's fractionComplete (e.g., vibrancyAnimator?.fractionComplete = intensity) and immediately call vibrancyAnimator?.stopAnimation(true) and vibrancyAnimator?.finishAnimation(at: .current) inline so the vibrancy effect is applied synchronously.android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt (1)
200-235: Consider extracting the ancestor-walk helpers to a shared utility.
findNearestScreenAncestor()andfindNearestReactRootView()are now duplicated verbatim inReactNativeBlurViewandReactNativeProgressiveBlurView, along with their priority-ordering logic infindOptimalBlurRoot(). A small private helper (e.g.BlurRootResolverobject withfindOptimalBlurRoot(view: View): ViewGroup?) would keep the two views in sync — particularly relevant given the New Architecture detection gap that needs to be patched in both places.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt` around lines 200 - 235, Extract the repeated ancestor-walk logic into a shared utility (e.g., an object/class named BlurRootResolver) that exposes a single entry function findOptimalBlurRoot(view: View): ViewGroup?; move the implementations of findOptimalBlurRoot(), findNearestScreenAncestor(), and findNearestReactRootView() from ReactNativeBlurView into that utility (keeping the same class-name checks for "com.swmansion.rnscreens.Screen", "com.facebook.react.ReactRootView", and "com.facebook.react.rootview.ReactRootView"), and update ReactNativeBlurView and ReactNativeProgressiveBlurView to call BlurRootResolver.findOptimalBlurRoot(this) instead of their local methods so both classes share the same logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt`:
- Around line 224-235: The method findNearestReactRootView in
ReactNativeBlurView and its duplicate in ReactNativeProgressiveBlurView fails to
recognize bridgeless/new-architecture surfaces and checks a non-existent class;
update both methods to also detect "com.facebook.react.runtime.ReactSurfaceView"
and remove the incorrect check for "com.facebook.react.rootview.ReactRootView",
keeping the existing "com.facebook.react.ReactRootView" check, and continue
returning the found parent cast as ViewGroup (as currently done).
---
Nitpick comments:
In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt`:
- Around line 200-235: Extract the repeated ancestor-walk logic into a shared
utility (e.g., an object/class named BlurRootResolver) that exposes a single
entry function findOptimalBlurRoot(view: View): ViewGroup?; move the
implementations of findOptimalBlurRoot(), findNearestScreenAncestor(), and
findNearestReactRootView() from ReactNativeBlurView into that utility (keeping
the same class-name checks for "com.swmansion.rnscreens.Screen",
"com.facebook.react.ReactRootView", and
"com.facebook.react.rootview.ReactRootView"), and update ReactNativeBlurView and
ReactNativeProgressiveBlurView to call
BlurRootResolver.findOptimalBlurRoot(this) instead of their local methods so
both classes share the same logic.
In `@ios/Views/BlurEffectView.swift`:
- Around line 50-58: VibrancyEffectView.updateEffect() still uses an async
wrapper and an identity guard (currentAnimator === blurAnimator) before calling
stopAnimation/finishAnimation; replicate the BlurEffectView fix by removing the
DispatchQueue.async wrapper and the identity check, and instead set the vibrancy
animator's fractionComplete (e.g., vibrancyAnimator?.fractionComplete =
intensity) and immediately call vibrancyAnimator?.stopAnimation(true) and
vibrancyAnimator?.finishAnimation(at: .current) inline so the vibrancy effect is
applied synchronously.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e165e0dc-0a49-49f3-b0b0-dcf2d2ea36e5
📒 Files selected for processing (3)
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.ktandroid/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.ktios/Views/BlurEffectView.swift
The second condition checking 'com.facebook.react.rootview.ReactRootView' referenced a package path that has never existed in React Native. Only 'com.facebook.react.ReactRootView' is needed.
|
👀🚀🔥 |
DanielAraldi
left a comment
There was a problem hiding this comment.
The #101 issue persists
| // Set intensity and immediately freeze the animator so the blur is | ||
| // applied synchronously. The previous async dispatch caused a one-frame | ||
| // window where the wrong intensity was visible, leading to flickers on | ||
| // navigation (issue #101) and fallback-color flashes with many instances | ||
| // on iOS 26.2 (issue #85). | ||
| animator?.fractionComplete = intensity | ||
| // Stop the animation at the current state | ||
| DispatchQueue.main.async { [weak self] in | ||
| self?.animator?.stopAnimation(true) | ||
| self?.animator?.finishAnimation(at: .current) | ||
| } | ||
| animator?.stopAnimation(true) | ||
| animator?.finishAnimation(at: .current) |
There was a problem hiding this comment.
I tested #101 with this example App: https://github.com/iqraKhaliq/BlurView
But now, the blur effect isn't applied.
See the snapshot:
Note: The blur effect should be applied after the third element in the list.
Code:
There was a problem hiding this comment.
Hey @DanielAraldi , sorry for the late fix
I have successfully fixed it
You can test if you got some time with this beta : @sbaiahmed1/react-native-blur@4.6.2-beta.0
There was a problem hiding this comment.
Ok my friend 🫡
tomorrow I’ll test it 😁
There was a problem hiding this comment.
♻️ Duplicate comments (1)
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt (1)
224-233:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInclude
ReactSurfaceViewin RN root fallback detection.At Line 227, the fallback only matches exact class name
com.facebook.react.ReactRootView. In new-architecture/bridgeless paths, parent may becom.facebook.react.runtime.ReactSurfaceView, so this can still miss the RN root and fall back to decor-view blurring.Suggested fix
private fun findNearestReactRootView(): ViewGroup? { var currentParent = this.parent while (currentParent != null) { - if (currentParent.javaClass.name == "com.facebook.react.ReactRootView") { + val className = currentParent.javaClass.name + if ( + className == "com.facebook.react.ReactRootView" || + className == "com.facebook.react.runtime.ReactSurfaceView" || + className == "com.facebook.react.bridgeless.ReactSurfaceView" + ) { return currentParent as? ViewGroup } currentParent = currentParent.parent } return null }Apply the same change to
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt(Lines 245-254).Confirm in current React Native Android sources whether bridgeless/new-architecture surfaces use `com.facebook.react.runtime.ReactSurfaceView`, and whether matching only `com.facebook.react.ReactRootView` can miss that ancestor.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt` around lines 224 - 233, The findNearestReactRootView() function currently only checks for "com.facebook.react.ReactRootView" and thus misses bridgeless/new-architecture roots like "com.facebook.react.runtime.ReactSurfaceView"; update the conditional in ReactNativeBlurView.kt's findNearestReactRootView() to treat either class name as a valid RN root (e.g., check parent.javaClass.name == "com.facebook.react.ReactRootView" || parent.javaClass.name == "com.facebook.react.runtime.ReactSurfaceView"), and apply the identical change to the same function in ReactNativeProgressiveBlurView.kt so both components detect ReactSurfaceView as a root fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt`:
- Around line 224-233: The findNearestReactRootView() function currently only
checks for "com.facebook.react.ReactRootView" and thus misses
bridgeless/new-architecture roots like
"com.facebook.react.runtime.ReactSurfaceView"; update the conditional in
ReactNativeBlurView.kt's findNearestReactRootView() to treat either class name
as a valid RN root (e.g., check parent.javaClass.name ==
"com.facebook.react.ReactRootView" || parent.javaClass.name ==
"com.facebook.react.runtime.ReactSurfaceView"), and apply the identical change
to the same function in ReactNativeProgressiveBlurView.kt so both components
detect ReactSurfaceView as a root fallback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7b43469b-7916-411f-afff-cea869757c6c
📒 Files selected for processing (4)
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.ktandroid/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.ktexample/app/(tabs)/index.tsxios/Views/BlurEffectView.swift
DanielAraldi
left a comment
There was a problem hiding this comment.
LGTM!
Both (iOS and Android) have worked perfectly!
Summary
iOS Facing issue with ios 26.2 #85 / Blur intensity changes unexpectedly on navigation (iOS) #101 —
BlurEffectView.setupBlur()was callingstopAnimation/finishAnimationinside aDispatchQueue.main.async, leaving a one-frame window where the animator ran at the wrongfractionComplete. This caused blur intensity to flicker on navigation and produced fallback-color flashes when 10+BlurViewinstances existed on screen (iOS 26.2). Fix: call both synchronously immediately after settingfractionComplete.iOS Scroll lag when using BlurView #100 — During
FlashListrecycling,Blur.updateUIViewfires on every layout pass even whenstyleandintensityare unchanged. Each call was unconditionally recreating theUIViewPropertyAnimator, causing scroll jank. Fix: early-exit guard inupdateBlur()— skipsetupBlur()when nothing changed.Android Blur Effect doesnt work on Android it gives full screen blur effect not parent component #89 —
findOptimalBlurRoot()returnednullwhen nocom.swmansion.rnscreens.Screenancestor was found (plainViewhierarchies without a Stack navigator).QmBlurViewthen defaulted to the activity decor view as its blur capture root, blurring the entire screen. Fix: addedfindNearestReactRootView()fallback that scopes capture tocom.facebook.react.ReactRootViewinstead. Applied to bothReactNativeBlurViewandReactNativeProgressiveBlurView.Test plan
BlurView— intensity must remain stable. Place 10+BlurViewinstances on one screen — none should flash to the fallback color.FlashListwith memoizedBlurViewcells and scroll quickly — FPS should stay ≥ 60.BlurViewinside a plainView(no react-native-screens Stack). Build a release APK — blur should be clipped to component bounds, not the entire screen.Summary by CodeRabbit