从健康类 app Runkeeper 到游戏 app 精灵宝可梦,位置服务对现代 app 来讲愈来愈重要。html
在本文中,咱们将建立一个 app,名字就叫作 City Guide。这个 app 容许用户搜索一个地点,使用 Google 地图显示这个地点的位置并监听用户的位置改变。java
咱们将学习如何使用 Google 地图 API for Android,Google 的位置服务 API 和 Google 的 Places API for Android 完成以下工做:android
打开 Android Studio,在快速启动菜单中选择 Start a new Android Studio projectgit
在建立新项目对话框,New Project 视图,输入 app 名称 City Guide,选择保存地址,点击 Next。web
在 Target Android Devices 窗口,勾选 Phone and Tablet 选框,选择你想要 app 支持的 minimum SDK。从 Minimum SDK 的下拉框中选择 API 14。而后点 Next。api
在 Add an Activity to Mobile 窗口,选择 Google Maps Activity 而后点 Next。服务器
在 Customize the Activity 窗口,点击 Finish,完成项目的建立。app
Android Studio 将启动 Gradle 并编译项目。这会花几分钟。
打开 MapsActivity.java。它应该是这个样子:ide
package com.raywenderlich.cityguide; import android.support.v4.app.FragmentActivity; import android.os.Bundle; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; // 1 public class MapsActivity extends FragmentActivity implements OnMapReadyCallback { private GoogleMap mMap; // 2 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_maps); // Obtain the SupportMapFragment and get notified when the map is ready to be used. SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mapFragment.getMapAsync(this); } // 3 @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; // Add a marker in Sydney and move the camera LatLng sydney = new LatLng(-34, 151); mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney")); mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney)); } }
Android Studio 在 manifests/AndroidManifest.xml 中添加了以下代码:学习
Android Studio 也在 build.gradle 中添加了一个 Google Play Service 的依赖。这个依赖将 Google 地图和定位服务 API 暴露给 app 使用。
当编译完成后,运行 app 你会看到:
你看到一个空白窗口,上面没有地图;你尚未为 Google Map 建立 API key。咱们将在下一节建立。
注意:若是你使用模拟器,模拟器所安装的版本必须知足 build.gradle 文件中 Google Play Service 所要求的版本。若是你看到提示须要升级模拟器的 Google Play Service 版本,你能够从 Android Studio SDK 管理器中下载最新的 Google APIs 并安装到虚拟设备,或者下降 gradle 依赖中的版本。
要使用任何 Google 地图 API,都须要建立一个 API key 并从开发者控制台中启用所需的 API。若是你没有 Google 帐号,如今就去建立它——免费的!
打开 res/values/google_maps_api.xml,你会看到:
在下一页,点 Create API key 按钮。
而后,复制 API key created 对话框中的 API key,点击 Close。
回到 google_maps_api.xml, 将 google_maps_key 替换成刚才拷贝的 API key。
运行 app,你会看到地图和地图上的红色大头钉。
回到 developer console,打开 Google Places API for Android。咱们会在后面用这个 API 查找 Place。
在编写 Java 代码以前,咱们须要配置一下 Android Studio 让它自动为咱们插入 import 语句,节省咱们的工做量。
依次打开 Android Studio > Preferences > Editor > General > Auto Import 菜单,选择 Add unambiguous imports on the fly 和 Show import popup 选项,点击 OK。
打开 MapsActivity.java ,让 MapsActivity 实现下列接口:
public class MapsActivity extends FragmentActivity implements OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, GoogleMap.OnMarkerClickListener, LocationListener
import LocationListener 一句产生了歧义,所以告诉 Android Studio 去 Google Mobile Services 进行导入:
import com.google.android.gms.location.LocationListener;
上述代码解释以下:
如今,实现上述接口定义的方法。要这样作,能够按如下步骤:
选择 Implement methods。
在 Select Methods to implement 对话框,点击 OK。
这些方法的实现会添加到类中。
要链接 Google Play Services 库中的 Google API,你须要先建立一个 GoogleApiClient。
在 MapsActivity.java 中添加一个字段:
private GoogleApiClient mGoogleApiClient;
在 onCreate() 中加入:
// 1 if (mGoogleApiClient == null) { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); }
添加两个方法:
@Override protected void onStart() { super.onStart(); // 2 mGoogleApiClient.connect(); } @Override protected void onStop() { super.onStop(); // 3 if( mGoogleApiClient != null && mGoogleApiClient.isConnected() ) { mGoogleApiClient.disconnect(); } }
代码说明:
添加下列代码到 onMapReady():
mMap.getUiSettings().setZoomControlsEnabled(true); mMap.setOnMarkerClickListener(this);
这里咱们开启了地图的缩放控制并指定了 MapsActdivity 做为回调,这样当用户点击大头钉时可以进行处理。
如今,点击地图上位于悉尼的大头钉,你会看到显示了标题文本:
输入另一个坐标,大头钉会移到你指定的位置。
添加下列代码将大头钉设置到纽约,标题文本设置“My Favorite City”:
LatLng myPlace = new LatLng(40.73, -73.99); // this is New York mMap.addMarker(new MarkerOptions().position(myPlace).title("My Favorite City")); mMap.moveCamera(CameraUpdateFactory.newLatLng(myPlace));
编译运行。
注意,地图自动将中心和大头钉对齐,moveCamera() 的做用就在于次。可是,地图的缩放比例不正确,由于它是缩得过小了。
将 moveCamera() 方法调用修改成:
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(myPlace, 12));
缩方比例 0 表示将地图缩小为最小的世界地图。大部分地图都支持缩放比例到 20,更远的地区仅仅支持到 13,将它设为两者之间的 12 比较合适,显示较多的细节且不会太近。
运行 app 以查看效果。
咱们的 app 须要使用 ACCESS_FINE_LOCATION 权限以得到用户定位信息;在 AndroidManifest.xml 中咱们已经进行了声明。
从 Android 6.0 开始,用户权限与以前发生了一点点区别。你不会在安装 app 时请求权限,而是在运行时,当权限真正须要用到时才请求。
权限分为两种类别:普通权限和危险权限。对于危险权限须要在运行时向用户请求受权。要求访问用户隐私的权限好比访问用户通信录、日历、定位等就属于危险权限。
打开 MapsActivity.java 添加下列变量:
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1;
新加一个方法 setUpMap() 。
private void setUpMap() { if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] {android.Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE); return; } }
上述代码判断 app 是否得到了 ACCESS_FINE_LOCATION 权限。若是没有,向用户请求受权。
而后在 onConnectded() 方法中调用这个方法:
@Override public void onConnected(@Nullable Bundle bundle) { setUpMap(); }
注意:关于用户权限的完整介绍超出了本文的范畴,请参考运行时请求受权的文档。
定位服务的最多见任务是得到用户当前坐标。咱们经过 Google Play 服务定位 API 请求用户设备的最新坐标来实现这个目的。
在 MapsActivity.java, 添加变量:
private Location mLastLocation;
而后,在setUpMap() 最后一句添加代码:
// 1 mMap.setMyLocationEnabled(true); // 2 LocationAvailability locationAvailability = LocationServices.FusedLocationApi.getLocationAvailability(mGoogleApiClient); if (null != locationAvailability && locationAvailability.isLocationAvailable()) { // 3 mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); // 4 if (mLastLocation != null) { LatLng currentLocation = new LatLng(mLastLocation.getLatitude(), mLastLocation .getLongitude()); mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLocation, 12)); } }
代码说明:
编译运行,查看效果。你会看到在用户当前坐标有一个浅蓝色的圆点:
要测试地图类 app,最好用真正的 Android 设备。若是由于某种缘由不得不在模拟器上测试,你能够用模拟器模拟出坐标数据。
要作到这个,一种办法是使用模拟器的扩展控制(extended controls)。你须要这样作:
打开模拟器。在右边面板中,点击 More 按钮(…) 以访问 extended controls。
在下图指定位置输入经纬度,点击 Send。
注意最后一次运行 app 时,用户位置所在的蓝点很是显眼。Android 地图 API 容许你使用大头钉,这是一种图标,用于放在地图上层的指定位置。
在 MapsActivity.java 中添加代码:
protected void placeMarkerOnMap(LatLng location) { // 1 MarkerOptions markerOptions = new MarkerOptions().position(location); // 2 mMap.addMarker(markerOptions); }
将 setUpMap() 方法替换为:
private void setUpMap() { if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] {android.Manifest.permission.ACCESS_FINE_LOCATION},LOCATION_PERMISSION_REQUEST_CODE); return; } mMap.setMyLocationEnabled(true); LocationAvailability locationAvailability = LocationServices.FusedLocationApi.getLocationAvailability(mGoogleApiClient); if (null != locationAvailability && locationAvailability.isLocationAvailable()) { mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); if (mLastLocation != null) { LatLng currentLocation = new LatLng(mLastLocation.getLatitude(), mLastLocation .getLongitude()); //add pin at user's location placeMarkerOnMap(currentLocation); mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLocation, 12)); } } }
setUpMap() 方法中的改变仅仅是调用了 placeMarkerOnMap() 以显示大头钉。
编译运行查看效果。你如今会在用户位置看到一个大头钉:
若是你不喜欢 Android 默认的大头钉样式,你也能够建立本身的图片取代。回到 placeMarkerOnMap() 方法,在 MarkerOptions 初始化以后加入下句:
markerOptions.icon(BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource
(getResources(), R.mipmap.ic_user_location)));
从这里下载自定义大头钉文件 ic_user_location,而后解压缩。将全部文件拷贝到 mipmap 目录:
编译运行查看效果。在你当前位置的大头钉如今使用了项目中的 ic_user_location 图片:
若是仅仅是修改默认大头钉的颜色呢?请自行进行尝试,若是有难度请参考这个答案:
在 placeMarkerOnMap() 中使用这句:
```java
markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));
“`
这会将默认大头钉的红色换成绿色。

根据 app 要实现的功能,通常的地图视图可能对你就不够用了。
Android 地图 API 提供了几种地图类型:MAP_TYPE_NORMAL、MAP_TYPE_SATELLITE、 MAP_TYPE_TERRAIN、MAP_TYPE_HYBRID。
在 setUpMain() 方法的 setMyLocationEnabled() 后面加入一句:
mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
GoogleMap.MAP_TYPE_TERRAIN 显示更详细的地形,显示地貌变化:
视图 a more detailed view of the area, showing changes in elevation:
其它类型的效果:
GoogleMap.MAP_TYPE_SATELLITE 显示卫星地图,没有文字标注。
GoogleMap.MAP_TYPE_HYBRID 显示卫星地图和普通视图的结合。
GoogleMap.MAP_TYPE_NORMAL 显示典型的街道地图并标注标签。这也是默认的类型。
如今你已经得到了用户的坐标,若是在用户点击大头钉时显示地理名称就行了。Google 有一个 Geocoder 就是用来干这个的。它将经纬度坐标转换为一我的类可读的地址,或者与此相反。
打开 MapsActivity,添加方法:
private String getAddress( LatLng latLng ) { // 1 Geocoder geocoder = new Geocoder( this ); String addressText = ""; List<Address> addresses = null; Address address = null; try { // 2 addresses = geocoder.getFromLocation( latLng.latitude, latLng.longitude, 1 ); // 3 if (null != addresses && !addresses.isEmpty()) { address = addresses.get(0); for (int i = 0; i < address.getMaxAddressLineIndex(); i++) { addressText += (i == 0)?address.getAddressLine(i):("\n" + address.getAddressLine(i)); } } } catch (IOException e ) { } return addressText; }
关键在于 Address 类是有歧义的,要解决这个问题,须要将 import 语句指定为:
import android.location.Address;
代码说明:
将 placeMarkerOnMap() 方法修改成:
protected void placeMarkerOnMap(LatLng location) { MarkerOptions markerOptions = new MarkerOptions().position(location); String titleStr = getAddress(location); // add these two lines markerOptions.title(titleStr); mMap.addMarker(markerOptions); }
在这个方法中添加了一句 getAddress() 调用,并将地址设置为大头钉标题。
编译运行以查看效果。点击大头钉,你会看到地址:
点击地图的其余地方,地址消失。
注意,若是你走动位置,蓝点会跟着你一块儿移动,但大头钉不会。若是你在真机上测试,试着四处移动一下位置。若是在模拟器上测试,将你的坐标用 emulator control 修改到别的地方。
大头钉不会移动是由于咱们的代码还不知道何时位置发生了变化。小蓝点位置由 Google API 本身管理,而不是咱们的代码作的。若是想让 marker 跟随小蓝点移动,须要在代码中接收位置变化通知。
随时知道用户的位置有助于提供一种良好体验。本节将介绍如何实时接收用户位置的变化。
为了作到这一点,你须要建立一个 location request。
打开 MapsActivity,增长变量:
// 1 private LocationRequest mLocationRequest; private boolean mLocationUpdateState; // 2 private static final int REQUEST_CHECK_SETTINGS = 2;
声明一个 LocationRequest 变量以及一个保存位置更新状态的变量。
REQUEST_CHECK_SETTINGS 是用于传递给 onActivityResult 方法的 request code。
而后添加方法:
protected void startLocationUpdates() { //1 if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE); return; } //2 LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this); }
而后添加方法:
// 1 protected void createLocationRequest() { mLocationRequest = new LocationRequest(); // 2 mLocationRequest.setInterval(10000); // 3 mLocationRequest.setFastestInterval(5000); mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() .addLocationRequest(mLocationRequest); PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, builder.build()); result.setResultCallback(new ResultCallback<LocationSettingsResult>() { @Override public void onResult(@NonNull LocationSettingsResult result) { final Status status = result.getStatus(); switch (status.getStatusCode()) { // 4 case LocationSettingsStatusCodes.SUCCESS: mLocationUpdateState = true; startLocationUpdates(); break; // 5 case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: try { status.startResolutionForResult(MapsActivity.this, REQUEST_CHECK_SETTINGS); } catch (IntentSender.SendIntentException e) { } break; // 6 case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: break; } } }); }
ResultCallback 类的 import 语句有歧义,所以添加下列 import 语句:
import com.google.android.gms.common.api.ResultCallback;
createLocationRequest() 方法代码说明以下:
RESOLUTION_REQUIRED 状态代表位置设置有一个问题有待修复。有多是由于用户的位置设置被关闭了。你能够向用户显示一个对话框:
如今添加下列方法:
// 1 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CHECK_SETTINGS) { if (resultCode == RESULT_OK) { mLocationUpdateState = true; startLocationUpdates(); } } } // 2 @Override protected void onPause() { super.onPause(); LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); } // 3 @Override public void onResume() { super.onResume(); if (mGoogleApiClient.isConnected() && !mLocationUpdateState) { startLocationUpdates(); } }
代码说明:
而后,在 onCreate() 方法的最后调用 createLocationRequest() 方法。
createLocationRequest();
而后,在 onConnected() 方法中添加以下语句:
if (mLocationUpdateState) { startLocationUpdates(); }
若是用户的位置设置是打开状态的话,启动位置更新。
在 onLocationChanged() 方法中加入:
mLastLocation = location; if (null != mLastLocation) { placeMarkerOnMap(new LatLng(mLastLocation.getLatitude(), mLastLocation.getLongitude())); }
这里,咱们修改 mLastLocation 为最新的位置并用新位置坐标刷新地图显示。
你的 app 如今已经能够接受位置变化通知了。当你改变位置,地图上的大头钉会随位置的改变而变。注意,点击大头钉仍然可以看到地址信息。
编译运行,四处走动查看变化:
由于 app 是用于扮演一个向导的角色,用户应该可以找到他们感兴趣的地方吧?
这就是 Google Places API 出场的时候了。它让你的 app 可以搜索数百万计的兴趣点和大型机构。Android 库有许多很是酷的功能,其中之一就是 Place Picker,这是一个 UI widget,容许你用寥寥数行代码就实现一个搜索 PIO(兴趣点)的功能。太好了,这是真的吗?你能够试一试。
打开MapsActivity,添加变量:
private static final int PLACE_PICKER_REQUEST = 3;
而后添加下列方法:
private void loadPlacePicker() { PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder(); try { startActivityForResult(builder.build(MapsActivity.this), PLACE_PICKER_REQUEST); } catch(GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) { e.printStackTrace(); } }
这个方法建立了新的 builder 用于建立 intent,这个 Intent 用于打开一个 Place Picker UI,而后打开这个 PlacePicker Intent。
将下列语句添加到 onActivityResult():
if (requestCode == PLACE_PICKER_REQUEST) { if (resultCode == RESULT_OK) { Place place = PlacePicker.getPlace(this, data); String addressText = place.getName().toString(); addressText += "\n" + place.getAddress().toString(); placeMarkerOnMap(place.getLatLng()); } }
在这里,若是请求代码是 PLACE_PICKER_REQUEST 且返回码是 RESULT_OK,则读取所选地点的信息。而后放一个大头钉在该位置。
搜索 PIO 基本搞定——剩下的就是调用 loadPlacePicker() 方法。
咱们须要建立一个浮动的 Action 按钮(FAB)在地图右下角并用于调用这个方法。FAB 须要使用 CoordinatorLayout,这是 design 支持库中的内容。
首先,打开 build.gradle 添加依赖 Android support design library:
dependencies { ... compile 'com.android.support:design:24.1.1' }
注意:一般,若是你用的 Android SDK 版本比较新,你可能须要同时升级这个依赖的的版本,以便两者匹配。
而后修改 res > layout > activity_maps.xml 为:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <fragment android:id="@+id/map" class="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:src="@android:drawable/ic_menu_search"/> </android.support.design.widget.CoordinatorLayout>
咱们在原先的地图上已经有一个用于显示地图的 fragment;如今所作的就是添加一个 FAB。
在 MapsActivity 的 onCreate() 方法,添加以下代码:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { loadPlacePicker(); } });
编译运行,点击地图下方的 search 按钮,会弹出 place picker:
https://koenig-media.raywenderlich.com/uploads/2016/09/placepickerdemo4.gif” width= “320”/>
从这里下载最终完成的项目。
关于 Google 地图 APIs,本文只介绍了不多一部分。在 Google 官方文档中,有更多关于 web service 和这个 Android API 的内容。
你还能够查看开发者页面中其它定制大头钉的方法。本文中的运行时用户权限检查须要改进,这里也有很好的东西能够参考:关于更高级的权限受权。
更多阅读,请参考开发者页面:Google Places API for Android、接受位置变化通知 和模拟位置数据模拟器的 extendet controls。
有问题和建议,请在下面留言。