在移动端开发中列表页是非常常见的页面, 在 React Native 中我们一般使用 FlatList 或 SectionList 组件实现这些列表视图通常列表页都会有大量的数据需要加载显示, 这时候就用到了分页加载, 因此对于列表组件来说, 实现下拉刷新和上拉加载在很多情况下是必不可少的
本篇文章基于 FlatList 封装一个支持下拉刷新和上拉加载的 RefreshListView, 对原始的 FlatList 进行封装之后, 再调用上拉和下拉刷新就十分方便了
下拉刷新的实现十分简单, 这里我们沿用 FlatList 本身的属性来实现
onRefresh
设置此选项后, 则会在列表头部添加一个标准的 RefreshControl 控件, 以便实现下拉刷新的功能同时你需要正确设置 refreshing 属性
refreshing
bool 值, 用来控制刷新控件的显示与隐藏刷新完成后设为 false
通过这两个属性设置我们就可以实现 FlatList 头部的刷新操作, 控件使用默认的样式, Android 和 iOS 沿用各自系统的组件来显示
重点在于上拉加载更多, React Native 的列表组件中没有这个功能, 需要我们自己实现 对于上拉加载, 通常我们有几种状态, 这里我创建一个 RefreshState.js 文件存放上拉加载的状态:
- export default {
- Idle: 'Idle', // 初始状态, 无刷新的情况
- CanLoadMore: 'CanLoadMore', // 可以加载更多, 表示列表还有数据可以继续加载
- Refreshing: 'Refreshing', // 正在刷新中
- NoMoreData: 'NoMoreData', // 没有更多数据了
- Failure: 'Failure' // 刷新失败
- }
然后根据这几种状态来封装一个 RefreshFooter 组件, 使其根据不同状态显示不同内容, 废话不多说上代码:
- import React, {Component} from 'react';
- import {View, Text, ActivityIndicator, StyleSheet, TouchableOpacity} from 'react-native';
- import RefreshState from './RefreshState';
- import PropTypes from 'prop-types';
- export default class RefreshFooter extends Component {
- static propTypes = {
- onLoadMore: PropTypes.func, // 加载更多数据的方法
- onRetryLoading: PropTypes.func, // 重新加载的方法
- };
- static defaultProps = {
- footerRefreshingText: "努力加载中",
- footerLoadMoreText: "上拉加载更多",
- footerFailureText: "点击重新加载",
- footerNoMoreDataText: "已全部加载完毕"
- };
- render() {
- let {state} = this.props;
- let footer = null;
- switch (state) {
- case RefreshState.Idle:
- // Idle 情况下为 null, 不显示尾部组件
- break;
- case RefreshState.Refreshing:
- // 显示一个 loading 视图
- footer =
- <View style={styles.loadingView}>
- <ActivityIndicator size="small"/>
- <Text style={styles.refreshingText}>{this.props.footerRefreshingText}</Text>
- </View>;
- break;
- case RefreshState.CanLoadMore:
- // 显示上拉加载更多的文字
- footer =
- <View style={styles.loadingView}>
- <Text style={styles.footerText}>{this.props.footerLoadMoreText}</Text>
- </View>;
- break;
- case RefreshState.NoMoreData:
- // 显示没有更多数据的文字, 内容可以自己修改
- footer =
- <View style={styles.loadingView}>
- <Text style={styles.footerText}>{this.props.footerNoMoreDataText}</Text>
- </View>;
- break;
- case RefreshState.Failure:
- // 加载失败的情况使用 TouchableOpacity 做一个可点击的组件, 外部调用 onRetryLoading 重新加载数据
- footer =
- <TouchableOpacity style={styles.loadingView} onPress={()=>{
- this.props.onRetryLoading && this.props.onRetryLoading();
- }}>
- <Text style={styles.footerText}>{this.props.footerFailureText}</Text>
- </TouchableOpacity>;
- break;
- }
- return footer;
- }
- }
- const styles = StyleSheet.create({
- loadingView: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- padding: 15,
- },
- refreshingText: {
- fontSize: 12,
- color: "#666666",
- paddingLeft: 10,
- },
- footerText: {
- fontSize: 12,
- color: "#666666"
- }
- });
注意, propTypes 是我们给 RefreshFooter 组件定义的给外部调用的方法, 方法类型需要使用 PropTypes 来指定, 需要安装 facebook 的 prop-types 依赖库, 最好使用
yarn add prop-types
安装, 不容易出错这里用作运行时的类型检查, 可以点击这里详细了解
defaultProps 中我们定义了几种不同状态下默认的文本内容, 可以在外部传值进行修改
接下来就要来实现这个 RefreshListView 了首先应该明确的是, 这个 RefreshListView 要有头部刷新和尾部刷新的调用方法, 具体调用数据的方法应该在外部实现先跟 RefreshFooter 一样定义两个方法:
- static propTypes = {
- onHeaderRefresh: PropTypes.func, // 下拉刷新的方法, 供外部调用
- onFooterRefresh: PropTypes.func, // 上拉加载的方法, 供外部调用
- };
上面说到头部的下拉刷新使用 FlatList 自带特性实现, 我们需要定义一个 bool 值 isHeaderRefreshing 来作为 refreshing 属性的值, 控制头部显示与否同时定义一个 isFooterRefreshing 来判断尾部组件的刷新状态定义 footerState 用来设定当前尾部组件的 state, 作为 RefreshFooter 的值
- constructor(props) {
- super(props);
- this.state = {
- isHeaderRefreshing: false, // 头部是否正在刷新
- isFooterRefreshing: false, // 尾部是否正在刷新
- footerState: RefreshState.Idle, // 尾部当前的状态, 默认为 Idle, 不显示控件
- }
- }
render 函数如下:
- render() {
- return (
- <FlatList
- {...this.props}
- onRefresh={()=>{ this.beginHeaderRefresh() }}
- refreshing={this.state.isHeaderRefreshing}
- onEndReached={() => { this.beginFooterRefresh() }}
- onEndReachedThreshold={0.1} // 这里取值 0.1(0~1 之间不包括 0 和 1), 可以根据实际情况调整, 取值尽量小
- ListFooterComponent={this._renderFooter}
- />
- )
- }
- _renderFooter = () => {
- return (
- <RefreshFooter
- state={this.state.footerState}
- onRetryLoading={()=>{
- this.beginFooterRefresh()
- }}
- />
- )
- };
可以看到上面的代码中有 beginHeaderRefresh 和 beginFooterRefresh 两个方法, 这两个方法就是用来调用刷新的, 但是在刷新之前还有一些逻辑情况需要判断比如头部和尾部不能够同时刷新, 不然数据处理结果可能受到影响, 正在刷新时要防止重复的刷新操作, 这些都是要考虑的这里我在代码中详细注释了:
- /// 开始下拉刷新
- beginHeaderRefresh() {
- if (this.shouldStartHeaderRefreshing()) {
- this.startHeaderRefreshing();
- }
- }
- /// 开始上拉加载更多
- beginFooterRefresh() {
- if (this.shouldStartFooterRefreshing()) {
- this.startFooterRefreshing();
- }
- }
- /***
- * 当前是否可以进行下拉刷新
- * @returns {boolean}
- *
- * 如果列表尾部正在执行上拉加载, 就返回 false
- * 如果列表头部已经在刷新中了, 就返回 false
- */
- shouldStartHeaderRefreshing() {
- if (this.state.footerState === RefreshState.refreshing ||
- this.state.isHeaderRefreshing ||
- this.state.isFooterRefreshing) {
- return false;
- }
- return true;
- }
- /***
- * 当前是否可以进行上拉加载更多
- * @returns {boolean}
- *
- * 如果底部已经在刷新, 返回 false
- * 如果底部状态是没有更多数据了, 返回 false
- * 如果头部在刷新, 则返回 false
- * 如果列表数据为空, 则返回 false(初始状态下列表是空的, 这时候肯定不需要上拉加载更多, 而应该执行下拉刷新)
- */
- shouldStartFooterRefreshing() {
- if (this.state.footerState === RefreshState.refreshing ||
- this.state.footerState === RefreshState.NoMoreData ||
- this.props.data.length === 0 ||
- this.state.isHeaderRefreshing ||
- this.state.isFooterRefreshing) {
- return false;
- }
- return true;
- }
其中 startHeaderRefreshing 和 startFooterRefreshing 的逻辑如下:
- /// 下拉刷新, 设置完刷新状态后再调用刷新方法, 使页面上可以显示出加载中的 UI, 注意这里 setState 写法
- startHeaderRefreshing() {
- this.setState(
- {
- isHeaderRefreshing: true
- },
- () => {
- this.props.onHeaderRefresh && this.props.onHeaderRefresh();
- }
- );
- }
- /// 上拉加载更多, 将底部刷新状态改为正在刷新, 然后调用刷新方法, 页面上可以显示出加载中的 UI, 注意这里 setState 写法
- startFooterRefreshing() {
- this.setState(
- {
- footerState: RefreshState.Refreshing,
- isFooterRefreshing: true
- },
- () => {
- this.props.onFooterRefresh && this.props.onFooterRefresh();
- }
- );
- }
在刷新之前, 我们需要将头部或尾部的组件显示出来, 然后再调用外部的数据接口方法这里 setState 这样写的好处是 state 中的值更新完成后才会调用箭头函数中的方法, 是有严格顺序的, 如果把
this.props.onFooterRefresh && this.props.onFooterRefresh()
写在 setState 外部, 在 UI 上我们可能看不到头部的 loading 或者尾部的努力加载中, 接口方法就已经调用完毕了
最后, 在刷新结束后我们还需要调用停止刷新的方法, 使头部或尾部组件不再显示, 否则一直是加载中还可能让人以为是 bug 下面看看停止刷新的方法:
- /**
- * 根据尾部组件状态来停止刷新
- * @param footerState
- *
- * 如果刷新完成, 当前列表数据源是空的, 就不显示尾部组件了
- * 这里这样做是因为通常列表无数据时, 我们会显示一个空白页, 如果再显示尾部组件如 "没有更多数据了" 就显得很多余
- */
- endRefreshing(footerState: RefreshState) {
- let footerRefreshState = footerState;
- if (this.props.data.length === 0) {
- footerRefreshState = RefreshState.Idle;
- }
- this.setState({
- footerState: footerRefreshState,
- isHeaderRefreshing: false,
- isFooterRefreshing: false
- })
- }
这里传入一个尾部组件状态的参数是为了更新尾部组件的样式同时对数据源 data 进行一个判断, 如果为空说明当前没有数据, 可以显示空白页面, 那么尾部组件也没必要显示了
以下是我使用 RefreshListView 实现的豆瓣电影页面分页加载的效果图:
来源: https://juejin.im/post/5a94d26d6fb9a0636263fd16