React Native for TVDrawer Navigation

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:

app/(drawer)/_layout.tsx
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 styling
  • useMenuContext to manage the open/closed state of the drawer
  • useNavigation 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 handling
  • SpatialNavigationRoot enables TV-specific navigation features
  • Drawer component from expo-router sets up the actual drawer

Key props of the Drawer component:

  • drawerContent={CustomDrawerContent}: Uses our custom drawer content component
  • defaultStatus="open": Drawer is open by default, common for TV apps
  • screenOptions: 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 the app/(drawer)/ directory
  • options: 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.