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

Adding freezeOnBlur for android and iOS #113

Closed
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
8 changes: 8 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ Badge to show on the tab icon.

Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.

#### `freezeOnBlur`

Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to `false`. Defaults to `true` when `enableFreeze()` from `react-native-screens` package is run at the top of the application.

Requires `react-native-screens` version >=3.16.0.

Only supported on iOS and Android.

### Events

The navigator can emit events on certain actions. Supported events are:
Expand Down
5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import LabeledTabs from './Examples/Labeled';
import NativeBottomTabs from './Examples/NativeBottomTabs';
import TintColorsExample from './Examples/TintColors';
import NativeBottomTabsVectorIcons from './Examples/NativeBottomTabsVectorIcons';
import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';

const FourTabsIgnoreSafeArea = () => {
return <FourTabs ignoresTopSafeArea />;
Expand Down Expand Up @@ -112,6 +113,10 @@ const examples = [
component: NativeBottomTabsVectorIcons,
name: 'Native Bottom Tabs with Vector Icons',
},
{
component: NativeBottomTabsFreezeOnBlur,
name: 'Native Bottom Tabs with FreezeOnBlur',
},
{ component: NativeBottomTabs, name: 'Native Bottom Tabs' },
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
{
Expand Down
115 changes: 115 additions & 0 deletions example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator';

const store = new Set<Dispatch>();

type Dispatch = (value: number) => void;

function useValue() {
const [value, setValue] = React.useState<number>(0);

React.useEffect(() => {
const dispatch = (value: number) => {
setValue(value);
};
store.add(dispatch);
return () => {
store.delete(dispatch);
};
}, [setValue]);

return value;
}

function HomeScreen() {
return (
<View style={styles.screenContainer}>
<Text>Home!</Text>
</View>
);
}

function DetailsScreen(props: any) {
const value = useValue();
const screenName = props?.route?.params?.screenName;
// only 1 'render' should appear at the time
console.log(`${Platform.OS} Details Screen render ${value} ${screenName}`);
return (
<View style={styles.screenContainer}>
<Text>Details!</Text>
<Text style={{ alignSelf: 'center' }}>
Details Screen {value} {screenName ? screenName : ''}{' '}
</Text>
</View>
);
}
const Tab = createNativeBottomTabNavigator();

export default function NativeBottomTabsFreezeOnBlur() {
React.useEffect(() => {
let timer = 0;
const interval = setInterval(() => {
timer = timer + 1;
store.forEach((dispatch) => dispatch(timer));
}, 3000);
return () => clearInterval(interval);
}, []);

return (
<Tab.Navigator
screenOptions={{
freezeOnBlur: true,
}}
>
<Tab.Screen
name="Article"
component={HomeScreen}
initialParams={{
screenName: 'Article',
}}
options={{
tabBarIcon: () => require('../../assets/icons/article_dark.png'),
}}
/>
<Tab.Screen
name="Albums"
component={DetailsScreen}
initialParams={{
screenName: 'Albums',
}}
options={{
tabBarIcon: () => require('../../assets/icons/grid_dark.png'),
}}
/>
<Tab.Screen
name="Contact"
component={DetailsScreen}
initialParams={{
screenName: 'Contact',
}}
options={{
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
}}
/>
<Tab.Screen
name="Chat"
component={DetailsScreen}
initialParams={{
screenName: 'Chat',
}}
options={{
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
}}
/>
</Tab.Navigator>
);
}

const styles = StyleSheet.create({
screenContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"react": "18.3.1",
"react-native": "0.75.3",
"react-native-builder-bob": "^0.30.2",
"react-native-screens": "^3.16.0",
"release-it": "^15.0.0",
"turbo": "^1.10.7",
"typescript": "^5.2.2"
Expand All @@ -110,11 +111,15 @@
"peerDependencies": {
"@react-navigation/native": ">=6",
"react": "*",
"react-native": "*"
"react-native": "*",
"react-native-screens": ">=3.16.0"
},
"peerDependenciesMeta": {
"@react-navigation/native": {
"optional": true
},
"react-native-screens": {
"optional": true
}
},
"workspaces": [
Expand Down
82 changes: 53 additions & 29 deletions src/TabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Image,
Platform,
StyleSheet,
View,
processColor,
} from 'react-native';

Expand All @@ -14,6 +13,10 @@ import TabViewAdapter from './TabViewAdapter';
import useLatestCallback from 'use-latest-callback';
import { useMemo, useState } from 'react';
import type { BaseRoute, NavigationState } from './types';
import {
MaybeScreen,
MaybeScreenContainer,
} from './react-navigation/views/ScreenFallback';

const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
icon?.sfSymbol;
Expand Down Expand Up @@ -104,6 +107,13 @@ interface Props<Route extends BaseRoute> {
focused: boolean;
}) => ImageSource | undefined;

/**
* Get freezeOnBlur for the current screen. Uses false by default.
* Defaults to `true` when `enableFreeze()` is run at the top of the application.
*
*/
getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;

/**
* Background color of the tab bar.
*/
Expand Down Expand Up @@ -136,6 +146,7 @@ const TabView = <Route extends BaseRoute>({
: route.focusedIcon,
barTintColor,
getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
tabBarActiveTintColor: activeTintColor,
tabBarInactiveTintColor: inactiveTintColor,
hapticFeedbackEnabled = true,
Expand Down Expand Up @@ -238,39 +249,48 @@ const TabView = <Route extends BaseRoute>({
barTintColor={barTintColor}
rippleColor={rippleColor}
>
{trimmedRoutes.map((route) => {
if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
// Don't render a screen if we've never navigated to it
if (Platform.OS === 'android') {
return null;
<MaybeScreenContainer style={styles.container}>
{trimmedRoutes.map((route) => {
const isFocused = route.key === focusedKey;

if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
// Don't render a screen if we've never navigated to it
if (Platform.OS === 'android') {
return null;
}
return (
<MaybeScreen
key={route.key}
visible={isFocused}
style={styles.fullWidth}
collapsable={false}
/>
);
}

const freezeOnBlur = getFreezeOnBlur({ route });

return (
<View
<MaybeScreen
key={route.key}
visible={isFocused}
freezeOnBlur={freezeOnBlur}
collapsable={false}
style={styles.fullWidth}
/>
style={[
styles.fullWidth,
Platform.OS === 'android' && {
display: isFocused ? 'flex' : 'none',
},
]}
>
{renderScene({
route,
jumpTo,
})}
</MaybeScreen>
);
}

return (
<View
key={route.key}
collapsable={false}
style={[
styles.fullWidth,
Platform.OS === 'android' && {
display: route.key === focusedKey ? 'flex' : 'none',
},
]}
>
{renderScene({
route,
jumpTo,
})}
</View>
);
})}
})}
</MaybeScreenContainer>
</TabViewAdapter>
);
};
Expand All @@ -280,6 +300,10 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
container: {
flex: 1,
overflow: 'hidden',
},
});

export default TabView;
9 changes: 9 additions & 0 deletions src/react-navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export type NativeBottomTabNavigationOptions = {
* Active tab color.
*/
tabBarActiveTintColor?: string;

/**
* Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
* Defaults to `true` when `enableFreeze()` is run at the top of the application.
* Requires `react-native-screens` version >=3.16.0.
*
* Only supported on iOS and Android.
*/
freezeOnBlur?: boolean;
};

export type NativeBottomTabDescriptor = Descriptor<
Expand Down
3 changes: 3 additions & 0 deletions src/react-navigation/views/NativeBottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export default function NativeBottomTabView({

return null;
}}
getFreezeOnBlur={({ route }) =>
descriptors[route.key]?.options.freezeOnBlur
}
getLazy={({ route }) => descriptors[route.key]?.options.lazy ?? true}
onTabLongPress={(index) => {
const route = state.routes[index];
Expand Down
35 changes: 35 additions & 0 deletions src/react-navigation/views/ScreenFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import { Platform, StyleProp, View, ViewProps, ViewStyle } from 'react-native';

type Props = {
visible: boolean;
children?: React.ReactNode;
freezeOnBlur?: boolean;
style?: StyleProp<ViewStyle>;
collapsable?: boolean;
};

let Screens: typeof import('react-native-screens') | undefined;

try {
Screens = require('react-native-screens');
} catch (e) {
// Ignore
}

export const MaybeScreenContainer = ({ children, ...rest }: ViewProps) => {
if (Platform.OS === 'android' && Screens?.screensEnabled()) {
return (
<Screens.ScreenContainer {...rest}>{children}</Screens.ScreenContainer>
);
}

return <>{children}</>;
};

export function MaybeScreen({ visible, ...rest }: Props) {
if (Screens?.screensEnabled()) {
return <Screens.Screen activityState={visible ? 2 : 0} {...rest} />;
}
return <View {...rest} />;
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type BaseRoute = {
focusedIcon?: ImageSourcePropType | AppleIcon;
unfocusedIcon?: ImageSourcePropType | AppleIcon;
activeTintColor?: string;
freezeOnBlur?: boolean;
};

export type NavigationState<Route extends BaseRoute> = {
Expand Down
Loading