ReactNative学习实践:Navigator实践
2016/06/17 · JavaScript
· HTML5,
Javascript,
React,
ReactNative
本文作者: 伯乐在线 –
D.son
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。
离上次写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)); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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)); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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} /> }} />
1
2
3
4
5
6
7
8
9
|
<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
1
2
3
4
5
6
7
8
9
10
|
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)}/> ··· (this)}>
确认> </TouchableOpacity> > </View> ); } }
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
|
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)}/>
···
(this)}>
确认>
</TouchableOpacity>
>
</View>
);
}
}
|
这是角色选择视图CharacterView的部分代码,由于Navigator并没有像NavigatorIOS那样提供可配置的顶栏、返回按钮,所以我把顶栏做成了一个克配置的公共组件TopBarView,Navigator里边的所有视图直接使用就可以了,点击TopBarView的返回按钮时,TopBarView会调用给它配置的onBackPress回调函数,这里onBackPress回调函数是CharacterView的handleNavBack方法,即执行了:
this.props.navigator.pop();
1
|
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} });
1
2
3
4
5
|
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> ); } }
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
|
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 }); }
1
2
3
4
5
6
|
handleShowUserdoc(){
this.props.innerNavigator.push({
name:"usrdoc",
component:RegisterUsrDocView
});
}
|
而在RegisterUsrDocView即用户协议视图组件中,点击确定按钮时我们调用了从InnerNav推出视图的方法:
handleHideUserdoc(){ this.props.innerNavigator.pop(); }
1
2
3
|
handleHideUserdoc(){
this.props.innerNavigator.pop();
}
|
这样内嵌的导航链上的视图就完成了压入和推出的完整功能,如果有需要,还可以添加更多组件。
在RegisterNavView组件的handleConfirm方法中,也就是点击注册之后调用的方法,此时向服务器发送数据并且需要进入注册的下一环节了,因此需要主流程的Navigator压入新的视图,所以调用的是this.props.navigator.push,而不是innderNavigator的方法。
好了,大概结构和流程就介绍到这里了,相对比较简单,实际开发中还是会遇到很多细节问题,比如整个注册流程中,数据都需要存储在本地,最后统一提交到服务器,如果导航链上有很多组件,那么数据就要一级一级以props的方式传递,非常蛋疼,因此才引入了Redux来统一存储和管理,下一篇文章会系统介绍Redux以及在这个小项目里引入Redux的过程。
打赏支持我写出更多好文章,谢谢!
打赏作者
一、Navigator
打赏支持我写出更多好文章,谢谢!
任选一种支付方式
1 赞 3 收藏
评论
1、简单介绍:大多数时候我们都需要导航器来应对不同场景(页面)间的切换。它通过路由对象来分辨不同的场景,我们这里采用的就是renderScene方法,根据指定的路由来渲染。
关于作者:D.son
80后码农兼伪文青一枚,闷骚而不木讷,猥琐不流浪荡
个人主页 ·
我的文章 ·
1
2、利用Navigator弹出用到的方法:
(1initialRoute={{ name: ‘home’, component: HomeScene }}
这个指定了默认的页面,也就是启动的组件页面
(2configureScene={() => {
return Navigator.SceneConfigs.HorizontalSwipeJump;
}}
这个是页面之间跳转时候的动画,可以看这个目录:
node_modules/react-native/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js(可以看其他跳转的时候的方向)
(3renderScene:两个参数中的route包含的事initial的时候传递的name和component,而navigator是一个我们需要用的Navigator的对象,所以当我们拿到route中的component的时候,我们就可以将navigator传递给它,正因为如此,我们的组件HomeScene才可以通过this.props.navigator,拿到路由。
(4
如果需要传参数,则在push的参数后面加多一个参数params,把JSON往死填就好了,这里的params,其实就是在renderScene中return的{…route.params},这样接收的页面只需要通过this.props.id,就可以拿到我们传递的参数
(5 OK,那参数怎么回传呢?回调!通过定义一个回调函数,利用this.props
去调用,把参数回传回来
完整代码:
/**
* Sample React Native App
*
*/’use strict’;varReact =
require(‘react-native’);//增加NavigatorIOSvar{ AppRegistry,
StyleSheet, Text, View, Navigator, TouchableHighlight,} =
React;/*– 启动组件 –*/varMainClass = React.createClass({
render:function()
{//component这里设置的是这个组件启动的时候显示的第一个子组件return({
return Navigator.SceneConfigs.HorizontalSwipeJump;
}} renderScene={(route, navigator) => { let
Component = route.component; if(route.component) {
return} }} >); },});/*– 首页页面组件 –*/var
HomeScene = React.createClass({ getInitialState:function () { return
{ id: ‘AXIBA001’, flag: null }; }, render: function() {
return (push me!{this.state.flag && ‘ I \’m ‘ + this.state.flag + ‘,
i come from second page’}); }, onPress: function() { var _me =
this; //或者写成 const navigator = this.props.navigator; const {
navigator } = this.props; if(navigator) { navigator.push({
name: ‘touch View’, component: SecondScene,
params: { id: this.state.id,
getSomething:function(flag) { _me.setState({
flag: flag }); } }
}) } }});/*– push后的页面组件 –*/var SecondScene =
React.createClass({ render: function() { return (push sucess!I get
{this.props.id},i want back!); }, onPress: function() { //或者写成
const navigator = this.props.navigator; const { navigator } =
this.props; if(this.props.getSomething) { var flag = ‘Axiba002’
this.props.getSomething(flag); } if(navigator) {
navigator.pop(); } }});/*布局样式*/var styles =
StyleSheet.create({ container: { flex: 1, // justifyContent:
‘center’, // alignItems: ‘center’, backgroundColor: ‘#F5FCFF’,
}, home: { paddingTop:74,
},});AppRegistry.registerComponent(‘AwesonProject’, () => MainClass);
补充一些可能会用到的Navigator方法:
getCurrentRoutes() –
获取当前栈里的路由,也就是push进来,没有pop掉的那些。
jumpBack() –
跳回之前的路由,当然前提是保留现在的,还可以再跳回来,会给你保留原样。
jumpForward() – 上一个方法不是调到之前的路由了么,用这个跳回来就好了。
jumpTo(route) – 跳转到已有的场景并且不卸载。
push(route) – 跳转到新的场景,并且将场景入栈,你可以稍后跳转过去
pop() – 跳转回去并且卸载掉当前场景
replace(route) – 用一个新的路由替换掉当前场景
replaceAtIndex(route, index) – 替换掉指定序列的路由场景
replacePrevious(route) – 替换掉之前的场景
immediatelyResetRouteStack(routeStack) – 用新的路由数组来重置路由栈
popToRoute(route) – pop到路由指定的场景,其他的场景将会卸载。
popToTop() – pop到栈中的第一个场景,卸载掉所有的其他场景。
二、NavigatorIOS
1、NavigatorIOS包装了UIKit的导航功能,可以使用左划功能来返回到上一界面。
2、同上包含的方法有:
push(route)- 导航器跳转到一个新的路由。
pop()- 回到上一页。
popN(n)- 回到N页之前。当N=1的时候,效果和pop()一样。
replace(route)- 替换当前页的路由,并立即加载新路由的视图。
replacePrevious(route)- 替换上一页的路由/视图。
replacePreviousAndPop(route)-
替换上一页的路由/视图并且立刻切换回上一页。
resetTo(route)- 替换最顶级的路由并且回到它。
popToRoute(route)- 一直回到某个指定的路由。
popToTop()- 回到最顶层的路由。
代码:
/**
* Sample React Native App
*
*/’use strict’;varReact =
require(‘react-native’);//增加NavigatorIOSvar{ AppRegistry,
StyleSheet, Text, View, NavigatorIOS, TouchableHighlight,} =
React;/*有这样一些需求?:自定义barBarItem,例如自定义名字、图片?*//*–
启动组件 –*/varMainClass = React.createClass({
onRightButtonPress:function(){this.refs.nav.push({ title:’push
view’, component: SecondScene, }); }, render:function()
{//component这里设置的是这个组件启动的时候显示的第一个子组件return();
},});/*– 首页页面组件 –*/var HomeScene = React.createClass({
render: function() { return (push me!); }, onPress: function() {
this.props.navigator.push({ title: ‘touch View’, component:
SecondScene, passProps: { myProp: ‘Axiba001’ }, }); }});/*–
push后的页面组件 –*/var SecondScene = React.createClass({ render:
function() { return (push sucess!{‘hi,this is prams:’+
this.props.myProp}); },});/*布局样式*/var styles =
StyleSheet.create({ container: { flex: 1, // justifyContent:
‘center’, // alignItems: ‘center’, backgroundColor: ‘#F5FCFF’,
}, home: { paddingTop:74,
},});AppRegistry.registerComponent(‘AwesonProject’, () => MainClass);
回调函数基本相同,但是注意还有一些对导航栏的控制,例如:
(1 barTintColorstring
导航条的背景颜色。
(2 initialRoute{component: function, title: string, passProps: object,
backButtonIcon: Image.propTypes.source, backButtonTitle: string,
leftButtonIcon: Image.propTypes.source, leftButtonTitle: string,
onLeftButtonPress: function, rightButtonIcon: Image.propTypes.source,
rightButtonTitle: string, onRightButtonPress: function, wrapperStyle:
[object Object]}
NavigatorIOS使用”路由”对象来包含要渲染的子视图、它们的属性、以及导航条配置。”push”和任何其它的导航函数的参数都是这样的路由对象。
(3
itemWrapperStyleView#style
导航器中的组件的默认属性。一个常见的用途是设置所有页面的背景颜色。
(4 navigationBarHiddenbool
一个布尔值,决定导航栏是否隐藏。
( 5 shadowHiddenbool
一个布尔值,决定是否要隐藏1像素的阴影
( 6 tintColorstring
导航栏上按钮的颜色。
( 7 titleTextColorstring
导航器标题的文字颜色。
(8 translucentbool
一个布尔值,决定是否导航条是半透明的。
相关的链接地址在这里:http://www.tuicool.com/articles/z226zin