在上篇文章中我们了解了ListView的使用方法,并且实现了三种不同样式的列表视图。ListView虽然使用广泛,但是它也有许多缺点,如:不支持单独的头部和尾部组件,当数据量过大时,占用内存明显增加,性能受到影响,出现丢帧的情况。所以随着React Native版本的迭代更新,ListView被FlatList和SectionList取代。FlatList用于无分组的列表,而SectionList用于分组列表的实现。FlatList和SectionList有以下优点:
* 完全跨平台。
* 支持水平布局模式。
* 行组件显示或隐藏时可配置回调事件。
* 支持单独的头部组件。
* 支持单独的尾部组件。
* 支持自定义行间分隔线。
* 支持下拉刷新。
* 支持上拉加载。
* 支持跳转到指定行(ScrollToIndex)。
本篇我们学习使用FlatList来一步步实现一个类似豆瓣电影的列表视图。首先还是要了解一下FlatList的相关属性,可以参考官网文档,讲解非常详细,这里只说明下关键的几个属性。
data——数据源数组,这里不同于ListView的dataSource,可以直接设置一个数组给data属性作为FlatList的数据源,API更加简单方便。
keyExtractor——函数,用来给列表中每个item生成一个不重复的key,Key的作用是使React能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标index。这个属性是ListView不具备的,它在FlatList和SectionList中用作每个item的唯一标识。
renderItem——根据列表中每行数据渲染每一行的组件。类似于ListView中的renderRow,区别在于FlatList需要指定keyExtractor,而renderRow是不需要这个key的。
通过以上三个属性我们就能实现一个列表视图了,但还有以下属性是非常重要的:
numColumns——非水平模式下设置列数可以实现item的网格布局,这点比起ListView就方便多了,不需要再设置flexWrap了。
columnWrapperStyle——如果设置了多列布局(即将numColumns值设为大于1的整数),则可以额外指定此样式作用在每行容器上。比如我们有一个4行3列的网格列表,每行就有3个item,这3个item处于一行显示,我们可以认为这3个item被一个容器包裹起来,columnWrapperStyle属性就是给这个容器设置样式,用来调整这3个item在容器中的显示位置,使UI更加美观。
onEndReachedThreshold——决定当距离内容最底部还有多远时触发onEndReached回调。注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。值在0~1之间,不包括0和1。这点要与ListView区分清楚。
OK,以上就是要提到的一些比较重要的属性,其它属性请自行参考官方文档。下面我们来看看怎么使用FlatList实现如下图所示的电影列表。

要实现电影列表,首先我们得有数据,这里我们使用豆瓣电影开放的接口获得电影数据。
首先,在项目根目录中创建src目录,在src目录中依次创建common、screen和widgets目录,分别用来存放通用类、UI界面和自定义UI组件,在common中 创建一个Service.js用来存放接口地址:
/// 查询正在上映的电影
export function queryMovies(city, start, count) {
return "https://api.douban.com/v2/movie/in_theaters?city=" + city + "&start=" + start + "&count=" + count
}
/// 查询即将上映的电影
export function comingMovies(city, start, count) {
return "https://api.douban.com/v2/movie/coming_soon?city=" + city + "&start=" + start + "&count=" + count
}
其中city是城市,为了方便我们直接写死为北京,start参数表示从第几条数据开始(初始为从0开始),count表示每次加载多少条数据。
定义一个Color.js文件用来存放app中需要用到的色值
export default {
themeColor: '#268dcd', // 主题颜色
separatorColor: '#e0e0e0', // 分割线颜色
backgroundColor: '#f3f3f3' // 背景色
}
要实现这样一个底部tabBar切换的效果,我们选用react-navigation来完成。找到项目根目录,在终端中输入npm install --save react-navigation
命令安装依赖库。如果不成功可以使用yarn add react-navigation
命令。
安装完成后我们先来创建相关界面,在screen目录中创建RootScreen、MovieListScreen。其中的内容我们可以先简单写成如下代码:
import React, {Component} from 'react';
import {View} from 'react-native';
export default class MovieListScreen extends Component {
render() {
return (
<View/>
)
}
}
按照效果图所示,我们有正在热映和即将上映两个界面,两个接口地址分别对应两个界面的数据,在分析了两个接口返回的数据之后发现,两个界面的数据结构是一致的,所以这里我们只需要创建一个MovieListScreen页面,两个界面共用一个js文件来实现,只需要对调用的接口做区分就可以了,避免重复代码。
我们已经有了两个电影页面的基本实现,现在需要一个tabBar的容器作为根视图控制这两个列表页。这里就需要使用react-navigation来实现RootScreen。
首先,我们要使用TabNavigator组件创建一个tab容器:
const Tab = TabNavigator(
{
First: {
screen: MovieListScreen,
navigationOptions: ({navigation}) => ({
tabBarLabel: '正在热映',
tabBarIcon: ({focused, tintColor}) => (
<TabBarItemComponent
tintColor={tintColor}
focused={focused}
normalImage={require('../../assets/image/playing.png')}
selectedImage={require('../../assets/image/playing-active.png')}
/>
)
}),
},
Second: {
screen: MovieListScreen,
navigationOptions: ({navigation}) => ({
tabBarLabel: '即将上映',
tabBarIcon: ({focused, tintColor}) => (
<TabBarItemComponent
tintColor={tintColor}
focused={focused}
normalImage={require('../../assets/image/coming.png')}
selectedImage={require('../../assets/image/coming-active.png')}
/>
)
})
}
},
{
tabBarComponent: TabBarBottom,
tabBarPosition: 'bottom',
swipeEnabled: false,
animationEnabled: false,
lazy: true,
tabBarOptions: {
activeTintColor: Color.themeColor,
inactiveTintColor: '#888888',
style: {backgroundColor: '#ffffff'}
}
}
);
可以看到容器中First和Second对应的screen都是MovieListScreen。其中TabBarItemComponent是自定义的tabItem组件,用来显示图标和文字。创建了tab容器之后,我们还需要一个导航栏用来显示标题,同时这个导航栏还起到页面导航的作用,就需要用到react-navigation中的StackNavigator组件:
const Navigator = StackNavigator(
{
Tab: {screen: Tab},
},
{
navigationOptions: {
headerBackTitle: null,
headerTintColor: '#ffffff',
headerStyle: {backgroundColor: Color.themeColor},
showIcon: true
}
}
);
接下来在RootScreen中render函数中,我们需要返回这个Navigator组件,这样大体的页面结构就完成了
render() {
return <Navigator/>
}
现在我们得到的就是一个底部有tabBar,顶部有导航栏的页面结构,如下

在MovieListScreen中我们需要调用接口获得数据再将数据进行适当处理后赋值给FlatList组件。首先在constructor函数中构造state数据
constructor(props) {
super(props);
this.state = {
movieList: [], // 电影列表的数据源
loaded: false, // 用来控制loading视图的显示,当数据加载完成,loading视图不再显示
};
}
使用fetch函数调用接口获取数据,这里加载正在热映的电影数据方法如下:
/**
* 加载正在上映的电影列表,此处默认城市为北京,取20条数据显示
*/
loadDisplayingMovies() {
let that = this;
fetch(queryMovies('北京', 0, 20)).then((response) => response.json()).then((json) => {
console.log(json);
let movies = [];
for (let idx in json.subjects) {
let movieItem = json.subjects[idx];
let directors = ""; // 导演
for (let index in movieItem.directors) {
// 得到每一条电影的数据
let director = movieItem.directors[index];
// 将多个导演的名字用空格分隔开显示
if (directors === "") {
directors = directors + director.name
} else {
directors = directors + " " + director.name
}
}
movieItem["directorNames"] = directors;
// 拼装主演的演员名字,多个名字用空格分隔显示
let actors = "";
for (let index in movieItem.casts) {
let actor = movieItem.casts[index];
if (actors === "") {
actors = actors + actor.name
} else {
actors = actors + " " + actor.name
}
}
movieItem["actorNames"] = actors;
movies.push(movieItem)
}
that.setState({
movieList: movies,
loaded: true
})
}).catch((e) => {
console.log("加载失败");
that.setState({
loaded: true
})
}).done();
}
在componentDidMount中根据当前页面来确定调用哪个接口获取对应的数据
componentDidMount() {
/// 根据routeName来判断当前是哪个界面,react-navigation中可以通过navigation.state.routeName来获取
let routeName = this.props.navigation.state.routeName;
if (routeName === 'First') {
this.loadDisplayingMovies();
} else {
this.loadComingMovies();
}
}
下面就是在render函数中渲染页面了
render() {
if (!this.state.loaded) {
return (
<View style={styles.loadingView}>
<ActivityIndicator animating={true} size="small"/>
<Text style={{color: '#666666', paddingLeft: 10}}>努力加载中</Text>
</View>
)
}
return (
<FlatList
data={this.state.movieList}
renderItem={this._renderItem}
keyExtractor={(item) => item.id}
/>
)
}
在接口请求还未结束前我们先渲染一个loading视图提示用户正在加载中,接口调用完成后将loaded字段设置为true,这样render就直接显示FlatList了,使用电影的id来作为每行的唯一标识实现keyExtractor。renderItem渲染每行视图:
_renderItem = (item) => {
return (
<MovieItemCell movie={item.item} onPress={() => {
console.log('点击了电影----' + item.item.title);
}}/>
)
};
MovieItemCell是自定义的行视图,用来显示每条电影数据。这里就不贴出代码了,其中用到了TouchableHighlight作为背景点击高亮显示的组件,需要注意的是TouchableHighlight不能用作容器像View一样包裹其它控件,而TouchableOpacity可以。
到这里一个简单的电影列表就完成了,具体demo地址点这里,完整版包含详情页的简单app在这里。总的说来,用React Native实现列表页面还是很简单的,下篇我们使用SectionList将这个demo改造成单一列表页并分组显示正在热映和即将上映的电影,持续学习和总结React Native相关内容。