React Native 列表的總結


React Native 列表的總結

FlatListSectionList都是React Native中高性能的列表組件。這些新的列表組件在性能方面都有了極大的提升, 其中最主要的一個是無論列表有多少行,它的內存使用都是常數級的。他們有着共同的特點:

  • 完全跨平台。
  • 行組件顯示或隱藏時可配置回調事件。
  • 支持單獨的頭部組件。
  • 支持單獨的尾部組件。
  • 支持自定義行間分隔線。
  • 支持下拉刷新。
  • 支持上拉加載。

實質兩者都是基於VirtualizedList組件的封裝,因此需要注意:

  • 當某行滑出渲染區域之外后,其內部狀態將不會保留。請確保你在行組件以外的地方保留了數據。
  • 為了優化內存占用同時保持滑動的流暢,列表內容會在屏幕外異步繪制。這意味着如果用戶滑動的速度超過渲染的速度,則會先看到空白的內容。這是為了優化不得不作出的妥協,而官方也在設法持續改進。
  • 本組件繼承自PureComponent而非通常的Component,這意味着如果其props在淺比較中是相等的,則不會重新渲染。所以請先檢查你的renderItem函數所依賴的props數據(包括data屬性以及可能用到的父組件的state),如果是一個引用類型(Object或者數組都是引用類型),則需要先修改其引用地址(比如先復制到一個新的Object或者數組中),然后再修改其值,否則界面很可能不會刷新。
  • 默認情況下每行都需要提供一個不重復的key屬性。你也可以提供一個keyExtractor函數來生成key。

當然,他們也有着一些不同的特性,下面來主要說明一下。

FlatList

FlatList是一個高性能的簡單列表組件,用於顯示一個垂直的滾動列表,其中的元素之間結構近似而僅數據不同。

除了上述的特性外,FlatList還有:

  • 支持水平布局模式。
  • 支持跳轉到指定行(ScrollToIndex)

FlatList更適於長列表數據,且元素個數可以增刪。和ScrollView不同的是,FlatList並不立即渲染所有元素,而是優先渲染屏幕上可見的元素。

FlatList組件必須的兩個屬性是datarenderItemdata是列表的數據源,而renderItem則從數據源中逐個解析數據,然后返回一個設定好格式的組件來渲染。

簡單的例子:


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

FlatList主要屬性

屬性 說明
data 為了簡化起見,data屬性目前只支持普通數組。如果需要使用其他特殊數據結構,例如immutable數組,請直接使用更底層的VirtualizedList組件。
getItemLayout getItemLayout是一個可選的優化,用於避免動態測量內容尺寸的開銷,不過前提是你可以提前知道內容的高度。如果你的行高是固定的,getItemLayout用起來就既高效又簡單,類似下面這樣:getItemLayout={(data, index) => ( {length: 行高, offset: 行高 * index, index} )}
keyExtractor 此函數用於為給定的item生成一個不重復的key。Key的作用是使React能夠區分同類元素的不同個體,以便在刷新時能夠確定其變化的位置,減少重新渲染的開銷。若不指定此函數,則默認抽取item.key作為key值。若item.key也不存在,則使用數組下標。
renderItem 根據行數據data渲染每一行的組件。

FlatList主要的方法

方法 說明 代碼
scrollTo() 滾動到指定的x, y偏移處。第三個參數為是否啟用平滑滾動動畫。 scrollTo(([y]: number), object, ([x]: number), ([animated]: boolean));
scrollToEnd() 滾動到視圖底部(水平方向的視圖則滾動到最右邊)。加上動畫參數scrollToEnd({animated: true})則啟用平滑滾動動畫,或是調用scrollToEnd({animated: false})來立即跳轉。如果不使用參數,則animated選項默認啟用。 scrollToEnd(([options]: object));
flashScrollIndicators() 短暫地顯示滾動指示器。 flashScrollIndicators();

實例:電影列表

以下是獲取豆瓣電影數據並展示成列表的實例。


import React, {Component} from "react";

import {ActivityIndicator, FlatList, Image, StyleSheet, Text, View} from "react-native";

const REQUEST_URL =
    "http://api.douban.com/v2/movie/top250?count=50";

export class SampleAppMovies extends Component {

    static navigationOptions = {
        title: '電影列表頁      ',
        headerStyle: {
            backgroundColor: '#8bc9ff',
        }
    };

    constructor(props) {
        super(props);
        this.state = {
            data: [],
            loaded: false
        };
        // 在ES6中,如果在自定義的函數里使用了this關鍵字,則需要對其進行“綁定”操作,否則this的指向會變為空
        // 像下面這行代碼一樣,在constructor中使用bind是其中一種做法(還有一些其他做法,如使用箭頭函數等)
        this.fetchData = this.fetchData.bind(this);
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData() {
        fetch(REQUEST_URL)
            .then(response => response.json())
            .then(responseData => {
                this.setState({
                    data: this.state.data.concat(responseData.subjects),
                    loaded: true
                });
            });
    }

    static renderLoadingView() {
        return (
            <View style={styles.container}>
                <ActivityIndicator size="large" color="#8bc9ff"/>
            </View>
        );
    }

    static renderMovie({item}) {
        // { item }是一種“解構”寫法,請閱讀ES2015語法的相關文檔
        // item也是FlatList中固定的參數名,請閱讀FlatList的相關文檔
        return (
            <View style={styles.container}>
                <Image
                    source={{uri: item.images.medium}}
                    style={styles.thumbnail}/>
                <View style={styles.rightContainer}>
                    <Text style={styles.title}>{item.title}</Text>
                    <Text style={styles.year}>{item.year}</Text>
                    <Text style={styles.introduce}>{"評分:"}
                        <Text
                            style={styles.ratingNum}>{item.rating.average === 0 ? "暫無評價" : item.rating.average}
                        </Text>
                    </Text>
                    <Text numberOfLines={1} style={styles.introduce}>{"導演:"}
                        <Text style={styles.info}>{item.directors[0].name}</Text>
                    </Text>
                    <Text numberOfLines={1} style={styles.introduce}>{"演員:"}
                        <Text style={styles.info}>{item.casts[0].name + " "}</Text>
                        <Text style={styles.info}>{item.casts[1].name + " "}</Text>
                        <Text style={styles.info}>{item.casts[2].name}</Text>
                    </Text>
                </View>
            </View>
        );
    }

    render() {
        if (!this.state.loaded) {
            return SampleAppMovies.renderLoadingView();
        }

        return (
            <FlatList
                data={this.state.data}
                ItemSeparatorComponent={ItemDivideComponent}
                renderItem={SampleAppMovies.renderMovie}
                style={styles.list}
                keyExtractor={(item, index) => item.id}
            />
        );
    }
}

class ItemDivideComponent extends Component {
    render() {
        return (
            <View style={{height: 0.5, backgroundColor: 'gray'}}/>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: "row",
        justifyContent: "center",
        backgroundColor: "#F5FCFF",
        padding: 5
    },
    rightContainer: {
        flex: 1,
        marginLeft: 10,
        flexDirection: 'column',
    },
    title: {
        color: '#000',
        fontWeight: 'bold',
        fontSize: 20,
        marginBottom: 9,
        justifyContent: 'flex-start',
    },
    year: {
        fontSize: 15,
        marginBottom: 5,
    },
    introduce: {
        flex: 1,
        fontSize: 14,
        color: '#000',
        marginBottom: 5,
    },
    ratingNum: {
        flex: 1,
        fontSize: 20,
        fontWeight: 'bold',
        color: '#ffad24'
    },
    info: {
        fontSize: 16,
        color: '#000',
        marginRight: 3,
        marginBottom: 5,
    },
    thumbnail: {
        width: 90,
        height: 145
    },
    list: {
        backgroundColor: "#FFF"
    }
});

電影列表

SectionList

如果要渲染的是一組需要分組的數據,也許還帶有分組標簽的,那么SectionList將是個不錯的選擇。

SectionList高性能的分組(section)列表組件。除了最一開始說明的共性外,它還有:

  • 支持分組的頭部組件。
  • 支持分組的分隔線。
  • 支持多種數據源結構。

簡單的例子:


<SectionList
  renderItem={({item}) => <ListItem title={item.title} />}
  renderSectionHeader={({section}) => <H1 title={section.key} />}
  sections={[ // homogeneous rendering between sections
    {data: [...], key: ...},
    {data: [...], key: ...},
    {data: [...], key: ...},
  ]}
/>

<SectionList
  sections={[ // heterogeneous rendering between sections
    {data: [...], key: ..., renderItem: ...},
    {data: [...], key: ..., renderItem: ...},
    {data: [...], key: ..., renderItem: ...},
  ]}
/>

SectionList主要屬性

屬性 說明
sections 用來渲染的數據,類似於FlatList中的data屬性。
renderItem 用來渲染每一個section中的每一個列表項的默認渲染器。可以在section級別上進行覆蓋重寫。必須返回一個react組件。
keyExtractor 此函數用於為給定的item生成一個不重復的key。Key的作用是使React能夠區分同類元素的不同個體,以便在刷新時能夠確定其變化的位置,減少重新渲染的開銷。若不指定此函數,則默認抽取item.key作為key值。若item.key也不存在,則使用數組下標。注意這只設置了每行(item)的key,對於每個組(section)仍然需要另外設置key。

SectionList主要的方法

方法 說明 代碼
scrollToLocation() 將可視區內位於特定sectionIndex 或 itemIndex (section內)位置的列表項,滾動到可視區的制定位置。 scrollToLocation(params);parms在下面說明
recordInteraction() 主動通知列表發生了一個事件,以使列表重新計算可視區域。比如說當waitForInteractions 為 true 並且用戶沒有滾動列表時,就可以調用這個方法。不過一般來說,當用戶點擊了一個列表項,或發生了一個導航動作時,我們就可以調用這個方法。 recordInteraction();
flashScrollIndicators() 短暫地顯示滾動指示器。 flashScrollIndicators();

Valid params keys are:

  • animated (boolean) - Whether the list should do an animation while scrolling. Defaults to true.
  • itemIndex (number) - Index within section for the item to scroll to. Required.
  • sectionIndex (number) - Index for section that contains the item to scroll to. Required.
  • viewOffset (number) - 一個以像素為單位,到最終位置偏移距離的固定值,比如為了彌補粘接的header所占據的空間。
  • viewPosition (number) - A value of 0 places the item specified by index at the top, 1 at the bottom, and 0.5 centered in the middle.

對於scrollToLocation(params);還有需要注意的地方: 如果沒有設置getItemLayout或是onScrollToIndexFailed,就不能滾動到位於外部渲染區的位置。

SectionList注意點

對於計算滑動到那個點,可以使用scrollToLocation


this.sectionList.scrollToLocation({
  sectionIndex: 2,
  itemIndex: 2,
viewOffset: 30,
})
....
<SectionList
    ref={ref => this.sectionList = ref}
/>

但是如果要調用scrollToLocation的時候很可能頁面還沒渲染好,RN並不知道需要滾動到哪個位置,這個時候需要配合getItemLayout來使用。如果在sectionList中使用了該屬性,RN會調用此方法計算列表中各項的顯示位置,從而提前得知怎么處理滾動。


getItemLayout={(data, index) => ({
    index,
    offset: OFFSET_FROM_TOP,
    length: ITEM_HEIGHT.
    })
  }

不過對於SectionList計算滾動到那個點的位置是比較困難的,要計算section頭部高度,也要計算item的高度,同時如果存在下畫線也需要考慮在內,這就如果滑動時可能會出現偏移。

在這里可以使用庫rn-section-list-get-item-layout來幫助我們解決問題:


...
constructor(props) {
    super(props)

    this.getItemLayout = sectionListGetItemLayout({
      // The height of the row with rowData at the given sectionIndex and rowIndex
      getItemHeight: (rowData, sectionIndex, rowIndex) => sectionIndex === 0 ? 100 : 50,

      // These four properties are optional
      getSeparatorHeight: () => 1 / PixelRatio.get(), // The height of your separators
      getSectionHeaderHeight: () => 20, // The height of your section headers
      getSectionFooterHeight: () => 10, // The height of your section footers
      listHeaderHeight: 40, // The height of your list header
    })
  }
...


實例:城市選擇列表


import React, {Component} from 'react';
import {SectionList, StyleSheet, Text, ToastAndroid, TouchableOpacity, View} from 'react-native';
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout'
import _ from 'lodash';
import cityData from '../json/city.json'

const ITEM_HEIGHT = 45;

//城市字母
const letters = _
    .range('A'.charCodeAt(0), 'Z'.charCodeAt(0) + 1)
    .map(n => String.fromCharCode(n).substr(0));

_.pull(letters, 'O', 'V');

//城市的數組
let city = [];

export class CitySelectList extends Component {

    static navigationOptions = {
        title: '列表頁      ',
        headerStyle: {
            backgroundColor: '#8bc9ff',
        }
    };

    constructor(props) {
        super(props);

        this.getItemLayout = sectionListGetItemLayout({
            getItemHeight: (rowData, sectionIndex, rowIndex) => ITEM_HEIGHT,
            getSeparatorHeight: () => 0,
            getSectionHeaderHeight: () => ITEM_HEIGHT,
            getSectionFooterHeight: () => 0,
            listHeaderHeight: 0
        })
    }

    componentWillMount() {
        //把城市放到對應的字母中
        for (let j = 0; j < letters.length; j++) {

            let each = [];

            for (let i = 0; i < cityData.CITIES.length; i++) {
                if (letters[j] === cityData.CITIES[i].name_en.substr(0, 1)) {
                    each.push(cityData.CITIES[i].name);
                }
            }

            let _city = {};
            _city.key = letters[j];
            _city.data = each;

            city.push(_city)
        }

        //同步城市信息
        this.setState({
            data: city
        })
    }

    //滑動到
    scrollTo(index) {
        this.sectionListRef.scrollToLocation({
            animated: true,
            sectionIndex: index,
            itemIndex: 0,
            viewPosition: 0,
            viewOffset: ITEM_HEIGHT
        });
    }

    //右側城市首字母列表
    renderLetters(letter, index) {
        return (
            <TouchableOpacity key={index} activeOpacity={0.6} onPress={() => {
                this.scrollTo(index)
            }}>
                <Text style={styles.letterText}>{letter}</Text>
            </TouchableOpacity>
        )
    }

    //城市Item
    renderCityItem(item) {
        return (
            <TouchableOpacity activeOpacity={0.6} onPress={() => {
                ToastAndroid.show(item, ToastAndroid.SHORT)
            }}>
                <Text style={styles.item}>{item}</Text>
            </TouchableOpacity>
        )
    }

    render() {
        return (
            <View style={styles.container}>
                <SectionList
                    sections={this.state.data}
                    initialNumToRender={383}
                    ref={ref => (this.sectionListRef = ref)}
                    renderItem={({item}) => this.renderCityItem(item)}
                    renderSectionHeader={({section}) => <Text style={styles.sectionHeader}>{section.key}</Text>}
                    keyExtractor={(item, index) => index}
                    refreshing={false}
                    getItemLayout={this.getItemLayout}
                    stickySectionHeadersEnabled={true}
                    showsVerticalScrollIndicator={false}
                />
                <View style={styles.letters}>
                    {letters.map((letter, index) => this.renderLetters(letter, index))}
                </View>
            </View>
        )
            ;
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    sectionHeader: {
        paddingTop: 5,
        paddingLeft: 10,
        paddingRight: 10,
        paddingBottom: 5,
        fontSize: 18,
        fontWeight: 'bold',
        color: '#000',
        height: ITEM_HEIGHT,
        backgroundColor: '#8bc9ff',
    },
    item: {
        padding: 10,
        fontSize: 18,
        height: ITEM_HEIGHT,
    }, letters: {
        position: 'absolute',
        top: 0,
        bottom: 0,
        right: 10,
        backgroundColor: 'transparent',
        justifyContent: 'center',
        alignItems: 'center',
    }, letterText: {
        padding: 2,
        fontSize: 13,
    }
});

城市列表

來源:https://blog.csdn.net/kimi985566/article/details/85088889


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM