Google Places API网络服务示例
Google Places API可用于查找附近的地方。
在本教程中,我们将开发一个应用程序,该应用程序显示我们选择的附近地点以及距当前位置的大概距离和时间。
我们将在应用程序中使用带有Distance Matrix API的Google Places API网络服务。
Google Places API
Google Places API网络服务使我们能够根据一些参数来查询位置,例如位置类型,位置是否现在打开等。
附近搜索请求是以下形式的HTTP URL:
https://maps.googleapis.com/maps/api/place/nearbysearch/output?parameters
json是推荐的output,另一个是xml
必需参数为:
- 密钥(API密钥)
- 位置
- rankby =距离或者半径:如果使用其中一个,则不能使用。
注意:rankby = distance
需要指定以下参数之一:
名称:值可以是mcdonalds,kfc等。
类型:值可以是餐厅,咖啡馆等。
关键词
可选参数可以是opennow
,pagetoken
等。
有关更多详细信息,请参阅此页面。
Google Distance Matrix API
"距离矩阵API"用于计算两个或者多个点之间的距离和时间。
Distance Matrix API网址的格式为:
https://maps.googleapis.com/maps/api/distancematrix/outputFormat?parameters
必需的参数是"来源","目的地"和"关键字"。
原点-这包含计算旅行距离和时间的起点。
我们可以传递多个由管道(|)分隔的坐标。
我们还可以传递地址/地点ID而不是坐标,并且服务会自动将其转换为纬度-经度坐标以计算距离和持续时间。
样例代码:
https://maps.googleapis.com/maps/api/distancematrix/json?origins=Washington,DC&destinations=New+York+City,NY&key=YOUR_API_KEY
可选参数为:
- 模式:这需要在"驾驶","骑车","步行","公交"中取一个值
- 避免:对路线实行限制,例如"收费站","室内"等
有关更多详细信息,请访问此页面。
启用API密钥
转到" https://console.developers.google.com/"并启用以下API:
- Google Maps Distance Matrix API
- Google Places API网络服务
- 适用于Android的Google Places API
转到凭据并创建一个新密钥。
现在将密钥限制设置为"无"。
让我们跳到本教程的业务端。
我们将开发一个应用程序,使我们可以根据当前位置搜索附近的地点,并在RecyclerView中显示这些地点。
我们将根据在EditText中输入并以空格分隔的类型和名称关键字来搜索地点。
例如:多米诺骨牌餐厅或者素食咖啡馆
Google Places API示例项目结构
该项目由一个活动组成。
RecyclerView的适配器类。
一个Model类,该类保存每个RecyclerView行的数据。
两个POJO类,用于将JSON响应从Google Places API和Distance Matrix API转换为Gson。
APIClient和ApiInterface用于使用Retrofit和端点。
Google Places API示例代码
在build.gradle
文件中添加以下依赖项
compile 'com.google.android.gms:play-services-location:10.2.1' compile 'com.google.android.gms:play-services-places:10.2.1' compile 'com.google.code.gson:gson:2.7' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.okhttp3:logging-interceptor:3.4.1' compile 'com.squareup.okhttp3:okhttps:3.4.1' compile 'io.nlopez.smartlocation:library:3.3.1' compile 'com.android.support:cardview-v7:25.3.0' compile 'com.android.support:recyclerview-v7:25.3.0'
`compile'io.nlopez.smartlocation:library:3.3.1'是一个LocationTracking第三方库,可减少样板代码。
APIClient.java代码如下:
package com.theitroad.nearbyplaces; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class APIClient { private static Retrofit retrofit = null; public static final String GOOGLE_PLACE_API_KEY = "ADD_YOUR_API_KEY_HERE"; public static String base_url = "https://maps.googleapis.com/maps/api/"; public static Retrofit getClient() { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).addInterceptor(interceptor).build(); retrofit = null; retrofit = new Retrofit.Builder() .baseUrl(base_url) .addConverterFactory(GsonConverterFactory.create()) .client(client) .build(); return retrofit; } }
ApiInterface.java代码如下
package com.theitroad.nearbyplaces; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface ApiInterface { @GET("place/nearbysearch/json?") Call<PlacesPOJO.Root> doPlaces(@Query(value = "type", encoded = true) String type, @Query(value = "location", encoded = true) String location, @Query(value = "name", encoded = true) String name, @Query(value = "opennow", encoded = true) boolean opennow, @Query(value = "rankby", encoded = true) String rankby, @Query(value = "key", encoded = true) String key); @GET("distancematrix/json") //origins/destinations: LatLng as string Call<ResultDistanceMatrix> getDistance(@Query("key") String key, @Query("origins") String origins, @Query("destinations") String destinations); }
PlacesPOJO.java是保存Places API响应的文件。
其代码如下
package com.theitroad.nearbyplaces; import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class PlacesPOJO { public class Root implements Serializable { @SerializedName("results") public List<CustomA> customA = new ArrayList<>(); @SerializedName("status") public String status; } public class CustomA implements Serializable { @SerializedName("geometry") public Geometry geometry; @SerializedName("vicinity") public String vicinity; @SerializedName("name") public String name; } public class Geometry implements Serializable{ @SerializedName("location") public LocationA locationA; } public class LocationA implements Serializable { @SerializedName("lat") public String lat; @SerializedName("lng") public String lng; } }
ResultDistanceMatrix.java类包含来自Distance Matrix API的响应。
代码如下:
package com.theitroad.nearbyplaces; import com.google.gson.annotations.SerializedName; import java.util.List; public class ResultDistanceMatrix { @SerializedName("status") public String status; @SerializedName("rows") public List<InfoDistanceMatrix> rows; public class InfoDistanceMatrix { @SerializedName("elements") public List elements; public class DistanceElement { @SerializedName("status") public String status; @SerializedName("duration") public ValueItem duration; @SerializedName("distance") public ValueItem distance; } public class ValueItem { @SerializedName("value") public long value; @SerializedName("text") public String text; } } }
下面给出了activity_main.xml文件
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#212121" tools:context="com.theitroad.nearbyplaces.MainActivity"> <EditText android:id="@+id/editText" android:layout_width="match_parent" android:textColor="@android:color/white" android:textColorHint="@android:color/white" android:text="restaurant mcdonalds" android:hint="type name" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/button" android:layout_toStartOf="@+id/button" <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:text="Search" <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/editText" android:scrollbars="vertical" </RelativeLayout>
下面给出了MainActivity.java类代码。
package com.theitroad.nearbyplaces; import android.annotation.TargetApi; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.location.Location; import android.os.Build; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import com.google.android.gms.maps.model.LatLng; import java.util.ArrayList; import java.util.List; import io.nlopez.smartlocation.OnLocationUpdatedListener; import io.nlopez.smartlocation.SmartLocation; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; public class MainActivity extends AppCompatActivity { private ArrayList<String> permissionsToRequest; private ArrayList<String> permissionsRejected = new ArrayList<>(); private ArrayList<String> permissions = new ArrayList<>(); private final static int ALL_PERMISSIONS_RESULT = 101; List<StoreModel> storeModels; ApiInterface apiService; String latLngString; LatLng latLng; RecyclerView recyclerView; EditText editText; Button button; List<PlacesPOJO.CustomA> results; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); permissions.add(ACCESS_FINE_LOCATION); permissions.add(ACCESS_COARSE_LOCATION); permissionsToRequest = findUnAskedPermissions(permissions); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (permissionsToRequest.size() > 0) requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT); else { fetchLocation(); } } else { fetchLocation(); } apiService = APIClient.getClient().create(ApiInterface.class); recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setNestedScrollingEnabled(false); recyclerView.setHasFixedSize(true); LinearLayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); editText = (EditText) findViewById(R.id.editText); button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String s = editText.getText().toString().trim(); String[] split = s.split("\s+"); if (split.length != 2) { Toast.makeText(getApplicationContext(), "Please enter text in the required format", Toast.LENGTH_SHORT).show(); } else fetchStores(split[0], split[1]); } }); } private void fetchStores(String placeType, String businessName) { /** * For Locations In San Franceco McDonalds stores aren't returned accurately */ //Call<PlacesPOJO.Root> call = apiService.doPlaces(placeType, latLngString,"\""+ businessName +"\"", true, "distance", APIClient.GOOGLE_PLACE_API_KEY); Call<PlacesPOJO.Root> call = apiService.doPlaces(placeType, latLngString, businessName, true, "distance", APIClient.GOOGLE_PLACE_API_KEY); call.enqueue(new Callback<PlacesPOJO.Root>() { @Override public void onResponse(Call<PlacesPOJO.Root> call, Response<PlacesPOJO.Root> response) { PlacesPOJO.Root root = response.body(); if (response.isSuccessful()) { if (root.status.equals("OK")) { results = root.customA; storeModels = new ArrayList<>(); for (int i = 0; i < results.size(); i++) { if (i == 10) break; PlacesPOJO.CustomA info = results.get(i); fetchDistance(info); } } else { Toast.makeText(getApplicationContext(), "No matches found near you", Toast.LENGTH_SHORT).show(); } } else if (response.code() != 200) { Toast.makeText(getApplicationContext(), "Error " + response.code() + " found.", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call<PlacesPOJO.Root> call, Throwable t) { //Log error here since request failed call.cancel(); } }); } private ArrayList<String> findUnAskedPermissions(ArrayList<String> wanted) { ArrayList<String> result = new ArrayList<>(); for (String perm : wanted) { if (!hasPermission(perm)) { result.add(perm); } } return result; } private boolean hasPermission(String permission) { if (canMakeSmores()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED); } } return true; } private boolean canMakeSmores() { return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1); } @TargetApi(Build.VERSION_CODES.M) @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case ALL_PERMISSIONS_RESULT: for (String perms : permissionsToRequest) { if (!hasPermission(perms)) { permissionsRejected.add(perms); } } if (permissionsRejected.size() > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) { showMessageOKCancel("These permissions are mandatory for the application. Please allow access.", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT); } } }); return; } } } else { fetchLocation(); } break; } } private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) { new AlertDialog.Builder(MainActivity.this) .setMessage(message) .setPositiveButton("OK", okListener) .setNegativeButton("Cancel", null) .create() .show(); } private void fetchLocation() { SmartLocation.with(this).location() .oneFix() .start(new OnLocationUpdatedListener() { @Override public void onLocationUpdated(Location location) { latLngString = location.getLatitude() + "," + location.getLongitude(); latLng = new LatLng(location.getLatitude(), location.getLongitude()); } }); } private void fetchDistance(final PlacesPOJO.CustomA info) { Call<ResultDistanceMatrix> call = apiService.getDistance(APIClient.GOOGLE_PLACE_API_KEY, latLngString, info.geometry.locationA.lat + "," + info.geometry.locationA.lng); call.enqueue(new Callback<ResultDistanceMatrix>() { @Override public void onResponse(Call<ResultDistanceMatrix> call, Response<ResultDistanceMatrix> response) { ResultDistanceMatrix resultDistance = response.body(); if ("OK".equalsIgnoreCase(resultDistance.status)) { ResultDistanceMatrix.InfoDistanceMatrix infoDistanceMatrix = resultDistance.rows.get(0); ResultDistanceMatrix.InfoDistanceMatrix.DistanceElement distanceElement = infoDistanceMatrix.elements.get(0); if ("OK".equalsIgnoreCase(distanceElement.status)) { ResultDistanceMatrix.InfoDistanceMatrix.ValueItem itemDuration = distanceElement.duration; ResultDistanceMatrix.InfoDistanceMatrix.ValueItem itemDistance = distanceElement.distance; String totalDistance = String.valueOf(itemDistance.text); String totalDuration = String.valueOf(itemDuration.text); storeModels.add(new StoreModel(info.name, info.vicinity, totalDistance, totalDuration)); if (storeModels.size() == 10 || storeModels.size() == results.size()) { RecyclerViewAdapter adapterStores = new RecyclerViewAdapter(results, storeModels); recyclerView.setAdapter(adapterStores); } } } } @Override public void onFailure(Call<ResultDistanceMatrix> call, Throwable t) { call.cancel(); } }); } }
在上面的代码中,我们首先要求运行时权限,然后使用SmartLocation库获取当前位置。
设置好之后,我们将通过类型来自EditText的第一个单词传递给最终调用Google Places API网络服务的fetchStores()方法的name参数中的第二个单词。
我们将搜索结果限制为10。
对于每个结果,我们在方法fetchDistance()
中计算距商店的距离和时间。
完成所有商店的操作后,我们将使用StoreModel.java数据类在RecyclerViewAdapter.java类中填充数据。
StoreModel.java代码如下:
package com.theitroad.nearbyplaces; public class StoreModel { public String name, address, distance, duration; public StoreModel(String name, String address, String distance, String duration) { this.name = name; this.address = address; this.distance = distance; this.duration = duration; } }
下面的xml中给出了RecyclerView每行的布局:store_list_row.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/activity_horizontal_margin" android:orientation="vertical"> <android.support.v7.widget.CardView xmlns:card_view="https://schemas.android.com/apk/res-auto" android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="wrap_content" card_view:cardCornerRadius="0dp" card_view:cardElevation="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="5dp"> <TextView android:id="@+id/txtStoreName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dp" android:textColor="#212121" <TextView android:id="@+id/txtStoreAddr" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dp" android:textColor="#212121" <TextView android:id="@+id/txtStoreDist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dp" </LinearLayout> </android.support.v7.widget.CardView> </LinearLayout>
下面给出了RecyclerViewAdapter.java代码。
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder> { private List<PlacesPOJO.CustomA> stLstStores; private List<StoreModel> models; public RecyclerViewAdapter(List<PlacesPOJO.CustomA> stores, List<StoreModel> storeModels) { stLstStores = stores; models = storeModels; } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.store_list_row, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.setData(stLstStores.get(holder.getAdapterPosition()), holder, models.get(holder.getAdapterPosition())); } @Override public int getItemCount() { return Math.min(5, stLstStores.size()); } public class MyViewHolder extends RecyclerView.ViewHolder { TextView txtStoreName; TextView txtStoreAddr; TextView txtStoreDist; StoreModel model; public MyViewHolder(View itemView) { super(itemView); this.txtStoreDist = (TextView) itemView.findViewById(R.id.txtStoreDist); this.txtStoreName = (TextView) itemView.findViewById(R.id.txtStoreName); this.txtStoreAddr = (TextView) itemView.findViewById(R.id.txtStoreAddr); } public void setData(PlacesPOJO.CustomA info, MyViewHolder holder, StoreModel storeModel) { this.model = storeModel; holder.txtStoreDist.setText(model.distance + "\n" + model.duration); holder.txtStoreName.setText(info.name); holder.txtStoreAddr.setText(info.vicinity); } } }
行动中的Google Places api示例应用程序的输出如下:
注意:Places API对于麦当劳和某些食品连锁店不正确,尤其是在旧金山。
一种解决方法是在双引号内的参数name
中传递值,例如:
Call call = apiService.doPlaces(placeType, latLngString,"\""+ businessName +"\"", true, "distance", APIClient.GOOGLE_PLACE_API_KEY);