離上次寫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的過程。