├── 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 |   
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 |   
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 | }
--------------------------------------------------------------------------------