前言
學(xué)習(xí)本系列內(nèi)容需要具備一定 HTML 開發(fā)基礎(chǔ),沒有基礎(chǔ)的朋友可以先轉(zhuǎn)至 HTML快速入門(一) 學(xué)習(xí)
本人接觸 React Native 時間并不是特別長渺尘,所以對其中的內(nèi)容和性質(zhì)了解可能會有所偏差,在學(xué)習(xí)中如果有錯會及時修改內(nèi)容咆爽,也歡迎萬能的朋友們批評指出脆丁,謝謝
文章第一版出自簡書音榜,如果出現(xiàn)圖片或頁面顯示問題蔚鸥,煩請轉(zhuǎn)至 簡書 查看 也希望喜歡的朋友可以點贊惜论,謝謝
更新公告:
- 2017.05.16 —— 根據(jù)一些朋友私信我的代碼,發(fā)現(xiàn)有些錯誤是文中有一些拼寫錯誤導(dǎo)致止喷,已進(jìn)行更正馆类,對此造成的不便,請見諒弹谁。
ListView組件介紹
ListView組件是React Native中一個比較核心的組件乾巧,用途非常廣,設(shè)計初衷就是用來高效的展示垂直滾動的列表數(shù)據(jù)
ListView 繼承了
ScrollView
的所有屬性-
使用步驟:
- 創(chuàng)建一個ListView.DataSource數(shù)據(jù)源预愤,然后給它傳遞一個普通的數(shù)組數(shù)據(jù)
getInitialState(){ // 初始化數(shù)據(jù)源(rowHasChanged是優(yōu)化的一種手段沟于,只有當(dāng)r1 !== r2的時候才會重新渲染) var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); return{ // 給dataSource傳遞一組 數(shù)組 dataSource: ds.cloneWithRows(['內(nèi)容0', '內(nèi)容1', '內(nèi)容2', '內(nèi)容3', '內(nèi)容4', '內(nèi)容5']) } },
- 使用數(shù)據(jù)源實例化一個ListView組件,定義一個renderRow回調(diào)函數(shù)植康,這個函數(shù)會接受數(shù)組中的每個數(shù)據(jù)作為參數(shù)社裆,并返回一個可渲染的組件(也就是該列表的每一行Item)
render() { return ( <View style={styles.container}> // 根據(jù)數(shù)據(jù)源實例化一個ListView <ListView style={{backgroundColor:'yellow'}} // 獲取數(shù)據(jù)源 dataSource={this.state.dataSource} // 根據(jù)數(shù)據(jù)源創(chuàng)建一個Item // 注:這里的this.renderRow是隱式寫法,系統(tǒng)會根據(jù)函數(shù)的需要向图,將對應(yīng)的參數(shù)傳遞過去(共有4個參數(shù):rowData, sectionID, rowID, highlightRow) renderRow={this.renderRow} /> </View> ); }, // 返回一個Item renderRow(rowData,sectionID,rowID) { return( // 實例化Item <View> <Text style={{backgroundColor:'red', height:44}}>內(nèi)容{rowData},在第{sectionID}組第{rowID}行</Text> </View> ) }
效果:
- 創(chuàng)建一個ListView.DataSource數(shù)據(jù)源预愤,然后給它傳遞一個普通的數(shù)組數(shù)據(jù)
ListView 同樣支持一些高級特性,包括設(shè)置每一組的粘性的頭部标沪、支持設(shè)置列表 header 和 footter 視圖榄攀、當(dāng)數(shù)據(jù)列表滑動到最底部的時候支持 onEndReached 方法回調(diào)、設(shè)備屏幕列表可見的視圖數(shù)據(jù)發(fā)生變化的時候回調(diào) onChangeVisibleRows 以及一些性能方面的優(yōu)化特性
ListView常用屬性
ScrollView 全部屬性
dataSource:設(shè)置ListView的數(shù)據(jù)源
initialListSize:指定在組件剛掛載的時候渲染多少行數(shù)據(jù)金句。用這個屬性來確保首屏顯示合適數(shù)量的數(shù)據(jù)檩赢,而不是花費太多幀逐步顯示出來
onChangeVisibleRows:((visibleRows, changedRows) => void)當(dāng)可見的行的集合變化的時候調(diào)用此回調(diào)函數(shù)。visibleRows 以 { sectionID: { rowID: true }}的格式包含了所有可見行违寞,而changedRows 以{ sectionID: { rowID: true | false }}的格式包含了所有剛剛改變了可見性的行贞瞒,其中如果值為true表示一個行變得可見,而為false表示行剛剛離開可視區(qū)域而變得不可見
onEndReached:當(dāng)所有的數(shù)據(jù)都已經(jīng)渲染過趁曼,并且列表被滾動到距離最底部不足onEndReachedThreshold個像素的距離時調(diào)用军浆。原生的滾動事件會被作為參數(shù)傳遞。譯注:當(dāng)?shù)谝淮武秩緯r挡闰,如果數(shù)據(jù)不足一屏(比如初始值是空的)乒融,這個事件也會被觸發(fā)
onEndReachedThreshold:調(diào)用onEndReached之前的臨界值掰盘,單位是像素
pageSize:每次事件循環(huán)(每幀)渲染的行數(shù)
removeClippedSubviews:用于提升大列表的滾動性能。需要給行容器添加樣式overflow:'hidden'赞季。(Android已默認(rèn)添加此樣式)此屬性默認(rèn)開啟
renderFooter:(() => renderable)頁頭與頁腳會在每次渲染過程中都重新渲染(如果提供了這些屬性)愧捕。如果它們重繪的性能開銷很大,把他們包裝到一個StaticContainer或者其它恰當(dāng)?shù)慕Y(jié)構(gòu)中申钩。頁腳會永遠(yuǎn)在列表的最底部次绘,而頁頭會在最頂部
renderHeader: 在每一次渲染過程中Footer(尾)該會一直在列表的底部,header(頭)該會一直在列表的頭部
-
renderRow:【(rowData, sectionID, rowID, highlightRow) => renderable
- 從數(shù)據(jù)源(Data source)中接受一條數(shù)據(jù)撒遣,以及它和它所在section的ID邮偎。返回一個可渲染的組件來為這行數(shù)據(jù)進(jìn)行渲染。默認(rèn)情況下參數(shù)中的數(shù)據(jù)就是放進(jìn)數(shù)據(jù)源中的數(shù)據(jù)本身愉舔,不過也可以提供一些轉(zhuǎn)換器
- 如果某一行正在被高亮(通過調(diào)用highlightRow函數(shù))钢猛,ListView會得到相應(yīng)的通知。當(dāng)一行被高亮?xí)r轩缤,其兩側(cè)的分割線會被隱藏命迈。行的高亮狀態(tài)可以通過調(diào)用highlightRow(null)來重置
renderScrollComponent:【(props) => renderable】指定一個函數(shù),在其中返回一個可以滾動的組件火的。ListView將會在該組件內(nèi)部進(jìn)行渲染壶愤。默認(rèn)情況下會返回一個包含指定屬性的ScrollView
-
renderSectionHeader:【(sectionData, sectionID) => renderable】
- 如果提供了此函數(shù),會為每個小節(jié)(section)渲染一個粘性的標(biāo)題馏鹤。
- 粘性是指當(dāng)它剛出現(xiàn)時赘方,會處在對應(yīng)小節(jié)的內(nèi)容頂部;繼續(xù)下滑當(dāng)它到達(dá)屏幕頂端的時候嚎卫,它會停留在屏幕頂端新锈,一直到對應(yīng)的位置被下一個小節(jié)的標(biāo)題占據(jù)為止
-
renderSeparator:【(sectionID, rowID, adjacentRowHighlighted) => renderable】
- 如果提供了此屬性,一個可渲染的組件會被渲染在每一行下面治力,除了小節(jié)標(biāo)題的前面的最后一行蒙秒。在其上方的小節(jié)ID和行ID,以及鄰近的行是否被高亮?xí)鳛閰?shù)傳遞進(jìn)來
scrollRenderAheadDistance:當(dāng)一個行接近屏幕范圍多少像素之內(nèi)的時候宵统,就開始渲染這一行
stickyHeaderIndices(iOS):一個子視圖下標(biāo)的數(shù)組晕讲,用于決定哪些成員會在滾動之后固定在屏幕頂端。舉個例子马澈,傳遞stickyHeaderIndices={[0]}會讓第一個成員固定在滾動視圖頂端瓢省。這個屬性不能和horizontal={true}一起使用
方法
getMetrics():導(dǎo)出一些用于性能分析的數(shù)據(jù)
-
scrollTo(...args):滾動到指定的x, y偏移處,可以指定是否加上過渡動畫痊班。
- 參考 ScrollView#scrollTo.
ListView簡單優(yōu)化建議
- ListView 設(shè)計的時候勤婚,當(dāng)需要動態(tài)加載非常大量或者渲染復(fù)雜的數(shù)據(jù)時,下面有一些方法可以提高 ListView 的性能
- 只渲染更新數(shù)據(jù)變化的那個Item涤伐,rowHasChange方法會告訴ListView組件是否需要重新渲染當(dāng)前Item
- 選擇渲染的頻率蛔六,默認(rèn)情況下荆永,每一個event-loop(事件循環(huán))只會渲染一行(可以同pageSize自定義屬性設(shè)置)這樣可以把大工作量進(jìn)行分隔,提供整體渲染性能
ListView 基本布局
-
這邊我們就按照下圖中的布局實現(xiàn)一個簡單的列表數(shù)據(jù)展示
-
分析上圖整體布局国章,我這邊就將其劃分為幾個模塊具钥,首先需要有個
大的View
來包裝內(nèi)部所有的內(nèi)容,其次再給標(biāo)題部分分配一個小View
以方便維護(hù)液兽,具體如下圖
-
接下來就可以開始干活啦~
- 首先骂删,
ListView
需要數(shù)據(jù)源,那么我們就先來自定義一下數(shù)據(jù)源的Json
數(shù)據(jù)四啰,
[ {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"}, {"title" : "icon", "img" : "icon"}, {"title" : "lufei", "img" : "lufei"} ]
-
有了數(shù)據(jù)后宁玫,我們就可以根據(jù)數(shù)據(jù)來實例化
ListView
- 獲取數(shù)據(jù)
var newData = require('./Data/localData.json');
- 初始化數(shù)據(jù)源
getInitialState(){ var ds = new ListView.DataSource({rowHasChanged:(r1, r2) => r1 != r2}); return{ // 將獲得的數(shù)組傳遞給dataSource dataSource : ds.cloneWithRows(newData) } },
-
接著就是根據(jù)數(shù)據(jù)源實例化
ListView
- 視圖部分
render(){ return( <View style={styles.container}> <ListView dataSource={this.state.dataSource} renderRow={this.renderRow} /> </View> ); }, // 返回一個Item renderRow(rowData){ return( <View style={styles.itemStyle}> <Image source={{uri:rowData.img}} style={styles.imageStyle}/> <View style={styles.subItemStyle}> <Text style={{marginTop:5, fontSize:17}}>{rowData.title}</Text> <Text style={{marginBottom:5, fontSize:13, color:'green'}}>簡介</Text> </View> </View> ); }
- 樣式部分
var styles = StyleSheet.create({ container: { flex:1 }, itemStyle: { // 主軸方向 flexDirection:'row', // 下邊框 borderBottomWidth:1, borderBottomColor:'gray' }, imageStyle: { // 尺寸 width:60, height:60, // 邊距 marginLeft:10, margin:10 }, subItemStyle: { // 對齊方式 justifyContent:'space-around' } });
- 視圖部分
- 獲取數(shù)據(jù)
效果:
- 首先骂删,
ListView 九宮格布局實現(xiàn)
- 先來看下大概的布局
-
從上面可以看出,這個案例是為了實現(xiàn)類似
CollectionView
效果(比如常見的瀑布流)柑晒,通常情況下欧瘪,ListView
是縱向排列的,而此案例我們需要它橫向排列匙赞,那么就需要使用到上面提到的contentContainerStyle
屬性佛掖,向里面添加flexDirection:'row'和
flexWrap:'wrap'` 兩個屬性contentViewStyle: { // 主軸方向 flexDirection:'row', // 換行 flexWrap:'wrap' },
當(dāng)然了,我們還是需要自定義一組數(shù)據(jù)供
ListView
使用涌庭,這邊就使用上面案例的數(shù)據(jù)-
根據(jù)數(shù)據(jù)實例化
ListView
芥被,參考上面案例,這里只粘貼Item部分
坐榆,其它的就不重復(fù)了- 視圖部分
var ListViewDemo = React.createClass({ getInitialState(){ // 初始化數(shù)據(jù)源 var ds = new ListView.DataSource({rowHasChanged:(r1, r2) => r1 != r2}); return{ dataSource : ds.cloneWithRows(newData) } }, render(){ return( <ListView dataSource={this.state.dataSource} renderRow={this.renderRow} // 設(shè)置contentContainerStyle contentContainerStyle={styles.contentViewStyle} /> ); }, // 返回一個Item renderRow(rowData){ return( {/* 實例化Item */} <View style={styles.itemStyle}> <Image source={{uri:rowData.img}} style={styles.itemImageStyle}/> <Text>{rowData.title}</Text> </View> ); } });
- 樣式部分
var styles = StyleSheet.create({ contentViewStyle: { // 主軸方向 flexDirection:'row', // 換行 flexWrap:'wrap' }, itemStyle: { // 對齊方式 alignItems:'center', justifyContent:'center', // 尺寸 width:itemWH, height:itemWH, // 左邊距 marginLeft:vMargin, marginTop:hMargin }, itemImageStyle: { // 尺寸 width:60, height:60, // 間距 marginBottom:5 } });
效果:
- 視圖部分
ListView 分組樣式的實現(xiàn)分析
在移動設(shè)備里面拴魄,經(jīng)常會看到
sticky效果
,比如常見的通訊錄在React Native中,ScrollView組件要實現(xiàn) sticky效果 很簡單席镀,只需要使用
stickyHeaderIndices
就可以了,但對于 ListView 來說匹中,stickyHeaderIndices是無效的
,下面我們就來分析怎樣才能使 ListView 實現(xiàn)吸頂效果-
首先豪诲,
ListView
要實現(xiàn) sticky效果 需要使用到cloneWithRowsAndSections
方法將 dataBlob(object), sectionIDs (array), rowIDs (array) 三個值傳遞出去- dataBlob:
包含ListView所需的所有數(shù)據(jù)(section header 和 rows)
顶捷,在ListView渲染數(shù)據(jù)時,使用getSectionData 和 getRowData 來渲染每一行數(shù)據(jù)跛溉。 dataBlob 的 key 值包含 sectionID + rowId,參考下面模擬的數(shù)據(jù)結(jié)構(gòu)
var dataBlob = { 'sectionID1' : {section1 data}, 'sectionID1:rowID0' : {row0 data}, 'sectionID1:rowID1' : {row1 data}, 'sectionID2' : {section1 data}, 'sectionID2:rowID0' : {row0 data}, 'sectionID2:rowID1' : {row1 data}, 'sectionID2:rowID2' : {row2 data}, ... };
- sectionIDs:sectionIDs 用于標(biāo)識每組section扮授,參考下面模擬的數(shù)據(jù)結(jié)構(gòu)
var sectionIDs = ['sectionID0','sectionID1','sectionID2', ...];
- rowIDs:rowIDs 用于描述每個 section 里的每行數(shù)據(jù)的位置及是否需要渲染芳室。在ListView渲染時,會先遍歷 rowIDs 獲取到對應(yīng)的 dataBlob 數(shù)據(jù)刹勃,參考下面模擬的數(shù)據(jù)結(jié)構(gòu)
var rowIDs = [['rowID0', 'rowID1', 'rowID2'...], ['rowID0', 'rowID1', ...], ['rowID0', 'rowID1'], ...];
- dataBlob:
ListView 分組樣式實現(xiàn)
上面我們大概地分析了下 ListView 實現(xiàn)分組的原理堪侯,接下來就根據(jù)上面的分析加上實際的案例,來更直觀地體驗下 ListView分組功能的實現(xiàn)
-
首先荔仁,因為要分組伍宦,所以數(shù)據(jù)肯定比之前的案例使用到的要復(fù)雜芽死,但是不用擔(dān)心,這邊會盡量詳細(xì)地將數(shù)組的處理表述出來次洼,先來看下我們需要使用到的數(shù)據(jù)
{ "data":[ { "title":"A", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" }, { "icon" : "icon" } ] }, { "title":"B", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" }, { "icon" : "icon" }, { "icon" : "lufei" } ] }, { "title":"C", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" } ] }, { "title":"D", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" }, { "icon" : "icon" }, { "icon" : "lufei" } ] }, { "title":"E", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" }, { "icon" : "lufei" } ] }, { "title":"F", "icons":[ { "icon" : "icon" }, { "icon" : "lufei" } ] } ] }
-
結(jié)合上面的分析关贵,我們先來初始化數(shù)據(jù)源
getInitialState(){ // 初始化getSectionData var getSectionData = (dataBlob, sectionID) => { return dataBlob[sectionID]; }; // 初始化getRowData var getRowData = (dataBlob, sectionID, rowID) => { return dataBlob[sectionID + ':' + rowID]; }; return { // 初始化數(shù)據(jù)源 dataSource: new ListView.DataSource({ getSectionData : getSectionData, getRowData : getRowData, rowHasChanged : (r1, r2) => r1 !== r2, sectionHeaderHasChanged : (s1, s2) => s1 !== s2 }) } },
-
接著是數(shù)組的解析,然后將解析好的數(shù)據(jù)提供給
dataSource
進(jìn)行更新卖毁,需要注意的是在實際開發(fā)中揖曾,數(shù)據(jù)的復(fù)雜程度遠(yuǎn)遠(yuǎn)要大于我們上面的數(shù)據(jù),這是比較耗時的操作亥啦,所以我們會選擇在異步線程中執(zhí)行炭剪,之前的文章中也提到過 —— 在React Native中,我們一般將耗時復(fù)雜的操作放到componentDidMount
中執(zhí)行// 耗時翔脱、復(fù)雜操作放到這里處理 componentDidMount(){ // 加載數(shù)據(jù) this.loadData(); }, // 加載數(shù)據(jù) loadData(){ // 拿到j(luò)son數(shù)據(jù)中的數(shù)組 var jsonData = iconData.data; // 定義變量 var dataBlob = {}, sectionIDs = [], rowIDs = [], icons = []; // 遍歷數(shù)組中對應(yīng)的數(shù)據(jù)并存入變量內(nèi) for (var i = 0; i<jsonData.length; i++){ // 將組號存入 sectionIDs 中 sectionIDs.push(i); // 將每組頭部需要顯示的內(nèi)容存入 dataBlob 中 dataBlob[i] = jsonData[i].title; // 取出該組所有的 icon icons = jsonData[i].icons; rowIDs[i] = []; // 遍歷所有 icon for (var j = 0; j<icons.length; j++){ // 設(shè)置標(biāo)識 rowIDs[i].push(j); // 根據(jù)標(biāo)識,將數(shù)據(jù)存入 dataBlob dataBlob[i + ':' + j] = icons[j]; } } // 刷新dataSource狀態(tài) this.setState({ dataSource:this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs) }); }
-
最后奴拦,就是設(shè)置樣式,將布局調(diào)到我們想要的效果就可以了
- 視圖部分
render(){ return( <View style={styles.container}> // 實例化頂部View <View style={styles.topViewStyle}> <Text style={{fontSize:21}}>分組樣式</Text> </View> // 實例化ListView <ListView dataSource={this.state.dataSource} renderRow={this.renderRow} renderSectionHeader={this.renderSectionHeader} /> </View> ); }, // 返回一個Item renderRow(rowData, sectionID, rowID){ return( <View style={styles.itemStyle}> <Image source={{uri:rowData.icon}} style={{width: 60, height:60, marginTop:10, marginLeft:10}}></Image> <Text style={{marginTop:15, marginLeft:10}}>示例</Text> </View> ); }, // 返回一個SectionHeader renderSectionHeader(sectionData, sectionID){ return( <Text style={{backgroundColor:'yellow'}}>{sectionData}</Text> ); },
- 樣式部分
var styles = StyleSheet.create({ container:{ flex:1 }, topViewStyle: { // 尺寸 height:44, // 邊距 marginTop:20, // 對齊方式 justifyContent:'center', alignItems:'center' }, itemStyle: { // 尺寸 height:80, // 主軸方向 flexDirection:'row', // 下邊框 borderBottomWidth:1, borderBottomColor:'gray' }, });
效果:
- 視圖部分