├── .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 | ![](https://github.com/superapp/react-native-location-view/blob/master/screenrecording/screengrab.gif?raw=true) 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 | } --------------------------------------------------------------------------------