动画效果预览
思路
全世界国家地区和区号数据 => 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-