ReactNative学习实践--Navigator实践
离上次写RN笔记有一段时间了,期间参与了一个新项目,只在最近的空余时间继续学习实践,因此进度比较缓慢,不过这并不代表没有新进展,其实这个小东西离上次发文时已经有了相当大的变化了,其中影响最大的变化就是引入了Redux,后面会系统介绍一下。
在开始主题之前,先补充一点上回说到的动画初探(像我这么靠谱严谨的攻城狮,必须精益求精,┗|`O′|┛ 嗷~~),上回文说到,经过我们自己定义了余弦动画函数之后,动态设定state的4个参数,实现了比较流畅的加载动画,这里可能有朋友已经注意到了,我们非常频繁的调用了setState方法,这在React和RN中都是相当忌讳的,每一次setState都会触发render方法,也就意味着更频繁的虚拟DOM对比,特别是在RN中,这还意味着更频繁的JSCore<==>iOS通信,尽管框架本身对多次setState做了优化,比如会合并同时调用的多个setState,但这对性能和体验还是会有较大影响,上回我们只是单独实现了一个loading动画,所以还比较流畅,当视图中元素较多并且有各自的动画的时候,就会看到比较严重的卡顿,这些其实是可以避免的,因为在loading动画的实现部分,我们清楚地知道只需要loading动画的特定组成部分更新而不是组件的所有部分以及继承链上的所有组件都需要更新,并且确信这个节点一定发生了变化,因此不需要经过虚拟DOM对比,那么如果我们能绕开setState,动画就应该会更流畅,即使在复杂的视图里边。这就是Animations文档最后提到的setNativeProps方法。
As mentioned in the Direction Manipulation section, setNativeProps allows us to modify properties of native-backed components (components that are actually backed by native views, unlike composite components) directly, without having to setState and re-render the component hierarchy.
setNativeProps允许我们直接操纵原生组件的属性,而不需要用到setState,也不会重绘继承链上的其他组件。这正是我们想要的效果,加上我们明确知道正在操纵的组件以及它与视图其他组件的关系,因此,这里我们可以放心地使用它,而且相当简单。
更新前:
loopAnimation(){
var t0=animationT,t1=t0+0.5,t2=t1+0.5,t3=t2+timeDelay,t4=t3+0.5;//这里分别是四个动画的当前时间,依次加上了0.5的延迟
var v1=Number(Math.cos(t0).toFixed(2))*animationN+animationM;//将cos函数的小数值只精确到小数点2位,提高运算效率
var v2=Number(Math.cos(t1).toFixed(2))*animationN+animationM;
var v3=Number(Math.cos(t2).toFixed(2))*animationN+animationM;
var v4=Number(Math.cos(t3).toFixed(2))*animationN+animationM;
this.setState({
fV:v1,
sV:v2,
tV:v3,
foV:v4
});
animationT+=0.35;//增加时间值,每次增值越大动画越快
requestAnimationFrame(this.loopAnimation.bind(this));
}
更新后:
loopAnimation(){
var t0=···
var v1=···
var v2=···
var v3=···
var v4=···
this.refs.line1.setNativeProps({
style:{width:w1,height:v1}
});
this.refs.line2.setNativeProps({
style:{width:w2,height:v2}
});
this.refs.line3.setNativeProps({
style:{width:w3,height:v3}
});
this.refs.line4.setNativeProps({
style:{width:w4,height:v4}
});
animationT+=0.35;//增加时间值,每次增值越大动画越快
requestAnimationFrame(this.loopAnimation.bind(this));
}
效果如下:
这里有意在注册请求完毕之后没有隐藏loading动画,因此同时执行了视图切换和loading两个动画,效果还行~
好了,该进入今天的正题了。先整体看一下这一阶段实现的效果(哒哒哒~):
主要是模拟了一个新用户注册流程,实现起来也并不复杂,整体结构是用一个RN组件Navigator来做导航,虽然有另一个NavigatorIOS组件在iOS系统上表现更加优异,但是考虑到RN本身希望能够同时在安卓和iOS上运行的初衷,我选择了可以兼容两个平台的Navigator来尝试,目前来看效果还能接受。
在最后的详细信息视图里边,尝试了各种组件,比如调用相机,Switch,Slider等,主要是尝鲜,哈哈~ 也自己实现了比较简单的check按钮。
首先最外层的结构是一个Navigator,它控制整个用户注册的视图切换:
<Navigator style={styles.navWrap}
initialRoute={{name: 'login', component:LoginView}}
configureScene={(route) => {
return Navigator.SceneConfigs.FloatFromRight;
}}
renderScene={(route, navigator) => {
let Component = route.component;
return <Component {...route.params} navigator={navigator} />
}} />
其中,initialRoute配置了Navigator的初始组件,这里就是LoginView组件,它本身既可以直接登录,也可以点击【我要注册】进入注册流程。configureScene属性则是用来配置Navigator中视图切换的动画类型,这里可以灵活配置切换方式:
Navigator.SceneConfigs.PushFromRight (default)
Navigator.SceneConfigs.FloatFromRight
Navigator.SceneConfigs.FloatFromLeft
Navigator.SceneConfigs.FloatFromBottom
Navigator.SceneConfigs.FloatFromBottomAndroid
Navigator.SceneConfigs.FadeAndroid
Navigator.SceneConfigs.HorizontalSwipeJump
Navigator.SceneConfigs.HorizontalSwipeJumpFromRight
Navigator.SceneConfigs.VerticalUpSwipeJump
Navigator.SceneConfigs.VerticalDownSwipeJump
renderScene属性则是必须配置的一个属性,它负责渲染给定路由对应的组件,也就是向Navigator所有路由对应的组件传递了"navigator"属性以及route本身携带的参数,如果不使用类似Flux或者Redux来全局存储或控制state的话,那么Navigator里数据的传递就全靠"route.params"了,比如用户注册流程中,首先是选择角色视图,然后进入注册视图填写账号密码短信码等,此时点击注册才会将所有数据发送给服务器,因此从角色选择视图到注册视图,需要将用户选择的角色传递下去,在注册视图发送给服务器。因此,角色选择视图的跳转事件需要把参数传递下去:
class CharacterView extends Component {
constructor(props){
super(props);
this.state={
character:"type_one"
}
}
handleNavBack(){
this.props.navigator.pop();
}
···
handleConfirm(){
this.props.navigator.push({
name:"registerNav",
component:RegisterNavView,
params:{character:this.state.character}
});
}
render(){
return (
<View style={styles.container}>
<TopBarView title="注册" hasBackArr={true} onBackPress={this.handleNavBack.bind(this)}/>
<View style={styles.main}>
···
<TouchableOpacity style={styles.confirmBtn} onPress={this.handleConfirm.bind(this)}>
<Text style={styles.confirmTxt}>确认</Text>
</TouchableOpacity>
</View>
</View>
);
}
}
这是角色选择视图CharacterView的部分代码,由于Navigator并没有像NavigatorIOS那样提供可配置的顶栏、返回按钮,所以我把顶栏做成了一个克配置的公共组件TopBarView,Navigator里边的所有视图直接使用就可以了,点击TopBarView的返回按钮时,TopBarView会调用给它配置的onBackPress回调函数,这里onBackPress回调函数是CharacterView的handleNavBack方法,即执行了:
this.props.navigator.pop();
关于this.props.navigator,这里我们并没有在导航链上的每个组件显式地传递navigator属性,而是在Navigator初始化的时候就在renderScene属性方法里统一配置了,导航链上所有组件的this.props.navigator其实都指向了一个统一的navigator对象,它有两个方法:push和pop,用来向导航链压入和推出组件,视觉上就是进入下一视图和返回上一视图,因此这里当点击顶栏返回按钮时,直接调用pop方法就返回上一视图了。其实也可以把navigator对象传递到TopBarView里,在TopBarView内部调用navigator的pop方法,原理是一样的。而在CharacterView的确认按钮事件里,需要保存用户选择的角色然后跳转到下一个视图,就是通过props传递的:
this.props.navigator.push({
name:"registerNav",
component:RegisterNavView,
params:{character:this.state.character}
});
这里就是调用的navigator属性的push方法向导航链压入新的组件,即进入下一视图。push方法接收的参数是一个包含三个属性的对象,name只是用来标识组件名称,而commponent和params则是标识组件以及传递给该组件的参数对象,这里的"commponent"和"params"两个key名称是和前面renderScene是对应的,在renderScene回调里边,用到的route.commponent和route.params,就是这里push传递的参数对应的值。
在用户注册视图中,有一个用户协议需要用户确认,这里用户协议视图的切换方式与主流程不太一样,而一个Navigator只能在最初配置一种切换方式,因此,这里在Navigator里嵌套了Navigator,效果如下:
CharacterView的跳转事件中,向navigator的push传递的组件并不是RegisterView组件,而是传递的RegisterNavView组件,它是被嵌套的一个Navigator,这个子导航链上包含了用户注册视图及用户协议视图。
class RegisterNavView extends Component {
constructor(props){
super(props);
}
handleConfirm(){
//send data to server
···
//
this.props.navigator.push({
component:nextView,
name:'userInfo'
});
}
render(){
return (
<View style={styles.container}>
<Navigator style={styles.navWrap}
initialRoute={{name: 'register', component:RegisterView,params:{navigator:this.props.navigator,onConfirm:this.handleConfirm.bind(this)}}}
configureScene={(route) => {
return Navigator.SceneConfigs.FloatFromBottom;
}}
renderScene={(route, navigator) => {
let Component = route.component;
return <Component {...route.params} innerNavigator={navigator} />
}} />
</View>
);
}
}
这个被嵌套的导航我们暂且称为InnerNav,它的初始路由组件就是RegisterView,展示了输入账号密码等信息的视图,它的configureScene设置为“FloatFromBottom”,即从底部浮上来,renderScene也略微不一样,在InnerNav导航链组件上传递的navigator对象名称改成了innerNavigator,以区别主流程Navigator,在RegisterView中有一个【用户协议】的文字按钮,在这个按钮上我们调用了向InnerNav压入协议视图的方法:
handleShowUserdoc(){
this.props.innerNavigator.push({
name:"usrdoc",
component:RegisterUsrDocView
});
}
而在RegisterUsrDocView即用户协议视图组件中,点击确定按钮时我们调用了从InnerNav推出视图的方法:
handleHideUserdoc(){
this.props.innerNavigator.pop();
}
这样内嵌的导航链上的视图就完成了压入和推出的完整功能,如果有需要,还可以添加更多组件。
在RegisterNavView组件的handleConfirm方法中,也就是点击注册之后调用的方法,此时向服务器发送数据并且需要进入注册的下一环节了,因此需要主流程的Navigator压入新的视图,所以调用的是this.props.navigator.push,而不是innderNavigator的方法。
好了,大概结构和流程就介绍到这里了,相对比较简单,实际开发中还是会遇到很多细节问题,比如整个注册流程中,数据都需要存储在本地,最后统一提交到服务器,如果导航链上有很多组件,那么数据就要一级一级以props的方式传递,非常蛋疼,因此才引入了Redux来统一存储和管理,下一篇文章会系统介绍Redux以及在这个小项目里引入Redux的过程。