RN 纯js实现ios&Android地区选择控件

RN 纯js实现ios&Android地区选择控件_第1张图片

动画效果预览

思路

全世界国家地区和区号数据 => 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 (
            
                
                    
                         this.searchList(text)}
                            onFocus={() => this.setState({hideRightBar: true})}
                        />
                         this.setState({isShow: false})}>
                            X
                        
                    
                    
                         this.sectionlist = w}
                            initialNumToRender={300}
                            style={[styles.sessionList]}
                            renderItem={({item}) => (this.state.matchItem.has(item.countryCode) || this.state.fullList) ?  this.phoneCodeSelected(item)}>{item.countryName}+{item.phoneCode}: }
                            renderSectionHeader={({section, index}) => (this.state.matchSection.has(section.key) || this.state.fullList) ? {section.key} : }
                            sections={countryCodeSession}
                            ItemSeparatorComponent={() => this.state.fullList ?  : }
                        />
                    
                    
                        {this.state.hideRightBar ?  :   this.handleRightBarPress(rowData[1])}>{rowData[0]}}
                        />}
                    
                
            
        );
    }
}

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列表渲染


                
                    
                         this.searchList(text)}
                            onFocus={() => this.setState({hideRightBar: true})}
                        />
                         this.setState({isShow: false})}>
                            X
                        
                    
                    
                         this.sectionlist = w}
                            initialNumToRender={300}
                            style={[styles.sessionList]}
                            renderItem={({item}) => (this.state.matchItem.has(item.countryCode) || this.state.fullList) ?  this.phoneCodeSelected(item)}>{item.countryName}+{item.phoneCode}: }
                            renderSectionHeader={({section, index}) => (this.state.matchSection.has(section.key) || this.state.fullList) ? {section.key} : }
                            sections={countryCodeSession}
                            ItemSeparatorComponent={() => this.state.fullList ?  : }
                        />
                    
                    
                        {this.state.hideRightBar ?  :   this.handleRightBarPress(rowData[1])}>{rowData[0]}}
                        />}
                    
                
            

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-

你可能感兴趣的:(RN 纯js实现ios&Android地区选择控件)