前言 學習本系列內容需要具備一定 HTML 開發基礎,沒有基礎的朋友可以先轉至 "HTML快速入門(一)" 學習 本人接觸 React Native 時間並不是特別長,所以對其中的內容和性質瞭解可能會有所偏差,在學習中如果有錯會及時修改內容,也歡迎萬能的朋友們批評指出,謝謝 文章第一版出自簡書,如果 ...
前言
學習本系列內容需要具備一定 HTML 開發基礎,沒有基礎的朋友可以先轉至 HTML快速入門(一) 學習
本人接觸 React Native 時間並不是特別長,所以對其中的內容和性質瞭解可能會有所偏差,在學習中如果有錯會及時修改內容,也歡迎萬能的朋友們批評指出,謝謝
文章第一版出自簡書,如果出現圖片或頁面顯示問題,煩請轉至 簡書 查看 也希望喜歡的朋友可以點贊,謝謝
ListView組件介紹
- ListView組件是React Native中一個比較核心的組件,用途非常廣,設計初衷就是用來高效的展示垂直滾動的列表數據
- ListView 繼承了
ScrollView
的所有屬性 - 使用步驟:
- 創建一個ListView.DataSource數據源,然後給它傳遞一個普通的數組數據
getInitialState(){ // 初始化數據源(rowHasChanged是優化的一種手段,只有當r1 !== r2的時候才會重新渲染) var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); return{ // 給dataSource傳遞一組 數組 dataSource: ds.cloneWithRows(['內容0', '內容1', '內容2', '內容3', '內容4', '內容5']) } },
- 使用數據源實例化一個ListView組件,定義一個renderRow回調函數,這個函數會接受數組中的每個數據作為參數,並返回一個可渲染的組件(也就是該列表的每一行Item)
render() { return ( <View style={styles.container}> // 根據數據源實例化一個ListView <ListView style={{backgroundColor:'yellow'}} // 獲取數據源 dataSource={this.state.dataSource} // 根據數據源創建一個Item // 註:這裡的this.renderRow是隱式寫法,系統會根據函數的需要,將對應的參數傳遞過去(共有4個參數:rowData, sectionID, rowID, highlightRow) renderRow={this.renderRow} /> </View> ); }, // 返回一個Item renderRow(rowData,sectionID,rowID) { return( // 實例化Item <View> <Text style={{backgroundColor:'red', height:44}}>內容{rowData},在第{sectionID}組第{rowID}行</Text> </View> ) }
效果:
- 創建一個ListView.DataSource數據源,然後給它傳遞一個普通的數組數據
ListView 同樣支持一些高級特性,包括設置每一組的粘性的頭部、支持設置列表 header 和 footter 視圖、當數據列表滑動到最底部的時候支持 onEndReached 方法回調、設備屏幕列表可見的視圖數據發生變化的時候回調 onChangeVisibleRows 以及一些性能方面的優化特性
ListView常用屬性
ScrollView 全部屬性
dataSource:設置ListView的數據源
initialListSize:指定在組件剛掛載的時候渲染多少行數據。用這個屬性來確保首屏顯示合適數量的數據,而不是花費太多幀逐步顯示出來
onChangeVisibleRows:((visibleRows, changedRows) => void)當可見的行的集合變化的時候調用此回調函數。visibleRows 以 { sectionID: { rowID: true }}的格式包含了所有可見行,而changedRows 以{ sectionID: { rowID: true | false }}的格式包含了所有剛剛改變了可見性的行,其中如果值為true表示一個行變得可見,而為false表示行剛剛離開可視區域而變得不可見
onEndReached:當所有的數據都已經渲染過,並且列表被滾動到距離最底部不足onEndReachedThreshold個像素的距離時調用。原生的滾動事件會被作為參數傳遞。譯註:當第一次渲染時,如果數據不足一屏(比如初始值是空的),這個事件也會被觸發
onEndReachedThreshold:調用onEndReached之前的臨界值,單位是像素
pageSize:每次事件迴圈(每幀)渲染的行數
removeClippedSubviews:用於提升大列表的滾動性能。需要給行容器添加樣式overflow:'hidden'。(Android已預設添加此樣式)此屬性預設開啟
renderFooter:(() => renderable)頁頭與頁腳會在每次渲染過程中都重新渲染(如果提供了這些屬性)。如果它們重繪的性能開銷很大,把他們包裝到一個StaticContainer或者其它恰當的結構中。頁腳會永遠在列表的最底部,而頁頭會在最頂部
renderHeader: 在每一次渲染過程中Footer(尾)該會一直在列表的底部,header(頭)該會一直在列表的頭部
- renderRow:【(rowData, sectionID, rowID, highlightRow) => renderable
- 從數據源(Data source)中接受一條數據,以及它和它所在section的ID。返回一個可渲染的組件來為這行數據進行渲染。預設情況下參數中的數據就是放進數據源中的數據本身,不過也可以提供一些轉換器
- 如果某一行正在被高亮(通過調用highlightRow函數),ListView會得到相應的通知。當一行被高亮時,其兩側的分割線會被隱藏。行的高亮狀態可以通過調用highlightRow(null)來重置
renderScrollComponent:【(props) => renderable】指定一個函數,在其中返回一個可以滾動的組件。ListView將會在該組件內部進行渲染。預設情況下會返回一個包含指定屬性的ScrollView
- renderSectionHeader:【(sectionData, sectionID) => renderable】
- 如果提供了此函數,會為每個小節(section)渲染一個粘性的標題。
- 粘性是指當它剛出現時,會處在對應小節的內容頂部;繼續下滑當它到達屏幕頂端的時候,它會停留在屏幕頂端,一直到對應的位置被下一個小節的標題占據為止
- renderSeparator:【(sectionID, rowID, adjacentRowHighlighted) => renderable】
- 如果提供了此屬性,一個可渲染的組件會被渲染在每一行下麵,除了小節標題的前面的最後一行。在其上方的小節ID和行ID,以及鄰近的行是否被高亮會作為參數傳遞進來
scrollRenderAheadDistance:當一個行接近屏幕範圍多少像素之內的時候,就開始渲染這一行
stickyHeaderIndices(iOS):一個子視圖下標的數組,用於決定哪些成員會在滾動之後固定在屏幕頂端。舉個例子,傳遞stickyHeaderIndices={[0]}會讓第一個成員固定在滾動視圖頂端。這個屬性不能和horizontal={true}一起使用
方法
getMetrics():導出一些用於性能分析的數據
- scrollTo(...args):滾動到指定的x, y偏移處,可以指定是否加上過渡動畫。
- 參考 ScrollView#scrollTo.
ListView簡單優化建議
- ListView 設計的時候,當需要動態載入非常大量或者渲染複雜的數據時,下麵有一些方法可以提高 ListView 的性能
- 只渲染更新數據變化的那個Item,rowHasChange方法會告訴ListView組件是否需要重新渲染當前Item
- 選擇渲染的頻率,預設情況下,每一個event-loop(事件迴圈)只會渲染一行(可以同pageSize自定義屬性設置)這樣可以把大工作量進行分隔,提供整體渲染性能
ListView 基本佈局
這邊我們就按照下圖中的佈局實現一個簡單的列表數據展示
分析上圖整體佈局,我這邊就將其劃分為幾個模塊,首先需要有個
大的View
來包裝內部所有的內容,其次再給標題部分分配一個小View
以方便維護,具體如下圖
- 接下來就可以開始幹活啦~
- 首先,
ListView
需要數據源,那麼我們就先來自定義一下數據源的Json
數據,
[ {"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"} ]
- 有了數據後,我們就可以根據數據來實例化
ListView
- 獲取數據
var newData = require('./Data/localData.json');
- 初始化數據源
getInitialState(){ var ds = new ListView.DataSource({rowHasChanged:(r1, r2) => r1 != r2}); return{ // 將獲得的數組傳遞給dataSource dataSource : ds.cloneWithRows(newData) } },
- 接著就是根據數據源實例化
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' } });
- 視圖部分
- 獲取數據
- 首先,
ListView 九宮格佈局實現
- 先來看下大概的佈局
從上面可以看出,這個案例是為了實現類似
CollectionView
效果(比如常見的瀑布流),通常情況下,ListView
是縱向排列的,而此案例我們需要它橫向排列,那麼就需要使用到上面提到的contentContainerStyle
屬性,向裡面添加flexDirection:'row'和
flexWrap:'wrap'` 兩個屬性contentViewStyle: { // 主軸方向 flexDirection:'row', // 換行 flexWrap:'wrap' },
- 當然了,我們還是需要自定義一組數據供
ListView
使用,這邊就使用上面案例的數據 - 根據數據實例化
ListView
,參考上面案例,這裡只粘貼Item部分
,其它的就不重覆了- 視圖部分
var ListViewDemo = React.createClass({ getInitialState(){ // 初始化數據源 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} // 設置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 分組樣式的實現分析
在移動設備裡面,經常會看到
sticky效果
,比如常見的通訊錄在React Native中,ScrollView組件要實現 sticky效果 很簡單,只需要使用
stickyHeaderIndices
就可以了,但對於 ListView 來說,stickyHeaderIndices是無效的
,下麵我們就來分析怎樣才能使 ListView 實現吸頂效果- 首先,
ListView
要實現 sticky效果 需要使用到cloneWithRowsAndSections
方法將 dataBlob(object), sectionIDs (array), rowIDs (array) 三個值傳遞出去- dataBlob:
包含ListView所需的所有數據(section header 和 rows)
,在ListView渲染數據時,使用getSectionData 和 getRowData 來渲染每一行數據。 dataBlob 的 key 值包含 sectionID + rowId,參考下麵模擬的數據結構
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 用於標識每組section,參考下麵模擬的數據結構
var sectionIDs = ['sectionID0','sectionID1','sectionID2', ...];
- rowIDs:rowIDs 用於描述每個 section 里的每行數據的位置及是否需要渲染。在ListView渲染時,會先遍歷 rowIDs 獲取到對應的 dataBlob 數據,參考下麵模擬的數據結構
var rowIDs = [['rowID0', 'rowID1', 'rowID2'...], ['rowID0', 'rowID1', ...], ['rowID0', 'rowID1'], ...];
- dataBlob:
ListView 分組樣式實現
上面我們大概地分析了下 ListView 實現分組的原理,接下來就根據上面的分析加上實際的案例,來更直觀地體驗下 ListView分組功能的實現
首先,因為要分組,所以數據肯定比之前的案例使用到的要複雜,但是不用擔心,這邊會儘量詳細地將數組的處理表述出來,先來看下我們需要使用到的數據
{ "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" } ] } ] }
結合上面的分析,我們先來初始化數據源
getInitialState(){ // 初始化getSectionData var getSectionData = (dataBlob, sectionID) => { return dataBlob[sectionID]; }; // 初始化getRowData var getRowData = (dataBlob, sectionID, rowID) => { return dataBlob[sectionID + ':' + rowID]; }; return { // 初始化數據源 dataSource: new ListView.dataSource({ getSectionData : getSectionData, getRowData : getRowData, rowHasChanged : (r1, r2) => r1 !== r2, sectionHeaderHasChanged : (s1, s2) => s1 !== s2 }) } },
接著是數組的解析,然後將解析好的數據提供給
dataSource
進行更新,需要註意的是在實際開發中,數據的複雜程度遠遠要大於我們上面的數據,這是比較耗時的操作,所以我們會選擇在非同步線程中執行,之前的文章中也提到過 —— 在React Native中,我們一般將耗時複雜的操作放到componentDidMount
中執行// 耗時、複雜操作放到這裡處理 componentDidMount(){ // 載入數據 this.loadData(); }, // 載入數據 loadData(){ // 拿到json數據中的數組 var jsonData = iconData.data; // 定義變數 var dataBlob = {}, sectionIDs = [], rowIDs = [], icons = []; // 遍曆數組中對應的數據並存入變數內 for (var i = 0; i<jsonData.length; i++){ // 將組號存入 sectionIDs 中 sectionIDs.push(i); // 將每組頭部需要顯示的內容存入 dataBlob 中 dataBlob[i] = jsonData[i].title; // 取出該組所有的 icon icons = jsonData[i].icons; rowIDs[i] = []; // 遍歷所有 icon for (var j = 0; j<icons.length; j++){ // 設置標識 rowIDs[i].push(j); // 根據標識,將數據存入 dataBlob dataBlob[i + ':' + j] = icons[j]; } } // 刷新dataSource狀態 this.setState({ dataSource:this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs) }); }
- 最後,就是設置樣式,將佈局調到我們想要的效果就可以了
- 視圖部分
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({ contairn:{ flex:1 }, topViewStyle: { // 尺寸 height:44, // 邊距 marginTop:20, // 對齊方式 justifyContent:'center', alignItems:'center' }, itemStyle: { // 尺寸 height:80, // 主軸方向 flexDirection:'row', // 下邊框 borderBottomWidth:1, borderBottomColor:'gray' }, });
效果:
- 視圖部分