Слишком много чего происходит в панели меню вашего приложения? Решением может быть разделение его на отдельные панели. Примером, с которым вы, вероятно, знакомы, является мобильное приложение Slack, в котором перечислены каналы и прямые сообщения на главной панели и на второй панели для переключения между рабочими областями. (Говоря о Slack, посмотрите сообщение Джамона Холмгрена о его любимых хитростях в Slack!)

Это простое руководство, но я предполагаю, что вы понимаете основы React Navigation. Если нет, то загляните в официальное руководство и вернитесь. Весь код доступен на GitHub здесь. Давайте начнем!

Добавить DrawerNavigator

Создайте свой RootNavigator с помощью createDrawerNavigator и установите contentComponent на компонент Drawer, который мы создадим дальше.

// app/navigation/root-navigator.ts
import { createDrawerNavigator } from "react-navigation"
import { Drawer } from "./drawer/drawer"
export const RootNavigator = createDrawerNavigator(
  {
    exampleScreen: { screen: FirstExampleScreen },
  },
  {
    initialRouteName: "exampleScreen",
    contentComponent: Drawer,
  },
)

Компонент Drawer на данный момент - это просто SafeAreaView.

// app/navigation/drawer/drawer.tsx
import * as React from "react"
import { SafeAreaView } from "react-native"
import { NavigationInjectedProps } from "react-navigation"
interface DrawerState {}
export class Drawer extends React.Component<NavigationInjectedProps, DrawerState> {
  render() {
    return <SafeAreaView />
  }
}

Прежде чем двигаться дальше, убедитесь, что ящик работает.

Создать панели

Теперь давайте добавим в ящик две панели. Эти панели могут содержать все, что вы хотите; один для каналов чата, а другой, например, для рабочих пространств. Мы оставим содержание на ваше усмотрение, и просто поставим ярлык для каждого из них.

// app/navigation/drawer/drawer.tsx
import * as React from "react"
import { Text, Animated, SafeAreaView } from "react-native"
import { NavigationInjectedProps } from "react-navigation"
interface DrawerState {
  drawerWidth?: number
}
export class Drawer extends React.Component<NavigationInjectedProps, DrawerState> {
  state = {
    drawerWidth: 0,
  }
  render() {
    const { drawerWidth } = this.state
    return (
      <SafeAreaView
        onLayout={event => {
          this.setState({
            drawerWidth: event.nativeEvent.layout.width,
          })
        }}
        style={{
          flexDirection: "row",
          height: "100%",
        }}
      >
        {/* Left Pane */}
        <Animated.View
          style={{
            left: -drawerWidth,
            width: drawerWidth,
            backgroundColor: "#00c3e3",
          }}
        >
          <Text>Left Pane</Text>
        </Animated.View>
        {/* Right Pane */}
        <Animated.View
          style={{
            left: -drawerWidth,
            width: drawerWidth,
            backgroundColor: "#ff4554",
          }}
        >
          <Text>Right Pane</Text>
        </Animated.View>
      </SafeAreaView>
    )
  }
}

onLayout - это обратный вызов, вызываемый всякий раз, когда среда выполнения React Native выполняет макет для компонента. Этот обратный вызов получает событие со свойствами, включая интересующее нас, ширину самого внешнего SafeAreaView ящика. Нам нужно это знать, чтобы размеры панелей были точно такими же, как у выдвижного ящика. Таким образом, когда панель отображается, она занимает весь ящик; ни больше ни меньше. В onLayout мы получим ширину и сохраним ее в состоянии ящика как drawerWidth. Обязательно инициализируйте drawerWidth, чтобы он не был неопределенным до того, как произойдет первый макет.

Давайте взглянем на наш ящик.

Видна только правая панель. Другая панель, естественно, находится слева от нее, за пределами экрана. Это потому, что мы устанавливаем свойство left каждой панели на минус ширину ящика, что перемещает левую панель за пределы экрана, а правую - в ящик.

Переключить панели

Чтобы переключаться между панелями, нам просто нужно изменить свойство left каждой панели обратно на ноль при нажатии кнопки. Добавьте логическое свойство к DrawerState, которое будет определять, переключаться ли на левый ящик; инициализируйте это значение равным false, чтобы в ящике отображалась правая панель. Затем создайте метод для рендеринга кнопки; это будет использоваться в обеих панелях.

interface DrawerState {
  drawerWidth?: number
  displayLeftPane?: boolean
}
export class Drawer extends React.Component<NavigationInjectedProps, DrawerState> {
  state = {
    drawerWidth: 0,
    displayLeftPane: false,
  }
  
// …
renderSlideButton() {
    return (
      <TouchableOpacity
        style={{
          height: 100,
          marginVertical: 50,
        }}
        onPress={() => {
          this.setState({
            displayLeftPane: !this.state.displayLeftPane,
          })
        }}
      >
        <Text>Slide Drawer</Text>
      </TouchableOpacity>
    )
  }
}

Теперь внутри render внесите следующие изменения. Извлеките displayLeftPane из состояния и используйте его для условного задания paneShift. Затем измените left в стиле каждой панели на paneShift. Также добавьте overflow: “hidden" к стилю SafeAreaView, чтобы правая панель не оставалась видимой, когда она выдвигается из области выдвижного ящика. Наконец, добавьте метод renderSlideButton, который мы только что написали, к содержимому каждой кнопки. Здесь вы можете увидеть весь файл в репо.

render() {
    const { drawerWidth, displayLeftPane } = this.state
    const paneShift = displayLeftPane ? 0 : -drawerWidth
    // …
    return (
      <SafeAreaView
        onLayout={…}
        style={{
          flexDirection: "row",
          height: "100%",
          overflow: "hidden",
        }}
      >
        {/* Left Pane */}
        <Animated.View
          style={{
            left: paneShift,
            width: drawerWidth,
            backgroundColor: "#00c3e3",
          }}
        >
          <Text>Left Pane</Text>
          {this.renderSlideButton()}
        </Animated.View>
        {/* Right Pane */}
        <Animated.View
          style={{
            left: paneShift,
            width: drawerWidth,
            backgroundColor: "#ff4554",
          }}
        >
          <Text>Right Pane</Text>
          {this.renderSlideButton()}
        </Animated.View>
      </SafeAreaView>
    )
  }

Давай увидим это!

Анимировать

Это здорово, но давайте сделаем так, чтобы панели скользили, а не скачкообразно прыгали. Это можно сделать с помощью нескольких простых изменений.

Сначала добавьте новое свойство Animated.Value в DrawerState и инициализируйте его следующим образом:

interface DrawerState {
  drawerWidth?: number
  displayLeftPane?: boolean
  slideProgress?: Animated.Value
}
export class Drawer extends React.Component<NavigationInjectedProps, DrawerState> {
  state = {
    drawerWidth: 0,
    displayLeftPane: false,
    slideProgress: new Animated.Value(0),
  }

Это значение будет отображать ход процесса скольжения, имея значение 0, когда ящик находится в начальном положении, и значение 1, когда он полностью сдвинулся влево, и промежуточные значения, когда он перемещается.

Затем оживите это свойство до его правильного значения внутри componentDidMount.

componentDidUpdate() {
    const { displayLeftPane, slideProgress } = this.state
    const toValue = displayLeftPane ? 1 : 0
    Animated.timing(slideProgress, {
      toValue,
      duration: 250,
    }).start()
  }

Наконец, используйте slideProgress для вычисления slidePosition:

const paneShift = this.state.slideProgress.interpolate({
      inputRange: [0, 1],
      outputRange: [-drawerWidth, 0],
    })

Вот и все! Теперь ящик будет плавно перемещаться вперед и назад.

Могу ли я использовать жесты для переключения между панелями?

До сих пор мне не удавалось сделать это с помощью встроенного компонента выдвижного ящика React Native. Ящик React Native происходит от response-navigation-drawer, который использует GestureHandler обработчика response-native-gesture. GestureHandler настроен для перехвата событий касания, которые позволят вам переключать панель с помощью жестов смахивания. (Экспериментируя, я выяснил, что это действительно работает, если вы начинаете свой жест с ящика, но это не является ни очевидным, ни хорошим UX). Если найдете способ, поделитесь, пожалуйста.

Спасибо, что ознакомились с этим руководством, дайте мне знать, что вы думаете, в комментариях или здесь, в Твиттере!