十八、地理位置和地图

在本章中,您将了解 React Native 的地理定位和映射功能。我们将从使用地理定位 API 开始;然后我们将继续使用MapView组件绘制感兴趣的点和区域。

我们将依靠react-native-maps包来实现 maps(https://github.com/airbnb/react-native-maps 。本章的目标是介绍 React Native for geolocation 和 React Native Maps for Maps 中的可用内容。

我在哪里?

web 应用用来确定用户所在位置的地理定位 API 也可以由 React 本地应用使用,因为相同的 API 已经过多次填充。在地图之外,此 API 对于从移动设备上的 GPS 获取精确坐标非常有用。然后,我们可以使用这些信息向用户显示有意义的位置数据。

不幸的是,地理定位 API 返回的数据本身用处不大;您的代码必须完成腿部工作才能将其转换为有用的内容。例如,纬度和经度对用户来说没有任何意义,但我们可以使用这些数据查找对用户有用的内容。这可能与显示用户当前所在的位置一样简单。

让我们实现一个示例,使用 React Native 的地理位置 API 查找坐标,然后使用这些坐标从 Google Maps API 查找人类可读的位置信息:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  Text, 
  View, 
} from 'react-native'; 
import { fromJS } from 'immutable'; 

import styles from './styles'; 

// For fetching human-readable address info. 
const URL = 'https://maps.google.com/maps/api/geocode/json?latlng='; 

class WhereAmI extends Component { 
  // The "address" state is "loading..." initially because 
  // it takes the longest to fetch. 
  state = { 
    data: fromJS({ 
      address: 'loading...', 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // We don't setup any geo data till the component 
  // is ready to mount. 
  componentWillMount() { 
    const setPosition = (pos) => { 
      // This component renders the "coords" data from 
      // a geolocation response. This can simply be merged 
      // into the state map. 
      this.data = this.data.merge(pos.coords); 

      // We need the "latitude" and the "longitude" 
      // in order to lookup the "address" from the 
      // Google Maps API. 
      const { 
        coords: { 
          latitude, 
          longitude, 
        }, 
      } = pos; 

      // Fetches data from the Google Maps API then sets 
      // the "address" state based on the response. 
      fetch(`${URL}${latitude},${longitude}`) 
        .then(resp => resp.json(), e => console.error(e)) 
        .then(({ 
          results: [ 
            { formatted_address }, 
          ], 
        }) => { 
          this.data = this.data 
            .set('address', formatted_address); 
        }); 
    }; 

    // First, we try to lookup the current position 
    // data and update the component state. 
    navigator.geolocation.getCurrentPosition(setPosition); 

    // Then, we setup a high accuracy watcher, that 
    // issues a callback whenever the position changes. 
    this.watcher = navigator.geolocation.watchPosition( 
      setPosition, 
      err => console.error(err), 
      { enableHighAccuracy: true } 
    ); 
  } 

  // It's always a good idea to make sure that this 
  // "watcher" is cleared when the component is removed. 
  componentWillUnmount() { 
    navigator.geolocation.clearWatch(this.watcher); 
  } 

  render() { 
    // Since we want to iterate over the properties 
    // in the state map, we need to convert the map 
    // to pairs using "entries()". Then we need to 
    // use the spread operator to make the map iterator 
    // into a plain array. The "sort()" method simply 
    // sorts the map based on it's keys. 
    const state = [ 
      ...this.data 
        .sortBy((v, k) => k) 
        .entries(), 
    ]; 

    // Iterates over the state properties and renders them. 
    return ( 
      <View style={styles.container}> 
        {state.map(([k, v]) => ( 
          <Text key={k} style={styles.label}> 
            {`${k[0].toUpperCase()}${k.slice(1)}`}: {v} 
          </Text> 
        ))} 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'WhereAmI', 
  () => WhereAmI 
); 

该组件的目标是在屏幕上呈现地理定位 API 返回的属性,以及查找用户的特定位置并显示它。如果你看一看componentWillMount()方法,你会发现这就是大多数有趣的代码所在。我们创建了一个setPosition()函数,在几个地方用作回调函数。它的任务是设置组件的状态。

首先,它设置coords属性。通常,我们不会直接显示这些数据,但这是一个显示作为地理定位 API 一部分可用的数据的示例。其次,它使用latitudelongitude值,使用谷歌地图 API 查找用户当前所在位置的名称。

setPosition()回调与getCurrentPosition()一起使用,组件挂载时只调用一次。我们还将setPosition()watchPosition()一起使用,它在用户位置发生变化时随时调用回调。

iOS emulator 和 Genymotion 允许您通过菜单选项更改位置。您不必每次都在物理设备上安装应用来测试更改位置。

让我们看看加载位置数据后此屏幕的外观:

Where am I?

如您所见,我们获取的地址信息在应用中可能比纬度和经度数据更有用。比物理地址文本更好的是在地图上可视化用户的物理位置;我们将在下一节讨论这个问题。

我周围是什么?

来自react-native-mapsMapView组件是在 React 本机应用中渲染贴图的主要工具。

让我们实现一个基本的MapView组件,看看我们能从中得到什么:

import React from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 
import MapView from 'react-native-maps'; 

import styles from './styles'; 

const WhatsAroundMe = () => ( 
  <View style={styles.container}> 
    <MapView 
      style={styles.mapView} 
      showsUserLocation 
      followUserLocation 
    /> 
  </View> 
); 

AppRegistry.registerComponent( 
  'WhatsAroundMe', 
  () => WhatsAroundMe 
); 

没什么大不了的。您传递给MapView 的两个布尔属性为您做了大量工作。showsUserLocation属性将激活地图上的标记,该标记表示运行此应用的设备的物理位置。followUserLocation属性告诉地图在设备移动时更新位置标记。让我们看看结果图:

What's around me?

设备的当前位置在地图上清楚地标出。默认情况下,兴趣点也会在地图上渲染。这些东西离用户很近,这样他们就可以看到周围的东西。

在使用showsUserLocation时,通常最好使用followUserLocation属性。这将使地图缩放到用户所在的区域。

注释兴趣点

到目前为止,您已经看到了MapView组件如何呈现用户的当前位置和用户周围的兴趣点。这里的挑战是,您可能希望显示与应用相关的兴趣点,而不是默认呈现的兴趣点。

在本节中,您将学习如何渲染地图上特定位置的标记,以及如何渲染地图上的区域。

标绘点

让我们规划一些当地的酿酒厂,好吗?以下是如何将注释传递给MapView组件:

import React from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 
import MapView from 'react-native-maps'; 

import styles from './styles'; 

const PlottingPoints = () => ( 
  <View style={styles.container}> 
    <MapView 
      style={styles.mapView} 
      showsPointsOfInterest={false} 
      showsUserLocation 
      followUserLocation 
    > 
      <MapView.Marker 

        description="Duff beer for me, Duff beer for you" 
        coordinate={{ 
          latitude: 43.8418728, 
          longitude: -79.086082, 
        }} 
      /> 
      <MapView.Marker 

        description="New! Patriot Light!" 
        coordinate={{ 
          latitude: -79.086082, 
          longitude: -79.085407, 
        }} 
      /> 
    </MapView> 
  </View> 
); 

AppRegistry.registerComponent( 
  'PlottingPoints', 
  () => PlottingPoints 
); 

注释就是它们听起来的样子,是在基本地图地理之上呈现的附加信息。事实上,在渲染MapView组件时,默认情况下会得到注释,因为它们会显示感兴趣的点。在本例中,我们通过将showsPointsOfInterest属性设置为 false 来选择退出此功能。现在的焦点完全集中在啤酒上。让我们看看这些啤酒厂的位置:

Plotting points

按下地图上显示啤酒厂位置的标记时,将显示详图索引。您给<MapView.Marker>titledescription属性值用于呈现此文本。

绘制覆盖图

在本章的最后一节中,您将学习如何渲染区域覆盖。点是单个纬度/经度坐标。将一个区域视为多个坐标点的连接图。地区可以用于许多目的,例如显示我们在哪里更可能找到 IPA 饮酒者而不是肥胖饮酒者。下面是代码的样子:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
  Text, 
} from 'react-native'; 
import MapView from 'react-native-maps'; 
import { fromJS } from 'immutable'; 

import styles from './styles'; 

// The "IPA" region coordinates and color... 
const ipaRegion = { 
  coordinates: [ 
    { latitude: 43.8486744, longitude: -79.0695283 }, 
    { latitude: 43.8537168, longitude: -79.0700046 }, 
    { latitude: 43.8518394, longitude: -79.0725697 }, 
    { latitude: 43.8481651, longitude: -79.0716377 }, 
    { latitude: 43.8486744, longitude: -79.0695283 }, 
  ], 
  strokeColor: 'coral', 
  strokeWidth: 4, 
}; 

// The "stout" region coordinates and color... 
const stoutRegion = { 
  coordinates: [ 
    { latitude: 43.8486744, longitude: -79.0693283 }, 
    { latitude: 43.8517168, longitude: -79.0710046 }, 
    { latitude: 43.8518394, longitude: -79.0715697 }, 
    { latitude: 43.8491651, longitude: -79.0716377 }, 
    { latitude: 43.8486744, longitude: -79.0693283 }, 
  ], 
  strokeColor: 'firebrick', 
  strokeWidth: 4, 
}; 

class PlottingOverlays extends Component { 
  // The "IPA" region is rendered first. So the "ipaStyles" 
  // list has "boldText" in it, to show it as selected. The 
  // "overlays" list has the "ipaRegion" in it. 
  state = { 
    data: fromJS({ 
      ipaStyles: [styles.ipaText, styles.boldText], 
      stoutStyles: [styles.stoutText], 
      overlays: [ipaRegion], 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // The "IPA" text was clicked... 
  onClickIpa = () => { 
    this.data = this.data 
      // Makes the IPA text bold... 
      .update('ipaStyles', i => i.push(styles.boldText)) 
      // Removes the bold from the stout text... 
      .update('stoutStyles', i => i.pop()) 
      // Replaces the stout overlay with the IPA overlay... 
      .update('overlays', i => i.set(0, ipaRegion)); 
  } 

  // The "stout" text was clicked... 
  onClickStout = () => { 
    this.data = this.data 
      // Makes the stout text bold... 
      .update('stoutStyles', i => i.push(styles.boldText)) 
      // Removes the bold from the IPA text... 
      .update('ipaStyles', i => i.pop()) 
      // Replaces the IPA overlay with the stout overlay... 
      .update('overlays', i => i.set(0, stoutRegion)); 
  } 

 render() { 
    const { 
      ipaStyles, 
      stoutStyles, 
      overlays, 
    } = this.data.toJS(); 

    return ( 
      <View style={styles.container}> 
        <View> 
          { /* Text that when clicked, renders the IPA 
               map overlay. */ } 
          <Text 
            style={ipaStyles} 
            onPress={this.onClickIpa} 
          > 
            IPA Fans 
          </Text> 

          { /* Text that when clicked, renders the stout 
               map overlay. */ } 
          <Text 
            style={stoutStyles} 
            onPress={this.onClickStout} 
          > 
            Stout Fans 
          </Text> 
        </View> 

        { /* Renders the map with the "overlays" array.  
             There will only ever be a single overlay  
             in this array. */ } 
        <MapView 
          style={styles.mapView} 
          showsPointsOfInterest={false} 
          showsUserLocation 
          followUserLocation 
        > 
          {overlays.map((v, i) => ( 
            <MapView.Polygon 
              key={i} 
              coordinates={v.coordinates} 
              strokeColor={v.strokeColor} 
              strokeWidth={v.strokeWidth} 
            /> 
          ))} 
        </MapView> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
 'PlottingOverlays', 
  () => PlottingOverlays 
); 

如您所见,区域数据由几个纬度/经度坐标组成,这些坐标定义了区域的形状和位置。这段代码的其余部分主要是关于按下两个文本链接时的处理状态。默认情况下,将渲染 IPA 区域:

Plotting overlays

按下粗壮文本时,将从地图中删除 IPA 覆盖,并添加粗壮区域:

Plotting overlays

总结

在本章中,您学习了 React Native 中的地理定位和映射。地理定位 API 的工作原理与 web API 相同。在 React 本机应用中使用映射的唯一可靠方法是安装第三方react-native-maps包。

您看到了基本配置MapView组件,以及它如何跟踪用户的位置,并显示相关的兴趣点。然后,您了解了如何绘制自己的兴趣点和兴趣区域。

在下一章中,您将学习如何使用与 HTML 表单控件类似的 React 本机组件收集用户输入。