├── LICENSE ├── README.ZH.md ├── README.md ├── TabView.js ├── icon-close-base.js ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 漫步者 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.ZH.md: -------------------------------------------------------------------------------- 1 | # react-native-city-picker 2 | 3 | [Chinese](https://github.com/AaronBank/react-native-city-picker/blob/master/README.ZH.md) | [English](https://github.com/AaronBank/react-native-city-picker/blob/master/README.md) 4 | 5 | ![](https://img.shields.io/badge/licence-MIT-%2332CD32.svg) ![](https://img.shields.io/badge/npm-6.4.1-%2332CD32.svg) ![](https://img.shields.io/badge/react--native-%3E%3D0.42.0-%234169E1.svg) 6 | 7 | **react-native 模仿JD多级联动地址选择器, 适用于Ios及Android平台** 8 | 9 | 10 | 11 | 12 | ## 安装 ## 13 | `npm install react-native-city-picker --save` 14 | 15 | 16 | ## 使用 ## 17 | 18 | > App.js 19 | ```javascript 20 | 21 | import React, { Component } from 'react' 22 | import { View, Text, Button } from 'react-native' 23 | 24 | import data from './data.json' 25 | 26 | import Address from 'react-native-city-picker' 27 | 28 | export default class extends Component{ 29 | constructor () { 30 | super() 31 | 32 | this.state = { 33 | selectAddress: [], 34 | provinceList: [] 35 | } 36 | 37 | this.addressList = [] 38 | } 39 | 40 | render () { 41 | return ( 42 | 43 | { this.state.selectAddress } 44 |
this.address = ref } 46 | load={ this.initPage.bind(this) } 47 | tabs={ this.state.selectAddress } 48 | prompt="请选择" 49 | result={ selectAddress => this.setState({ selectAddress }) } 50 | /> 51 | 52 | 53 | ) 54 | } 55 | 56 | async componentDidMount() { 57 | let provinceList = [] 58 | 59 | for (let i in data) { 60 | for (let j in data[i]) { 61 | provinceList.push(j) 62 | } 63 | } 64 | 65 | this.addressList.push(localAddress) 66 | this.setState({ provinceList }) 67 | } 68 | 69 | // 处理省份数据 70 | dispatchData (prev, index) { 71 | const prevAddress = this.addressList[index - 1] 72 | let result = [] 73 | 74 | if (typeof prevAddress[0] === 'string') return false 75 | 76 | let firstFilter = prevAddress.filter(address => { 77 | let current = Object.keys(address)[0] 78 | return current === prev 79 | })[0] 80 | 81 | result = firstFilter[prev] 82 | this.addressList[index] = firstFilter[prev] 83 | 84 | if (typeof firstFilter[prev][0] === 'object') { 85 | result = firstFilter[prev].map(item => Object.keys(item)[0]) 86 | } 87 | 88 | return result 89 | } 90 | 91 | // 数据调用及初始化 92 | async initPage (prev, index) { 93 | if (index === 0) { 94 | return await promises(this.state.provinceList) 95 | } 96 | 97 | return await promises(this.dispatchData(prev, index)) 98 | } 99 | 100 | // 模拟数据(真实请求则不需要) 101 | promises (data) { 102 | return new Promise((resolve, reject) => { 103 | setTimeout(() => { 104 | resolve(data) 105 | }, 1000) 106 | }) 107 | } 108 | } 109 | 110 | ``` 111 | 112 | > data.json 113 | 114 | ```javascript 115 | [{"北京市":[{"北京市":["东城区","西城区","朝阳区","丰台区","石景山区","海淀区","门头沟区","房山区","通州区","顺义区","昌平区","大兴区","怀柔区","平谷区","密云县","延庆县"]}]},{"天津市":[{"天津市":["和平区","河东区","河西区","南开区","河北区","红桥区","东丽区","西青区","津南区","北辰区","武清区","宝坻区","滨海新区","宁河县","静海县","蓟县"]}]}]}] 116 | ``` 117 | 118 | 119 | 120 | ## Props ## 121 | 122 | Prop | 类型 | 可选 | 默认值 | 说明 123 | ---------------------- | --------- | ------- | --------- | ----------- 124 | load | function | 否 | | 数据传递方法 125 | tabs | Array | 否 | | tab集合 126 | prompt | string | 是 | '请选择' | 默认tab展示文字 127 | result | function | 否 | | 选择完成回调 128 | titleStyle | object | 是 | | 弹框顶部样式 129 | contentStyle | object | 是 | | 弹框内容区样式 130 | listStyle | object | 是 | | 列表样式 131 | tabStyle | object | 是 | | tab样式 132 | activeColor | string | 是 | `#e4393c` | 选中颜色色值 133 | 134 | 135 | ## 参数详细信息 ## 136 | 137 | ### load ### 138 | 139 | 初始化及选中列表中某一项都将调用此方法,并且接受两个参数,(prev: 上一级被选中项, index: 当前级别需要展示数据联动级数,从0开始,0代表初始化第一项,1代表第二项,以此类推),该方法必须提供**返回值**,当存在下一级则需返回下一级数据集合,若不存则返回false即可。返回false时,弹窗将收起,并且调用result方法传回选中后的文字集合 140 | 141 | ### result ### 142 | 143 | 该方法接受一个参数,参数类型值为`Array`的选中后的文字集合,该方法会在选择完成后将被执行 144 | 145 | 146 | ## 样式修改案例 ## 147 | 148 | ```javascript 149 | 150 | import React, { Component } from 'react' 151 | import { View, Text, Button } from 'react-native' 152 | 153 | import data from './data.json' 154 | 155 | import Address from 'react-native-city-picker' 156 | import { px2dp } from 'react-native-style-adaptive' 157 | 158 | export default class extends Component{ 159 | constructor () { 160 | super() 161 | 162 | this.state = { 163 | selectAddress: [], 164 | provinceList: [] 165 | } 166 | 167 | this.addressList = [] 168 | } 169 | 170 | render () { 171 | return ( 172 | 173 | { this.state.selectAddress } 174 |
this.address = ref } 176 | load={ this.initPage.bind(this) } 177 | tabs={ this.state.selectAddress } 178 | prompt="请选择" 179 | titleStyle={ { 180 | content: { borderTopLeftRadius: px2dp(22), borderTopRightRadius: px2dp(22) } 181 | } } 182 | contentStyle={ { backgroundColor: '#F1F1F1' } } 183 | activeColor="red" 184 | result={ selectAddress => this.setState({ selectAddress }) } 185 | /> 186 | 187 | 188 | ) 189 | } 190 | 191 | async componentDidMount() { 192 | let provinceList = [] 193 | 194 | for (let i in data) { 195 | for (let j in data[i]) { 196 | provinceList.push(j) 197 | } 198 | } 199 | 200 | this.addressList.push(localAddress) 201 | this.setState({ provinceList }) 202 | } 203 | 204 | // 处理省份数据 205 | dispatchData (prev, index) { 206 | const prevAddress = this.addressList[index - 1] 207 | let result = [] 208 | 209 | if (typeof prevAddress[0] === 'string') return false 210 | 211 | let firstFilter = prevAddress.filter(address => { 212 | let current = Object.keys(address)[0] 213 | return current === prev 214 | })[0] 215 | 216 | result = firstFilter[prev] 217 | this.addressList[index] = firstFilter[prev] 218 | 219 | if (typeof firstFilter[prev][0] === 'object') { 220 | result = firstFilter[prev].map(item => Object.keys(item)[0]) 221 | } 222 | 223 | return result 224 | } 225 | 226 | // 数据调用及初始化 227 | async initPage (prev, index) { 228 | if (index === 0) { 229 | return await promises(this.state.provinceList) 230 | } 231 | 232 | return await promises(this.dispatchData(prev, index)) 233 | } 234 | 235 | // 模拟数据(真实请求则不需要) 236 | promises (data) { 237 | return new Promise((resolve, reject) => { 238 | setTimeout(() => { 239 | resolve(data) 240 | }, 1000) 241 | }) 242 | } 243 | } 244 | 245 | ``` 246 | 247 | ### 修改后效果 ### 248 | 249 | 250 | 251 | ## Licence ## 252 | **MIT** 253 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-city-picker 2 | 3 | [Chinese](https://github.com/AaronBank/react-native-city-picker/blob/master/README.ZH.md) | [English](https://github.com/AaronBank/react-native-city-picker/blob/master/README.md) 4 | 5 | ![](https://img.shields.io/badge/licence-MIT-%2332CD32.svg) ![](https://img.shields.io/badge/npm-6.4.1-%2332CD32.svg) ![](https://img.shields.io/badge/react--native-%3E%3D0.42.0-%234169E1.svg) 6 | 7 | **React-native imitates JD multi-level linkage address selector for Ios and Android platforms** 8 | 9 | 10 | 11 | 12 | ## Installation ## 13 | `npm install react-native-city-picker --save` 14 | 15 | 16 | ## Usage ## 17 | 18 | > App.js 19 | ```javascript 20 | 21 | import React, { Component } from 'react' 22 | import { View, Text, Button } from 'react-native' 23 | 24 | import data from './data.json' 25 | 26 | import Address from 'react-native-city-picker' 27 | 28 | export default class extends Component{ 29 | constructor () { 30 | super() 31 | 32 | this.state = { 33 | selectAddress: [], 34 | provinceList: [] 35 | } 36 | 37 | this.addressList = [] 38 | } 39 | 40 | render () { 41 | return ( 42 | 43 | { this.state.selectAddress } 44 |
this.address = ref } 46 | load={ this.initPage.bind(this) } 47 | tabs={ this.state.selectAddress } 48 | prompt="请选择" 49 | result={ selectAddress => this.setState({ selectAddress }) } 50 | /> 51 | 52 | 53 | ) 54 | } 55 | 56 | async componentDidMount() { 57 | let provinceList = [] 58 | 59 | for (let i in data) { 60 | for (let j in data[i]) { 61 | provinceList.push(j) 62 | } 63 | } 64 | 65 | this.addressList.push(localAddress) 66 | this.setState({ provinceList }) 67 | } 68 | 69 | // Processing provincial data 70 | dispatchData (prev, index) { 71 | const prevAddress = this.addressList[index - 1] 72 | let result = [] 73 | 74 | if (typeof prevAddress[0] === 'string') return false 75 | 76 | let firstFilter = prevAddress.filter(address => { 77 | let current = Object.keys(address)[0] 78 | return current === prev 79 | })[0] 80 | 81 | result = firstFilter[prev] 82 | this.addressList[index] = firstFilter[prev] 83 | 84 | if (typeof firstFilter[prev][0] === 'object') { 85 | result = firstFilter[prev].map(item => Object.keys(item)[0]) 86 | } 87 | 88 | return result 89 | } 90 | 91 | // Data call and initialization 92 | async initPage (prev, index) { 93 | if (index === 0) { 94 | return await promises(this.state.provinceList) 95 | } 96 | 97 | return await promises(this.dispatchData(prev, index)) 98 | } 99 | 100 | // Analog data (required for real requests) 101 | promises (data) { 102 | return new Promise((resolve, reject) => { 103 | setTimeout(() => { 104 | resolve(data) 105 | }, 1000) 106 | }) 107 | } 108 | } 109 | 110 | ``` 111 | 112 | > data.json 113 | 114 | ```javascript 115 | [{"北京市":[{"北京市":["东城区","西城区","朝阳区","丰台区","石景山区","海淀区","门头沟区","房山区","通州区","顺义区","昌平区","大兴区","怀柔区","平谷区","密云县","延庆县"]}]},{"天津市":[{"天津市":["和平区","河东区","河西区","南开区","河北区","红桥区","东丽区","西青区","津南区","北辰区","武清区","宝坻区","滨海新区","宁河县","静海县","蓟县"]}]}]}] 116 | ``` 117 | 118 | 119 | 120 | ## Props ## 121 | 122 | Prop | Type | Optional | Default | Description 123 | ---------------------- | --------- | ------- | --------- | ----------- 124 | load | function | No | | Data transfer method 125 | tabs | Array | No | | Tab collection 126 | prompt | string | Yes | '请选择' | Default tab display text 127 | result | function | No | | Choose to complete the callback 128 | titleStyle | object | Yes | | Box top style 129 | contentStyle | object | Yes | | Bullet box content area style 130 | listStyle | object | Yes | | List style 131 | tabStyle | object | Yes | | Tab style 132 | activeColor | string | Yes | `#e4393c` | Check color color value 133 | 134 | 135 | ## Parameter details ## 136 | 137 | ### load ### 138 | 139 | Initialization and select an item in the list will invoke this method, and receive two parameters, (prev: selected item at the next higher level, the index: current level needs to display data linkage series, starting from 0, 0 represents the initialized first, 1 represents the second, and so on), the method must provide * * * *, the return value when there is a lower level will be expected to return to the next level data collection, if not to return false. When false is returned, the pop-up closes and the result method is called to return the selected set of text 140 | 141 | ### result ### 142 | 143 | This method accepts a parameter, the parameter type value of ` Array ` selected text collection, this method will after the choice will be executed 144 | 145 | 146 | ## 样式修改案例 ## 147 | 148 | ```javascript 149 | 150 | import React, { Component } from 'react' 151 | import { View, Text, Button } from 'react-native' 152 | 153 | import data from './data.json' 154 | 155 | import Address from 'react-native-city-picker' 156 | import { px2dp } from 'react-native-style-adaptive' 157 | 158 | export default class extends Component{ 159 | constructor () { 160 | super() 161 | 162 | this.state = { 163 | selectAddress: [], 164 | provinceList: [] 165 | } 166 | 167 | this.addressList = [] 168 | } 169 | 170 | render () { 171 | return ( 172 | 173 | { this.state.selectAddress } 174 |
this.address = ref } 176 | load={ this.initPage.bind(this) } 177 | tabs={ this.state.selectAddress } 178 | prompt="请选择" 179 | titleStyle={ { 180 | content: { borderTopLeftRadius: px2dp(22), borderTopRightRadius: px2dp(22) } 181 | } } 182 | contentStyle={ { backgroundColor: '#F1F1F1' } } 183 | listStyle={ { 184 | content: {}, 185 | text: { color: '#666', fontSize: px2dp(28) } 186 | } } 187 | activeColor="red" 188 | result={ selectAddress => this.setState({ selectAddress }) } 189 | /> 190 | 191 | 192 | ) 193 | } 194 | 195 | async componentDidMount() { 196 | let provinceList = [] 197 | 198 | for (let i in data) { 199 | for (let j in data[i]) { 200 | provinceList.push(j) 201 | } 202 | } 203 | 204 | this.addressList.push(localAddress) 205 | this.setState({ provinceList }) 206 | } 207 | 208 | // Processing provincial data 209 | dispatchData (prev, index) { 210 | const prevAddress = this.addressList[index - 1] 211 | let result = [] 212 | 213 | if (typeof prevAddress[0] === 'string') return false 214 | 215 | let firstFilter = prevAddress.filter(address => { 216 | let current = Object.keys(address)[0] 217 | return current === prev 218 | })[0] 219 | 220 | result = firstFilter[prev] 221 | this.addressList[index] = firstFilter[prev] 222 | 223 | if (typeof firstFilter[prev][0] === 'object') { 224 | result = firstFilter[prev].map(item => Object.keys(item)[0]) 225 | } 226 | 227 | return result 228 | } 229 | 230 | // Data call and initialization 231 | async initPage (prev, index) { 232 | if (index === 0) { 233 | return await promises(this.state.provinceList) 234 | } 235 | 236 | return await promises(this.dispatchData(prev, index)) 237 | } 238 | 239 | // Analog data (required for real requests) 240 | promises (data) { 241 | return new Promise((resolve, reject) => { 242 | setTimeout(() => { 243 | resolve(data) 244 | }, 1000) 245 | }) 246 | } 247 | } 248 | 249 | ``` 250 | 251 | ### Modified effect ### 252 | 253 | 254 | 255 | ## Licence ## 256 | **MIT** 257 | -------------------------------------------------------------------------------- /TabView.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PureComponent } from 'react' 2 | import { 3 | View, 4 | Text, 5 | Animated, 6 | ScrollView, 7 | TouchableOpacity, 8 | ActivityIndicator, 9 | StyleSheet 10 | } from 'react-native' 11 | import ScrollAbleTabView from 'react-native-scrollable-tab-view' 12 | import { px2dp, isIPhoneX, getBottomSpace, deviceWidth, isIos } from 'react-native-style-adaptive' 13 | 14 | class Tab extends PureComponent { 15 | static defaultProps = { 16 | tabStyle: { 17 | content: {}, 18 | text: {}, 19 | line: {} 20 | } 21 | } 22 | constructor () { 23 | super() 24 | 25 | this.state = { 26 | tabWidth: px2dp(80), 27 | progress: new Animated.Value(px2dp(32)) 28 | } 29 | 30 | this.touchRefs = [] 31 | this.list = [] 32 | this.list_len = 0 33 | this.contentOffsetX = 0 34 | this.onScrollEnd = this.onScrollEnd.bind(this) 35 | this.onAndroidScrollEnd = this.onAndroidScrollEnd.bind(this) 36 | } 37 | 38 | render() { 39 | const { activeColor, switchTabs, activeTab, tabStyle } = this.props 40 | 41 | return ( 42 | 43 | this.scrollView = ref } 45 | style={ { flex: 1 } } 46 | horizontal 47 | showsHorizontalScrollIndicator={ false } 48 | alwaysBounceHorizontal={ false } 49 | onMomentumScrollEnd={ this.onScrollEnd } 50 | onScroll={ this.onAndroidScrollEnd } 51 | scrollEventThrottle={15} 52 | > 53 | { 54 | switchTabs.map((tab, i) => { 55 | return this.touchRefs[i] = ref } 57 | style={ tabStyles.tabJoke } 58 | activeOpacity={ 0.9 } 59 | key={ tab + i } 60 | onPress={ () => this.tabOnPress(i) } 61 | onLayout={ e => this.setListLayout(e.nativeEvent, i) } 62 | removeClippedSubviews={false} 63 | > 64 | 65 | { tab } 66 | 67 | 68 | }) 69 | } 70 | 79 | 80 | 81 | 82 | ) 83 | } 84 | 85 | componentDidUpdate () { 86 | this.load() 87 | } 88 | 89 | componentDidMount () { 90 | this.load() 91 | } 92 | 93 | load () { 94 | this.setX(this.props.activeTab) 95 | this.animation() 96 | } 97 | 98 | setListLayout(e, i) { 99 | this.list[i] = e.layout.width 100 | this.list_len = this.list.reduce((prev, next) => prev + next, 0) 101 | } 102 | 103 | setX(i) { 104 | let len = 0 105 | 106 | for (let index = 0; index < i; index++) len += this.list[index] 107 | 108 | if (len > deviceWidth() / 2 && len < this.list_len - deviceWidth() / 2) { 109 | this.scrollView.scrollTo({ x: len }) 110 | } else if (len > this.list_len - deviceWidth() / 2) { 111 | this.scrollView.scrollToEnd() 112 | } else { 113 | this.scrollView.scrollTo({ x: 0 }) 114 | } 115 | } 116 | 117 | animation () { 118 | this.state.progress.stopAnimation() 119 | this.touchRefs[this.props.activeTab].measure((frameX, frameY, width, height, pageX, pageY) => { 120 | this.setState({ tabWidth: width }) 121 | this.animated = Animated.spring( 122 | this.state.progress, 123 | { 124 | toValue: pageX + this.contentOffsetX, 125 | velocity: 20, 126 | tension: 10, 127 | } 128 | ).start() 129 | }) 130 | } 131 | 132 | tabOnPress (i) { 133 | this.props.goToPage(i) 134 | this.setX(i) 135 | this.animation() 136 | } 137 | 138 | onScrollEnd (e) { 139 | this.contentOffsetX = e.nativeEvent.contentOffset.x 140 | } 141 | 142 | onAndroidScrollEnd (e) { 143 | if (isIos) return 144 | 145 | this.contentOffsetX = e.nativeEvent.contentOffset.x 146 | this.animation() 147 | } 148 | 149 | componentWillUnmount () { 150 | this.state.progress.stopAnimation() 151 | } 152 | } 153 | 154 | const tabStyles = StyleSheet.create({ 155 | container: { 156 | height: px2dp(80), 157 | borderBottomColor: '#EEE', 158 | borderBottomWidth: 1, 159 | position: 'relative', 160 | backgroundColor: '#fff' 161 | }, 162 | tabJoke: { 163 | height: px2dp(80), 164 | marginHorizontal: px2dp(32), 165 | flexDirection: 'row', 166 | alignItems: 'center' 167 | }, 168 | line: { 169 | position: 'absolute', 170 | height: 2, 171 | bottom: 0 172 | }, 173 | text: { 174 | fontSize: px2dp(28), 175 | color: '#252426' 176 | } 177 | }) 178 | 179 | export default class extends Component { 180 | static defaultProps = { 181 | listStyle: { 182 | content: {}, 183 | text: {} 184 | }, 185 | activeColor: '#e4393c' 186 | } 187 | 188 | 189 | constructor (props) { 190 | super(props) 191 | 192 | this.state = { 193 | page: props.initPage, 194 | address: props.address, 195 | tabs: props.tabs, 196 | loading: false, 197 | tags: props.tags || [] 198 | } 199 | } 200 | 201 | selected (item, index, i) { 202 | let newTabs = this.state.tabs 203 | let tags = this.state.tags.splice(0, index) 204 | 205 | newTabs[this.state.page] = typeof item === 'object' ? item.name : item 206 | 207 | tags[index] = i 208 | 209 | this.setState({ tabs: newTabs, loading: true, tags }, () => { 210 | this.props.selected(item, index + 1, newTabs) 211 | }) 212 | } 213 | 214 | _renderOptions (index) { 215 | const { listStyle, activeColor } = this.props 216 | const address = this.state.address[index] || [] 217 | 218 | return address.map((item, i) => this.selected(item, index, i) } 223 | > 224 | 225 | { typeof item === 'string' ? item : item.name } 226 | 227 | ) 228 | } 229 | 230 | _renderTabView () { 231 | return this.state.tabs.map((item, index) => 237 | { this._renderOptions(index) } 238 | ) 239 | } 240 | 241 | setNativeProps (singleAddress) { 242 | let newTabs = [] 243 | let newAddress = [] 244 | 245 | const { page, tabs, address } = this.state 246 | 247 | if (page < tabs.length - 1) { 248 | newTabs = tabs.splice(0, page + 1) 249 | newAddress = address.splice(0, page + 1) 250 | } else { 251 | newTabs = tabs 252 | newAddress = address 253 | } 254 | 255 | this.setState({ 256 | address: [...newAddress, singleAddress], 257 | tabs: [...newTabs, this.props.prompt], 258 | page: page + 1, 259 | loading: false 260 | }) 261 | } 262 | 263 | render () { 264 | const { page, tabs } = this.state 265 | const { style, initPage, tabStyle, activeColor } = this.props 266 | return ( 267 | 268 | } 278 | page={ page } 279 | onChangeTab={ ({ i }) => this.setState({ page: i }) } 280 | > 281 | { this._renderTabView() } 282 | 283 | { 284 | this.state.loading && 285 | { this.props.loading && } 286 | 287 | } 288 | 289 | ) 290 | } 291 | } 292 | 293 | const styles = StyleSheet.create({ 294 | container: { 295 | flex: 1, 296 | position: 'relative', 297 | paddingBottom: isIPhoneX ? getBottomSpace() : 0 298 | }, 299 | main: { 300 | flex: 1 301 | }, 302 | content: { 303 | paddingHorizontal: px2dp(32), 304 | marginVertical: px2dp(10) 305 | }, 306 | list: { 307 | paddingVertical: px2dp(33) 308 | }, 309 | loading: { 310 | position: 'absolute', 311 | left: 0, 312 | right: 0, 313 | top: 0, 314 | bottom: 0, 315 | backgroundColor: 'rgba(255, 255, 255, 0)', 316 | justifyContent: 'center', 317 | alignItems: 'center' 318 | }, 319 | listText: { 320 | color: '#333', 321 | fontSize: px2dp(28) 322 | } 323 | }) -------------------------------------------------------------------------------- /icon-close-base.js: -------------------------------------------------------------------------------- 1 | module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAECUlEQVRYR72ZWW8bZRSGn/NNSInYAkQgIYQElEVqKWtZqsIVooUEClzkD0SW45v8mChKNBOEfwEVNGkLpFyAaCCRClQti4ra9IJUQtxQELVrmDnoWGPkGC+zePpJvvJn+53nW877Hovv+2vAfYAAp4EPgS/L5fJVbuBYXFy80zn3ioi8A7wAXAd+Ed/3fwAeADwR+VpVP1LVTxuNxsW5uTmbVPioVqs3NxqNR4DXReQtVX0OuAZcNoE/Ag8Cu4A/VPWsiKxGUXSiUqmcL1qdqpqGfSIypapTIrIXuBX4E7goQRC8p6q7gWeA24B/gHURORpF0VqRJFvkROSQqtrSHoiBXBWRzSiKfjb1k8CjNkFEbIIXq/8WOF4UyXZygGl4IiZn22odOApckGq1Ol6v1+8XEZs0KSJPFU2yC7mX4kNqB/MbA+OcO1Gr1bbt5DI/P79rdHT0YRF5TVXfFRH7wEgRJPuRU9XTtrXCMDw1MTGxNT093WgKbI2lpaW9RZJMSq5UKtnBbY4dAoskmWDPfdBOrqvAokhmIddX4DBJZiXXV+CwSOYhl0hgHpJ5ySUSmJXkMMilEpiA5KqqHp+dnTXjwfLy8r4oiqYAe3VWiK6ntVfN33HNDDIGPe7Jv4EvzKaJyEYYhiIiB0XkCPBytwrRfs8N+s1UAvuQ/E1VzznntmzvAWY+zJXcFfs6q62pyKVa4s6nNJLOuTeAw8CeuHYbSfNwJtDskgPMlZyLouhjz/NOpiGXS6CRHBsbeygMw1eBt0Vkfyyy/Vl+Bzbjpf9sfHz8stXWQUva+X6qJe78cBAE+1V1NrZL93a8vy0iK2EYBpVKxaxbppFLoO/7lh3KscB7OhRcAVacc8ulUulMJnWdZiHpl9g9V6/Xd5s9swwBPBvvu/avMMu+aRnH87xTtVrtUpaMk5pgW4WYVNXDIvJ4mxP+K1Z4O3BTnHHOi8gnWZ15KoF9KsSvwFlgK14VS2h2QU/kzTiJBQ6orZ/HeXrDOSdhGB6I861d1LmceSKBCWrrCnCyXC7/ZEscBMGeuNTlzjgDBWZxJQlqd+K02FdgAnLN9NWrQgwj4/QUmIVc5zU1DJJdBeYl1612Z02L/xM4DHLDJLlD4LDJDYPkfwKLIDcMkk2BRZPLQ1IWFhbuHhkZseaR5YduXaZMTrif8Uh6ulV12/qD1tF8DLD+3It5M0RSR2Tzet2TqvqVNZFU9YL1B98HrLg/DdySN0OkEdiPpIicaQn8Pm4BjwFm05uNy34VIo2IJHM7SD4JmF0zP3nJCH7X1kTfUNVjYRiutfpzSX4g75w2koeANwFrotdbTfTV1t8QqrrunDvmed76zMyMPcENG77v3wEcVNUjIvI8YAFr+1+qgM0tSRtH6AAAAABJRU5ErkJggg==" -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | Modal, 7 | Animated, 8 | TouchableOpacity, 9 | ActivityIndicator, 10 | TouchableWithoutFeedback, 11 | StyleSheet 12 | } from 'react-native' 13 | 14 | import { deviceWidth, deviceHeight, px2dp } from 'react-native-style-adaptive' 15 | 16 | import TabView from './TabView' 17 | 18 | const Icon = require('./icon-close-base') 19 | 20 | export default class extends Component { 21 | static defaultProps = { 22 | prompt: '请选择', 23 | titleStyle: { 24 | content: {}, 25 | text: {} 26 | }, 27 | loading: true 28 | } 29 | 30 | constructor (props) { 31 | super(props) 32 | 33 | this.initH = parseInt(deviceHeight() * 4 / 6) 34 | 35 | this.state = { 36 | progress: new Animated.Value(this.initH), 37 | visible: false, 38 | initPage: 0, 39 | address: [], 40 | tabs: [], 41 | tags: [] 42 | } 43 | 44 | this.timer = null 45 | this.onBackClicked = this.onBackClicked.bind(this) 46 | this.setAddress = this.setAddress.bind(this) 47 | } 48 | 49 | async load () { 50 | let tabs = this.props.tabs.length ? this.props.tabs : [this.props.prompt] 51 | let address = new Array(tabs.length) 52 | let tags = this.state.tags 53 | 54 | await tabs.reduce(async (prev, next, index) => { 55 | if (!prev && !!index) return null 56 | 57 | const eachAddress = await this.getAddress(await prev, index) 58 | const result = await this._than(eachAddress, next) 59 | const tag = parseInt(result.key) 60 | 61 | address[index] = eachAddress 62 | tags[index] = isNaN(tag) ? '' : tag 63 | 64 | return result.option 65 | }, null) 66 | 67 | this.setState({ address, tags, tabs, initPage: tabs.length - 1 }) 68 | } 69 | 70 | _than (data, target) { 71 | if (!target && target != 0) return { option: null, key: '' } 72 | 73 | if (Array.isArray(data)) { 74 | for (let key of Object.keys(data)) { 75 | if (typeof target === 'object') { 76 | if (data[key] === target) { 77 | return { option: data[key], key } 78 | } else { 79 | return this._deepComparison(data[key], target) 80 | } 81 | } else { 82 | if (data[key] === target) return { option: data[key], key } 83 | 84 | if (typeof data[key] === 'object') { 85 | for (let keys of Object.keys(data[key])) { 86 | if (data[key][keys] === target) { 87 | return { option: data[key], key } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | return { option: null, key: '' } 96 | } 97 | 98 | _deepComparison (data, target) { 99 | if (Object.prototype.toString(data) !== Object.prototype.toString(target)) return { option: null, key: '' } 100 | 101 | for (let key of Object.keys(data)) { 102 | if (data[key] === target[key]) return { option: data[key], key } 103 | } 104 | 105 | return { option: null, key: '' } 106 | } 107 | 108 | async getAddress (target, index, tabs) { 109 | const address = await this.props.load(target, index) 110 | 111 | if (!address) { 112 | this.setState({ tabs, initPage: tabs.length - 1 }) 113 | this.props.result(tabs) 114 | this.close() 115 | 116 | return false 117 | } 118 | 119 | if (!Array.isArray(address)) throw Error(`The load method expects an Array type value, but it is actually an ${Object.prototype.toString.call(address).match(/^\[object (.*?)\]$/)[1] } type value`) 120 | 121 | return address 122 | } 123 | 124 | async setAddress (target, index, tabs) { 125 | 126 | const address = await this.getAddress(target, index, tabs) 127 | 128 | if (!address) return false 129 | 130 | await this.tabView.setNativeProps(address) 131 | } 132 | 133 | render () { 134 | const { visible, progress, initPage, address, tags, tabs } = this.state 135 | const { titleStyle, contentStyle, prompt, listStyle, tabStyle, activeColor, loading } = this.props 136 | 137 | return ( 138 | this.onBackClicked.bind(this) } 143 | style={ styles.container } 144 | > 145 | this.close() } 147 | > 148 | 156 | 157 | 162 | 163 | 配送至 164 | this.close()}> 165 | 169 | 170 | 171 | { !tabs.length ? 172 | 173 | 174 | 175 | : this.tabView = ref } 178 | initPage={ initPage } 179 | address={ address } 180 | tags={ tags } 181 | selected={ this.setAddress } 182 | tabs={ tabs } 183 | prompt={ prompt } 184 | listStyle={ listStyle } 185 | tabStyle={ tabStyle } 186 | activeColor={ activeColor } 187 | loading={ loading } 188 | />} 189 | 190 | 191 | ) 192 | } 193 | 194 | onBackClicked () { 195 | this.close() 196 | } 197 | 198 | open () { 199 | this.load() 200 | this.setState({ visible: true }, () => { 201 | Animated.spring( 202 | this.state.progress, 203 | { 204 | toValue: 0, 205 | velocity: 40, 206 | tension: 10, 207 | } 208 | ).start() 209 | }) 210 | } 211 | 212 | close () { 213 | Animated.spring( 214 | this.state.progress, 215 | { 216 | toValue: this.initH, 217 | velocity: 10, 218 | tension: 30, 219 | } 220 | ).start() 221 | 222 | this.timer = setTimeout(() => { 223 | clearTimeout(this.timer) 224 | this.setState({ visible: false, tabs: [] }) 225 | }, 1000) 226 | } 227 | 228 | componentWillUnmount () { 229 | this.state.visible && this.close() 230 | clearTimeout(this.timer) 231 | } 232 | } 233 | 234 | const styles = StyleSheet.create({ 235 | container: { 236 | flex: 1, 237 | position: 'relative' 238 | }, 239 | mask: { 240 | flex: 1, 241 | backgroundColor: '#000', 242 | opacity: 0 243 | }, 244 | content: { 245 | position: 'absolute', 246 | left: 0, 247 | bottom: 0, 248 | width: deviceWidth(), 249 | height: parseInt(deviceHeight() * 4 / 6), 250 | }, 251 | tabView: { 252 | backgroundColor: '#fff' 253 | }, 254 | tabViewLoad: { 255 | flex: 1, 256 | backgroundColor: '#fff', 257 | justifyContent: 'center', 258 | alignItems: 'center' 259 | }, 260 | titleMain: { 261 | flexDirection: 'row', 262 | justifyContent: 'flex-end', 263 | alignItems: 'center', 264 | height: px2dp(80), 265 | backgroundColor: '#fff', 266 | borderBottomColor: '#EEE', 267 | borderBottomWidth: 1, 268 | position: 'relative' 269 | }, 270 | title: { 271 | position: 'absolute', 272 | left: 0, 273 | right: 0, 274 | flex: 1, 275 | fontSize: px2dp(30), 276 | color: '#222', 277 | textAlign: 'center' 278 | }, 279 | icons: { 280 | position: 'absolute', 281 | justifyContent: 'center', 282 | alignItems: 'center', 283 | width: px2dp(80), 284 | top: 0, 285 | bottom: 0, 286 | right: 0, 287 | zIndex: 999 288 | }, 289 | close: { 290 | width: px2dp(21), 291 | height: px2dp(21) 292 | } 293 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-city-picker", 3 | "version": "1.0.3", 4 | "description": "a three-level linkage selector", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/AaronBank/react-native-city-picker.git" 12 | }, 13 | "keywords": [ 14 | "city-picker", 15 | "react-native", 16 | "picker", 17 | "city", 18 | "地址选择器", 19 | "三级联动" 20 | ], 21 | "author": "Aaron Shen", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/AaronBank/react-native-city-picker/issues" 25 | }, 26 | "homepage": "https://github.com/AaronBank/react-native-city-picker#readme", 27 | "peerDependencies": { 28 | "react-native": ">=0.42.0" 29 | }, 30 | "dependencies": { 31 | "react-native-scrollable-tab-view": "^0.10.0", 32 | "react-native-style-adaptive": "^1.1.5" 33 | } 34 | } --------------------------------------------------------------------------------