Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom screen transitions with native-stack v7 #11943

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

tboba
Copy link
Contributor

@tboba tboba commented Apr 16, 2024

Motivation

This PR intents to add implementation of custom screen transitions, recently added in react-native-screens. You can find more about this feature here.

Test plan

Go to the code of the native stack in example (NativeStack.tsx) and wrap whole navigator with (import from react-native-screens/gesture-handler). After that, use goBackGesture prop on the desired screen.

Ready to paste code 🍝
import { Button, Text, useHeaderHeight } from '@react-navigation/elements';
import { type PathConfigMap, useTheme } from '@react-navigation/native';
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
  useAnimatedHeaderHeight,
} from '@react-navigation/native-stack';
import * as React from 'react';
import { Animated, Platform, ScrollView, StyleSheet, View } from 'react-native';

import { COMMON_LINKING_CONFIG } from '../constants';
import { Albums } from '../Shared/Albums';
import { Article } from '../Shared/Article';
import { NewsFeed } from '../Shared/NewsFeed';
import { GestureDetectorProvider } from 'react-native-screens/gesture-handler';

export type NativeStackParams = {
  Article: { author: string } | undefined;
  NewsFeed: { date: number };
  Albums: undefined;
};

const linking: PathConfigMap<NativeStackParams> = {
  Article: COMMON_LINKING_CONFIG.Article,
  NewsFeed: COMMON_LINKING_CONFIG.NewsFeed,
  Albums: 'albums',
};

const scrollEnabled = Platform.select({ web: true, default: false });

const ArticleScreen = ({
  navigation,
  route,
}: NativeStackScreenProps<NativeStackParams, 'Article'>) => {
  return (
    <View>
      <ScrollView contentInsetAdjustmentBehavior="automatic">
        <View style={styles.buttons}>
          <Button
            variant="filled"
            onPress={() => navigation.push('NewsFeed', { date: Date.now() })}
          >
            Push feed
          </Button>
          <Button
            variant="filled"
            onPress={() => navigation.replace('NewsFeed', { date: Date.now() })}
          >
            Replace with feed
          </Button>
          <Button variant="filled" onPress={() => navigation.popTo('Albums')}>
            Pop to Albums
          </Button>
          <Button variant="tinted" onPress={() => navigation.pop()}>
            Pop screen
          </Button>
        </View>
        <Article
          author={{ name: route.params?.author ?? 'Unknown' }}
          scrollEnabled={scrollEnabled}
        />
      </ScrollView>
      <HeaderHeightView />
    </View>
  );
};

const NewsFeedScreen = ({
  route,
  navigation,
}: NativeStackScreenProps<NativeStackParams, 'NewsFeed'>) => {
  React.useLayoutEffect(() => {
    navigation.setOptions({
      headerSearchBarOptions: {
        placeholder: 'Search',
      },
    });
  }, [navigation]);

  return (
    <View>
      <ScrollView contentInsetAdjustmentBehavior="automatic">
        <View style={styles.buttons}>
          <Button variant="filled" onPress={() => navigation.push('Albums')}>
            Push Albums
          </Button>
          <Button variant="tinted" onPress={() => navigation.goBack()}>
            Go back
          </Button>
        </View>
        <NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
      </ScrollView>
      <HeaderHeightView />
    </View>
  );
};

const AlbumsScreen = ({
  navigation,
}: NativeStackScreenProps<NativeStackParams, 'Albums'>) => {
  const headerHeight = useHeaderHeight();

  return (
    <View>
      <ScrollView contentContainerStyle={{ paddingTop: headerHeight }}>
        <View style={styles.buttons}>
          <Button
            variant="filled"
            onPress={() =>
              navigation.navigate('Article', { author: 'Babel fish' })
            }
          >
            Navigate to article
          </Button>
          <Button variant="tinted" onPress={() => navigation.pop(2)}>
            Pop by 2
          </Button>
        </View>
        <Albums scrollEnabled={scrollEnabled} />
      </ScrollView>
      <HeaderHeightView hasOffset />
    </View>
  );
};

const HeaderHeightView = ({
  hasOffset = Platform.OS === 'ios',
}: {
  hasOffset?: boolean;
}) => {
  const { colors } = useTheme();

  const animatedHeaderHeight = useAnimatedHeaderHeight();
  const headerHeight = useHeaderHeight();

  return (
    <Animated.View
      style={[
        styles.headerHeight,
        {
          backgroundColor: colors.card,
          borderColor: colors.border,
          shadowColor: colors.border,
        },
        hasOffset && {
          transform: [{ translateY: animatedHeaderHeight }],
        },
      ]}
    >
      <Text>{headerHeight.toFixed(2)}</Text>
    </Animated.View>
  );
};

const Stack = createNativeStackNavigator<NativeStackParams>();

export function NativeStack() {
  const { colors } = useTheme();

  return (
    <GestureDetectorProvider>
      <Stack.Navigator>
        <Stack.Screen
          name="Article"
          component={ArticleScreen}
          options={({ route }) => ({
            title: `Article by ${route.params?.author ?? 'Unknown'}`,
            headerLargeTitle: true,
            headerLargeTitleShadowVisible: false,
          })}
          initialParams={{ author: 'Gandalf' }}
        />
        <Stack.Screen
          name="NewsFeed"
          component={NewsFeedScreen}
          options={{
            title: 'Feed',
            fullScreenGestureEnabled: true,
            goBackGesture: 'verticalSwipe',
          }}
        />
        <Stack.Screen
          name="Albums"
          component={AlbumsScreen}
          options={{
            title: 'Albums',
            presentation: 'modal',
            headerTransparent: true,
            headerBlurEffect: 'light',
            headerStyle: {
              // Add a background color since Android doesn't support blur effect
              backgroundColor: colors.card,
            },
          }}
        />
      </Stack.Navigator>
    </GestureDetectorProvider>
  );
}

NativeStack.title = 'Native Stack';
NativeStack.linking = linking;
NativeStack.options = {
  gestureEnabled: false,
};

const styles = StyleSheet.create({
  buttons: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 12,
    padding: 12,
  },
  headerHeight: {
    position: 'absolute',
    top: 0,
    right: 0,
    padding: 12,
    borderWidth: StyleSheet.hairlineWidth,
    borderRightWidth: 0,
    borderTopWidth: 0,
    borderBottomLeftRadius: 3,
  },
});

Presentation

Screen.Recording.2024-04-16.at.18.18.51.mov

Copy link

netlify bot commented Apr 16, 2024

Deploy Preview for react-navigation-example ready!

Name Link
🔨 Latest commit fa9e5b1
🔍 Latest deploy log https://app.netlify.com/sites/react-navigation-example/deploys/661eabec776be10008c2b8bd
😎 Deploy Preview https://deploy-preview-11943--react-navigation-example.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@tboba
Copy link
Contributor Author

tboba commented Apr 16, 2024

Looks like CIs are failing because of type interference from Typescript 🤔 lemme fix that quickly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant