Skip to content

列表组件

本小节我们来看一下 RN 中所提供的列表组件,主要包含:

FlatList#

FlatList 组件出现之前,RN 使用 ListView 组件来实现列表功能,不过在列表数据比较多的情况下,ListView 组件的性能并不是很好,所以在 0.43.0 版本中,RN 引入了 FlatList 组件。相比 ListView 组件,FlatList 组件适用于加载长列表数据,而且性能也更佳。

ListView 组件类似,FlatList 组件的使用也非常的简单,只需要给 FlatList 组件提供 datarenderItem 两个属性即可,如下所示:

<FlatList
data={[{key:"a"},{key:"b"}]}
renderItem={({item})=><Text>{item.key}</Text>}
>

其中 data 表示数据源,一般为数组格式,renderItem 表示每行的绘制方法。除了 datarenderItem 两个必须属性外,FlatList 还支持诸如 ListHeaderComponentListFooterComponent 等属性,具体可以参阅官方文档: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 组件还有如下一些使用频率比较高的属性和方法:

<FlatList
...
keyExtractor={(item) => item.id}
/>
<View style={{ borderTopWidth: 0, borderBottomWidth: 1 }}>...</View>

需要注意的是,使用 borderBottom 实现分割线时,列表顶部和底部的组件是不需要绘制的。

当然,更简单的方式是使用 ItemSeparatorComponent 属性,具体使用方式可以参阅官方文档:https://reactnative.dev/docs/flatlist

下拉刷新#

下拉刷新是一个常见的需求,当用户已经处于列表的最顶端,此时继续往下拉动页面的话,就会有一个数据刷新的操作。

FlatList 中,提供了下拉刷新的功能,我们只需要设置 onRefreshrefreshing 这两个属性值即可。

下面来看一个具体的示例。代码片段如下:

// 渲染电影列表
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 端的分页效果。因为数据量过多,所以一般我们不会一次性加载所有的数据,此时就会进行一个分页的显示。而在移动端,分页显示变成了上拉加载的形式,当用户到达列表底部时,自动获取下一页的数据,并且拼接到原有数据的后面。

这里我们会用到两个属性,分别是:

下面来看一个具体的示例。代码片段如下:

// 渲染电影列表
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 组件的使用方法也非常简单,只需要提供 renderItemrenderSectionHeadersections 等必要的属性即可。

<SectionList
renderItem={({item})=> <ListItem title={item.title}/>}
renderSectionHeader={({section})=><Header title={section.key}/>}
sections={[
{data:[...],title:...},
{data:[...],title:...},
{data:[...],title:...},
]}
/>

常用的属性如下:

有关 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').width
export 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',
},
})