離上次寫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,也不會重繪繼承鏈上的其他元件。這正是我們想要的效果,加上我們明確知道正在操縱的元件以及它與檢視其他元件的關係,因此,這裡我們可以放心地使用它,而且相當簡單。
更新前:
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)); } |
更新後:
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,它控制整個使用者註冊的檢視切換:
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中檢視切換的動畫型別,這裡可以靈活配置切換方式:
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”了,比如使用者註冊流程中,首先是選擇角色檢視,然後進入註冊檢視填寫賬號密碼簡訊碼等,此時點選註冊才會將所有資料傳送給伺服器,因此從角色選擇檢視到註冊檢視,需要將使用者選擇的角色傳遞下去,在註冊檢視傳送給伺服器。因此,角色選擇檢視的跳轉事件需要把引數傳遞下去:
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方法,即執行了:
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傳遞的:
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,這個子導航鏈上包含了使用者註冊檢視及使用者協議檢視。
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壓入協議檢視的方法:
1 2 3 4 5 6 |
handleShowUserdoc(){ this.props.innerNavigator.push({ name:"usrdoc", component:RegisterUsrDocView }); } |
而在RegisterUsrDocView即使用者協議檢視元件中,點選確定按鈕時我們呼叫了從InnerNav推出檢視的方法:
1 2 3 |
handleHideUserdoc(){ this.props.innerNavigator.pop(); } |
這樣內嵌的導航鏈上的檢視就完成了壓入和推出的完整功能,如果有需要,還可以新增更多元件。
在RegisterNavView元件的handleConfirm方法中,也就是點選註冊之後呼叫的方法,此時向伺服器傳送資料並且需要進入註冊的下一環節了,因此需要主流程的Navigator壓入新的檢視,所以呼叫的是this.props.navigator.push,而不是innderNavigator的方法。
好了,大概結構和流程就介紹到這裡了,相對比較簡單,實際開發中還是會遇到很多細節問題,比如整個註冊流程中,資料都需要儲存在本地,最後統一提交到伺服器,如果導航鏈上有很多元件,那麼資料就要一級一級以props的方式傳遞,非常蛋疼,因此才引入了Redux來統一儲存和管理,下一篇文章會系統介紹Redux以及在這個小專案裡引入Redux的過程。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式