1.封装独立的视图组件
背景是要开发的一个RN页面,需要调用时间选择器组件,然后发现RN中在iOS端有DatePickerIOS组件,但是对应到安卓端,却是一个DatePickerAndroid.open()类方法,这就有了把这两个合二为一拼成一个DatePicker的组件,这样使用就不用每次在区分平台调用了。
UI设计稿的样式是这样的:

因为我是iOS开发,所以就先封装在iOS平台下的组件,新建了DatePicker.tsx文件,在里面封装了样式,这时候的代码大概是这样的:
interface IProps {
title: string;
date: Date;
mode: 'date' | 'time' | 'datetime';
cancelAction?: () => void;
confirmAction: (selectedDate: Date) => void;
}
class DatePicker extends PureComponent<IProps, {}> {
render() {
return (
<View style={viewStyles.container}>
<View style={viewStyles.contentContainer}>
{this._renderHeader()}
{this._renderDatePicker()}
</View>
</View>
);
}
_renderHeader = () => {
return (
<View style={viewStyles.headerContainer}>
<TouchableOpacity
style={viewStyles.actionContainer}
activeOpacity={1.0}
onPress={this.onCancelClicked}>
<Text style={viewStyles.cancel}>取消</Text>
</TouchableOpacity>
<Text style={viewStyles.title}>{this.props.title}</Text>
<TouchableOpacity
style={viewStyles.actionContainer}
activeOpacity={1.0}
onPress={this.onConfirmClicked}>
<Text style={viewStyles.confirm}>确定</Text>
</TouchableOpacity>
</View>
);
};
_renderDatePicker = () => {
if (Platform.OS === 'ios') {
return (
<DatePickerIOS
date={this.props.date ? this.props.date : new Date()}
mode={this.props.mode ? this.props.mode : 'datetime'}
onDateChange={this.onDateChange}
/>
);
} else {
return null;
}
};
// actions
onDateChange = (newDate: Date) => {
this.selectedDate = newDate;
};
onCancelClicked = () => {};
onConfirmClicked = () => {
this.props.confirmAction(this.selectedDate);
};
}
然后开始想怎么给其他页面使用,开始是直接在其他页面中,通过state中的一个showDatePicker :boolen属性来设置DatePicker组件的显示隐藏,代码类似这样:
render() {
return (
<View>
{this._renderDatePicker()}
</View>
)
}
_renderDatePicker = () => {
if (!this.state.showDatePicker) {
return null;
}
return <DatePicker />;
};
这样写引起的不爽的就是每个页面需要使用DatePicker时,都需要添加一个state来控制显示隐藏。
然后就思考是不是有什么方法能直接通过一个类方法像这样的DatePicker.show()能直接显示,不侵入代码呢。然后开始查资料问同事,发现了RootSiblings这个东西,他是一个第三方库react-native-root-siblings,他的使用方法可以看下这边文章:React Native 进行 Modal 的封装使用。
我理解的RootSiblings就是他可以动态的注入你想插入的代码段到页面中,这样就能实现我想要达到的不入侵页面布局代码的效果了。
修改之后的代码主要是添加了一个类方法public static showDatePicker,通过类方法就能直接显示出来DatePicker组件,隐藏会在组件内自己实现,代码是这样的:
// class外部变量
let datePicker: RootSiblings;
public static showDatePicker(
title: string,
selectedDate: Date | null,
datePickerMode: 'date' | 'time' | 'datetime',
confirmAction: (selectedDate: Date) => void,
cancelCallback?: () => void
) {
if (Platform.OS === 'ios') {
// 使用RootSiblings包裹起来DatePicker
datePicker = new RootSiblings(
(
<DatePicker
title={title}
date={selectedDate || new Date()}
mode={datePickerMode}
cancelAction={cancelCallback}
confirmAction={confirmAction}
/>
)
);
return datePicker;
} else if (Platform.OS === 'android') {
DatePickerAndroid.open();
}
}
onCancelClicked = () => {
datePicker && datePicker.destroy();
};
onConfirmClicked = () => {
this.props.confirmAction(this.selectedDate);
datePicker && datePicker.destroy();
};
2.RN中navigate使用
需要使用第三方库react-navigation
使用参考文章:
直接代码:
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
const RootStack = createStackNavigator(
{
PageA: {
screen: PageA,
navigationOptions: {
header: () => {
return <></>;
}
}
},
PageB: {
screen: PageB,
navigationOptions: {
header: () => {
return <></>;
}
}
}
},
{
initialRouteName: 'PageA',
headerMode: 'screen',
mode: 'card', // android端左右切换不生效
// 自定义切换动画
transitionConfig: () => ({
screenInterpolator: sceneProps => {
const { layout, position, scene } = sceneProps;
const { index } = scene;
const width = layout.initWidth;
const w = deviceInfo.isAndroid ? width : (124 * width) / 414;
const translateX = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [width, 0, -w]
});
const opacity = position.interpolate({
inputRange: [index - 1, index - 0.99, index],
outputRange: [0, 1, 1]
});
return {
opacity,
transform: [{ translateX }]
};
},
transitionSpec: {
timing: Animated.timing,
duration: 500,
easing: Easing.out(Easing.poly(4))
}
})
}
);
const AppContainer = createAppContainer(RootStack);
export default class App extends PureComponent {
render() {
return <AppContainer />;
}
}
简单使用理解:
1.createStackNavigator()内放入所有在本组导航跳转中的页面,本例子里有PageA和PageB两个页面,不加入的话无法通过navigation跳转的
2.createStackNavigator()第一个参数:Screen相当于这个注册的标识,将来会通过这个标识跳转,navigationOptions里有很多参数;这里用到的<header>是指对应页面中导航栏头部的样式,这里放回的是<></>空标签,表示的是不使用自带的header,会在页面中自定义header
3.createStackNavigator()第二个参数:initialRouteName表示的是初始化创建的页面,也就是第一个显示的页面;其他参数查看文档中说明即可,不一定会用上
4.通过createAppContainer()就可以把创建的一组页面放到AppContainer容器中
使用的时候就很简单了,直接一句代码就能实现:
import { NavigationScreenProp, NavigationState } from 'react-navigation';
interface IProps {
navigation: NavigationScreenProp<NavigationState>;
}
class PageA extends Component<IProps, {}> {
// 其他省略不写
// 点击跳转页面PageB
onClicked = () => {
this.props.navigation.navigate({
routeName: 'PageB'
});
}
}
上面的
IProps中的navigation不用自己传入,通过上面包裹到AppContainer中后,可以直接使用
3.基本动画实现
import React, { Component } from 'react';
import { StyleSheet, View, Animated } from 'react-native';
import RootSiblings from 'react-native-root-siblings';
import deviceInfo from 'src/helper/deviceInfo';
let tipView: RootSiblings;
// 描述动画变量值的属性
interface IState {
animatedBacgroundColor: Animated.Value;
animatedContentTop: Animated.Value;
}
export default class PrivacySettingTipView extends Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
animatedBacgroundColor: new Animated.Value(0),
animatedContentTop: new Animated.Value(deviceInfo.windowInfo.height)
};
}
componentDidMount() {
Animated.parallel([
Animated.timing(this.state.animatedBacgroundColor, {
toValue: 0.5,
duration: 300
}),
Animated.timing(this.state.animatedContentTop, {
toValue: 54,
duration: 300
})
]).start();
}
public static show() {
tipView = new RootSiblings((<PrivacySettingTipView />));
return tipView;
}
render() {
const color = this.state.animatedBacgroundColor.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(0,0,0,0)', 'rgba(0,0,0,1)']
});
return (
<Animated.View
style={[
viewStyle.container,
{
backgroundColor: color
}
]}>
<Animated.View
style={[
viewStyle.backContainer,
{ marginTop: this.state.animatedContentTop }
]}>
</Animated.View>
</Animated.View>
);
}
private closeView = () => {
Animated.parallel([
Animated.timing(this.state.animatedBacgroundColor, {
toValue: 0,
duration: 300
}),
Animated.timing(this.state.animatedContentTop, {
toValue: deviceInfo.windowInfo.height,
duration: 300
})
]).start(() => {
tipView && tipView.destroy();
});
};
}
const viewStyle = StyleSheet.create({
container: {
zIndex: 100000,
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0
},
backContainer: {
flex: 1,
backgroundColor: '#ffffff',
borderTopLeftRadius: 15,
borderTopRightRadius: 15
}
});
关于
Animated的详细介绍可以看ReactNative中文网中Animated的介绍