diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt index c71fa93a08..de7c9a8dc6 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt @@ -10,7 +10,6 @@ import android.view.View import android.view.ViewGroup import android.view.ViewParent import android.view.WindowManager -import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerHelper @@ -187,7 +186,7 @@ class ScreenModalFragment : override fun removeToolbar(): Unit = throw IllegalStateException("[RNScreens] Modal screens on Android do not support header right now") - override fun setToolbar(toolbar: Toolbar): Unit = + override fun setToolbar(toolbar: CustomToolbar): Unit = throw IllegalStateException("[RNScreens] Modal screens on Android do not support header right now") override fun setToolbarShadowHidden(hidden: Boolean): Unit = diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt index 5445079091..095d452801 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat @@ -57,6 +58,8 @@ class ScreenStackFragment : private var isToolbarShadowHidden = false private var isToolbarTranslucent = false + private var lastActiveHeaderConfig: ScreenStackHeaderConfig? = null + private lateinit var sheetTransitionCoordinator: BottomSheetTransitionCoordinator private var lastFocusedChild: View? = null @@ -103,7 +106,8 @@ class ScreenStackFragment : toolbar = null } - override fun setToolbar(toolbar: Toolbar) { + override fun setToolbar(toolbar: CustomToolbar) { + lastActiveHeaderConfig = toolbar.config appBarLayout?.addView(toolbar) toolbar.layoutParams = AppBarLayout @@ -301,6 +305,25 @@ class ScreenStackFragment : super.onViewCreated(view, savedInstanceState) } + override fun onDestroyView() { + // ScreenStackHeaderConfig.onUpdate() calls activity.setSupportActionBar(toolbar) each time + // the top screen updates. AppCompatDelegateImpl stores the resulting ToolbarActionBar in + // its mActionBar field for the lifetime of the activity. When a screen is popped and the new + // top screen does not install a replacement action bar (e.g. headerShown: false), + // the stale ToolbarActionBar — and the entire object graph hanging off the toolbar is never released. + // When this fragment is being removed, we're clearing the activity's support action bar if it + // still belongs to us. This will break the retention chain: + // - AppCompatDelegateImpl.mActionBar + // - ToolbarActionBar.mDecorToolbar + // - ToolbarWidgetWrapper.mToolbar + // - DebugMenuToolbar.config + // - ScreenStackHeaderConfig.mParent + // - Screen.fragment + lastActiveHeaderConfig?.clearActionBarIfOwned(activity as? AppCompatActivity) + lastActiveHeaderConfig = null + super.onDestroyView() + } + override fun onCreateAnimation( transit: Int, enter: Boolean, diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragmentWrapper.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragmentWrapper.kt index 440892860e..d95ae52a86 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragmentWrapper.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragmentWrapper.kt @@ -1,12 +1,10 @@ package com.swmansion.rnscreens -import androidx.appcompat.widget.Toolbar - interface ScreenStackFragmentWrapper : ScreenFragmentWrapper { // Toolbar management fun removeToolbar() - fun setToolbar(toolbar: Toolbar) + fun setToolbar(toolbar: CustomToolbar) fun setToolbarShadowHidden(hidden: Boolean) diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt index e76bf173ca..8ab37c8869 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt @@ -9,6 +9,7 @@ import android.view.Gravity import android.view.View.OnClickListener import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.app.ActionBar import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment @@ -58,6 +59,7 @@ class ScreenStackHeaderConfig( private var isBackButtonHidden = false private var isShadowHidden = false private var isDestroyed = false + private var actionBar: ActionBar? = null private var backButtonInCustomView = false private var tintColor = 0 private var isAttachedToWindow = false @@ -113,6 +115,17 @@ class ScreenStackHeaderConfig( isDestroyed = true } + internal fun clearActionBarIfOwned(activity: AppCompatActivity?) { + actionBar?.let { ownedActionBar -> + activity?.let { appCompat -> + if (appCompat.supportActionBar === ownedActionBar) { + appCompat.setSupportActionBar(null) + } + } + } + actionBar = null + } + /** * Native toolbar should notify the header config component that it has completed its layout. */ @@ -243,15 +256,16 @@ class ScreenStackHeaderConfig( activity.setSupportActionBar(toolbar) // non-null toolbar is set in the line above and it is used here - val actionBar = requireNotNull(activity.supportActionBar) + val newActionBar = requireNotNull(activity.supportActionBar) + actionBar = newActionBar // hide back button - actionBar.setDisplayHomeAsUpEnabled( + newActionBar.setDisplayHomeAsUpEnabled( screenFragment?.canNavigateBack() == true && !isBackButtonHidden, ) // title - actionBar.title = title + newActionBar.title = title if (TextUtils.isEmpty(title)) { isTitleEmpty = true } @@ -325,7 +339,7 @@ class ScreenStackHeaderConfig( ?: throw JSApplicationIllegalArgumentException( "Back button header config view should have Image as first child", ) - actionBar.setHomeAsUpIndicator(firstChild.drawable) + newActionBar.setHomeAsUpIndicator(firstChild.drawable) i++ continue } diff --git a/apps/src/tests/issue-tests/Test3867.tsx b/apps/src/tests/issue-tests/Test3867.tsx new file mode 100644 index 0000000000..d448660880 --- /dev/null +++ b/apps/src/tests/issue-tests/Test3867.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { View, Text, Button, StyleSheet } from 'react-native'; +import { NavigationContainer, ParamListBase } from '@react-navigation/native'; +import { + createNativeStackNavigator, + NativeStackNavigationProp, +} from '@react-navigation/native-stack'; + +type RouteParamList = { + Home: undefined; + Details: undefined; + Settings: undefined; +}; + +type NavigationProp = { + navigation: NativeStackNavigationProp; +}; + +type StackNavigationProp = NavigationProp; + +const Stack = createNativeStackNavigator(); + +function HomeScreen({ navigation }: StackNavigationProp) { + return ( + + Home Screen +