RN 純js實現ios&Android地區選擇控件

動畫效果預覽

思路

全世界國家地區和區號數據 => RN列表渲染 => 右側首字母navigator與列表的聯動 => 頂部輸入搜索框與列表的聯動 => 選中當前國家的數據回調

源碼

import React from 'react';
import {View, Text, StyleSheet, TextInput, SectionList, ListView, TouchableHighlight, TouchableWithoutFeedback, Modal} from "react-native";
import PropTypes from 'prop-types';

const countryCodeSession = require('./lib/countryCode.json');

const styles = StyleSheet.create({
    container: {
        flex: 1,
        // flexDirection: 'column',
        justifyContent: "center",
        alignItems: "center",
        backgroundColor: "#F5FCFF",
    },
    container1: {
        flex: 1,
        backgroundColor: '#aaa',
        flexDirection: 'row',
        padding: 8,
    },
    container2: {
        flex: 11,
        flexDirection: 'row',
        paddingRight: 15,
        // backgroundColor: '#000'
    },
    searchInput: {
        flex: 1,
        backgroundColor: '#fff',
        borderRadius: 8,
        height: 40,
        paddingLeft: 10,
        marginTop: 3,
    },
    sessionList: {
        flex: 1
    },
    rightBar: {
        position: 'absolute',
        width: 15,
        right: 0,
        top: 70,
    },
    rightBarText: {
        color: 'blue',
        textAlign: 'center',
        lineHeight: 20
    },
    sessionListItemContainer: {
        flex: 1,
        flexDirection: 'row',
        padding: 8,
        paddingLeft: 0,
        // borderBottomWidth: 0.6,
        // borderBottomColor: '#eee'
    },
    sessionListItem1: {
        flex: 1
    },
    sessionListItem2: {
        flex: 1,
        textAlign: 'right',
        color: '#999'
    },
    sessionHeader: {
        backgroundColor: '#eee'
    },
    itemSeparator: {
        flex: 1,
        height: 1,
        backgroundColor: '#eee'
    },
    cancelBtn: {
        height: 40,
        lineHeight: 40,
        paddingLeft: 5,
    },
});

export default class extends React.Component {
    static propTypes= {
        isShow: PropTypes.bool,
        onPick: PropTypes.func,
        animationType: PropTypes.string,
        // onCancel: PropTypes.func
    };
    sectionlist: SectionList;
    constructor(props) {
        super(props);
        this.state = {
            fullList: true,
            matchItem: new Set(),
            matchSection: new Set(),
            hideRightBar: false,
            isShow: this.props.isShow
        };
        this.handleRightBarPress = this.handleRightBarPress.bind(this);
        this.searchList = this.searchList.bind(this);
    };
    handleRightBarPress (itemIndex) {
        this.sectionlist.scrollToLocation({itemIndex: itemIndex})
    };
    searchList (text) {
        this.setState({fullList: false});
        if (!text) {
            this.setState({fullList: true});
            return
        }
        if (~text.indexOf(' ')) {
            this.setState({fullList: false});
            return
        }
        let matchItem = new Set();
        let matchSection = new Set();
        for (let i = 0; i < countryCodeSession.length; i++) {
            for (let j = 0; j < countryCodeSession[i].data.length; j++) {
                if (countryCodeSession[i].data[j].phoneCode.toString().match(text) || countryCodeSession[i].data[j].countryName.match(text)) {
                    matchItem.add(countryCodeSession[i].data[j].countryCode);
                    !matchSection.has(countryCodeSession[i].key) && matchSection.add(countryCodeSession[i].key);
                }
            }
        }
        if (matchItem.size) {
            this.setState({matchItem, matchSection})
        } else {
            this.setState({matchItem, matchSection}, () => {
                this.setState({fullList: false})
            })
        }
    };
    phoneCodeSelected (item) {
        this.props.onPick(item)
        this.setState({isShow: false})
    };
    render(){
        const title = this.props.title || 'No Title';
        const data = this.props.data || 'No Data';
        const sectionMapArr = [
            ['A', -1],
            ['B', 20],
            ['C', 47],
            ['D', 51],
            ['E', 59],
            ['F', 64],
            ['G', 78],
            ['H', 93],
            ['I', 104],
            ['J', 106],
            ['K', 119],
            ['L', 132],
            ['M', 146],
            ['N', 176],
            ['O', 191],
            ['P', 193],
            ['Q', 198],
            ['R', 200],
            ['S', 205],
            ['T', 233],
            ['U', 247],
            ['V', 249],
            ['W', 251],
            ['X', 262],
            ['Y', 272],
            ['Z', 288]
        ];
        let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
        return (
            <Modal visible={this.state.isShow} animationType={this.props.animationType || 'slide'} transparent={false}>
                <View style={styles.container}>
                    <View style={[styles.container1]}>
                        <TextInput
                            style={[styles.searchInput]}
                            placeholder='請輸入國家或地區'
                            onChangeText={(text) => this.searchList(text)}
                            onFocus={() => this.setState({hideRightBar: true})}
                        />
                        <TouchableWithoutFeedback onPress={() => this.setState({isShow: false})}>
                            <Text style={[styles.cancelBtn]}>X</Text>
                        </TouchableWithoutFeedback>
                    </View>
                    <View style={[styles.container2]}>
                        <SectionList
                            ref={w => this.sectionlist = w}
                            initialNumToRender={300}
                            style={[styles.sessionList]}
                            renderItem={({item}) => (this.state.matchItem.has(item.countryCode) || this.state.fullList) ? <TouchableHighlight onPress={() => this.phoneCodeSelected(item)}><View style={[styles.sessionListItemContainer]} ><Text style={[styles.sessionListItem1]}>{item.countryName}</Text><Text style={[styles.sessionListItem2]}>+{item.phoneCode}</Text></View></TouchableHighlight>: <View></View>}
                            renderSectionHeader={({section, index}) => (this.state.matchSection.has(section.key) || this.state.fullList) ? <View><Text style={[styles.sessionHeader]}>{section.key}</Text></View> : <View></View>}
                            sections={countryCodeSession}
                            ItemSeparatorComponent={() => this.state.fullList ? <View style={[styles.itemSeparator]}></View> : <View></View>}
                        />
                    </View>
                    <View style={[styles.rightBar]}>
                        {this.state.hideRightBar ? <View></View> : <ListView
                            dataSource={ds.cloneWithRows(sectionMapArr)}
                            renderRow={(rowData) => <Text style={[styles.rightBarText]} onPress={() => this.handleRightBarPress(rowData[1])}>{rowData[0]}</Text>}
                        />}
                    </View>
                </View>
            </Modal>
        );
    }
}

1.全世界國家地區和區號數據

數據來源基于 https://github.com/mohuilin/CountryCode,在此基礎上重新改造了數據以適應RN 列表渲染,改造后的數據 https://github.com/StephenKe/react-native-country-code-picker/tree/master/lib ,就是源碼中的json數據

const countryCodeSession = require('./lib/countryCode.json');

2.RN列表渲染

<Modal visible={this.state.isShow} animationType={this.props.animationType || 'slide'} transparent={false}>
                <View style={styles.container}>
                    <View style={[styles.container1]}>
                        <TextInput
                            style={[styles.searchInput]}
                            placeholder='請輸入國家或地區'
                            onChangeText={(text) => this.searchList(text)}
                            onFocus={() => this.setState({hideRightBar: true})}
                        />
                        <TouchableWithoutFeedback onPress={() => this.setState({isShow: false})}>
                            <Text style={[styles.cancelBtn]}>X</Text>
                        </TouchableWithoutFeedback>
                    </View>
                    <View style={[styles.container2]}>
                        <SectionList
                            ref={w => this.sectionlist = w}
                            initialNumToRender={300}
                            style={[styles.sessionList]}
                            renderItem={({item}) => (this.state.matchItem.has(item.countryCode) || this.state.fullList) ? <TouchableHighlight onPress={() => this.phoneCodeSelected(item)}><View style={[styles.sessionListItemContainer]} ><Text style={[styles.sessionListItem1]}>{item.countryName}</Text><Text style={[styles.sessionListItem2]}>+{item.phoneCode}</Text></View></TouchableHighlight>: <View></View>}
                            renderSectionHeader={({section, index}) => (this.state.matchSection.has(section.key) || this.state.fullList) ? <View><Text style={[styles.sessionHeader]}>{section.key}</Text></View> : <View></View>}
                            sections={countryCodeSession}
                            ItemSeparatorComponent={() => this.state.fullList ? <View style={[styles.itemSeparator]}></View> : <View></View>}
                        />
                    </View>
                    <View style={[styles.rightBar]}>
                        {this.state.hideRightBar ? <View></View> : <ListView
                            dataSource={ds.cloneWithRows(sectionMapArr)}
                            renderRow={(rowData) => <Text style={[styles.rightBarText]} onPress={() => this.handleRightBarPress(rowData[1])}>{rowData[0]}</Text>}
                        />}
                    </View>
                </View>
            </Modal>

DOM結構從外向內看:

Modal

觸發控件相當于在當前頁面覆蓋一個Modal,Modal的visible和animationType屬性接收外部傳入參數,不傳默認是visible: false,animationType: 'slide'

SectionList

SectionList的屬性能極好的實現該控件,具體有關它的介紹請移步RN官網

  • ref: 將SectionList實例賦給this.sectionlist,后面可以直接使用this.sectionlist...調起所有SectionList實例方法
  • initialNumToRender: 初始化列表時加載幾列,這里我寫死的300,因為整個渲染數據不過200多條,是讓它一次性全部渲染出來,后續優化可以按需加載,SectionList有提供相關方法
  • sections: 列表加載的源數據
  • ItemSeparatorComponent: 列表行間分割組件。這里是一條線,由this.state.fullList控制是否渲染
  • renderSectionHeader: 分組數據。源數據中根據地區首字母分組,對應key字段
  • renderItem: 渲染行。這里顯示源數據的countryName和phoneCode字段,這里當輸入框檢索進行過濾操作的時候只需要顯示符合條件的行,就是this.state.matchItem.has(item.countryCode) || this.state.fullList 的含義,具體邏輯控制已注釋:
searchList (text) {
        // 重置列表初始狀態
        this.setState({fullList: false}); 
        // 輸入為空時重置列表初始狀態
        if (!text) {
            this.setState({fullList: true});
            return
        }
        // 輸入不為空時不渲染行間分割線
        if (~text.indexOf(' ')) {
            this.setState({fullList: false});
            return
        }
        let matchItem = new Set();
        let matchSection = new Set();
        for (let i = 0; i < countryCodeSession.length; i++) {
            for (let j = 0; j < countryCodeSession[i].data.length; j++) {
                // 匹配當前檢索到的數據
                // 檢索到的所有行的countryCode存入matchItem
                // 檢索到的所有行對應的分組key存入matchSection
                if (countryCodeSession[i].data[j].phoneCode.toString().match(text) || countryCodeSession[i].data[j].countryName.match(text)) {
                    matchItem.add(countryCodeSession[i].data[j].countryCode);
                    !matchSection.has(countryCodeSession[i].key) && matchSection.add(countryCodeSession[i].key);
                }
            }
        }
        if (matchItem.size) {
            // 當前有檢索到數據則重新渲染
            this.setState({matchItem, matchSection})
        } else {
            // 當前沒有檢索到數據則重置列表狀態
            this.setState({matchItem, matchSection}, () => {
                this.setState({fullList: false})
            })
        }
    };

當前行被選中觸發phoneCodeSelected函數,將當前選中的行數據回調:

phoneCodeSelected (item) {
        this.props.onPick(item)
        this.setState({isShow: false})
    };
TextInput
  • onChangeText: 觸發searchList函數,以上已解釋
  • onFocus: 當輸入框被選中時不渲染右側首字母navigator
TouchableWithoutFeedback
  • onPress: 隱藏控件
ListView
  • dataSource: 源數據sectionMapArr,每行渲染首字母并且沒個首字母關聯了sectionlist對應的itemIndex (根據不同數據源itemIndex對應具體數值也不同)
const sectionMapArr = [
            ['A', -1],
            ['B', 20],
            ['C', 47],
            ['D', 51],
            ['E', 59],
            ['F', 64],
            ['G', 78],
            ['H', 93],
            ['I', 104],
            ['J', 106],
            ['K', 119],
            ['L', 132],
            ['M', 146],
            ['N', 176],
            ['O', 191],
            ['P', 193],
            ['Q', 198],
            ['R', 200],
            ['S', 205],
            ['T', 233],
            ['U', 247],
            ['V', 249],
            ['W', 251],
            ['X', 262],
            ['Y', 272],
            ['Z', 288]
        ];
  • onPress: 觸發handleRightBarPress函數,sectionlist跳轉到對應的首字母分組
handleRightBarPress (itemIndex) {
        this.sectionlist.scrollToLocation({itemIndex: itemIndex})
    };

總結

該控件已開源 https://github.com/StephenKe/react-native-country-code-picker ,并且已在npm發布,可以在項目中使用:

npm install react-native-country-code-picker --save

or

yarn add react-native-country-code-picker --save

使用很簡單,具體可查看README
歡迎star、fork、issue、pr
- 0-

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容