react-native 模仿餓了么點(diǎn)餐列表練手Demo

剛接觸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ō)了即舌,上圖:

Simulator Screen Shot - iPhone 6 - 2018-05-09 at 11.34.13.png

恩,略難看挎袜,反正大概布局就這樣子了顽聂,代碼大概是這樣子的:

<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)單碌尔。

IMG_0331.JPG

1525854830111.jpg

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ù)是不固定的齿椅,而sectionHeadersectionFooter是固定占兩個(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)就完成了膝迎。


2018-05-10 15_58_22.gif

接下來(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};
        }

    }

然后看看效果:

2018-05-11 15_07_14.gif

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è)itemref纺座,所以左邊List要使用ScrlloView組件代替FlatList息拜。回到左邊列表,修改下布局:

<ScrollView>
  {
      menuDatas.map((data, idx) => this._renderMenuItem(data, idx))
  }
</ScrollView>

_renderMenuItem方法中需要將每個(gè)itemref保存起來(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è)問題实昨,我使用了onMomentumScrollBeginonMomentumScrollEnd事件,官網(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沒解決挑辆。。孝情。鱼蝉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市箫荡,隨后出現(xiàn)的幾起案子魁亦,更是在濱河造成了極大的恐慌,老刑警劉巖羔挡,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洁奈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡绞灼,警方通過(guò)查閱死者的電腦和手機(jī)睬魂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)镀赌,“玉大人氯哮,你說(shuō)我怎么就攤上這事。” “怎么了喉钢?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵姆打,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我肠虽,道長(zhǎng)幔戏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任税课,我火速辦了婚禮闲延,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘韩玩。我一直安慰自己垒玲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布找颓。 她就那樣靜靜地躺著合愈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪击狮。 梳的紋絲不亂的頭發(fā)上佛析,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音彪蓬,去河邊找鬼寸莫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛档冬,可吹牛的內(nèi)容都是我干的储狭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捣郊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辽狈!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起呛牲,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤刮萌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后娘扩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體着茸,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年琐旁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涮阔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灰殴,死狀恐怖敬特,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤伟阔,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布辣之,位于F島的核電站,受9級(jí)特大地震影響皱炉,放射性物質(zhì)發(fā)生泄漏怀估。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一合搅、第九天 我趴在偏房一處隱蔽的房頂上張望多搀。 院中可真熱鬧,春花似錦灾部、人聲如沸康铭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蒸痹,卻和暖如春春弥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叠荠。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工匿沛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人榛鼎。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓逃呼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親者娱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抡笼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容