Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
97 changes: 97 additions & 0 deletions apps/src/tests/issue-tests/Test3867.tsx
Original file line number Diff line number Diff line change
@@ -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<ParamList extends ParamListBase> = {
navigation: NativeStackNavigationProp<ParamList>;
};

type StackNavigationProp = NavigationProp<RouteParamList>;

const Stack = createNativeStackNavigator<RouteParamList>();

function HomeScreen({ navigation }: StackNavigationProp) {
return (
<View style={styles.container}>
<Text style={styles.title}>Home Screen</Text>
<Button
title="Push Details"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}

function DetailScreen({ navigation }: StackNavigationProp) {
return (
<View style={styles.container}>
<Text style={styles.title}>Details Screen</Text>
<Button
title="Push Settings"
onPress={() => navigation.navigate('Settings')}
/>
<View style={styles.spacer} />
<Button
title="Pop (Go Back)"
onPress={() => navigation.goBack()}
color="red"
/>
</View>
);
}

function DeepDetailScreen({ navigation }: StackNavigationProp) {
return (
<View style={styles.container}>
<Text style={styles.title}>Settings Screen</Text>
<View style={styles.spacer} />
<Button
title="Pop to Top"
onPress={() => navigation.popToTop()}
color="red"
/>
</View>
);
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ headerShown: false }}
/>
<Stack.Screen name="Details" component={DetailScreen} />
<Stack.Screen name="Settings" component={DeepDetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
},
spacer: {
height: 20,
},
});
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export { default as Test3793 } from './Test3793';
export { default as Test3816 } from './Test3816';
export { default as Test3833 } from './Test3833';
export { default as Test3835 } from './Test3835';
export { default as Test3867 } from './Test3867';
export { default as TestScreenAnimation } from './TestScreenAnimation';
// The following test was meant to demo the "go back" gesture using Reanimated
// but the associated PR in react-navigation is currently put on hold
Expand Down
Loading