TV App Drawer Navigation Implementation
The drawer navigation is usually the main navigation component of our TV app, providing an intuitive way for users to navigate between different sections using a remote control. Let’s examine the app/(drawer)/_layout.tsx
file in detail to understand how this navigation is implemented.
File Overview
Open app/(drawer)/_layout.tsx
in your code editor. This file is responsible for setting up the overall structure of our app’s navigation. Here’s the complete content of the file:
import { useNavigation } from 'expo-router';
import { useCallback } from 'react';
import { StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Drawer } from 'expo-router/drawer';
import { SpatialNavigationRoot } from 'react-tv-space-navigation';
import { Direction } from '@bam.tech/lrud';
import { DrawerActions } from '@react-navigation/native';
import { useMenuContext } from '../../components/MenuContext';
import CustomDrawerContent from '@/components/CustomDrawerContent';
import { scaledPixels } from '@/hooks/useScale';
export default function DrawerLayout() {
const styles = useDrawerStyles();
const { isOpen: isMenuOpen, toggleMenu } = useMenuContext();
const navigation = useNavigation();
console.log('isMenuOpen:', isMenuOpen);
const onDirectionHandledWithoutMovement = useCallback(
(movement: Direction) => {
console.log("Direction " + movement);
if (movement === 'right') {
navigation.dispatch(DrawerActions.closeDrawer());
toggleMenu(false);
}
},
[toggleMenu, navigation],
);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SpatialNavigationRoot
isActive={isMenuOpen}
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}>
<Drawer
drawerContent={CustomDrawerContent}
defaultStatus="open"
screenOptions={{
headerShown: false,
drawerActiveBackgroundColor: '#3498db',
drawerActiveTintColor: '#ffffff',
drawerInactiveTintColor: '#bdc3c7',
drawerStyle: styles.drawerStyle,
drawerLabelStyle: styles.drawerLabelStyle,
}}>
<Drawer.Screen
name="index"
options={{
drawerLabel: 'Home',
title: 'index',
}}
/>
<Drawer.Screen
name="explore"
options={{
drawerLabel: 'Explore',
title: 'explore',
}}
/>
<Drawer.Screen
name="tv"
options={{
drawerLabel: 'TV',
title: 'tv',
}}
/>
</Drawer>
</SpatialNavigationRoot>
</GestureHandlerRootView>
);
}
const useDrawerStyles = function () {
return StyleSheet.create({
drawerStyle: {
width: scaledPixels(300),
backgroundColor: '#2c3e50',
paddingTop: scaledPixels(0),
},
drawerLabelStyle: {
fontSize: scaledPixels(18),
fontWeight: 'bold',
marginLeft: scaledPixels(10),
},
});
};
Let’s break this down section by section:
Imports
import { useNavigation } from 'expo-router';
import { useCallback } from 'react';
import { StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Drawer } from 'expo-router/drawer';
import { SpatialNavigationRoot } from 'react-tv-space-navigation';
import { Direction } from '@bam.tech/lrud';
import { DrawerActions } from '@react-navigation/native';
import { useMenuContext } from '../../components/MenuContext';
import CustomDrawerContent from '@/components/CustomDrawerContent';
import { scaledPixels } from '@/hooks/useScale';
These imports bring in necessary components and hooks for our drawer navigation:
- expo-router components for navigation
- react-tv-space-navigation for TV-specific navigation features
- Custom components and hooks (CustomDrawerContent, useMenuContext, scaledPixels)
DrawerLayout Component
export default function DrawerLayout() {
const styles = useDrawerStyles();
const { isOpen: isMenuOpen, toggleMenu } = useMenuContext();
const navigation = useNavigation();
// ... (onDirectionHandledWithoutMovement callback)
return (
// ... (component JSX)
);
}
This is the main component that sets up our drawer layout. It uses:
useDrawerStyles
for stylinguseMenuContext
to manage the open/closed state of the draweruseNavigation
for programmatic navigation
Closing the drawer
const onDirectionHandledWithoutMovement = useCallback(
(movement: Direction) => {
console.log("Direction " + movement);
if (movement === 'right') {
navigation.dispatch(DrawerActions.closeDrawer());
toggleMenu(false);
}
},
[toggleMenu, navigation],
);
This callback handles directional input when the drawer is open. It specifically closes the drawer when a ‘right’ direction is detected, which is intuitive for TV navigation.
Drawer Component Structure
<GestureHandlerRootView style={{ flex: 1 }}>
<SpatialNavigationRoot
isActive={isMenuOpen}
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}>
<Drawer
drawerContent={CustomDrawerContent}
defaultStatus="open"
screenOptions={{
headerShown: false,
drawerActiveBackgroundColor: '#3498db',
drawerActiveTintColor: '#ffffff',
drawerInactiveTintColor: '#bdc3c7',
drawerStyle: styles.drawerStyle,
drawerLabelStyle: styles.drawerLabelStyle,
}}>
{/* Drawer.Screen components */}
</Drawer>
</SpatialNavigationRoot>
</GestureHandlerRootView>
This structure sets up our drawer navigation:
GestureHandlerRootView
is required for gesture handlingSpatialNavigationRoot
enables TV-specific navigation featuresDrawer
component from expo-router sets up the actual drawer
Key props of the Drawer component:
drawerContent={CustomDrawerContent}
: Uses our custom drawer content componentdefaultStatus="open"
: Drawer is open by default, common for TV appsscreenOptions
: Defines the visual style of the drawer
Custom Drawer Content
// components/CustomDrawerContent.tsx
export default function CustomDrawerContent(props: any) {
const router = useRouter();
const { isOpen: isMenuOpen, toggleMenu } = useMenuContext();
const styles = useDrawerStyles();
const drawerItems = [
{ name: '/', label: 'Home' },
{ name: 'explore', label: 'Explore'},
{ name: 'tv', label: 'TV'},
];
return (
<SpatialNavigationRoot isActive={isMenuOpen}>
<DrawerContentScrollView {...props} style={styles.container} scrollEnabled={false}>
{/* Drawer content implementation */}
</DrawerContentScrollView>
</SpatialNavigationRoot>
);
}
Key features:
- Implements
SpatialNavigationRoot
for TV navigation - Uses context for menu state management
- Provides scrollable content container
Drawer Screens
<Drawer.Screen
name="index"
options={{
drawerLabel: 'Home',
title: 'index',
}}
/>
<Drawer.Screen
name="explore"
options={{
drawerLabel: 'Explore',
title: 'explore',
}}
/>
<Drawer.Screen
name="tv"
options={{
drawerLabel: 'TV',
title: 'tv',
}}
/>
These Drawer.Screen
components define the main sections of our app:
name
: Corresponds to the file name in theapp/(drawer)/
directoryoptions
: Defines how the screen is displayed in the drawer
Styles
const useDrawerStyles = function () {
return StyleSheet.create({
drawerStyle: {
width: scaledPixels(300),
backgroundColor: '#2c3e50',
paddingTop: scaledPixels(0),
},
drawerLabelStyle: {
fontSize: scaledPixels(18),
fontWeight: 'bold',
marginLeft: scaledPixels(10),
},
});
};
This function creates styles for the drawer, using scaledPixels
to ensure the design is responsive across different TV screen sizes.
Conclusion
This drawer navigation setup is optimized for TV interfaces:
- It uses spatial navigation for remote control input
- The drawer is open by default for easy access
- Styles are scaled appropriately for TV screens
- Custom drawer content allows for TV-specific design
In the next section, we’ll explore how to customize this drawer further and add new screens to our TV app.