FlatList元件學習和封裝

黎偉傑發表於2018-05-15

FlatList元件學習和封裝

是一個用於呈現簡單的平面列表的高效能元件,支援下面功能:

  • 跨平臺
  • 支援水平
  • 支援可配置的可視性回撥
  • 支援頭部
  • 支援尾部
  • 支援下拉重新整理
  • 支援上來載入
  • 支援跳轉到指定行 他是繼承自ScrollView以及VirtualizedList的,所以具有他們二者的props;

簡單例子

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

        </View>
    }
複製程式碼

其中包含了最重要兩個元件datarenderItem,data是資料list,是一個一個物件組合而成的,renderItem是條目。

FlatList重要方法

1 renderItem 他是一個方法,用於渲染具體的Item,他回撥有幾個引數,定義如下

renderItem(item,index,separators){

}
複製程式碼

需要注意的是我們使用的時候一般需要進行解構賦值去取item的單個內容,下面談到封裝的時候再給例子。

2 data 他是FlastList的資料來源,是一個陣列

3 ListEmptyComponent 需要返回一個Component,當FlatList資料為空的時候的元件

4 ListFooterComponent 需要返回一個Component,FlatList的footer元件

5 ListHeaderComponent 需要返回一個Component,FlatList的頭部元件,需要注意他不是重新整理的那個元件,只是一個不同於普通item的頭部元件

6 horizontal 一個布林值,表明 是垂直的還是水平的FlatList,true是水平,false是垂直。

7 keyExtractor 定義是

(item: object, index: number) => string;
複製程式碼

指定一個function,返回item的index,相當於Android的adapter的getIndex()方法,返回一個獨立Item的唯一標識,需要返回一個index

8 numColumns 是一個number,表明該FlatList是幾列。

9 onEndReached 當FlatLst到達底部的時候的回撥

10 onRefresh FlatList觸發重新整理時候的回撥

11 refreshing 控制是否處於重新整理狀態

12 ItemSeparatorComponent 需要返回一個Component,作為item之間的分割線

13 FlatList提供的一些scroll方法,比如滾動到底部,滾動到具體item,滾動多少距離等。

scrollToEnd //滾動到底部
scrollToIndex //滾動到第幾個item
scrollToItem //滾動到某個item 
scrollToOffset //滾動多少距離
recordInteraction //告訴列表滾動傳送了,會去呼叫viewability的計算。
flashScrollIndicators //隨時顯示滾動指示器
複製程式碼

封裝FlatList

對於FlatList的重新整理樣式,我們能夠定義或者是改造的可行性比較難,他的樣式一般是比較固定,我們,假如需要自定義重新整理的元件,可以參考我給出的推薦學習。

對於FlatList的封裝,整體需要做到開閉原則,我們有幾點需求需要明確:

  1. 提供重新整理+載入
  2. 可分別設定是否重新整理+載入是否可用
  3. 提供樣式外部可定義以及一套預設樣式
  4. 提供多種列表狀態,比如重新整理中,載入中,載入完成,載入失敗,空資料等
  5. 提供一個onRequest(isRefresh:boolean)方法給外部,進行重新整理或者載入處理
  6. 對於一些方法,提供預設配置,但是外部也可以進行修改。
  • 定義FlatList的狀態,預設props,styles以及需要的state
//state
export const State = {
  NORMAL: 0,//正常狀態
  REFRESHING: 1,//重新整理中
  LOADING: 2,//正在載入
  LOAD_END: 3,//上拉載入完成
  ERROR: 4,//上拉載入發生錯誤
  NO_DATA: 5,//無資料情況
};

/**
 * 預設樣式
 */
const styles = StyleSheet.create({
  //底部預設樣式
  footerContainer: {
    flex: 1,
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    padding: 10,
    height: 44,
  },
  footerText: {
    fontSize: 14,
    color: "#555555"
  }
});

//props+state
    static defaultProps = {
    loadingText: "資料載入中...",
    loadErrorText: "點選重新載入...",
    loadEndText: "已載入全部資料",
    loadEmptyText: "暫時沒有相關資料",
    requestState: State.NORMAL,
    footerContainer: {},
    footerText: {},
    data: [],
    id: "flat_list",
    onRequest: (isRefresh) => {
    },
  };
  constructor(props) {
    super(props);
    //是否重新整理/載入可用,外部可以通過呼叫方法設定,預設都可以
    this.state = {
      enableLoadMore: true,
      enableRefresh: true,
    };
  }
複製程式碼
  • 外部設定是否可用方法
  /**
   * 不會顯示頂部可以重新整理的UI
   * @param enableLoad
   */
  setEnableLoad(enableLoad) {
    this.setState({
      enableLoad: enableLoad,
    });
  }
  /**
   * 他是不會顯示底部重新整理的UI的
   * @param enableRefresh
   */
  setEnableRefresh(enableRefresh) {
    this.setState({
      enableRefresh: enableRefresh,
    });
  }
複製程式碼
  • 資料載入方法
  /**
   * 重新整理觸發
   * @private
   */
  _onRefresh = () => {
    if (this._enableRefresh()) {
      this.props.onRequest && this.props.onRequest(true);
    }
  };
  /**
   * 是否可以頂部重新整理
   * 當前需要 非重新整理,而且也不是正在載入
   * @returns {boolean}
   */
  _enableRefresh = () => {
    return !(this.props.requestState === State.REFRESHING || this.props.requestState === State.LOADING);
  };
  _onEndReached = () => {
    if (this._enableLoad()) {
      this.props.onRequest && this.props.onRequest(false);
    }
  };
  /**
   * 是否可以載入,當前資料不能是0(因為進入的時候必須是refresh獲取的資料顯示)
   * @returns {boolean}
   */
  _enableLoad = () => {
    let { requestState, data } = this.props;
    if (data.length === 0) {
      return false;
    }
    return requestState === State.NORMAL;
  };
  /**
   * 重新整理或載入
   * @param isRefresh
   * @private
   */
  _reRequest = (isRefresh) => {
    //回撥外部方法
    this.props.onRequest && this.props.onRequest(isRefresh);
  };
  _keyExtractor = (item, index) => {
    const { keyExtractor } = this.props;
    if (keyExtractor) {
      return keyExtractor(item, index);
    }
    return index.toString();
  };
複製程式碼
  • Item的唯一標識以及預設的分割線
  _keyExtractor = (item, index) => {
    const { keyExtractor } = this.props;
    if (keyExtractor) {
      return keyExtractor(item, index);
    }
    return index.toString();
  };

_separator = () => {
    const { separator } = this.props;
    if (separator) {
      return separator();
    }
    return <View style={{
      height: 1,
      backgroundColor: "#ededed",
      marginLeft: 10,
      marginRight: 10
    }}/>;
  };
複製程式碼
  • Footer,footer對於不同的FlatList狀態會返回不同的Component
/**
   * 渲染底部
   * @returns {*}
   */
  _renderFooter = () => {
    let footer = null;
    let footerContainerStyle = [styles.footerContainer, this.props.footerContainer];
    let footerTextStyle = [styles.footerText, this.props.footerText];
    let { loadingText, loadErrorText, loadEndText, loadEmptyText } = this.props;
    const hasData = this.props.data && this.props.data.length > 0;
    switch (this.props.requestState) {
      case State.NORMAL:
        footer = (<View style={footerContainerStyle}/>);
        break;
      case State.ERROR: {
        //是否有資料
        footer = hasData ? (
          <TouchableOpacity
            activeOpacity={0.8}
            style={footerContainerStyle}
            onPress={this._reRequest}
          >
            <Text style={footerTextStyle}>{loadErrorText}</Text>
          </TouchableOpacity>
        ) : (<EmptyData onPress={this._reRequest} tips={loadErrorText}/>);
        break;
      }
      case State.NO_DATA: {
        footer = <EmptyData onPress={this._reRequest} tips={loadEmptyText}/>;
        break;
      }
      case State.LOADING: {
        footer = (
          <View style={footerContainerStyle}>
            <ActivityIndicator size="small" color="#888888"/>
            <Text style={[footerTextStyle, { marginLeft: 7 }]}>{loadingText}</Text>
          </View>
        );
        break;
      }
      case State.LOAD_END: {
        footer = (
          <View style={footerContainerStyle}>
            <Text style={footerTextStyle}>{loadEndText}</Text>
          </View>
        );
        break;
      }
    }
    return footer;
  };
複製程式碼

其中Empty是空資料的元件,另外獨立定義了他的Component

class EmptyData extends Component {
  static defaultProps = {
    tips: "暫無資料",
  };
  _onPress = () => {
    const { onPress } = this.props;
    if (onPress) {
      onPress(true);
    }
  };
  render() {
    return (
      <TouchableOpacity onPress={this._onPress} activeOpacity={0.8}>
        <View style={{
          flex: 1,
          alignItems: "center",
          justifyContent: "center"
        }}>
          <Image
            style={{
              marginTop: 10,
              marginBottom: 10,
              width: 120,
              height: 120,
              resizeMode: "contain"
            }}
            source={require("../../img/pic_empty_data.png")}
          />
          <Text>{this.props.tips}</Text>
        </View>
      </TouchableOpacity>
    );
  }
}
複製程式碼
  • render方法
/**
   * 渲染
   * @returns {*}
   */
  render() {
    let {
      renderItem = () => {
      }
    } = this.props;
    return (
      <FlatList
        onEndReached={this._onEndReached}
        onRefresh={this.state.enableRefresh ? this._onRefresh : null}
        refreshing={this.state.enableRefresh && (this.props.requestState === State.REFRESHING)}
        ListFooterComponent={this.state.enableLoad ? this._renderFooter : null}
        onEndReachedThreshold={0.1}
        renderItem={renderItem}
        {...this.props}
        ItemSeparatorComponent={this._separator}
        keyExtractor={this._keyExtractor} Ò
      />
    );
  }
}
複製程式碼

這樣子我們就定義了一個完整的PullLoadComponent。

使用

我們用玩Android開發的Api,這裡感謝玩Android提供的Api介面。

const API = "http://www.wanandroid.com/article/list/";
export default class FlatListDemoPage extends Component {
  static navigationOptions = {
    // headerTitle: 'first',
    title: "FlatListDemoPage",
  };
  constructor(props) {
    super(props);
    this.state = {
      requestState: State.NORMAL,
      data: []
    };
    this.currentPage = 0;
  }
  componentDidMount() {
    this._request(true);
  }
  _request = (isRefresh) => {
    this.currentPage = isRefresh ? 0 : this.currentPage + 1;
    this.setState({ requestState: isRefresh ? State.REFRESHING : State.LOADING });
    //發起請求
    fetch(API + this.currentPage + "/json") // 返回一個Promise物件
      .then((res) => {
        return res.json();
      })
      .then((res) => {
        const { data } = res;
        const { datas } = data;
        this._setData(datas);
      })
      .catch(e => {
        //載入失敗
        console.log(e);
        this.setState({ requestState: State.ERROR });
      });
  };
  _setData = (data, isRefresh) => {
    if (data && data.length > 0) {
      //判斷一下是否還有下一頁,因為一頁最多20條,不滿足則是無法去載入更多了
      this.setState({
        requestState: data.length >= 20 ? State.NORMAL : State.LOAD_END,
        data: isRefresh ? data : this.state.data.concat(data)
      });
    } else {
      //已經沒有資料了,需要對footer進行處理
      if (this.currentPage === 0) {
        //第一頁沒有資料,那麼就是當前介面無資料
        this.setState({
          requestState: State.NO_DATA,
          data: []
        });
      } else {
        //不是第一頁,新頁返回空,就是接下來沒有資料了
        this.setState({
          requestState: State.LOAD_END,
          data: this.state.data
        });
      }
    }
  };
  _renderItem = ({ item }) => {
    const { title } = item;
    return <Text style={{
      color: "black",
      textAlign: "center",
    }}>{title}</Text>;
  };
  render() {
    console.log("requestState=" + this.state.requestState);
    return (
      <View>
        <PullLoadComponent
          ref={(flat_list) => {
            this.flat_list = flat_list;
          }}
          onRequest={this._request}
          data={this.state.data}
          requestState={this.state.requestState}
          renderItem={this._renderItem}
        />
      </View>
    );
  }
}
複製程式碼

Demo地址: ReactNativeDemo

推薦學習

相關文章