React Native 實現城市選擇元件

collin發表於2019-10-10

目錄

前言

很多 App 都有城市選擇的功能,今天帶大家編寫一個城市選擇元件。下面是這個元件的效果圖。

功能分析

從上圖中可以看出,我們將所有城市按照字母區分塊,右邊是字母索引。透過點選右邊的字母可以跳轉到相對應的塊,同樣的,在移動左邊的列表的時候,右邊也會跟隨移動來顯示不同的高亮。

這個元件中,我們透過 React Native 提供的 FlatList 來實現。最開始我的實現是透過自己計算高度,在閱讀文件的時候,發現 FlatList 元件提供了幾個很好用的特性:

  • onViewableItemsChanged: 在可見行元素變化時呼叫。可見範圍和變化頻率等引數的配置請設定viewabilityConfig屬性。
  • scrollToIndex: 將位於指定位置的元素滾動到可視區的指定位置,當viewPosition 為 0 時將它滾動到螢幕頂部,為 1 時將它滾動到螢幕底部,為 0.5 時將它滾動到螢幕中央。

思路

城市選擇元件最重要是需要城市資料的來源,可以透過網路獲取,但是由於資料量過大,網路的效能不太理想。我提前準備了一個 json 檔案放到專案中。資料的結構如下:

{
    "data": [
        {
            "key": "A",
            "cities": [
                {
                    "key": "152900",
                    "city": "阿拉善盟"
                },
                ...
            ]
        },
        {
            "key": "B",
            "cities": [
                {
                    "key": "130600",
                    "city": "保定市"
                },
                ...
            ]
        },
        ...
    ]
}

在渲染的時候,透過 FlatList 將每個字母渲染出來,每個字母中的城市透過遍歷來渲染出來。在每次可見專案變化時,右邊的字母列表透過判斷是否等於當前可見專案來判斷高亮狀態。點選右邊的字母時,則跳轉到指定的 index 上。

實現

先準備好城市資料放到 src/assets 目錄中:

接下來在 src 目錄中新建一個 ChoseCity.js 檔案作為元件。在元件中現將檔案內容匯入進來,用作 FlatList 元件的資料。

import React, { Component } from 'react';
import { FlatList, View, StyleSheet } from 'react-native';

export default class ChoseCity extends Component {
    constructor(props) {
        super(props);

        this.state = {
            data: [],               // 用於存放所有的城市資料
            right: [],              // 右邊的字母導航資料
            currentLetter: 'A'      // 當前選中的城市
        }
    }

    async componentDidMount() {
        const { data } = await require('./assets/cities.json');
        console.log(data);
    }

    render() {
        return (
            <View>

            </View>
        );
    }
}

在除錯資訊中我們可以看到 data 的資料結構:

接下來我們將 data 中的資料存放到 state 中:

async componentDidMount() {
    const { data } = await require('./assets/cities.json');
    let cityInfo = [];
    let right = [];

    // 這裡的保證了城市資料和右邊的字母導航同步
    data.map((item, index) => {
        cityInfo[index] = { key: item.key, data: item.cities };
        right[index] = item.key;
    });

    this.setState({ data: cityInfo, right: right });
}

接下來我們就開始渲染 FlatList 中的資料:

renderItem = ({ item, index }) => (
    <View style={styles.cityPiece}>
        <Text style={styles.keyText}>{item.key}</Text>
        <View style={styles.cities}>
            {item.data.map(({ city }, index) => (
                <TouchableOpacity key={index} style={styles.cityItem}>
                    <Text>{city}</Text>
                </TouchableOpacity>
            ))}
        </View>
    </View>
);

render() {
    return (
        <View>
            <FlatList
                data={this.state.data}
                renderItem={this.renderItem}
                keyExtractor={item => item.key}
            />
        </View>
    );
}

上面程式碼中,我使用了 map 來遍歷每個字母中包含的城市。效果如下:

接下來我們就來實現右邊的導航,在 render 方法的根 View 元件中新增下面程式碼:

<View style={styles.right}>
    {/* 由於資料不多,也直接使用 map 來遍歷 */}
    {this.state.right.map((item, index) => (
        <TouchableOpacity key={index}>
            <Text style={[this.state.currentLetter === item && { color: '#FD7700' }]}>{item}</Text>
        </TouchableOpacity>
    ))}
</View>
}

到這裡,外觀部分已經全部實現了,這裡是樣式程式碼:

const styles = StyleSheet.create({
    keyText: {
        fontSize: 16,
        fontWeight: 'bold',
    },
    cityPiece: {
        marginTop: 6,
        backgroundColor: '#FFF',
        paddingLeft: 21,
        paddingRight: 21,
        paddingTop: 15,
        paddingBottom: 15
    },
    cities: {
        flexWrap: 'wrap',
        flexDirection: 'row',
    },
    cityItem: {
        flex: 0,
        backgroundColor: '#F6F5F5',
        paddingLeft: 22,
        paddingRight: 22,
        paddingTop: 11,
        paddingBottom: 11,
        borderRadius: 18,
        marginTop: 14,
        marginRight: 10,
    },
    right: {
        position: 'absolute',
        top: 0,
        right: 0,
        bottom: 0,
        paddingRight: 5,
        paddingTop: 5,
        paddingBottom: 5,
        justifyContent: 'space-between',
        backgroundColor: '#F6F5F5',
        paddingLeft: 10,
    },
})

接下來我們先實現右邊導航跟隨資料的滾動來改變高亮。這裡需要用到 onViewableItemsChanged。這裡回撥函式給了我們兩個引數:viewableItemschanged,我們來看看它們的結構。

從這裡可以看出,viewableItems 陣列中第一個元素是當前可見的第一項,所以我們只需要第一個可見元素作為當前項即可。

onViewableItemsChanged = ({ viewableItems, changed }) => {
    // 將第一個可見的元素,作為當前元素
    this.setState({
        currentLetter: viewableItems[0].key
    });
}

render() {
    return (
        <View>
            <FlatList
                data={this.state.data}
                showsVerticalScrollIndicator={false}
                renderItem={this.renderItem}
                keyExtractor={item => item.key}
                onViewableItemsChanged={this.handleViewableItemsChanged}
            />
        </View>
    );
}

到這裡已經實現了跟隨高亮,怎麼樣很簡單吧,是不是比想象中更簡單,接下來我們來實現點選字母跳轉到指定字母塊,這裡要用到 scrollToIndex,程式碼如下:

scrollTo = (index) => {
    this.list.scrollToIndex({ viewOffset: -6, viewPosition: 0, index, animated: true });
}

render() {
    return (
        <View style={{backgroundColor: '#F6F5F5'}}>
            <View style={styles.right}>
                {/* 由於資料不多,也直接使用 map 來遍歷 */}
                {this.state.right.map((item, index) => (
                    {/* 利用 index 來進行跳轉 */}
                    <TouchableOpacity key={index} onPress={() => {this.scrollTo(index)}}>
                        <Text style={[this.state.currentLetter === item && { color: '#FD7700' }]}>{item}</Text>
                    </TouchableOpacity>
                ))}
            </View>

            <FlatList
                style={{marginRight: 30}}
                data={this.state.data}
                ref={flatList => this.list = flatList}
                showsVerticalScrollIndicator={false}
                renderItem={this.renderItem}
                keyExtractor={item => item.key}
                onViewableItemsChanged={this.handleViewableItemsChanged}
            />
        </View>
    );
}

所有功能完成後的效果:

總結

這個元件並不是一個完善的元件,還有一個小 bug,當滾動到 z 的時候,右邊 z 並不會高亮,這裡可以判斷是否已經到底,如果到底,則高亮字母 z。如果還需要其它功能,大家自己擴充套件即可。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章