├── .gitignore
├── .npmignore
├── .prettierrc
├── .watchmanconfig
├── README.md
├── index.js
├── npmignore
├── package.json
├── screenrecording
└── screengrab.gif
├── src
├── AutoCompleteInput.js
├── AutoCompleteListView.js
└── LocationView.js
└── utils
└── debounce.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | node_modules
3 | yarn.lock
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 |
2 | # OSX
3 | #
4 | .DS_Store
5 |
6 | # node.js
7 | #
8 | node_modules/
9 | npm-debug.log
10 | yarn-error.log
11 |
12 |
13 | # Xcode
14 | #
15 | build/
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata
25 | *.xccheckout
26 | *.moved-aside
27 | DerivedData
28 | *.hmap
29 | *.ipa
30 | *.xcuserstate
31 | project.xcworkspace
32 |
33 |
34 | # Android/IntelliJ
35 | #
36 | build/
37 | .idea
38 | .gradle
39 | local.properties
40 | *.iml
41 |
42 | # BUCK
43 | buck-out/
44 | \.buckd/
45 | *.keystore
46 | screenrecording
47 | .prettierrc
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "trailingComma": "es5",
4 | "singleQuote": true
5 | }
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": [
3 | ".git",
4 | "node_modules"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-location-view
2 |
3 | Simple location picker with maps and Google Places API support.
4 |
5 | ### Preview
6 |
7 | 
8 |
9 | ### Installation
10 |
11 | Download an install the library
12 |
13 | ```npm install react-native-location-view --save```
14 |
15 | Or if you are using yarn
16 |
17 | ```yarn add react-native-location-view```
18 |
19 | This library depends upon 2 other native libraries
20 |
21 | 1. [react-native-maps](https://github.com/react-community/react-native-maps)
22 | 2. [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons)
23 |
24 | Make sure to install these before you install react-native-location-view
25 |
26 | For Google Places API go to [this](https://developers.google.com/places/documentation/) page and enable "Google Places API Web Service" (NOT Android or iOS) in the console.
27 |
28 | ### Example
29 |
30 | ```jsx
31 | import React from 'react';
32 | import LocationView from "react-native-location-view";
33 | import {View} from "react-native";
34 |
35 |
36 | export default class SelectLocationScreen extends React.Component {
37 | state = {
38 |
39 | };
40 |
41 | render() {
42 | return(
43 |
44 |
51 |
52 | );
53 | }
54 | }
55 | ```
56 |
57 | ### Supported Props
58 |
59 | | Prop | Type | Required |
60 | | ---- | ---- | -------- |
61 | | apiKey | string | Yes |
62 | | initialLocation | object | Yes |
63 | | markerColor | string | No |
64 | | actionButtonStyle | object (style) | No |
65 | | actionTextStyle | object (style) | No
66 | | actionText | string | No |
67 | | onLocationSelect | function | No |
68 | | debounceDuration | number | No |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import LocationView from "./src/LocationView";
2 |
3 | export default LocationView;
--------------------------------------------------------------------------------
/npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superapp/react-native-location-view/42730d2666bb27fcde306cbed0e0a9aef9beba10/npmignore
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-location-view",
3 | "version": "0.3.0",
4 | "description": "A package to help pick user location with autocomplete and current location support. Uses react-native-maps",
5 | "main": "index.js",
6 | "author": "Hunaid Hassan ",
7 | "license": "MIT",
8 | "private": false,
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/superapp/react-native-location-view"
12 | },
13 | "scripts": {
14 | "test": "echo \"Error: no test specified\" && exit 1",
15 | "format": "prettier --write \"src/**/*.js\"",
16 | "lint": "eslint --fix",
17 | "prepublishOnly": "npm run lint",
18 | "preversion": "npm run lint",
19 | "version": "npm run format && git add -A src",
20 | "postversion": "git push && git push --tags"
21 | },
22 | "keywords": [
23 | "autocomplete",
24 | "google",
25 | "places",
26 | "react-component",
27 | "react-native",
28 | "ios",
29 | "android",
30 | "location",
31 | "location-picker"
32 | ],
33 | "bugs": {
34 | "url": "https://github.com/superapp/react-native-location-view"
35 | },
36 | "dependencies": {
37 | "axios": "^0.19.0",
38 | "prop-types": "^15.6.0",
39 | "react-native-simple-events": "^1.0.1"
40 | },
41 | "files": [
42 | "index.js",
43 | "src/*",
44 | "utils/*"
45 | ],
46 | "peerDependencies": {
47 | "react-native": ">= 0.50",
48 | "react-native-maps": ">= 0.19.0",
49 | "react-native-vector-icons": "^4.4.3",
50 | "@react-native-community/geolocation": "^2.0.0"
51 | },
52 | "devDependencies": {
53 | "eslint": "^6.0.1",
54 | "eslint-config-prettier": "^6.0.0",
55 | "eslint-plugin-prettier": "^3.1.0",
56 | "prettier": "^1.18.2"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/screenrecording/screengrab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superapp/react-native-location-view/42730d2666bb27fcde306cbed0e0a9aef9beba10/screenrecording/screengrab.gif
--------------------------------------------------------------------------------
/src/AutoCompleteInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { TextInput, View, StyleSheet, Animated, TouchableOpacity } from 'react-native';
4 | import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
5 | import AutoCompleteListView from './AutoCompleteListView';
6 | import axios, { CancelToken } from 'axios';
7 | import Events from 'react-native-simple-events';
8 | import debounce from '../utils/debounce';
9 |
10 | const AUTOCOMPLETE_URL = 'https://maps.googleapis.com/maps/api/place/autocomplete/json';
11 | const REVRSE_GEO_CODE_URL = 'https://maps.googleapis.com/maps/api/geocode/json';
12 |
13 | export default class AutoCompleteInput extends React.Component {
14 | static propTypes = {
15 | apiKey: PropTypes.string.isRequired,
16 | language: PropTypes.string,
17 | debounceDuration: PropTypes.number.isRequired,
18 | components: PropTypes.arrayOf(PropTypes.string),
19 | };
20 |
21 | static defaultProps = {
22 | language: 'en',
23 | components: [],
24 | };
25 |
26 | constructor(props) {
27 | super(props);
28 | this._request = debounce(this._request.bind(this), this.props.debounceDuration);
29 | }
30 |
31 | componentWillUnmount() {
32 | this._abortRequest();
33 | }
34 |
35 | state = {
36 | predictions: [],
37 | loading: false,
38 | inFocus: false,
39 | };
40 |
41 | _abortRequest = () => {
42 | if (this.source) {
43 | this.source.cancel('Operation canceled by the user.');
44 | }
45 | };
46 |
47 | fetchAddressForLocation = location => {
48 | this.setState({ loading: true, predictions: [] });
49 | let { latitude, longitude } = location;
50 | this.source = CancelToken.source();
51 | axios
52 | .get(`${REVRSE_GEO_CODE_URL}?key=${this.props.apiKey}&latlng=${latitude},${longitude}`, {
53 | cancelToken: this.source.token,
54 | })
55 | .then(({ data }) => {
56 | this.setState({ loading: false });
57 | let { results } = data;
58 | if (results.length > 0) {
59 | let { formatted_address } = results[0];
60 | this.setState({ text: formatted_address });
61 | }
62 | });
63 | };
64 |
65 | _request = text => {
66 | this._abortRequest();
67 | if (text.length >= 3) {
68 | this.source = CancelToken.source();
69 | axios
70 | .get(AUTOCOMPLETE_URL, {
71 | cancelToken: this.source.token,
72 | params: {
73 | input: text,
74 | key: this.props.apiKey,
75 | language: this.props.language,
76 | components: this.props.components.join('|'),
77 | },
78 | })
79 | .then(({ data }) => {
80 | let { predictions } = data;
81 | this.setState({ predictions });
82 | });
83 | } else {
84 | this.setState({ predictions: [] });
85 | }
86 | };
87 |
88 | _onChangeText = text => {
89 | this._request(text);
90 | this.setState({ text });
91 | };
92 |
93 | _onFocus = () => {
94 | this._abortRequest();
95 | this.setState({ loading: false, inFocus: true });
96 | Events.trigger('InputFocus');
97 | };
98 |
99 | _onBlur = () => {
100 | this.setState({ inFocus: false });
101 | Events.trigger('InputBlur');
102 | };
103 |
104 | blur = () => {
105 | this._input.blur();
106 | };
107 |
108 | _onPressClear = () => {
109 | this.setState({ text: '', predictions: [] });
110 | };
111 |
112 | _getClearButton = () =>
113 | this.state.inFocus ? (
114 |
115 |
116 |
117 | ) : null;
118 |
119 | getAddress = () => (this.state.loading ? '' : this.state.text);
120 |
121 | render() {
122 | return (
123 |
124 |
125 | (this._input = input)}
127 | value={this.state.loading ? 'Loading...' : this.state.text}
128 | style={styles.textInput}
129 | underlineColorAndroid={'transparent'}
130 | placeholder={'Search'}
131 | onFocus={this._onFocus}
132 | onBlur={this._onBlur}
133 | onChangeText={this._onChangeText}
134 | outlineProvider="bounds"
135 | autoCorrect={false}
136 | spellCheck={false}
137 | />
138 | {this._getClearButton()}
139 |
140 |
141 |
142 |
143 |
144 | );
145 | }
146 | }
147 |
148 | const styles = StyleSheet.create({
149 | textInputContainer: {
150 | flexDirection: 'row',
151 | height: 40,
152 | zIndex: 99,
153 | paddingLeft: 10,
154 | borderRadius: 5,
155 | backgroundColor: 'white',
156 | shadowOffset: {
157 | width: 0,
158 | height: 2,
159 | },
160 | shadowRadius: 2,
161 | shadowOpacity: 0.24,
162 | alignItems: 'center',
163 | },
164 | textInput: {
165 | flex: 1,
166 | fontSize: 17,
167 | color: '#404752',
168 | },
169 | btn: {
170 | width: 30,
171 | height: 30,
172 | padding: 5,
173 | justifyContent: 'center',
174 | alignItems: 'center',
175 | },
176 | listViewContainer: {
177 | paddingLeft: 3,
178 | paddingRight: 3,
179 | paddingBottom: 3,
180 | },
181 | });
182 |
--------------------------------------------------------------------------------
/src/AutoCompleteListView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | FlatList,
4 | Text,
5 | View,
6 | StyleSheet,
7 | TouchableOpacity,
8 | LayoutAnimation,
9 | Platform,
10 | TouchableNativeFeedback,
11 | } from 'react-native';
12 | import Events from 'react-native-simple-events';
13 | import PropTypes from 'prop-types';
14 |
15 | export default class AutoCompleteListView extends React.Component {
16 | static propTypes = {
17 | predictions: PropTypes.array.isRequired,
18 | onSelectPlace: PropTypes.func,
19 | };
20 |
21 | state = {
22 | inFocus: false,
23 | };
24 |
25 | componentDidMount() {
26 | Events.listen('InputBlur', 'ListViewID', this._onTextBlur);
27 | Events.listen('InputFocus', 'ListViewID', this._onTextFocus);
28 | }
29 |
30 | componentWillUnmount() {
31 | Events.rm('InputBlur', 'ListViewID');
32 | Events.rm('InputFocus', 'ListViewID');
33 | }
34 |
35 | _onTextFocus = () => {
36 | this.setState({ inFocus: true });
37 | };
38 |
39 | _onTextBlur = () => {
40 | this.setState({ inFocus: false });
41 | };
42 |
43 | componentDidUpdate() {
44 | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
45 | }
46 |
47 | _renderItem({ item }) {
48 | const TouchableControl = Platform.OS === 'ios' ? TouchableOpacity : TouchableNativeFeedback;
49 | const { structured_formatting } = item;
50 | return (
51 | Events.trigger('PlaceSelected', item.place_id)}>
52 |
53 |
54 | {structured_formatting.main_text}
55 |
56 |
57 | {structured_formatting.secondary_text}
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | _getFlatList = () => {
65 | const style = this.state.inFocus ? null : { height: 0 };
66 | return (
67 | }
74 | keyboardShouldPersistTaps={'handled'}
75 | keyExtractor={item => item.id}
76 | />
77 | );
78 | };
79 |
80 | render() {
81 | return Platform.OS === 'android' ? (
82 | this._getFlatList()
83 | ) : (
84 | {this._getFlatList()}
85 | );
86 | }
87 | }
88 |
89 | const styles = StyleSheet.create({
90 | row: {
91 | width: '100%',
92 | height: 50,
93 | justifyContent: 'center',
94 | paddingLeft: 8,
95 | paddingRight: 5,
96 | },
97 | list: {
98 | backgroundColor: 'white',
99 | borderBottomRightRadius: 10,
100 | borderBottomLeftRadius: 10,
101 | maxHeight: 220,
102 | },
103 | listContainer: {
104 | shadowOffset: {
105 | width: 0,
106 | height: 2,
107 | },
108 | shadowRadius: 2,
109 | shadowOpacity: 0.24,
110 | backgroundColor: 'transparent',
111 | borderBottomRightRadius: 10,
112 | borderBottomLeftRadius: 10,
113 | },
114 | separator: {
115 | height: StyleSheet.hairlineWidth,
116 | backgroundColor: 'rgba(0,0,0,0.3)',
117 | },
118 | primaryText: {
119 | color: '#545961',
120 | fontSize: 14,
121 | },
122 | secondaryText: {
123 | color: '#A1A1A9',
124 | fontSize: 13,
125 | },
126 | });
127 |
--------------------------------------------------------------------------------
/src/LocationView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, StyleSheet, Animated, Platform, UIManager,
4 | TouchableOpacity, Text, ViewPropTypes } from 'react-native';
5 | import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
6 | import Entypo from 'react-native-vector-icons/Entypo';
7 | import axios from 'axios';
8 | import Events from 'react-native-simple-events';
9 | import MapView from 'react-native-maps';
10 | import Geolocation from '@react-native-community/geolocation';
11 | import AutoCompleteInput from './AutoCompleteInput';
12 |
13 |
14 | const PLACE_DETAIL_URL = 'https://maps.googleapis.com/maps/api/place/details/json';
15 | const DEFAULT_DELTA = { latitudeDelta: 0.015, longitudeDelta: 0.0121 };
16 |
17 | export default class LocationView extends React.Component {
18 | static propTypes = {
19 | apiKey: PropTypes.string.isRequired,
20 | initialLocation: PropTypes.shape({
21 | latitude: PropTypes.number,
22 | longitude: PropTypes.number,
23 | }).isRequired,
24 | markerColor: PropTypes.string,
25 | actionButtonStyle: ViewPropTypes.style,
26 | actionTextStyle: Text.propTypes.style,
27 | actionText: PropTypes.string,
28 | onLocationSelect: PropTypes.func,
29 | debounceDuration: PropTypes.number,
30 | components: PropTypes.arrayOf(PropTypes.string),
31 | timeout: PropTypes.number,
32 | maximumAge: PropTypes.number,
33 | enableHighAccuracy: PropTypes.bool
34 | };
35 |
36 | static defaultProps = {
37 | markerColor: 'black',
38 | actionText: 'DONE',
39 | onLocationSelect: () => ({}),
40 | debounceDuration: 300,
41 | components: [],
42 | timeout: 15000,
43 | maximumAge: Infinity,
44 | enableHighAccuracy: true
45 | };
46 |
47 | constructor(props) {
48 | super(props);
49 | if (Platform.OS === 'android') {
50 | UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
51 | }
52 | }
53 |
54 | componentDidMount() {
55 | Events.listen('InputBlur', this.constructor.displayName, this._onTextBlur);
56 | Events.listen('InputFocus', this.constructor.displayName, this._onTextFocus);
57 | Events.listen('PlaceSelected', this.constructor.displayName, this._onPlaceSelected);
58 | }
59 |
60 | componentWillUnmount() {
61 | Events.rm('InputBlur', this.constructor.displayName);
62 | Events.rm('InputFocus', this.constructor.displayName);
63 | Events.rm('PlaceSelected', this.constructor.displayName);
64 | }
65 |
66 | state = {
67 | inputScale: new Animated.Value(1),
68 | inFocus: false,
69 | region: {
70 | ...DEFAULT_DELTA,
71 | ...this.props.initialLocation,
72 | },
73 | };
74 |
75 | _animateInput = () => {
76 | Animated.timing(this.state.inputScale, {
77 | toValue: this.state.inFocus ? 1.2 : 1,
78 | duration: 300,
79 | }).start();
80 | };
81 |
82 | _onMapRegionChange = region => {
83 | this._setRegion(region, false);
84 | if (this.state.inFocus) {
85 | this._input.blur();
86 | }
87 | };
88 |
89 | _onMapRegionChangeComplete = region => {
90 | this._input.fetchAddressForLocation(region);
91 | };
92 |
93 | _onTextFocus = () => {
94 | this.state.inFocus = true;
95 | this._animateInput();
96 | };
97 |
98 | _onTextBlur = () => {
99 | this.state.inFocus = false;
100 | this._animateInput();
101 | };
102 |
103 | _setRegion = (region, animate = true) => {
104 | this.state.region = { ...this.state.region, ...region };
105 | if (animate) this._map.animateToRegion(this.state.region);
106 | };
107 |
108 | _onPlaceSelected = placeId => {
109 | this._input.blur();
110 | axios.get(`${PLACE_DETAIL_URL}?key=${this.props.apiKey}&placeid=${placeId}`).then(({ data }) => {
111 | let region = (({ lat, lng }) => ({ latitude: lat, longitude: lng }))(data.result.geometry.location);
112 | this._setRegion(region);
113 | this.setState({placeDetails: data.result});
114 | });
115 | };
116 |
117 | _getCurrentLocation = () => {
118 | const { timeout, maximumAge, enableHighAccuracy } = this.props;
119 | Geolocation.getCurrentPosition(
120 | position => {
121 | const { latitude, longitude } = position.coords;
122 | this._setRegion({latitude, longitude});
123 | },
124 | error => console.log(error.message),
125 | {
126 | enableHighAccuracy,
127 | timeout,
128 | maximumAge,
129 | }
130 | );
131 | };
132 |
133 | render() {
134 | let { inputScale } = this.state;
135 | return (
136 |
137 | (this._map = mapView)}
139 | style={styles.mapView}
140 | region={this.state.region}
141 | showsMyLocationButton={true}
142 | showsUserLocation={false}
143 | onPress={({ nativeEvent }) => this._setRegion(nativeEvent.coordinate)}
144 | onRegionChange={this._onMapRegionChange}
145 | onRegionChangeComplete={this._onMapRegionChangeComplete}
146 | />
147 |
153 |
154 | (this._input = input)}
156 | apiKey={this.props.apiKey}
157 | style={[styles.input, { transform: [{ scale: inputScale }] }]}
158 | debounceDuration={this.props.debounceDuration}
159 | components={this.props.components}
160 | />
161 |
162 |
166 |
167 |
168 | this.props.onLocationSelect({ ...this.state.region, address: this._input.getAddress(), placeDetails: this.state.placeDetails })}
171 | >
172 |
173 | {this.props.actionText}
174 |
175 |
176 | {this.props.children}
177 |
178 | );
179 | }
180 | }
181 |
182 | const styles = StyleSheet.create({
183 | container: {
184 | flex: 1,
185 | justifyContent: 'center',
186 | alignItems: 'center',
187 | },
188 | mapView: {
189 | ...StyleSheet.absoluteFillObject,
190 | },
191 | fullWidthContainer: {
192 | position: 'absolute',
193 | width: '100%',
194 | top: 80,
195 | alignItems: 'center',
196 | },
197 | input: {
198 | width: '80%',
199 | padding: 5,
200 | },
201 | currentLocBtn: {
202 | backgroundColor: '#000',
203 | padding: 5,
204 | borderRadius: 5,
205 | position: 'absolute',
206 | bottom: 70,
207 | right: 10,
208 | },
209 | actionButton: {
210 | backgroundColor: '#000',
211 | height: 50,
212 | position: 'absolute',
213 | bottom: 10,
214 | left: 10,
215 | right: 10,
216 | justifyContent: 'center',
217 | alignItems: 'center',
218 | borderRadius: 5,
219 | },
220 | actionText: {
221 | color: 'white',
222 | fontSize: 23,
223 | },
224 | });
225 |
--------------------------------------------------------------------------------
/utils/debounce.js:
--------------------------------------------------------------------------------
1 | export default function debounce(callback, wait, context = this) {
2 | let timeout = null;
3 | let callbackArgs = null;
4 |
5 | const later = () => {
6 | callback.apply(context, callbackArgs);
7 | timeout = null;
8 | };
9 |
10 | return function () {
11 | if (timeout) {
12 | clearTimeout(timeout);
13 | }
14 | callbackArgs = arguments;
15 | timeout = setTimeout(later, wait);
16 | }
17 | }
--------------------------------------------------------------------------------