What I want
I'm trying to animate the react navigation native stack header by changing the background from a transparent to a gray color when the user scrolls down. I was reading the documentation and it suggests using the navigation.setOptions to interact with the screen info.
I am using react-native-reanimated to capture the scroll value and change it when the user interacts with the screen.
The problem
I'm capturing the scroll value and using it inside the setOptions method but it doesn't work, it just doesn't execute the changes.
import React from 'react';
import {
useAnimatedScrollHandler,
useSharedValue,
} from 'react-native-reanimated';
const MyScreen = ({ navigation }) => {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
scrollY.value = e.contentOffset.y;
},
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: scrollY.value > 0 ? 'black' : 'transparent',
},
headerTransparent: scrollY.value === 0,
});
}, [ navigation, scrollY.value ]);
}
Deps
"react-native": "0.67.2",
"react-native-reanimated": "2.9.1",
"#react-navigation/native": "^6.0.6",
"#react-navigation/native-stack": "^6.2.5",
It's possible to animate the Native Stack header, but since only Animated components accept animated styles in Reanimated 2, you'd probably have to create a new component for the header (an Animated.Something)...
We can achieve this by using the header option, which can be found here.
A simple example built with Expo:
import React from "react";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { NavigationContainer, useNavigation } from "#react-navigation/native";
import { createNativeStackNavigator } from "#react-navigation/native-stack";
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedScrollHandler,
interpolateColor,
} from "react-native-reanimated";
const Stack = createNativeStackNavigator();
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
},
item: {
padding: 20,
margin: 15,
backgroundColor: "whitesmoke",
},
header: {
paddingTop: 50,
padding: 15,
borderColor: "whitesmoke",
borderBottomWidth: 1,
},
headerTitle: {
fontSize: 20,
fontWeight: "bold",
},
});
function WelcomeScreen() {
const navigation = useNavigation();
const translationY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler((event) => {
translationY.value = event.contentOffset.y;
});
const aStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
translationY.value,
[0, 50],
["white", "skyblue"],
"RGB"
),
}));
React.useLayoutEffect(() => {
navigation.setOptions({
header: () => (
<Animated.View style={[styles.header, aStyle]}>
<Text style={styles.headerTitle}>Testing</Text>
</Animated.View>
),
});
}, [aStyle, navigation]);
return (
<View style={styles.container}>
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
{Array(15)
.fill(0)
.map((_, index) => (
<View style={styles.item} key={`${index}`}>
<Text>Item {`${index}`}</Text>
</View>
))}
</Animated.ScrollView>
<StatusBar style="auto" />
</View>
);
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen component={WelcomeScreen} name="Welcome" />
</Stack.Navigator>
</NavigationContainer>
);
}
Note that in this example, we're passing an Animated.View to the header option, passing our animated style (aStyle) to it as a style.
Also, just like you did, I'm using useAnimatedScrollHandler to track the scroll position, and interpolating (with interpolateColor) the backgroundColor of the header accordingly (between 'white' and 'skyblue', so it's easier to visualize).
Uploaded this example to this Snack so you can easily test it if you want.
I Hope this helps you solve your problem!
Related
I have a fairly simple React Native app that I am using to showcase a custom component library. I recently upgraded the app from React Native version 0.67.2 to 0.70.6. React Navigation is still on version 5.x (latest is 6.x). My issue is that when I attempt to open my navigation drawer, I see a shadow over my home screen, but the drawer does not appear. The ellipsis menu also fails to appear.
Here is the home screen on app load:
Here is the home screen when I tap the hamburger menu:
I have narrowed down the problem to my custom header component. When I comment out the Actions in the header, including the children, the drawer works.
Here is the home page:
HomeStack.tsx:
import { CustomHeader } from '#custom-components/Header';
import { createStackNavigator } from '#react-navigation/stack';
import React from 'react';
import { Appbar, Menu, Divider, useTheme } from 'react-native-paper';
import { HomeScreen } from '../screens';
const Stack = createStackNavigator();
const HomeStack = (): JSX.Element => {
const theme: ReactNativePaper.Theme = useTheme();
const [isMenuVisible, setMenuVisible] = React.useState(false);
const openMenu = () => setMenuVisible(true);
const closeMenu = () => setMenuVisible(false);
return (
<Stack.Navigator
headerMode="float"
screenOptions={{
header: () => (
<CustomHeader title="Home">
<Menu
visible={isMenuVisible}
onDismiss={closeMenu}
anchor={
<Appbar.Action
icon="dots-vertical"
testID="header-action-1"
touchSoundDisabled={false}
onPress={openMenu}
color={theme.colors.text}
/>
}
>
<Menu.Item onPress={() => {}} title="Item 1" />
<Menu.Item onPress={() => {}} title="Item 2" />
<Menu.Item onPress={() => {}} title="Item 3" />
</Menu>
</CustomHeader>
),
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
);
};
export default HomeStack;
And the header:
CustomHeader.tsx
import { DrawerActions, useNavigation } from '#react-navigation/native';
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Appbar, useTheme } from 'react-native-paper';
import { common } from '../theme/colors';
export type CustomHeaderProps = {
title: string;
subtitle?: string;
scaleMenuIcon?: number;
headerHeight?: number;
titleSize?: number;
subtitleSize?: number;
children?: React.ReactNode;
backgroundColor?: string;
color?: string;
};
export const CustomHeader = ({
title,
subtitle,
scaleMenuIcon,
titleSize = 20,
subtitleSize = 15,
headerHeight,
children,
backgroundColor = common.offWhite,
color = common.black,
}: CustomHeaderProps): JSX.Element => {
const navigation = useNavigation();
const theme = useTheme();
const openMenu = () => {
navigation.dispatch(DrawerActions.openDrawer());
};
return (
<Appbar.Header style={
backgroundColor: theme.dark ? undefined : backgroundColor,
height: headerHeight
}>
<Appbar.Action
icon="menu"
testID="header-button"
style={styles.menuIcon}
onPress={openMenu}
touchSoundDisabled={true}
size={scaleMenuIcon}
color={theme.dark ? undefined : color}
/>
<Appbar.Content
testID="appbar-content"
color={theme.dark ? undefined : color}
style={styles.headerText}
title={title}
titleStyle={{ fontSize: titleSize }}
subtitle={subtitle}
subtitleStyle={{ fontSize: subtitleSize }}
/>
// If I comment out this View, the drawer appears.
<View style={styles.headerImageText}>
{children && (
<View testID="appbar-actions" style={styles.children}>
{children}
</View>
)}
</View>
</Appbar.Header>
);
};
const styles = StyleSheet.create({
children: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: 30,
...Platform.select({
ios: {
left: -10,
},
android: {
left: 0,
},
}),
},
headerImageText: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-start',
paddingLeft: 10,
},
headerText: {
flexGrow: 6,
flexShrink: 1,
fontSize: 20,
fontWeight: 'bold',
justifyContent: 'center',
left: -10,
letterSpacing: 1,
},
menuIcon: {
left: 10,
},
});
I have to implement a prototype and I'm stuck on how to implement the navigation.
Here is an image of the expected result:
The tabs on the left have a classic tab behavior and I don't have any problem to implement them. However, the 2 icons on the left use a stack navigation, the screen comes from the right and is put on top of the actual screen (and it's available on Calendar and Feed tab).
I tried to make a custom tabBar with my tab bar item and another navigator on the right but I have the error of multiple navigators on the same screen.
I use the ignite starter for development.
Here is the navigator code:
import React from "react"
import { HomeFeedScreen } from "../../screens/home-feed/home-feed-screen"
import { createMaterialTopTabNavigator } from "#react-navigation/material-top-tabs"
import { HomeCalendarScreen } from "../../screens/home-calendar/home-calendar-screen"
import { color } from "../../theme"
import { Text, TextStyle, ViewStyle } from "react-native"
export type HomeTopTabNavigatorParamList = {
feed: undefined
calendar: undefined
}
const TAB_BAR: ViewStyle = {
backgroundColor: color.background,
}
const TAB_ITEM: ViewStyle = {
width: "auto",
}
const TAB_LABEL: TextStyle = {
color: color.palette.white,
fontSize: 14,
textTransform: "capitalize",
}
const TopTab = createMaterialTopTabNavigator<HomeTopTabNavigatorParamList>()
export const HomeTopTabNavigator = () => {
// const insets = useSafeAreaInsets()
// const { isGuest } = useStores()
return (
<TopTab.Navigator
style={{ backgroundColor: color.palette.orange }}
screenOptions={({ route }) => ({
tabBarShowLabel: true,
tabBarStyle: TAB_BAR,
tabBarItemStyle: TAB_ITEM,
tabBarLabel: ({ focused }) => {
return (
// <View>
<Text
style={{
...TAB_LABEL,
fontWeight: focused ? "bold" : "normal",
}}
>
{route.name}
</Text>
// </View>
)
},
})}
>
<TopTab.Screen name="feed" component={HomeFeedScreen} />
<TopTab.Screen name="calendar" component={HomeCalendarScreen} />
</TopTab.Navigator>
)
}
Result:
I have BottomTab navigator with 2 screens Home and Activity, nested inside a Drawer Navigator. When I switch from one screen to second one using BottomTab, my header of Drawer Navigator hides with flickering effect and same thing happens again when I show it up on previous screen. I am handling headerShown:true and headerShown:false in listeners prop of Tab.Screen using focus and blur of that screen.
It seems like header is rendering after rendering of components below it. This header showing and hiding has more delay if I have multiple components inside both screens.
Snack repo is attached.
https://snack.expo.dev/#a2nineu/bottomtab-inside-drawer
I suggest you use Interaction Manager
As react-native is single threaded, JS gets blocked while running animation which can cause UI to lag and jitter.
Attaching code with changes
import 'react-native-gesture-handler';
import * as React from 'react';
import {
Text,
View,
StyleSheet,
Image,
InteractionManager,
} from 'react-native';
import Constants from 'expo-constants';
import { NavigationContainer } from '#react-navigation/native';
import { createDrawerNavigator } from '#react-navigation/drawer';
import { createMaterialBottomTabNavigator } from '#react-navigation/material-bottom-tabs';
const Drawer = createDrawerNavigator();
const Tab = createMaterialBottomTabNavigator();
const Home = (props) => {
React.useEffect(() => {
const interactionPromise = InteractionManager.runAfterInteractions(() =>
setShowContent(true)
);
return () => interactionPromise.cancel();
}, []);
const [showContent, setShowContent] = React.useState(false);
return showContent ? (
<View>
<Text>Home</Text>
<Image
source={{
uri: 'https://images.unsplash.com/photo-1596265371388-43edbaadab94?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80',
}}
style={{ height: 500, width: 500 }}
resizeMode={'cover'}></Image>
</View>
) : null;
};
const Activity = () => {
return <Text>Activity</Text>;
};
const BottomTab = () => {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen
name="Activity"
component={Activity}
listeners={({ navigation, route }) => ({
focus: () => {
navigation.getParent().setOptions({
headerShown: false,
});
},
blur: () => {
navigation.getParent().setOptions({
headerShown: true,
});
},
})}
/>
</Tab.Navigator>
);
};
export default function App() {
return (
<View style={styles.container}>
<NavigationContainer>
<Drawer.Navigator screenOptions={{ headerMode: 'float' }}>
<Drawer.Screen
options={{ headerStyle: { height: 100 } }}
name="BottomTab"
component={BottomTab}
/>
</Drawer.Navigator>
</NavigationContainer>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
I'm trying to apply layout animation to a FlatList upon adding and deleting a goal (list item) using the Reanimated API. I'm mainly following this tutorial from the Reanimated docs but I don't know why the animation is not applied when list items are added or removed. I should also inform that I'm only testing this on an Android device. Here is the code:
App.js (contains FlatList)
import { useState } from "react";
import { Button, FlatList, StyleSheet, View } from "react-native";
import GoalInput from "./components/GoalInput";
import GoalItem from "./components/GoalItem";
export default function App() {
const [goalList, setGoalList] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const styles = StyleSheet.create({
appContainer: {
paddingTop: 50,
},
});
function startAddGoalHandler() {
setIsModalOpen(true);
}
// spread existing goals and add new goal
function addGoalHandler(enteredGoalText) {
setGoalList((currentGoals) => [
...currentGoals,
{ text: enteredGoalText, id: Math.random().toString() },
]);
}
function deleteGoalHandler(id) {
setGoalList((currentGoals) =>
currentGoals.filter((existingGoals) => existingGoals.id !== id)
);
}
return (
<View style={styles.appContainer}>
<Button
title='Add New Goal'
color='indigo'
onPress={startAddGoalHandler}
/>
{isModalOpen && (
<GoalInput
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
onAddGoal={addGoalHandler}
></GoalInput>
)}
<FlatList
keyExtractor={(item, index) => {
return item.id;
}}
data={goalList}
renderItem={(itemData) => {
return (
<GoalItem
onGoalDelete={deleteGoalHandler}
itemData={itemData}
/>
);
}}
/>
</View>
);
}
GoalItem.js (list item)
import React from "react";
import { Pressable, StyleSheet, Text } from "react-native";
import Animated, { Layout, LightSpeedInLeft, LightSpeedOutRight } from "react-native-reanimated";
const GoalItem = ({ itemData, onGoalDelete }) => {
const styles = StyleSheet.create({
goalCards: {
elevation: 20,
backgroundColor: "white",
shadowColor: "black",
height: 60,
marginHorizontal: 20,
marginVertical: 10,
borderRadius: 10,
},
});
return (
<Animated.View
style={styles.goalCards}
entering={LightSpeedInLeft}
exiting={LightSpeedOutRight}
layout={Layout.springify()}
>
<Pressable
style={{ padding: 20 }}
android_ripple={{ color: "#dddddd" }}
onPress={() => onGoalDelete(itemData.item.id)}
>
<Text style={{ textAlign: "center" }}>
{itemData.item.text}
</Text>
</Pressable>
</Animated.View>
);
};
export default GoalItem;
I've even tried replacing the FlatList with View but to no avail. I suspect that Reanimated isn't properly configured for my project, if I wrap some components with <Animated.View>...</Animated.View> (Animated from Reanimated and not the core react-native module) for example the child components will not show. Reanimated is installed through npm
Any help is appreciated, thanks!
My idea is to have nav.js contain the navigation container for my mobile application, along with all of the screens here (i.e. the homescreen). This file would theoretically let me include buttons in another file that would let me navigate to the screen in the nav.js file that I specified with the button.
I made a constant for this called NavBar and exported it, but when I tried to import into my photo.js file (an example file where I want to include a button that lets me navigate to my 'Tasks' screen), I got the error:
Error: Looks like you have nested a 'NavigationContainer' inside another. Normally you need only one container at the root of the app, so this was probably an error. If this was intentional, pass 'independent={true}' explicitly. Note that this will make the child navigators disconnected from the parent and you won't be able to navigate between them.
I tried simply making my own button in photo.js instead of :
<Button title="Go to tasks" color="lightblue" onPress={() => navigation.navigate('Tasks')}/>, but that only gave me the error that the variable navigation could not be found.
nav.js
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import { StyleSheet, View, Button, ScrollView } from 'react-native';
import TaskApp from './taskapp';
import AddTank from './tank';
const NavBar = () => {
const Stack = createNativeStackNavigator();
{/* Homescreen */}
const HomeScreen = ( {navigation} ) => {
return (
<ScrollView style={homeStyle.container}>
<AddTank />
<Button title="Go to tasks" color="lightblue" onPress={() => navigation.navigate('Tasks')}/>
</ScrollView>
)
}
{/* Default config */}
const Config = ( {navigation } ) => {
return (
<View></View>
)
}
return (
<NavigationContainer>
<Stack.Navigator initialRouteName='Home'>
<Stack.Screen name="Home" component={HomeScreen}/>
<Stack.Screen name="Tasks" component={TaskApp}/>
<Stack.Screen name="Config" component={Config}/>
</Stack.Navigator>
</NavigationContainer>
)
}
const homeStyle = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#E8EAED',
},
tasks: {
flex: 1,
height: 30,
width: 30,
backgroundColor: '#E8EAED',
},
});
export default NavBar;
photo.js
import React, {useState, useEffect} from 'react';
import { StyleSheet, Text, View, Button, Image, ImageBackground } from 'react-native';
import { NavigationContainer, StackActions } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import * as ImagePicker from 'expo-image-picker';
import NavBar from './nav';
const AddPhoto = () => {
const add = {uri: 'https://media.discordapp.net/attachments/639282516997701652/976293252082839582/plus.png?width=461&height=461'}
const Stack = createNativeStackNavigator();
const [hasGalleryPermission, setHasGalleryPermission] = useState(null);
const[ image, setImage ] = useState(null);
useEffect(() => {
(async () => {
const galleryStatus = await ImagePicker.requestCameraMediaLibraryPermissionAsync();
setHasGalleryPermission(galleryStatus.status === 'granted');
})();
}, []);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4,3],
quality:1,
});
console.log(result);
if (!result.cancelled){
setImage(result.uri);
}
};
if (hasGalleryPermission === false){
return <Text>No access to internal storage.</Text>
}
return (
<View style={styles.container}>
{image && <ImageBackground source={{uri: image}} style={styles.tankPhoto}/>}
<Button title="Add photo" color="lightgreen" onPress={() => pickImage()} />
<NavBar />
</View>
)
}
const styles = StyleSheet.create({
container: {
borderColor: '#C0C0C0',
borderWidth: 1,
backgroundColor: '#FFF',
borderRadius: 50,
width: 330,
flex: 1,
alignItems: 'center',
marginLeft: 'auto',
marginRight: 'auto',
margin: 30,
},
tankPhoto: {
flex: 1,
height: 150,
width: 200,
borderWidth: 1,
margin: 25,
},
})
export default AddPhoto;
What am I doing wrong, or is there a better way to do this? What I need:
let me include buttons in another file that would let me navigate to the screen in the nav.js file that I specified with the button.
React native navigation documentation: https://reactnavigation.org/docs/getting-started
Move these lines outside the NavBar component:
const Stack = createNativeStackNavigator();
{/* Homescreen */}
const HomeScreen = ( {navigation} ) => {
return (
<ScrollView style={homeStyle.container}>
<AddTank />
<Button title="Go to tasks" color="lightblue" onPress={() => navigation.navigate('Tasks')}/>
</ScrollView>
)
}
{/* Default config */}
const Config = ( {navigation } ) => {
return (
<View></View>
)
}
I ended up making this work. I defined
const navigation = useNavigation();
in my code (under AddPhoto constant) and imported:
import { useNavigation } from '#react-navigation/native'; in the same file (photo.js).