Skip to content

BlurView v3 from Dimezis appears to lack support for integration with Bottom Tabs. #3218

@DanielAraldi

Description

@DanielAraldi

Description

Hello, some time ago I opened a PR here regarding an issue involving RNScreens and Dimezis’s BlurView (v2.x). Now, I’ve updated the Dimezis BlurView library to version 3.1.0.

I was able to successfully update it to the new version, following the update approach suggested in an Expo discussion.

The approach worked very well at first, but when I use Blur as a background or as a customizable tab component with BottomTab, it renders correctly the first time. However, when switching screens, the blur effect is either not applied or remains frozen.

Below, I will show two examples, one with the BottomTab animation set to none and another with the fade animation. I’m seriously unsure whether the issue comes from my library or from react-native-screens not supporting something required in this case. I’m a bit confused about it 😄.

animation-none.mov
animation-fade.mov

It can be observed that when the animation is set to none, switching tabs causes the previously blurred tab to appear blank. Is there any internal configuration in BottomTabs that changes the background color? And when the animation is set to fade, the blur effect becomes frozen, showing only the result from the initial render of the blur.

Below I will provide my routes and screen code from the React Native side.

// routes.tsx
import { StyleSheet } from 'react-native';
import { createStaticNavigation } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BlurView } from '@azify/react-native-blur';

import { First, Second } from '../screens';
import { styles } from './styles';

const styles = StyleSheet.create({
  tabBackground: {
    width: '100%',
    height: 256,
  },
});

const RootTabs = createBottomTabNavigator({
  screens: {
    First,
    Second,
  },
  initialRouteName: 'First',
  screenOptions: {
    headerShown: false,
    animation: 'fade',
    tabBarStyle: {
      position: 'absolute',
    },
    tabBarBackground: () => (
      <BlurView targetId="target" style={styles.tabBackground} />
    ),
  },
});

export const Routes = createStaticNavigation(RootTabs);
// screens/First.tsx
import { useMemo } from 'react';
import { Pressable, ScrollView, Text, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { BlurTarget, BlurView } from '@azify/react-native-blur';

import { styles } from './styles';

export function First() {
  const navigate = useNavigation();

  function onNextScreen() {
    navigate.navigate('Second' as never);
  }

  const renderLabels = useMemo(
    () =>
      Array.from({ length: 50 }).map((_, i) => (
        <Text key={i} style={styles.label}>
          First Screen
        </Text>
      )),
    []
  );

  return (
    <View style={styles.container}>
      <BlurView
        targetId="target"
        type="light"
        radius={10}
        style={styles.blurViewHeader}
      >
        <View style={styles.header}>
          <View style={styles.headerWrapper}>
            <Text style={styles.label}>Header</Text>

            <View style={styles.avatar}>
              <Text style={styles.paragraph}>A</Text>
            </View>
          </View>

          <Pressable style={styles.button} onPress={onNextScreen}>
            <Text style={styles.paragraph}>Next Screen</Text>
          </Pressable>
        </View>
      </BlurView>

      <BlurTarget id="target" style={styles.main}>
        <ScrollView
          style={styles.main}
          contentContainerStyle={styles.mainContent}
          showsVerticalScrollIndicator={false}
        >
          {renderLabels}
        </ScrollView>
      </BlurTarget>
    </View>
  );
}

This is my native code in Kotlin:

// ReactNativeBlurView -> BlurView
package com.azify.reactnativeblur

import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.util.Log
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import eightbitlab.com.blurview.BlurTarget
import eightbitlab.com.blurview.BlurView
import androidx.core.graphics.drawable.toDrawable

class ReactNativeBlurView : BlurView {
  private var targetId: String? = null
  private var overlayColor: OverlayColor = OverlayColor.fromString("light")
  private var radius: Float = 10f
  private var isInitialized: Boolean = false
  private var rootView: BlurTarget? = null

  private enum class OverlayColor(val color: Int) {
    LIGHT(Color.argb(20, 255, 255, 255)),
    DARK(Color.argb(60, 0, 0, 0));

    companion object {
      fun fromString(color: String): OverlayColor {
        return when (color.lowercase()) {
          "light" -> LIGHT
          "dark" -> DARK
          else -> LIGHT
        }
      }
    }
  }

  companion object {
    private const val TAG: String = "ReactNativeBlurView"
  }

  constructor(context: Context?) : super(context) {
    this.setupBlurView()
  }

  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
    this.setupBlurView()
  }

  constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
    context,
    attrs,
    defStyleAttr
  ) {
    this.setupBlurView()
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()

    if (!this.isInitialized) {
      this.reinitialize()
    }
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()

    this.rootView = null
    this.isInitialized = false
    this.removeCallbacks(null)
  }

  private fun setupBlurView() {
    super.setBackgroundColor(this.overlayColor.color)
    super.clipChildren = true
    super.clipToOutline = true
    super.layoutParams = LayoutParams(
      LayoutParams.MATCH_PARENT,
      LayoutParams.MATCH_PARENT
    )
  }

  // Wait all views are mounted in interface
  private fun reinitialize() {
    post {
      this.initialize()
    }
  }

  private fun initialize() {
    // Find rootView only on first mount (when the initialization is false)
    if (!this.isInitialized) {
      this.rootView = this.findRootTargetView()

      if (this.rootView == null) {
        super.setBackgroundColor(this.overlayColor.color)
        super.setOverlayColor(this.overlayColor.color)
        super.setBlurEnabled(false)

        Log.w(TAG, "Target view not found: $targetId")
        return
      }
    }

    val drawable = this.getAppropriateBackground()
    super.setupWith(this.rootView!!, 4f, false)
      .setBlurRadius(this.radius)
      .setOverlayColor(this.overlayColor.color)
      .setBlurAutoUpdate(true)
      .setBlurEnabled(true)
      .setFrameClearDrawable(drawable)

    this.isInitialized = true
  }

  private fun findRootTargetView(): BlurTarget? {
    if (this.targetId == null) {
      Log.w(TAG, "TargetId is null")

      return null
    }

    val activityRoot = this.getRootView()
    activityRoot?.let { root ->
      val target = findViewWithTagInViewGroup(root as? ViewGroup, targetId!!)
      if (target != null) return target
    }

    var parent = this.parent
    while (parent != null) {
      if (parent is ViewGroup) {
        val target = findViewWithTagInViewGroup(parent, targetId!!)
        if (target != null) return target
      }
      parent = parent.parent
    }

    Log.w(TAG, "Target not found anywhere: $targetId")
    return null
  }

  private fun findViewWithTagInViewGroup(viewGroup: ViewGroup?, tag: String): BlurTarget? {
    if (viewGroup == null) return null

    if (viewGroup.tag == tag && viewGroup is BlurTarget) {
      return viewGroup
    }

    for (i in 0 until viewGroup.childCount) {
      val child = viewGroup.getChildAt(i)
      if (child.tag == tag && child is BlurTarget) {
        return child
      }

      if (child is ViewGroup) {
        val found = this.findViewWithTagInViewGroup(child, tag)
        if (found != null) return found
      }
    }

    return null
  }

  private fun getAppropriateBackground(): android.graphics.drawable.Drawable? {
    try {
      val activity = this.getActivityFromContext()
      activity?.window?.decorView?.background?.let {
        return it
      }

      activity?.window?.let { window ->
        val windowBackground = window.decorView.background
        windowBackground?.let {
          return it
        }
      }

      return when (this.overlayColor) {
        OverlayColor.LIGHT -> Color.WHITE
        OverlayColor.DARK -> Color.BLACK
      }.toDrawable()
    } catch (e: Exception) {
      Log.e(TAG, "Error getting background: ${e.message}")
      
      return this.overlayColor.color.toDrawable()
    }
  }

  private fun getActivityFromContext(): AppCompatActivity? {
    var context = this.context

    while (context != null) {
      when (context) {
        is AppCompatActivity -> return context
        is android.content.ContextWrapper -> {
          context = context.baseContext
        }
        else -> break
      }
    }

    return null
  }

  private fun clipRadius(radius: Float): Float {
    return when {
      radius <= 0 -> 0f
      radius >= 100 -> 100f
      else -> radius
    }
  }

  fun setOverlayColor(overlayColor: String) {
    val overlay = OverlayColor.fromString(overlayColor)
    this.overlayColor = overlay

    super.setBackgroundColor(overlay.color)

    if (this.isInitialized) {
      super.setOverlayColor(overlay.color)
      this.isInitialized = false
      this.reinitialize()
    }
  }

  fun setRadius(radius: Float) {
    this.radius = radius

    if (this.isInitialized) {
      val clippedRadius = this.clipRadius(radius)
      super.setBlurRadius(clippedRadius)
      this.reinitialize()
    }
  }

  fun setTargetId(targetId: String?) {
    val oldTargetId = this.targetId
    this.targetId = targetId

    if (oldTargetId != targetId && this.isAttachedToWindow) {
      this.isInitialized = false
      this.rootView = null
      this.reinitialize()
    }
  }
}
// ReactNativeTargetView -> BlurTarget
package com.azify.reactnativeblur

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import eightbitlab.com.blurview.BlurTarget

class ReactNativeTargetView : BlurTarget {
  private var id: String? = null
  private var isInitialized: Boolean = false

  companion object {
    private const val TAG: String = "ReactNativeTargetView"
  }

  constructor(context: Context): super(context) {
    this.setupBlurTarget()
  }

  constructor(context: Context, attrs: AttributeSet): super(context, attrs) {
    this.setupBlurTarget()
  }

  constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
    this.setupBlurTarget()
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()

    if (!this.isInitialized) {
      this.reinitialize()
    }
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()

    this.isInitialized = false
    this.removeCallbacks(null)
  }

  private fun setupBlurTarget() {
    super.layoutParams = LayoutParams(
      LayoutParams.MATCH_PARENT,
      LayoutParams.MATCH_PARENT
    )
  }

  private fun reinitialize() {
    post {
      this.initialize()
    }
  }

  private fun initialize() {
    if (this.id != null) {
      super.tag = this.id
      this.isInitialized = true
    } else {
      Log.w(TAG, "Target view id is null")

      this.isInitialized = false
    }
  }

  fun setId(id: String?) {
    val oldId = this.id
    this.id = id

    if (oldId != id && this.isAttachedToWindow) {
      this.isInitialized = false
      this.reinitialize()
    }
  }
}

If you find any issues in my native code, please let me know. So far, I haven’t detected any errors or warnings in the LogCat of Android Studio, but this problem shown in the videos above still persists. From my perspective, the issue seems to be related to the BottomTab animations — it appears to freeze the blur effect and also prevent it from re-rendering when navigating to a new screen.

Steps to reproduce

  1. Install @react-navigation/bottom-tabs (^7.4.7), @react-navigation/native (^7.1.17) and react-native-screens (4.16.0).

Snack or a link to a repository

None

Screens version

4.16.0

React Native version

0.79.2

Platforms

Android

JavaScript runtime

None

Workflow

React Native (without Expo)

Architecture

Fabric (New Architecture)

Build type

None

Device

Android emulator

Device model

Pixel 9 (API 36.0)

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    Missing reproThis issue need minimum repro scenarioPlatform: AndroidThis issue is specific to Android

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions