React Native——自定義下拉重新整理上拉載入的列表

不變旋律發表於2018-02-27

在移動端開發中列表頁是非常常見的頁面,在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實現的豆瓣電影頁面分頁載入的效果圖:

React Native——自定義下拉重新整理上拉載入的列表

完整的Demo地址:github.com/mrarronz/re…,感謝閱讀!

相關文章