前言
我的App应用程序上需要一个抽屉菜单。React navigation drawer导航支持此功能,但是改变了屏幕的结构,我不希望更改,因为我只是其中一个屏幕上需要用到这个简单抽屉菜单组件。大致的效果如下:
安装依赖
react-native-modal
组件大致可以满足我的需求,模态框加上左右移入移出的动画加上手势基本能实现侧拉抽屉的组件。现在安装它:
yarn add react-native-modal -save
编写SideMenu.js
SideMenu组件是侧拉菜单里面展示的内容
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;
编写主页面
引用组件,通过isVisible
参数控制菜单显示隐藏,toggleSideMenu
方法控制切换显示隐藏,还有一些控制入场动画的参数。我为了使它更接近抽屉组件,所以使用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;
封装
因为其他时候有可能部分页面也需要类似的抽屉组件,所以把这个Modal
组件封装一下,首先想到我们需要在外部(有可能是在页面头部菜单)点击出发显示菜单的功能,所以必须暴露给父组件调用子组件内部方法,那么我们就需要forwardRef
和useImperativeHandle
:forwardRef
:引用父组件的ref实例,成为子组件的一个参数,可以引用父组件的ref绑定到子组件自身的节点上.useImperativeHandle
: 第一个参数,接收一个通过forwardRef引用父组件的ref实例,第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法;
官方建议useImperativeHandle
和forwardRef
同时使用,减少暴露给父组件的属性,避免使用 ref 这样的命令式代码。
正常情况下 ref 是不能挂在到函数组件上的,因为函数组件没有实例,但是 useImperativeHandle
为我们提供了一个类似实例的东西。它帮助我们通过 useImperativeHandle
的第 2 个参数,所返回的对象的内容挂载到 父组件的 ref.current
上。forwardRef
会创建一个React
组件,这个组件能够将其接受的 ref
属性转发到其组件树下的另一个组件中。
封装Drawer
后,如下:
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;
父组件使用:
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;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。