ReactNative——使用FlatList实现豆瓣电影列表

5,177 阅读8分钟

在上篇文章中我们了解了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相关内容。