本小节我们来看一下 RN 中所提供的列表组件,主要包含:
- FlatList
- 下拉刷新
- 上拉加载更多
- SectionList
FlatList#
在 FlatList 组件出现之前,RN 使用 ListView 组件来实现列表功能,不过在列表数据比较多的情况下,ListView 组件的性能并不是很好,所以在 0.43.0 版本中,RN 引入了 FlatList 组件。相比 ListView 组件,FlatList 组件适用于加载长列表数据,而且性能也更佳。
和 ListView 组件类似,FlatList 组件的使用也非常的简单,只需要给 FlatList 组件提供 data 和 renderItem 两个属性即可,如下所示:
<FlatList data={[{key:"a"},{key:"b"}]} renderItem={({item})=><Text>{item.key}</Text>}>其中 data 表示数据源,一般为数组格式,renderItem 表示每行的绘制方法。除了 data 和 renderItem 两个必须属性外,FlatList 还支持诸如 ListHeaderComponent、ListFooterComponent 等属性,具体可以参阅官方文档:https://reactnative.dev/docs/flatlist#itemseparatorcomponent
下面是一个使用 FlatList 渲染电影列表的示例:
首先定义了一个名为 MovieItemCell 的电影项目组件,用于渲染具体的电影项目,包含电影的标题、上映日期、评分、海报、导演、主演等信息。组件代码如下:
import React from 'react'import { TouchableHighlight, View, Image, Text, StyleSheet } from 'react-native'
export default function MovieItemCell(props) { const moveInfo = props.movie.item let hasAverageScore = moveInfo.average != '0' return ( <TouchableHighlight onPress={props.onPress}> <View style={styles.container}> <Image source={{ uri: moveInfo.movieImg }} style={styles.thumbnail} /> <View style={styles.rightContainer}> <Text style={styles.title}>{moveInfo.title}</Text> <Text style={styles.year}>{moveInfo.year}</Text> {hasAverageScore ? ( <View style={styles.horizontalView}> <Text style={styles.titleTag}>评分:</Text> <Text style={styles.score}>{moveInfo.average}</Text> </View> ) : ( <View style={styles.horizontalView}> <Text style={styles.titleTag}>暂无评分</Text> </View> )} <View style={styles.horizontalView}> <Text style={styles.titleTag}>导演:</Text> <Text style={styles.name}>{moveInfo.directors}</Text> </View> <View style={styles.horizontalView}> <Text style={styles.titleTag}>主演:</Text> <Text style={styles.name}> {moveInfo.casts.length > 13 ? moveInfo.casts.slice(0, 13) + '...' : moveInfo.casts} </Text> </View> </View> </View> </TouchableHighlight> )}
const styles = StyleSheet.create({ container: { flexDirection: 'row', justifyContent: 'center', backgroundColor: '#F5FCFF', padding: 10, borderBottomWidth: 1, borderColor: '#e0e0e0', }, thumbnail: { width: 110, height: 150, backgroundColor: '#f0f0f0', }, rightContainer: { flex: 1, paddingLeft: 10, paddingTop: 5, paddingBottom: 5, }, title: { fontSize: 16, fontWeight: 'bold', color: '#333333', textAlign: 'left', }, year: { textAlign: 'left', color: '#777777', marginTop: 10, }, horizontalView: { flexDirection: 'row', marginTop: 10, }, titleTag: { color: '#666666', }, score: { color: '#ff8800', fontWeight: 'bold', }, name: { color: '#333333', flex: 1, },})接下来,我们在 App.js 根组件中使用 FlatList 来做列表渲染,如下:
import React, { useState, useEffect } from 'react'import { View, FlatList, Dimensions, Text, ActivityIndicator, StyleSheet, StatusBar, SafeAreaView,} from 'react-native'import { queryMovies } from './data/Service'import MovieItemCell from './view/MovieItemCell'
export const width = Dimensions.get('window').width
export default function App() { // 初始化电影数据 const data = queryMovies() // 初始化电影列表和加载状态 const [movieList, setMovieList] = useState([]) const [loaded, setLoaded] = useState(false)
useEffect(() => { setTimeout(() => { setMovieList(data) setLoaded(true) }, 1000) }, [])
// 渲染标题 function renderTitle() { return ( <View style={styles.bayStyle}> <Text style={styles.txtStyle}>电影列表</Text> </View> ) }
// 渲染加载条 function renderLoad() { if (!loaded) { return ( <View style={styles.container}> <ActivityIndicator animating={true} size="small" /> <Text style={{ color: '#666666', paddingLeft: 10 }}>努力加载中</Text> </View> ) } }
function renderItem(item) { return ( <MovieItemCell movie={item} onPress={() => { alert('点击电影:' + item.item.title) }} /> ) }
// 渲染电影列表 function renderList() { return ( <FlatList data={movieList} renderItem={renderItem} keyExtractor={(item) => item.id} /> ) }
return ( <SafeAreaView style={styles.flex}> {/* 标题区域 */} {renderTitle()} {/* 加载条 */} {renderLoad()} {/* 列表区域 */} {renderList()} </SafeAreaView> )}
const styles = StyleSheet.create({ flex: { flex: 1, backgroundColor: '#268dcd', }, container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', flexDirection: 'row', }, bayStyle: { height: 48, width: width, justifyContent: 'center', backgroundColor: '#268dcd', }, txtStyle: { color: '#fff', textAlign: 'center', fontSize: 18, },})其中,data 表示数据源,一般为数组格式,renderItem 表示每行的列表项。为了方便对列表单元视图进行复用,通常的做法是将列表单元视图独立出来,例如我们上面的 MovieItemCell 组件。
除此之外,FlatList 组件还有如下一些使用频率比较高的属性和方法:
- item 的 key:使用 FlatList 组件实现列表效果时,系统要求给每一行子组件设置一个 key,key 是列表项的唯一标识,目的是当某个子视图的数据发生改变时可以快速地重绘改变的子组件。一般,我们使用 FlatList 组件提供的 keyExtractor 属性来达到此效果。
<FlatList ... keyExtractor={(item) => item.id}/>- 分割线 seperator:FlatList 组件本身的分割线并不是很明显,如果要实现分割线,主要有两种策略:设置 borderBottom 或者 ItemSeperatorComponent 属性。如果只是一条简单的分割线,在 Item 组件里面添加 borderBottom 相关属性即可。
<View style={{ borderTopWidth: 0, borderBottomWidth: 1 }}>...</View>需要注意的是,使用 borderBottom 实现分割线时,列表顶部和底部的组件是不需要绘制的。
当然,更简单的方式是使用 ItemSeparatorComponent 属性,具体使用方式可以参阅官方文档:https://reactnative.dev/docs/flatlist
下拉刷新#
下拉刷新是一个常见的需求,当用户已经处于列表的最顶端,此时继续往下拉动页面的话,就会有一个数据刷新的操作。
在 FlatList 中,提供了下拉刷新的功能,我们只需要设置 onRefresh 和 refreshing 这两个属性值即可。
- onRefresh:下拉刷新操作触发时要进行的动作,对应是一个函数
- refreshing:是否显示下拉刷新的等待图标,对应一个布尔值
下面来看一个具体的示例。代码片段如下:
// 渲染电影列表function renderList() { return ( <FlatList data={movieList} renderItem={renderItem} keyExtractor={(item) => item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1) } onRefresh={beginHeaderRefresh} refreshing={isHeaderRefreshing} /> )}在上面的代码中,当用户下拉刷新时,触发 onRefresh 所对应的 beginHeaderRefresh 函数,此函数对应的操作如下:
// 下拉刷新function beginHeaderRefresh() { setIsHeaderRefreshing(true) // 模拟刷新了两条数据 const newMovie = randomRefreshMovies() const data = [...movieList] data.unshift(newMovie[0], newMovie[1]) setTimeout(() => { setMovieList(data) setIsHeaderRefreshing(false) }, 1000)}首先我们将 isHeaderRefreshing 设置为 true,以便出现下拉等待图标,之后调用 randomRefreshMovies 方法随机获取两条电影数据,之后模拟异步场景在一秒钟后更新 movieList 并且关闭 isHeaderRefreshing。
其中 randomRefreshMovies 是从其他文件导入的,代码如下:
// 随机刷新两部电影export function randomRefreshMovies() { let randomStartIndex = Math.floor(Math.random() * (moviesData.length - 2)) return moviesData.slice(randomStartIndex, randomStartIndex + 2)}至此,一个模拟的下拉刷新效果就做完了,每次下拉都会随机刷新两部电影在最前面。
上拉加载更多#
上拉加载也是列表中一个常见的操作,上拉加载其实质就是以前 PC 端的分页效果。因为数据量过多,所以一般我们不会一次性加载所有的数据,此时就会进行一个分页的显示。而在移动端,分页显示变成了上拉加载的形式,当用户到达列表底部时,自动获取下一页的数据,并且拼接到原有数据的后面。
这里我们会用到两个属性,分别是:
- onEndReached:上拉加载操作触发时要进行的动作,对应是一个函数
- onEndReachedThreshold:表示距离底部多远时触发 onEndReached
下面来看一个具体的示例。代码片段如下:
// 渲染电影列表function renderList() { return ( <FlatList data={movieList} renderItem={renderItem} keyExtractor={(item) => item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1) } onRefresh={beginHeaderRefresh} refreshing={isHeaderRefreshing} onEndReached={beginFooterRefresh} onEndReachedThreshold={0.1} // 这里取值0.1,可以根据实际情况调整,取值尽量小 /> )}首先,在 FlatList 中添加了两个属性,onEndReached 对应 beginFooterRefresh 函数,表示触底时要进行的操作,onEndReachedThreshold 为阀值,这里我们设置的 0.1。
beginFooterRefresh 函数对应内容如下:
// 上拉加载function beginFooterRefresh() { setIsFooterLoad(true) if (currentPage < totalPage) { currentPage++ const newMovie = queryMovies(currentPage, 10) const data = [...movieList] data.push(...newMovie) setTimeout(() => { setMovieList(data) setIsFooterLoad(false) }, 1000) }}在 onEndReached 对应的 beginFooterRefresh 函数中,我们首先设置 isFooterLoad 值为 true,这样就能显示下拉加载的等待画面,对应的代码如下:
function renderFooterLoad() { if (isFooterLoad) { return ( <View style={styles.footerStyle}> <ActivityIndicator animating={true} size="small" /> <Text style={{ color: '#666', paddingLeft: 10 }}>努力加载中</Text> </View> ) }}
return ( <SafeAreaView style={styles.flex}> {/* 标题区域 */} {renderTitle()} {/* 加载条 */} {renderLoad()} {/* 列表区域 */} {renderList()} {/* 根据 isFooterLoad 的值决定是否渲染下拉加载的等待画面 */} {renderFooterLoad()} </SafeAreaView>)之后仍然是在 setTimeout 中调用 queryMovies 函数来模拟异步请求,拿到数据后拼接到原来的 movieList 后面,并且关闭下拉加载的等待画面。
至此,一个模拟的上拉加载效果就做完了,每次上拉的时候都会加载 10 条新的电影数据在后面。
SectionList#
和 FlatList 一样,SectionList 组件也是由 VirtualizedList 组件扩展来的。不同于 FlatList 组件,SectionList 组件主要用于开发列表分组、吸顶悬浮等功能。
SectionList 组件的使用方法也非常简单,只需要提供 renderItem、renderSectionHeader 和 sections 等必要的属性即可。
<SectionList renderItem={({item})=> <ListItem title={item.title}/>} renderSectionHeader={({section})=><Header title={section.key}/>} sections={[ {data:[...],title:...}, {data:[...],title:...}, {data:[...],title:...}, ]}/>常用的属性如下:
- keyExtractor:和 FlatList 组件一样,表示项目的唯一标识
- renderSectionHeader:用来渲染每个 section 的头部视图
- renderItem:用来渲染每一个 section 中的每一个列表项视图
- sections:用来渲染视图的数据源,类似于 FlatList 中的 data 属性
- stickySectionHeadersEnabled:当 section 把它前一个 section 的可视区推离屏幕时,这个 section 的 header 是否粘连在屏幕顶端
有关 SectionList 组件更多的属性,可以参阅官方文档:https://reactnative.dev/docs/sectionlist
下面我们来看一个 SectionList 组件的具体示例:
import React, { useState, useEffect } from 'react'import { View, Dimensions, Text, ActivityIndicator, StyleSheet, SafeAreaView, SectionList,} from 'react-native'import { queryMovies } from './data/Service'import MovieItemCell from './view/MovieItemCell'
export const width = Dimensions.get('window').widthexport const height = Dimensions.get('window').height
export default function App() { // 初始化电影数据 const displayingMovies = queryMovies(1, 10) const incomingMovies = queryMovies(2, 10)
// 初始化电影列表和加载状态 const [sectionData, setSectionData] = useState([]) const [loaded, setLoaded] = useState(false)
useEffect(() => { setTimeout(() => { setSectionData([ { title: '正在上映', data: displayingMovies }, { title: '即将上映', data: incomingMovies }, ]) setLoaded(true) }, 1000) }, [])
// 渲染标题 function renderTitle() { return ( <View style={styles.barStyle}> <Text style={styles.txtStyle}>电影列表</Text> </View> ) }
// 渲染加载条 function renderLoad() { if (!loaded) { return ( <View style={styles.container}> <ActivityIndicator animating={true} size="small" /> <Text style={{ color: '#666666', paddingLeft: 10 }}>努力加载中</Text> </View> ) } }
function renderItem({ item }) { return ( <MovieItemCell movie={item} onPress={() => { alert('点击电影:' + item.title) }} /> ) }
function renderSectionHeader({ section }) { return ( <View style={styles.sectionHeader}> <Text style={styles.sectionTitle}>{section.title}</Text> </View> ) }
// 渲染电影列表 function renderList() { return ( <SectionList keyExtractor={(item) => item.id} renderSectionHeader={renderSectionHeader} renderItem={renderItem} sections={sectionData} stickySectionHeadersEnabled={true} /> ) }
return ( <SafeAreaView style={styles.flex}> {/* 标题区域 */} {renderTitle()} {/* 加载条 */} {renderLoad()} {/* 列表区域 */} {renderList()} </SafeAreaView> )}
const styles = StyleSheet.create({ flex: { flex: 1, backgroundColor: '#fff', }, container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', flexDirection: 'row', }, loadingView: { flex: 1, height: height, backgroundColor: '#F5FCFF', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', padding: 10, }, barStyle: { height: 48, width: width, justifyContent: 'center', backgroundColor: '#fff', }, txtStyle: { color: '#000', textAlign: 'center', fontSize: 18, }, sectionHeader: { padding: 10, backgroundColor: '#268dcd', }, sectionTitle: { fontSize: 16, fontWeight: 'bold', color: '#fff', },})