前言

我的App应用程序上需要一个抽屉菜单。React navigation drawer导航支持此功能,但是改变了屏幕的结构,我不希望更改,因为我只是其中一个屏幕上需要用到这个简单抽屉菜单组件。大致的效果如下:
drawer.gif

安装依赖

react-native-modal 组件大致可以满足我的需求,模态框加上左右移入移出的动画加上手势基本能实现侧拉抽屉的组件。现在安装它:

1
yarn add react-native-modal -save

编写SideMenu.js

SideMenu组件是侧拉菜单里面展示的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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;

1
2
3
4
5
6
7
8
9
10
11
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组件封装一下,首先想到我们需要在外部(有可能是在页面头部菜单)点击出发显示菜单的功能,所以必须暴露给父组件调用子组件内部方法,那么我们就需要forwardRefuseImperativeHandle:
forwardRef:引用父组件的ref实例,成为子组件的一个参数,可以引用父组件的ref绑定到子组件自身的节点上.
useImperativeHandle : 第一个参数,接收一个通过forwardRef引用父组件的ref实例,第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法;
官方建议useImperativeHandleforwardRef同时使用,减少暴露给父组件的属性,避免使用 ref 这样的命令式代码。
正常情况下 ref 是不能挂在到函数组件上的,因为函数组件没有实例,但是 useImperativeHandle 为我们提供了一个类似实例的东西。它帮助我们通过 useImperativeHandle 的第 2 个参数,所返回的对象的内容挂载到 父组件的 ref.current 上。
forwardRef会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
封装Drawer后,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;

父组件使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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;