思路
全世界國家地區和區號數據 => 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-