Google Places for Android 入门指南

前言

最近由于项目的原因,接触到了 Google Places 的使用与开发,学习并在项目中实现了简单的地点定位自动补全功能。

在整个实践过程中,发现除了官方文档外,网上可以参考的中文教程寥寥无几且质量都不高,遂决定自己写篇入门指南(其实也没多少干货啦),希望可以帮助到,有这方面需求的同学们。下面,我将以一个实际的例子,图文并茂的带着你快速了解并使用上 Google Places .


在 Google Developers Console 中创建 API 项目

如果你之前从未在应用中使用过 Google 提供的相关服务(API),那么请直接使用系统引导帮你完成整个流程并自动激活 Google Places API for Android ,请点击此链接,按下面截图所示进行操作,便可轻松完成创建。

  1. 选择「Create a new project」
  2. 选择「Go to Credentials」
  3. 此处直接选择「Create」,既默认所有应用均可使用同样的「API key」
  4. 好啦,此时主角「API key」便自动生成了

配置你的应用

使用 Google Places API for Android 的所有应用均需执行以下配置步骤。

添加 Google Play 服务

要访问 Google Places API for Android,应用的开发项目必须包含 Google Play 服务。通过 SDK 管理器下载并安装 Google Play 服务组件,然后将库添加至您的项目。

-buidl.gradle (app)

1
2
3
dependencies {
compile 'com.google.android.gms:play-services:7.5.0'
}

添加相应权限

-AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
<!-- PlacePicker requires the ACCESS_FINE_LOCATION permission and a geo API key.-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- PlacePicker also requires OpenGL ES version 2 -->
<uses-feature
trueandroid:glEsVersion="0x00020000"
android:required="true" />

添加 API 密钥

请按照以下代码示例所示,将你的 API 密钥添加至 Manifest ,并将 API_KEY 替换为您自己的 API 密钥:

-AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
<application>
...
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="API_KEY"/>
</application>


当前地点

获取当前位置

要查找本地商家或设备的最后已知所在地点,请调用 PlaceDetectionApi.getCurrentPlace()

您可以选择指定一个 PlaceFilter,以将结果限制为一个或多个地点 ID(最多 10 个),或者仅选择当前打开的地点。如果未指定筛选器,则不会筛选结果。

API 会在 PendingResult 中返回 PlaceLikelihoodBufferPlaceLikelihoodBuffer 包含表示类似地点的 PlaceLikelihood 对象列表。对于每个地点,结果中都包含指示地点是正确地点的可能性信息。如果没有与筛选条件对应的已知地点,缓冲区可能为空。

您可以调用 PlaceLikelihood.getPlace() 来检索 Place 对象,调用 PlaceLikelihood.getLikelihood() 来获取地点的可能性评分。值越高,表示该地点是最佳匹配项的可能性越大。

地点自动完成

要获取预测地点名称和/或地址的列表,请调用 GeoDataApi.getAutocompletePredictions(),传递以下参数:

  • 必填:query 字符串包含用户键入的文本。
  • 必填:LatLngBounds 对象,将结果限制为通过纬度和经度边界指定的特定区域。
  • 可选:AutocompleteFilter,包含一组地点类型,您可以使用它们将结果限制为一种或多种地点类型,如商店、学校、邮局等。有关可用地点类型的列表,请参见支持的类型列表。

API 会在 PendingResult 中返回 AutocompletePredictionBufferAutocompletePredictionBuffer 包含表示预测地点的 AutocompletePrediction 对象列表。如果没有与查询和筛选条件对应的已知地点,缓冲区可能为空。

对于每个预测地点,都可以调用以下方法来检索地点详情:

  • getDescription() – 返回地点说明。
  • getMatchedSubstrings() – 通过与此地点匹配的 query 返回子字符串列表。例如,您可以使用这些子字符串突出显示用户查询中的匹配文本。
  • getDescription() – 返回预测地点的地点 ID。地点 ID 是唯一标识地点的文本标识符。有关地点 ID 的详细信息,请参阅地点 ID 概览。
  • `getPlaceTypes() – 返回与此地点关联的地点类型列表。

具体实现

screenshot

-activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_16dp"
android:orientation="vertical">


<AutoCompleteTextView
android:id="@+id/autocomplete_places"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:hint="@string/autocomplete_hint" />


<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="@dimen/margin_8dp">


<Button
android:id="@+id/ll_current_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/get_current_place"
android:textAppearance="?android:attr/textAppearanceSmall" />

</LinearLayout>

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:src="@drawable/powered_by_google_light" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/selected_place"
android:textAppearance="?android:attr/textAppearanceMedium" />


<TextView
android:id="@+id/place_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:paddingTop="@dimen/margin_8dp"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium" />


<TextView
android:id="@+id/place_attribution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:paddingTop="@dimen/margin_16dp"
android:text=""
android:textAppearance="?android:attr/textAppearanceSmall" />


</LinearLayout>

-PlaceAutocompleteAdapter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
public class PlaceAutocompleteAdapter
extends ArrayAdapter<PlaceAutocompleteAdapter.PlaceAutocomplete> implements Filterable {


private static final String TAG = "PlaceAutocompleteAdapter";
/**
* Current results returned by this adapter.
*/

private ArrayList<PlaceAutocomplete> mResultList;

/**
* Handles autocomplete requests.
*/

private GoogleApiClient mGoogleApiClient;

/**
* The bounds used for Places Geo Data autocomplete API requests.
*/

private LatLngBounds mBounds;

/**
* The autocomplete filter used to restrict queries to a specific set of place types.
*/

private AutocompleteFilter mPlaceFilter;

/**
* Initializes with a resource for text rows and autocomplete query bounds.
*
* @see ArrayAdapter#ArrayAdapter(Context, int)
*/

public PlaceAutocompleteAdapter(Context context, int resource, GoogleApiClient googleApiClient,
LatLngBounds bounds, AutocompleteFilter filter)
{

super(context, resource);
mGoogleApiClient = googleApiClient;
mBounds = bounds;
mPlaceFilter = filter;
}

/**
* Sets the bounds for all subsequent queries.
*/

public void setBounds(LatLngBounds bounds) {
mBounds = bounds;
}

/**
* Returns the number of results received in the last autocomplete query.
*/

@Override
public int getCount() {
return mResultList.size();
}

/**
* Returns an item from the last autocomplete query.
*/

@Override
public PlaceAutocomplete getItem(int position) {
return mResultList.get(position);
}

/**
* Returns the filter for the current set of autocomplete results.
*/

@Override
public Filter getFilter() {
Filter filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
// Skip the autocomplete query if no constraints are given.
if (constraint != null) {
// Query the autocomplete API for the (constraint) search string.
mResultList = getAutocomplete(constraint);
if (mResultList != null) {
// The API successfully returned results.
results.values = mResultList;
results.count = mResultList.size();
}
}
return results;
}

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
// The API returned at least one result, update the data.
notifyDataSetChanged();
} else {
// The API did not return any results, invalidate the data set.
notifyDataSetInvalidated();
}
}
};
return filter;
}

private ArrayList<PlaceAutocomplete> getAutocomplete(CharSequence constraint) {
if (mGoogleApiClient.isConnected()) {
Log.i(TAG, "Starting autocomplete query for: " + constraint);

// Submit the query to the autocomplete API and retrieve a PendingResult that will
// contain the results when the query completes.
PendingResult<AutocompletePredictionBuffer> results =
Places.GeoDataApi
.getAutocompletePredictions(mGoogleApiClient, constraint.toString(),
mBounds, mPlaceFilter);

// This method should have been called off the main UI thread. Block and wait for at most 60s
// for a result from the API.
AutocompletePredictionBuffer autocompletePredictions = results
.await(60, TimeUnit.SECONDS);

// Confirm that the query completed successfully, otherwise return null
final Status status = autocompletePredictions.getStatus();
if (!status.isSuccess()) {
Toast.makeText(getContext(), "Error contacting API: " + status.toString(),
Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error getting autocomplete prediction API call: " + status.toString());
autocompletePredictions.release();
return null;
}

Log.i(TAG, "Query completed. Received " + autocompletePredictions.getCount()
+ " predictions.");

// Copy the results into our own data structure, because we can't hold onto the buffer.
// AutocompletePrediction objects encapsulate the API response (place ID and description).

Iterator<AutocompletePrediction> iterator = autocompletePredictions.iterator();
ArrayList resultList = new ArrayList<>(autocompletePredictions.getCount());
while (iterator.hasNext()) {
AutocompletePrediction prediction = iterator.next();
// Get the details of this prediction and copy it into a new PlaceAutocomplete object.
resultList.add(new PlaceAutocomplete(prediction.getPlaceId(),
prediction.getDescription()));
}

// Release the buffer now that all data has been copied.
autocompletePredictions.release();

return resultList;
}
Log.e(TAG, "Google API client is not connected for autocomplete query.");
return null;
}

/**
* Holder for Places Geo Data Autocomplete API results.
*/

class PlaceAutocomplete {

public CharSequence placeId;
public CharSequence description;

PlaceAutocomplete(CharSequence placeId, CharSequence description) {
this.placeId = placeId;
this.description = description;
}

@Override
public String toString() {
return description.toString();
}
}
}

-MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.OnConnectionFailedListener {

/**
* GoogleApiClient wraps our service connection to Google Play Services and provides access
* to the user's sign in state as well as the Google's APIs.
*/

private static final int GOOGLE_API_CLIENT_ID = 0;

protected GoogleApiClient mGoogleApiClient;

private PlaceAutocompleteAdapter mAdapter;

private AutoCompleteTextView mAutocompleteView;

private TextView mPlaceDetailsText;

private TextView mPlaceDetailsAttribution;

private Button mCurrentLocation;

private static final String TAG = "MainActivity";

private static final LatLngBounds BOUNDS_GREATER_SYDNEY = new LatLngBounds(
new LatLng(-34.041458, 150.790100), new LatLng(-33.682247, 151.383362));

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Construct a GoogleApiClient for the {@link Places#GEO_DATA_API} using AutoManage
// functionality, which automatically sets up the API client to handle Activity lifecycle
// events. If your activity does not extend FragmentActivity, make sure to call connect()
// and disconnect() explicitly.
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this, GOOGLE_API_CLIENT_ID /* clientId */, this)
.addApi(Places.GEO_DATA_API)
.addApi(Places.PLACE_DETECTION_API)
.build();

setContentView(R.layout.activity_main);

// Retrieve the AutoCompleteTextView that will display Place suggestions.
mAutocompleteView = (AutoCompleteTextView)
findViewById(R.id.autocomplete_places);

// Register a listener that receives callbacks when a suggestion has been selected
mAutocompleteView.setOnItemClickListener(mAutocompleteClickListener);

// Retrieve the TextViews that will display details and attributions of the selected place.
mPlaceDetailsText = (TextView) findViewById(R.id.place_details);
mPlaceDetailsAttribution = (TextView) findViewById(R.id.place_attribution);

// CurrentLocation
mCurrentLocation = (Button) findViewById(R.id.ll_current_location);
mCurrentLocation.setOnClickListener(mOnClickListener);

// Set up the adapter that will retrieve suggestions from the Places Geo Data API that cover
// the entire world.
mAdapter = new PlaceAutocompleteAdapter(this, android.R.layout.simple_list_item_1,
mGoogleApiClient, BOUNDS_GREATER_SYDNEY, null);
mAutocompleteView.setAdapter(mAdapter);
}

private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
PendingResult<PlaceLikelihoodBuffer> result = Places.PlaceDetectionApi
.getCurrentPlace(mGoogleApiClient, null);
result.setResultCallback(new ResultCallback<PlaceLikelihoodBuffer>() {
@Override
public void onResult(PlaceLikelihoodBuffer likelyPlaces) {
if (!likelyPlaces.getStatus().isSuccess()) {
// Request did not complete successfully
Log.e(TAG, "Place query did not complete. Error: " + likelyPlaces.getStatus().toString());
likelyPlaces.release();
return;
}
String placeName = String.format("%s", likelyPlaces.get(0).getPlace().getName());
String placeAttributuion = String.format("%s", likelyPlaces.get(0).getPlace().getAddress());
mPlaceDetailsText.setText(placeName);
mPlaceDetailsAttribution.setText(placeAttributuion);
likelyPlaces.release();
}
});
}
};

/**
* Listener that handles selections from suggestions from the AutoCompleteTextView that
* displays Place suggestions.
* Gets the place id of the selected item and issues a request to the Places Geo Data API
* to retrieve more details about the place.
*
* @see com.google.android.gms.location.places.GeoDataApi#getPlaceById(com.google.android.gms.common.api.GoogleApiClient,
* String...)
*/

private AdapterView.OnItemClickListener mAutocompleteClickListener
= new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
/*
Retrieve the place ID of the selected item from the Adapter.
The adapter stores each Place suggestion in a PlaceAutocomplete object from which we
read the place ID.
*/

final PlaceAutocompleteAdapter.PlaceAutocomplete item = mAdapter.getItem(position);
final String placeId = String.valueOf(item.placeId);
Log.i(TAG, "Autocomplete item selected: " + item.description);

/*
Issue a request to the Places Geo Data API to retrieve a Place object with additional
details about the place.
*/

PendingResult<PlaceBuffer> placeResult = Places.GeoDataApi
.getPlaceById(mGoogleApiClient, placeId);
placeResult.setResultCallback(mUpdatePlaceDetailsCallback);
Log.i(TAG, "Called getPlaceById to get Place details for " + item.placeId);
}
};

/**
* Callback for results from a Places Geo Data API query that shows the first place result in
* the details view on screen.
*/

private ResultCallback<PlaceBuffer> mUpdatePlaceDetailsCallback
= new ResultCallback<PlaceBuffer>() {
@Override
public void onResult(PlaceBuffer places) {
if (!places.getStatus().isSuccess()) {
// Request did not complete successfully
Log.e(TAG, "Place query did not complete. Error: " + places.getStatus().toString());
places.release();
return;
}
// Get the Place object from the buffer.
final Place place = places.get(0);

// Format details of the place for display and show it in a TextView.
mPlaceDetailsText.setText(formatPlaceDetails(getResources(), place.getName(),
place.getId(), place.getAddress(), place.getPhoneNumber(),
place.getWebsiteUri()));

// Display the third party attributions if set.
final CharSequence thirdPartyAttribution = places.getAttributions();
if (thirdPartyAttribution == null) {
mPlaceDetailsAttribution.setVisibility(View.GONE);
} else {
mPlaceDetailsAttribution.setVisibility(View.VISIBLE);
mPlaceDetailsAttribution.setText(Html.fromHtml(thirdPartyAttribution.toString()));
}

Log.i(TAG, "Place details received: " + place.getName());

places.release();
}
};

private static Spanned formatPlaceDetails(Resources res, CharSequence name, String id,
CharSequence address, CharSequence phoneNumber, Uri websiteUri)
{

Log.e(TAG, res.getString(R.string.place_details, name, id, address, phoneNumber,
websiteUri));
return Html.fromHtml(res.getString(R.string.place_details, name, id, address, phoneNumber,
websiteUri));

}

/**
* Called when the Activity could not connect to Google Play services and the auto manager
* could resolve the error automatically.
* In this case the API is not available and notify the user.
*
* @param connectionResult can be inspected to determine the cause of the failure
*/

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {

Log.e(TAG, "onConnectionFailed: ConnectionResult.getErrorCode() = "
+ connectionResult.getErrorCode());

// TODO(Developer): Check error code and notify the user of error state and resolution.
Toast.makeText(this,
"Could not connect to Google API Client: Error " + connectionResult.getErrorCode(),
Toast.LENGTH_SHORT).show();
MainActivity.this.finish();
}

}

screenshot screenshot


最后

  • Demo 下载链接:https://github.com/tangqi92/MyGooglePlaces
  • 友情提醒:以上的功能,手机需要安装 Google Play services 才能正常使用,至于如何安装,就交给万能的 Google 吧:)
  • 如本文存在任何错误或遗漏,欢迎指正。

References

  • https://developers.google.com/places/android
  • https://github.com/googlesamples/android-play-places

期待与你成为朋友

我知道是不会有人点的,但万一有人想不开呢?