剛接觸react-native時(shí)寫的一個(gè)demo搬味,當(dāng)時(shí)是在預(yù)研一個(gè)項(xiàng)目少漆。項(xiàng)目做完后,又轉(zhuǎn)到android原生開發(fā)仿畸,所以后面就沒怎么繼續(xù)學(xué)習(xí)react-native相關(guān)開發(fā),中途幾經(jīng)輾轉(zhuǎn)朗和,一直到現(xiàn)在的react前端開發(fā)错沽。。眶拉。
剛一動(dòng)手是這樣的千埃,粗略一想好像并沒有什么問題,然而忆植?放可??emmmmmm...
class OrderMenu extends Component {
constructor(props){
super(props);
this.state = {
selectItem:0
}
}
_renderMenuItem = ({item,index})=> {
let itemstyle = s.menuItems;
let textstyle = s.menuText;
if(index === this.state.selectItem){
itemstyle = [s.menuItems,{backgroundColor:'white'}];
textstyle = s.menuSelectText;
}
return (
<TouchableOpacity onPress={()=>this.clickOnItem(index)}>
<View style={itemstyle}>
<Text style={textstyle}>{item}</Text>
</View>
</TouchableOpacity>
)
}
clickOnItem(index){
console.log('index = ',index);
this.setState({selectItem:index})
}
render(){
return (
<View style={s.root}>
<View style={s.menuList}>
<FlatList data={menuDatas}
keyExtractor={(items, index) => index+''}
renderItem={this._renderMenuItem} />
</View>
<View style={s.itemList}>
</View>
</View>
)
}
}
跑起來(lái)后發(fā)現(xiàn)朝刊,點(diǎn)擊items并沒有出現(xiàn)選中效果耀里,也就是說(shuō)FlatList并沒有刷新,大概也許這是個(gè)BUG拾氓?冯挎??不不不咙鞍,我們看看react-native中文網(wǎng)房官,有這么一段話:
- 給FlatList指定extraData={this.state}屬性,是為了保證state.selected變化時(shí)续滋,能夠正確觸發(fā)FlatList的更新翰守。如果不指定此屬性,則FlatList不會(huì)觸發(fā)更新疲酌,因?yàn)樗且粋€(gè)PureComponent蜡峰,其props在===比較中沒有變化則不會(huì)觸發(fā)更新。
簡(jiǎn)單說(shuō)徐勃,就是刷新FlatList需要改變props并且是為淺比較
,劃重點(diǎn)早像,期末要考的僻肖。
這中間有點(diǎn)波折,因?yàn)镕latList的刷新機(jī)制卢鹦,起先想的是重新setState一次listData就能刷了臀脏,然后突然看到劝堪,其實(shí)只需要增加extraData={this.state}
就行的,只要this.state改變FlatList就會(huì)刷新了揉稚。
然后由此改下我們的代碼秒啦,如下
<FlatList data={this.state.listData}
extraData={this.state}
keyExtractor={(items, index) => index+''}
renderItem={this._renderMenuItem} />
重新跑起來(lái)看看
發(fā)現(xiàn)TouchableOpacity
組件會(huì)有部分延遲,因?yàn)樾枰獔?zhí)行透明效果后才走回調(diào)搀玖,所以果斷替換成了TouchableHighlight
余境。
接下來(lái),開始布局右邊列表灌诅。因?yàn)椴藛瘟斜硎莻€(gè)長(zhǎng)列表芳来,考慮性能問題,我選用了SectionList
猜拾,布局過(guò)程就不細(xì)說(shuō)了即舌,上圖:
恩,略難看挎袜,反正大概布局就這樣子了顽聂,代碼大概是這樣子的:
<View style={s.itemList}>
<SectionList keyExtractor={(item,index)=>index+''}
renderItem={this.renderSectionItem}
renderSectionHeader={this.renderSectionHeader}
sections={sections} />
</View>
renderSectionHeader = ({section,index})=>{
return (
<View style={s.sectionTitle} key={index}>
<Text style={s.sectionText}>{section.title}</Text>
</View>
)
}
renderSectionItem = ({item,index})=>{
return (
<View style={s.sectionItem} key={index}>
<Image source={require('./img/noGoodsIcon.png')}/>
<View style={{flex:1,marginLeft:8,paddingVertical:8}}>
<Text style={{fontSize:15,fontWeight:'bold',color:'#333'}}>{item.name}</Text>
<Text style={{fontSize:12,color:'#999'}} numberOfLines={2}>{item.content}</Text>
<View style={{flexDirection:'row',flex:1,alignItems:'flex-end',justifyContent:'space-between'}}>
<Text>¥{item.price}</Text>
<Image style={{width:20,height:20}} source={require('./img/加號(hào).png')}/>
</View>
</View>
</View>
)
}
界面布好了,就該開始考慮左右列表的聯(lián)動(dòng)問題了盯仪。先從簡(jiǎn)單的開始紊搪,左邊點(diǎn)擊聯(lián)動(dòng)右邊列表對(duì)應(yīng)的滾動(dòng),此處的點(diǎn)擊事件此前已經(jīng)寫好磨总,只需要調(diào)用右邊列表的滾動(dòng)方法即可嗦明,怎樣精確的控制SectionList
滾動(dòng)到對(duì)應(yīng)的位置呢?遇事不決找官網(wǎng)(建議去FB官網(wǎng)看蚪燕,因?yàn)橹形木W(wǎng)有很多內(nèi)容是沒寫的)娶牌,發(fā)現(xiàn)有scrollToLocation
方法,參數(shù)如下:
- 'animated' (boolean) - 這是控制是否需要滾動(dòng)動(dòng)畫馆纳,默認(rèn)true诗良;
- 'itemIndex' (number) - 滾動(dòng)到section里的哪個(gè)item,必填鲁驶;
- 'sectionIndex' (number) - 滾動(dòng)到哪個(gè)section鉴裹,必填;
- 'viewOffset' (number) - 滾動(dòng)之后的偏移量钥弯,用以調(diào)整最終位置径荔,默認(rèn)0;
- 'viewPosition' (number) - 這是指滾動(dòng)到指定Item的哪個(gè)部位脆霎,值為0-1(代表頭部-底部)总处,其實(shí)這是必填的,不填就報(bào)錯(cuò).睛蛛。
了解之后鹦马,去到點(diǎn)擊事件添加scrollToLocation
方法
clickOnItem(index){
this.setState({selectItem:index});
if(this.sectionList){
this.sectionList.scrollToLocation({sectionIndex:index,itemIndex:0,viewPosition:0});
}
}
只需要跳到指定section的第一個(gè)item胧谈,所以參數(shù)是{sectionIndex:index,itemIndex:0,viewPosition:0}
,而this.sectionList
又是哪來(lái)的荸频?別慌菱肖,在SectionList里加上ref={o=>this.sectionList = o}
,利用ref取到SectionList實(shí)例對(duì)象旭从,然后用這對(duì)象調(diào)用方法就行了稳强。
添加完成之后,別急著跑遇绞,還有一個(gè)需要注意的地方键袱,旁邊有個(gè)小tips
Note: Cannot scroll to locations outside the render window without specifying the getItemLayout prop.
大概意思就是,如果不設(shè)置getItemLayout
參數(shù)摹闽,則無(wú)法滾動(dòng)到屏幕之外的地方去蹄咖。。付鹿。所以這getItemLayout
參數(shù)是什么鬼澜汤??舵匾?SectionList里沒有提到俊抵,大概是FB懶得寫,然后在FlatList里找到了相關(guān)描述:
getItemLayout
(data, index) => {length: number, offset: number, index: number}
getItemLayout is an optional optimization that let us skip measurement of dynamic content if you know the height of items a priori. getItemLayout is the most efficient, and is easy to use if you have fixed height items, for example:getItemLayout={(data, index) => ( {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} )}
就是說(shuō)坐梯,list實(shí)際并不知道要移動(dòng)的距離徽诲,需要我們自己計(jì)算,然后借由getItemLayout
返回給list吵血,emmmm谎替。上面說(shuō)這計(jì)算很簡(jiǎn)單的,就像offset: ITEM_HEIGHT * index
一樣蹋辅,是的钱贯,我們來(lái)加上這段代碼試試。
首先在SectionList中侦另,加上getItemLayout={this.getItemLayout}
秩命,this.getItemLayout定義為如下:
getItemLayout = (data, index) => {
return {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index + HEADER_HEIGHT, index}
}
我機(jī)智的加上了HEADER_HEIGHT
,是為SectionHeader的高度褒傅,跑起來(lái)后是這樣的:
這是GIF弃锐。。殿托。
嗯霹菊,當(dāng)我把getItemLayout
傳遞的index
打印出來(lái)后,發(fā)現(xiàn)事情并沒有這么簡(jiǎn)單碌尔。
index
從0-53浇辜,總計(jì)54個(gè)items...而我的本地?cái)?shù)據(jù)只有4x9+9 = 45個(gè)數(shù)據(jù)。黑人問號(hào).jpg唾戚。然后百度得知柳洋,它這個(gè)
index
啊,很皮叹坦,每個(gè)section
不止包括sectionHeader
熊镣,還包括一個(gè)sectionFooter
,不管你有沒有這個(gè)sectionFooter
募书。所以按照我的數(shù)據(jù)計(jì)算绪囱,應(yīng)該是4x9+2x9 = 54,這就對(duì)上了莹捡。
之后鬼吵,getItemLayout
的算法,也不再是簡(jiǎn)單的相乘篮赢,因?yàn)槊總€(gè)section
里的item
數(shù)是不固定的齿椅,而sectionHeader
、sectionFooter
是固定占兩個(gè)數(shù)启泣,這時(shí)候需要結(jié)合data
來(lái)進(jìn)行計(jì)算涣脚。按照想法,自己實(shí)現(xiàn)了一下寥茫,發(fā)現(xiàn)始終會(huì)有些偏移遣蚀,就去借鑒了一下rn-section-list-get-item-layout的源碼苛秕,然后用JS翻譯了一下(原來(lái)是對(duì)length
參數(shù)理解錯(cuò)誤)
getItemLayout = (data, index) => {
let sectioinIndex = 0;
let offset = -20; // 這里為什么是-20拆座?大概是因?yàn)槭讉€(gè)SectionHeader占用20?
let item = {type: 'header'};
for (let i = 0; i < index; ++i) {
switch (item.type) {
case 'header': {
let sectionData = data[sectioinIndex].data;
offset += HEADER_HEIGHT;
sectionData.length === 0 ? item = {type: 'footer'} : item = {type: 'row', index: 0};
}break;
case 'row': {
let sectionData = data[sectioinIndex].data;
offset += ITEM_HEIGHT;
++item.index;
if (item.index === sectionData.length) {
item = {type: 'footer'};
}
}break;
case 'footer':
item = {type: 'header'};
++sectioinIndex;
break;
default:
console.log('err');
}
}
let length = 0;
switch (item.type) {
case 'header':
length = HEADER_HEIGHT;
break;
case 'row':
length = ITEM_HEIGHT;
break;
case 'footer':
length = 0;
break;
}
return {length: length, offset: offset, index}
}
這樣音半,左邊列表點(diǎn)擊聯(lián)動(dòng)右邊列表滾動(dòng)就完成了膝迎。
接下來(lái)實(shí)現(xiàn)粥帚,右邊列表滾動(dòng)聯(lián)動(dòng)左邊列表選中效果。
這就需要監(jiān)控SectionList
的滾動(dòng)限次,在官方文檔中芒涡,可以找到onViewableItemsChanged
,這個(gè)函數(shù)會(huì)在item
發(fā)生變化時(shí)調(diào)用卖漫,并且可以通過(guò)viewabilityConfig
控制調(diào)用頻率费尽,這個(gè)稍后講。onViewableItemsChanged
返回的參數(shù)是一個(gè)包含兩對(duì)key值的對(duì)象
'viewableItems' (array of ViewTokens)
'changed' (array of ViewTokens)
其實(shí)就是兩個(gè)包含item
的數(shù)組羊始,viewableItems
是當(dāng)前可視的item
集合旱幼,changed
是變化的item
集合。根據(jù)需求突委,我們需要使用viewableItems
柏卤,只要取到當(dāng)前的顯示的第一個(gè)item
冬三,就可以知道是滾動(dòng)到哪個(gè)section
了,上代碼:
itemOnChanged = ({viewableItems, changed}) => {
let firstItem = viewableItems[0];
if (firstItem && firstItem.section) {
// 這里可以直接取到section的title
let name = firstItem.section.title;
let idx = menuDatas.indexOf(name);
this.setState({selectItem:idx};
}
}
然后看看效果:
WTF缘缚。勾笆。。左邊列表跳動(dòng)延遲很大桥滨,且不準(zhǔn)確窝爪。第一時(shí)間我就想到,可能是
setState
的問題齐媒,因?yàn)?code>setState是異步的蒲每,執(zhí)行完之后并不會(huì)立即刷新,且每調(diào)用一次setState
喻括,react-native
就會(huì)使用diff
算法對(duì)比一次虛擬DOM
的變化邀杏,而onViewableItemsChanged
方法存在高頻率刷新問題,所以性能損耗非常大唬血,使用xcode查看其CPU峰值高達(dá)89%;床!刁品!
對(duì)
diff
算法感興趣的可以看看這篇文章React 源碼剖析系列 - 不可思議的 react diff
setState
不能用泣特,那怎么刷新界面呢?做過(guò)前端的同學(xué)都知道挑随,刷新界面直接操作DOM節(jié)點(diǎn)就行了状您。是的,現(xiàn)在需要的就是直接操作真實(shí)DOM節(jié)點(diǎn)兜挨,react-native
提供了setNativeProps
方法膏孟,setNativeProps
就是等價(jià)于直接操作DOM節(jié)點(diǎn)的方法,去翻找了下源碼沒找到拌汇,官網(wǎng)的鏈接也已經(jīng)404了orz柒桑。。噪舀。
setNativeProps
參數(shù)是個(gè)props
對(duì)象魁淳,傳什么具體還是看組件支持哪些props
,目前我們只需要改變style
与倡,傳個(gè)style
對(duì)象就好界逛。
由于setNativeProps
要使用組件對(duì)象調(diào)用,我們需要每個(gè)item
的ref
纺座,所以左邊List
要使用ScrlloView
組件代替FlatList
息拜。回到左邊列表,修改下布局:
<ScrollView>
{
menuDatas.map((data, idx) => this._renderMenuItem(data, idx))
}
</ScrollView>
_renderMenuItem
方法中需要將每個(gè)item
的ref
保存起來(lái):
addItemsRef(o,idx){
// 這里加判斷是為了保證this.items不會(huì)出現(xiàn)內(nèi)存泄漏
if(idx < this.items.length){
this.items[idx] = o;
}else{
this.items.push(o);
}
}
_renderMenuItem = (item, index) => {
let textstyle = textNormalStyle;
if (index === this.state.selectItem) {
textstyle = textSelected;
}
return (
<TouchableHighlight onPress={() => this.clickOnItem(index)} key={index} underlayColor="#fff">
<View style={s.menuItems}>
<Text ref={o => this.addItemsRef(o,index)} style={textstyle}>{item}</Text>
</View>
</TouchableHighlight>
)
}
上面代碼只保存了Text
組件的引用少欺,因?yàn)橹恍枰淖?code>Text的樣式即可實(shí)現(xiàn)選中效果喳瓣。
然后回到onViewableItemsChanged
,將setState
替換成setNativeProps
itemOnChanged = ({viewableItems, changed}) => {
let firstItem = viewableItems[0];
if (firstItem && firstItem.section) {
let name = firstItem.section.title;
let idx = menuDatas.indexOf(name);
// this.setState({selectItem:idx})
// 這里需要改變兩個(gè)item的樣式赞别,之前選中的和現(xiàn)在選中的
let bef = this.items[this.state.selectItem];
let now = this.items[idx];
bef.setNativeProps({style: textNormalStyle});
now.setNativeProps({style: textSelected});
this.state.selectItem = idx; // 不使用setState夫椭,直接改變selectItem的值
}
}
這時(shí),我們已經(jīng)可以看到效果了氯庆,右邊列表的聯(lián)動(dòng)也基本完成,不過(guò)還有一點(diǎn)需要注意扰付,左邊列表的點(diǎn)擊帶動(dòng)右邊列表滾動(dòng)也會(huì)觸發(fā)onViewableItemsChanged
事件堤撵,所以我們需要再做一個(gè)判斷,讓右邊列表非用戶觸摸滾動(dòng)不觸發(fā)onViewableItemsChanged
事件羽莺。
為解決這個(gè)問題实昨,我使用了onMomentumScrollBegin
和onMomentumScrollEnd
事件,官網(wǎng)上只是簡(jiǎn)單說(shuō)這兩個(gè)是列表動(dòng)畫開始與結(jié)束的回調(diào)盐固,其實(shí)onMomentumScrollBegin
只會(huì)在用戶劃動(dòng)List
的手勢(shì)結(jié)束后荒给,慣性動(dòng)畫開始前調(diào)用,而使用API的滾動(dòng)動(dòng)畫是不會(huì)觸發(fā)這個(gè)回調(diào)的刁卜,所以可以簡(jiǎn)單利用下這個(gè)特性
<SectionList
keyExtractor={(item, index) => index + ''}
ref={o => this.sectionList = o}
renderItem={this.renderSectionItem}
renderSectionHeader={this.renderSectionHeader}
sections={sections}
getItemLayout={this.getItemLayout}
onViewableItemsChanged={this.itemOnChanged}
viewabilityConfig={VIEWABILITY_CONFIG}
onMomentumScrollBegin={() => {this.scrollBegin = true;}}
onMomentumScrollEnd={()=>{this.scrollBegin = false}}
/>
然后onViewableItemsChanged
的回調(diào)也添加一個(gè)判斷
itemOnChanged = ({viewableItems, changed}) => {
// 這里加個(gè)判斷
if (!this.scrollBegin) {
return;
}
let firstItem = viewableItems[0];
if (firstItem && firstItem.section) {
let name = firstItem.section.title;
let idx = menuDatas.indexOf(name);
// this.setState({selectItem:idx})
let bef = this.items[this.state.selectItem];
let now = this.items[idx];
bef.setNativeProps({style: textNormalStyle});
now.setNativeProps({style: textSelected});
this.state.selectItem = idx;
}
}
這樣就解決了兩個(gè)列表滾動(dòng)沖突的問題志电,O了個(gè)K。
稍等蛔趴,還有BUG沒解決挑辆。。孝情。鱼蝉。