今天我要實現一個 類似于 iOS 開發中帶有分組的colllectionView 樣式的布局, 每個section都要有個組頭。
首先我們要先決定要使用什么控件。ScrollView和ListView/FlatList還有SectionList都是可以選擇的。
- ScrollView 會把所有子元素一次性全部渲染出來。使用上最簡單。但是如果你有一個特別長的列表需要顯示,可能會需要好幾屏的高度。這時就會占用很大的內存去創建和渲染那些屏幕以外的JS組件和原生視圖,性能上也會有所拖累。
- ListView 更適用于長列表數據。它會惰性渲染子元素,并不會立即渲染所有元素,而是優先渲染屏幕上可見的元素。
- FlatList 是0.43版本開始新出的改進版的ListView,性能更優,但是官方說現在可能不夠穩定,尚待時間考驗。但是它不能夠分組/類/區(section)。
- SectionList 也是0.43版本推出的, 高性能的分組列表組件。但是它不支持頭部吸頂懸浮的效果,但是也不要傷心,官方在下一個版本開始就可以支持懸浮的section頭部啦 ??。
好啦, 綜上所訴我選擇使用SectionList ,現在開始干活吧 ??
首先
第一步我們先把要顯示的樣式寫好作為子控件, 把數據源整理好。
例一、
<SectionList
renderItem={({item}) => <ListItem title={item.title} />}
renderSectionHeader={({section}) => <H1 title={section.key} />}
sections={[ // 不同section渲染相同類型的子組件
{data: [...], key: ...},
{data: [...], key: ...},
{data: [...], key: ...},
]}
/>
例二、
<SectionList
sections={[ // 不同section渲染不同類型的子組件
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
]}
/>
sections 就是我們的數據源,每一個data 就是我們要用的item, renderItem就是你要顯示的子控件哦。如果你每個組都復用一個子組件那就按照例一的結構, 如果你想要不同的組返回不同樣式的子組件那就按照例二的結構返回不同的renderItem即可。
這里提個醒, key一定要有, 不同的section 要設置不同的key才會渲染相應的section, 如果你key值都相同, 那可能會出現只顯示一組數據的情況哦~
下面來看看我的代碼:
<SectionList
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
sections={[ // 不同section渲染相同類型的子組件
{ data: [{ title: this.state.appModel[0] }], key: this.state.groupsModel[0].title },
{ data: [{ title: this.state.appModel[1] }], key: this.state.groupsModel[1].title },
{ data: [{ title: this.state.appModel[2] }], key: this.state.groupsModel[2].title },
{ data: [{ title: this.state.appModel[3] }], key: this.state.groupsModel[3].title },
]}
/>
這樣有了最基礎的樣式, 四組縱向的列表, 但是我要橫向的, 于是我要設置他的樣式啦。
接下來
這里我添加兩個屬性:
contentContainerStyle={styles.list}//設置cell的樣式
pageSize={4} // 配置pageSize確認網格數量
const styles = StyleSheet.create({
list: {
//justifyContent: 'space-around',
flexDirection: 'row',//設置橫向布局
flexWrap: 'wrap', //設置換行顯示
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
});
好啦, 讓我們來看看效果。
??這是什么鬼???
為什么它的組頭也在左邊 , 并且他的其他組數據都橫著了, 對于小白我來說只有大寫的懵~。不知道你們有沒有遇到這種情況, 是什么原因導致的, 我很是困惑啊, 當我把
renderSectionHeader={this._renderSectionHeader}
這行代碼注掉的時候, 它的顯示是正常的...
這就尷尬了...
它的每一個小方塊是一個item,達不到我要的效果啊, 于是我決定換個思路, 誰讓我是打不死的小白呢??
重新來
我決定讓每個section是一個item。在每個item上創建多個可點擊的板塊。
_renderItem = ({ item}) => (
<View style={styles.list}>
{
item.map((item, i) => this.renderExpenseItem(item, i))
}
</View>
);
renderExpenseItem(item, i) {
return <TouchableOpacity key={i} onPress={() => this._pressRow(item)} underlayColor="transparent">
<View style={styles.row}>
<CellView source={item.img}></CellView>
</View>
</TouchableOpacity>;
}
_renderSectionHeader = ({ section }) => (
<View style={{ flex: 1, height: 25 }}>
<Text style={styles.sectionHeader} >{section.key}</Text>
</View>
);
render() {
return (
<View style={{ flex: 1 }}>
<Text style={styles.navigatorStyle}> 發現 </Text>
<View style={{ flex: 1, backgroundColor: '#F7F6F8' }}>
<SectionList
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
showsVerticalScrollIndicator={false}
sections={ // 不同section渲染相同類型的子組件
this.state.dataSource
}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
list: {
//justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
row: {
backgroundColor: '#FFFFFF',
justifyContent: 'center',
width: (ScreenWidth - 1) / 4,
height: (ScreenWidth - 1) / 4,
alignItems: 'center',
},
sectionHeader: {
marginLeft: 10,
padding: 6.5,
fontSize: 12,
color: '#787878'
},
});
這里的dataSource 我是在之前數據的基礎上又包了一層[],然后在renderItem里做了map映射, 這樣每個renderItem上返回了每一組我所需要的子組件。快來看看我的變化吧??
腫么樣, 達到效果了吧, 但是你有沒有發現 底部為啥是黃色的?,我可沒有去設置這么丑的顏色哦,其實它是提醒我們有不完美的地方, 下面就讓我們解決一下這個不完美吧 。
最后解決問題
最后讓我們來解決問題。它警告我們每個item 要有不同的key ,還記不記得我上面的提醒,我也犯這個錯誤了。
- 默認情況下每行都需要提供一個不重復的key屬性。你也可以提供一個keyExtractor函數來生成key。
// 把這個屬性添加到 <SectionList/> 里面
keyExtractor = {this._extraUniqueKey}
_extraUniqueKey(item ,index){
return "index"+index+item;
}
這是每個item要設置key, 同樣每個子控件也不能放過, 一定要設置它的key, 要不然這個屎黃色一直伴著你 多煩~~~
最后看一下我最終的代碼吧!
var Dimensions = require('Dimensions');//獲取屏幕的寬高
var ScreenWidth = Dimensions.get('window').width;
var ScreenHeight = Dimensions.get('window').height;
// const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);// 這個是創建動畫
export default class Explore extends Component {
constructor(props) {
super(props);
this.state = {
appModel: null,
groupsModel: null,
dataSource: null,
}
}
//Component掛載完畢后調用
componentDidMount() {
this.fetchData();
}
async fetchData() {
try {
let model = await NetFetch.post(_req_url_path, {
});
let apps = model.apps;
let groups = model.groups;
let data = [];
for (let i = 0; i < model.groups.length; i++) {
let row = [];
for (let j = 0; j < model.apps.length; j++) {
if (model.groups[i].appIds.indexOf(model.apps[j].appId) >= 0) {
row.push(model.apps[j]);
}
}
data.push({ data: [row], key: model.groups[i].title });
}
// 這里我重組了一下數據結構, 看沒看見我在row外面又包了一層, 為了我循環創建每個section的子組件。
this.setState({
appModel: model.apps,
groupsModel: model.groups,
dataSource: data
});
} catch (error) {
alert(error.msg);
}
}
_renderItem = ({ item}) => (
<View style={styles.list}>
{
item.map((item, i) => this.renderExpenseItem(item, i))
}
</View>
);
renderExpenseItem(item, i) {
return <TouchableOpacity key={i} onPress={() => this._pressRow(item)} underlayColor="transparent">
<View style={styles.row}>
<CellView source={item.img}></CellView>
</View>
</TouchableOpacity>;
}
_renderSectionHeader = ({ section }) => (
<View style={{ flex: 1, height: 25 }}>
<Text style={styles.sectionHeader} >{section.key}</Text>
</View>
);
_listHeaderComponent() {
return (
<HeaderView integral={0}></HeaderView>
);
}
_listFooterComponent() {
return (
<Text style={[styles.remark]}>*預期收益非平臺承諾收益,市場有風險,投資需謹慎</Text>
);
}
_pressRow(item) {
this.props.navigator.pushTo(item.go)
}
_extraUniqueKey(item ,index){
return "index"+index+item;
}
render() {
if (!this.state.dataSource) {
return (
<View></View>
);
}
return (
<View style={{ flex: 1 }}>
<Text style={styles.navigatorStyle}> 發現 </Text>
<View style={{ flex: 1, backgroundColor: '#F7F6F8' }}>
<SectionList
contentInset={{top:0,left:0,bottom:49,right:0}}// 設置他的滑動范圍
renderItem={this._renderItem}
ListFooterComponent={this._listFooterComponent}
ListHeaderComponent={this._listHeaderComponent}
renderSectionHeader={this._renderSectionHeader}
showsVerticalScrollIndicator={false}
keyExtractor = {this._extraUniqueKey}// 每個item的key
// contentContainerStyle={styles.list}
// horizontal={true}
// pageSize={4} // 配置pageSize確認網格數量
sections={ // 不同section渲染相同類型的子組件
this.state.dataSource
}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
navigatorStyle: {
height: 64,
backgroundColor: '#FFFFFF',
textAlign: 'center',
paddingTop: 33.5,
fontSize: 17,
fontWeight: '600',
},
list: {
//justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: '#FFFFFF'
},
row: {
backgroundColor: '#FFFFFF',
justifyContent: 'center',
width: (ScreenWidth - 1) / 4,
height: (ScreenWidth - 1) / 4,
alignItems: 'center',
// borderWidth: 0.5,
// borderRadius: 5,
// borderColor: '#E6E6E6'
},
sectionHeader: {
marginLeft: 10,
padding: 6.5,
fontSize: 12,
color: '#787878'
},
remark: {
margin: 10,
fontSize: 10,
color: '#D2D2D2',
marginBottom: 10,
alignSelf: 'center',
},
});
看下最終效果圖吧
最最后總結一下在開發中遇到的疑難雜癥還有sectionList的重要屬性。
不知道你有沒有遇見這個問題, 看起來很簡單, 應該是我沒有引入Text組件, 但是我確實引入了。最終發現這個問題竟是因為我有段代碼是這樣寫的
<Image> source={require('../../assets/image/deadline.png')} style={styles.iconStyle} </Image>
<Text style={styles.userNameStyle}>賺積分,換好禮!</Text>
不知道你有沒有發現錯誤, 由于習慣我<Image>組件寫成<Image></Image>,Image是自封閉標簽所以
<Image source={require('../../assets/image/deadline.png')} style={styles.iconStyle} />
<Text style={styles.userNameStyle}>賺積分,換好禮!</Text>
這樣問題就解決了, 但是我不清楚它為啥會報這樣的錯,反正開發中總是會出現一些不知所以的錯, 所以平時寫代碼的時候不斷總結起來就好啦...
SectionList 屬性
- ItemSeparatorComponent?: ?ReactClass<any>
行與行之間的分隔線組件。不會出現在第一行之前和最后一行之后。
- ListFooterComponent?: ?ReactClass<any>
尾部組件
- ListHeaderComponent?: ?ReactClass<any>
頭部組件
- keyExtractor: (item: Item, index: number) => string
此函數用于為給定的item生成一個不重復的key。Key的作用是使React能夠區分同類元素的不同個體,以便在刷新時能夠確定其變化的位置,減少重新渲染的開銷。若不指定此函數,則默認抽取item.key作為key值。若item.key也不存在,則使用數組下標。
- onEndReached?: ?(info: {distanceFromEnd: number}) => void
當所有的數據都已經渲染過,并且列表被滾動到距離最底部不足onEndReachedThreshold個像素的距離時調用。
- onRefresh?: ?() => void
如果設置了此選項,則會在列表頭部添加一個標準的RefreshControl控件,以便實現“下拉刷新”的功能。同時你需要正確設置refreshing屬性。
- refreshing?: ?boolean
是否刷新嘍
- renderItem: (info: {item: Item, index: number}) => ?React.Element<any>
根據行數據data渲染每一行的組件。
除data外還有第二個參數index可供使用。
- renderSectionHeader?: ?(info: {section: SectionT}) => ?React.Element<any>
這就是我用到的每個section的組頭
- sections: Array<SectionT>
你的數據源
最后再提醒一下不要忘了key key key 哦 。