Preface

I need a drawer menu on my App. React navigation drawer supports this function, but it changes the structure of the screen. I don’t want to change it because I only need to use this simple drawer menu component on one of the screens. The general effect is as follows:
drawer.gif

Installation dependencies

react-native-modal component can roughly meet my needs. The modal frame plus the animation of moving in and out of left and right plus gestures can basically realize the component of pulling the drawer side by side. Install it now:

yarn add react-native-modal -save

Write SideMenu.js

The SideMenu component is the content displayed in the side menu

import React from 'react';
import { Text, View, SafeAreaView } from 'react-native';
import styles from './styles';
const Title = ({ title }) => {
  return <Text style={styles.title}>{title}</Text>;
};

const SwitchText = ({ text }) => {
  return <Text style={styles.switchText}>{text}</Text>;
};

const Description = ({ text }) => {
  return <Text style={styles.description}>{text}</Text>;
};
const SideMenu = props => {
  return (
    <SafeAreaView style={styles.safeAreaView}>
      <View style={styles.container}>
        <Title title="Timeline" />

        <View>
          <View style={styles.swithBlock}>
            <SwitchText text="Ratings with reviews only" />
          </View>
          <Description text="When enabled, on your timeline we will only show ratings with reviews." />
        </View>
      </View>
      <View style={styles.footer}>
        <Text style={styles.link}>Press to call parent function</Text>
      </View>
    </SafeAreaView>
  );
};

export default SideMenu;

import { StyleSheet } from 'react-native';
import { screenSize } from '../../../utils/tools';
const styles = StyleSheet.create({
  safeAreaView: {
    flex: 1,
    backgroundColor: '#fff'
  },
  container: {
    margin: 12,
    flex: 1
  },
  title: {
    marginTop: 15,
    marginBottom: 10,
    color: '#444',
    fontSize: 14
  },
  swithBlock: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  switchText: {
    fontSize: 14,
    color: '#222'
  },
  link: {
    padding: 5,
    color: '#892853'
  },
  description: {
    fontSize: 13,
    color: '#555',
    marginTop: 12,
    marginBottom: 6
  }
});

export default styles;

Write the main page

Reference components, through the isVisible parameter control menu display and hide, the toggleSideMenu method controls the switch display and hide, and there are some parameters that control the entrance animation. To make it closer to the drawer assembly, I use slideInLeft .

import React, { useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { ButtonGroup, Header } from 'react-native-elements';
import common from '../../styles/common';
import Modal from 'react-native-modal';
import SideMenu from './SideMenu';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { useNavigation } from '@react-navigation/native';

const ProjectDetail = props => {
  const { route } = props;
  console.log('路由参数', route.params);
  const [visible, setVisible] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const navigation = useNavigation();
  const toggleSideMenu = () => {
    setVisible(!visible);
  };
  const updateIndex = index => {
    setSelectedIndex(index);
  };
  const component1 = () => <Text>文件</Text>;
  const component2 = () => <Text>流程中心</Text>;
  const buttons = [{ element: component1 }, { element: component2 }];
  return (
    <View style={common.container}>
      <Header
        leftComponent={
          <View>
            <TouchableOpacity onPress={navigation.goBack}>
              <Ionicons name="arrow-back" size={24} color={'#fff'} />
            </TouchableOpacity>
          </View>
        }
        centerComponent={{ text: 'MY TITLE', style: { color: '#fff' } }}
        rightComponent={
          <View>
            <TouchableOpacity onPress={toggleSideMenu}>
              <Ionicons name="menu" size={24} color={'#fff'} />
            </TouchableOpacity>
          </View>
        }
      />
      <ButtonGroup
        onPress={updateIndex}
        selectedIndex={selectedIndex}
        buttons={buttons}
        containerStyle={{ height: 28 }}
      />
      <Modal
        isVisible={visible}
        onBackdropPress={toggleSideMenu} // Android back press
        onSwipeComplete={toggleSideMenu} // Swipe to discard
        animationIn="slideInLeft" // Has others, we want slide in from the left
        animationOut="slideOutLeft" // When discarding the drawer
        swipeDirection="left" // Discard the drawer with swipe to left
        useNativeDriver // Faster animation
        hideModalContentWhileAnimating // Better performance, try with/without
        propagateSwipe // Allows swipe events to propagate to children components (eg a ScrollView inside a modal)
        style={styles.sideMenuStyle}
      >
        <SideMenu />
      </Modal>
    </View>
  );
};

export default ProjectDetail;
import { StyleSheet } from 'react-native';
import { screenSize } from '../../utils/tools';
const styles = StyleSheet.create({
  sideMenuStyle: {
    width: screenSize.width * 0.75,
    margin: 0
  }
});

export default styles;

Encapsulation

Because some pages may also need similar drawer components at other times, Modal component. First of all, we need to click on the function to display the menu externally (possibly in the header of the page), so it must be exposed to the parent The component calls the internal method of the sub-component, then we need forwardRef and useImperativeHandle :
forwardRef : Refers to the ref instance of the parent component and becomes a parameter of the child component, which can reference the ref of the parent component and bind it to the node of the child component itself.
useImperativeHandle : The first parameter receives a ref instance that refers to the parent component through forwardRef, and the second parameter is a callback function that returns an object, which stores the properties or methods that need to be exposed to the parent component;
The official recommendation useImperativeHandle and forwardRef at the same time to reduce the attributes exposed to the parent component and avoid the use of imperative codes such as ref.
Under normal circumstances, ref cannot be attached to a function component, because the function component does not have an instance, but useImperativeHandle provides us with something similar to an instance. It helps us through useImperativeHandle second parameter, the contents of the object returned to the parent mount components ref.current on.
forwardRef will create a React component, which can ref attributes it accepts to another component under its component tree.
After encapsulating Drawer , it is as follows:

import React, { useState, useImperativeHandle } from 'react';
import styles from './styles';
import Modal from 'react-native-modal';

const Drawer = React.forwardRef((props, ref) => {
  const [visible, setVisible] = useState(false);
  const toggleSideMenu = () => {
    setVisible(!visible);
  };
  useImperativeHandle(ref, () => ({
    toggleSideMenu: () => toggleSideMenu()
  }));
  return (
    <Modal
      isVisible={visible}
      onBackdropPress={toggleSideMenu} // Android back press
      onSwipeComplete={toggleSideMenu} // Swipe to discard
      animationIn="slideInLeft" // Has others, we want slide in from the left
      animationOut="slideOutLeft" // When discarding the drawer
      swipeDirection="left" // Discard the drawer with swipe to left
      useNativeDriver // Faster animation
      hideModalContentWhileAnimating // Better performance, try with/without
      propagateSwipe // Allows swipe events to propagate to children components (eg a ScrollView inside a modal)
      style={styles.sideMenuStyle}
    >
      {props.children}
    </Modal>
  );
});

export default Drawer;

Parent component use:

import React, { useRef, useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { ButtonGroup, Header } from 'react-native-elements';
import common from '../../styles/common';
import SideMenu from './SideMenu';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { useNavigation } from '@react-navigation/native';
import Drawer from '../../components/Drawer';

const ProjectDetail = props => {
  const { route } = props;
  console.log('路由参数', route.params);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const navigation = useNavigation();
  const drawerRef = useRef();
  const updateIndex = index => {
    setSelectedIndex(index);
  };
  const component1 = () => <Text>文件</Text>;
  const component2 = () => <Text>流程中心</Text>;
  const buttons = [{ element: component1 }, { element: component2 }];
  return (
    <View style={common.container}>
      <Header
        leftComponent={
          <View>
            <TouchableOpacity onPress={navigation.goBack}>
              <Ionicons name="arrow-back" size={24} color={'#fff'} />
            </TouchableOpacity>
          </View>
        }
        centerComponent={{ text: 'MY TITLE', style: { color: '#fff' } }}
        rightComponent={
          <View>
            <TouchableOpacity
              onPress={() => drawerRef.current.toggleSideMenu()}
            >
              <Ionicons name="menu" size={24} color={'#fff'} />
            </TouchableOpacity>
          </View>
        }
      />
      <ButtonGroup
        onPress={updateIndex}
        selectedIndex={selectedIndex}
        buttons={buttons}
        containerStyle={{ height: 28 }}
      />
      <Drawer ref={drawerRef}>
        <SideMenu />
      </Drawer>
    </View>
  );
};

export default ProjectDetail;

MangoGoing
774 声望1.2k 粉丝

开源项目:详见个人详情