地理围栏结合了对用户当前位置的感知以及对用户与可能感兴趣的位置的接近程度的感知。要标记感兴趣的位置,请指定其纬度和经度。要调整位置的接近程度,请添加半径。纬度、经度和半径定义了一个地理围栏,在感兴趣的位置周围创建一个圆形区域或围栏。
您可以拥有多个活动的地理围栏,每个应用、每个设备用户最多可使用 100 个。对于每个地理围栏,您可以要求位置服务向您发送进入和退出事件,或者您可以在地理围栏区域内指定等待时间或停留时间,然后再触发事件。您可以通过以毫秒为单位指定过期时间来限制任何地理围栏的持续时间。地理围栏过期后,位置服务会自动将其移除。
本课程将向您展示如何添加和移除地理围栏,然后使用BroadcastReceiver
监听地理围栏转换。
注意:在 Wear 设备上,地理围栏 API 无法有效利用电量。我们不建议在 Wear 上使用这些 API。阅读节约电量和电池以获取更多信息。
设置地理围栏监控
请求地理围栏监控的第一步是请求必要的权限。要使用地理围栏,您的应用必须请求以下权限:
-
ACCESS_FINE_LOCATION
-
ACCESS_BACKGROUND_LOCATION
(如果您的应用面向 Android 10(API 级别 29)或更高版本)
要了解更多信息,请参阅有关如何请求位置权限的指南。
如果要使用BroadcastReceiver
监听地理围栏转换,请添加一个指定服务名称的元素。此元素必须是 <application>
元素的子元素。
<application android:allowBackup="true"> ... <receiver android:name=".GeofenceBroadcastReceiver"/> <application/>
要访问位置 API,您需要创建 Geofencing 客户端的实例。要了解如何连接您的客户端:
Kotlin
lateinit var geofencingClient: GeofencingClient override fun onCreate(savedInstanceState: Bundle?) { // ... geofencingClient = LocationServices.getGeofencingClient(this) }
Java
private GeofencingClient geofencingClient; @Override public void onCreate(Bundle savedInstanceState) { // ... geofencingClient = LocationServices.getGeofencingClient(this); }
创建和添加地理围栏
您的应用需要使用位置 API 的构建器类创建和添加地理围栏以创建 Geofence 对象,以及使用便利类添加它们。此外,为了处理地理围栏转换发生时位置服务发送的意图,您可以定义一个PendingIntent
,如本节所示。
注意:在单用户设备上,每个应用最多可使用 100 个地理围栏。对于多用户设备,每个应用、每个设备用户最多可使用 100 个地理围栏。
创建地理围栏对象
首先,使用 Geofence.Builder
创建地理围栏,设置地理围栏所需的半径、持续时间和转换类型。例如,要填充列表对象:
Kotlin
geofenceList.add(Geofence.Builder() // Set the request ID of the geofence. This is a string to identify this // geofence. .setRequestId(entry.key) // Set the circular region of this geofence. .setCircularRegion( entry.value.latitude, entry.value.longitude, Constants.GEOFENCE_RADIUS_IN_METERS ) // Set the expiration duration of the geofence. This geofence gets automatically // removed after this period of time. .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS) // Set the transition types of interest. Alerts are only generated for these // transition. We track entry and exit transitions in this sample. .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT) // Create the geofence. .build())
Java
geofenceList.add(new Geofence.Builder() // Set the request ID of the geofence. This is a string to identify this // geofence. .setRequestId(entry.getKey()) .setCircularRegion( entry.getValue().latitude, entry.getValue().longitude, Constants.GEOFENCE_RADIUS_IN_METERS ) .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) .build());
此示例从常量文件中提取数据。在实际应用中,应用可能会根据用户的位置动态创建地理围栏。
指定地理围栏和初始触发器
以下代码段使用 GeofencingRequest
类及其嵌套的 GeofencingRequestBuilder
类来指定要监控的地理围栏,并设置相关地理围栏事件的触发方式。
Kotlin
private fun getGeofencingRequest(): GeofencingRequest { return GeofencingRequest.Builder().apply { setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) addGeofences(geofenceList) }.build() }
Java
private GeofencingRequest getGeofencingRequest() { GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); builder.addGeofences(geofenceList); return builder.build(); }
此示例显示了两个地理围栏触发器的用法。 GEOFENCE_TRANSITION_ENTER
转换在设备进入地理围栏时触发,而 GEOFENCE_TRANSITION_EXIT
转换在设备退出地理围栏时触发。指定 INITIAL_TRIGGER_ENTER
告诉位置服务,如果设备已在地理围栏内,则应触发 GEOFENCE_TRANSITION_ENTER
。
在许多情况下,最好改用 INITIAL_TRIGGER_DWELL
,它仅在用户在地理围栏内停留一段时间后触发事件。此方法有助于减少设备短暂进入和退出地理围栏时大量通知导致的“警报垃圾邮件”。获得地理围栏最佳结果的另一种策略是设置 100 米的最小半径。这有助于考虑典型 Wi-Fi 网络的位置精度,也有助于降低设备功耗。
定义用于地理围栏转换的广播接收器
来自位置服务的Intent
可以触发应用中的各种操作,但不应让它启动 Activity 或 Fragment,因为组件仅应响应用户操作而可见。在许多情况下,BroadcastReceiver
是处理地理围栏转换的好方法。BroadcastReceiver
在发生事件(例如进入或离开地理围栏的转换)时获取更新,并且可以启动长时间运行的后台工作。
以下代码片段显示了如何定义启动BroadcastReceiver
的PendingIntent
Kotlin
class MainActivity : AppCompatActivity() { // ... private val geofencePendingIntent: PendingIntent by lazy { val intent = Intent(this, GeofenceBroadcastReceiver::class.java) // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling // addGeofences() and removeGeofences(). PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } }
Java
public class MainActivity extends AppCompatActivity { // ... private PendingIntent getGeofencePendingIntent() { // Reuse the PendingIntent if we already have it. if (geofencePendingIntent != null) { return geofencePendingIntent; } Intent intent = new Intent(this, GeofenceBroadcastReceiver.class); // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when // calling addGeofences() and removeGeofences(). geofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent. FLAG_UPDATE_CURRENT); return geofencePendingIntent; }
添加地理围栏
要添加地理围栏,请使用
方法。提供GeofencingClient.addGeofences()
GeofencingRequest
对象和PendingIntent
。以下代码片段演示了如何处理结果
Kotlin
geofencingClient?.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run { addOnSuccessListener { // Geofences added // ... } addOnFailureListener { // Failed to add geofences // ... } }
Java
geofencingClient.addGeofences(getGeofencingRequest(), getGeofencePendingIntent()) .addOnSuccessListener(this, new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Geofences added // ... } }) .addOnFailureListener(this, new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Failed to add geofences // ... } });
处理地理围栏转换
当位置服务检测到用户已进入或退出地理围栏时,它会发送包含在您在添加地理围栏的请求中包含的PendingIntent
中的Intent
。像GeofenceBroadcastReceiver
这样的广播接收器会注意到Intent
已被调用,然后可以从 Intent 中获取地理围栏事件,确定地理围栏转换类型,并确定哪个已定义的地理围栏被触发。广播接收器可以指示应用开始执行后台工作,或者(如果需要)发送通知作为输出。
注意:在 Android 8.0(API 级别 26)及更高版本上,如果应用在后台运行时正在监控地理围栏,则设备每隔几分钟就会响应地理围栏事件。要了解如何使您的应用适应这些响应限制,请参阅后台位置限制。
以下代码片段显示了如何定义一个BroadcastReceiver
,该接收器在发生地理围栏转换时发布通知。当用户点击通知时,应用的主 Activity 将出现
Kotlin
class GeofenceBroadcastReceiver : BroadcastReceiver() { // ... override fun onReceive(context: Context?, intent: Intent?) { val geofencingEvent = GeofencingEvent.fromIntent(intent) if (geofencingEvent.hasError()) { val errorMessage = GeofenceStatusCodes .getStatusCodeString(geofencingEvent.errorCode) Log.e(TAG, errorMessage) return } // Get the transition type. val geofenceTransition = geofencingEvent.geofenceTransition // Test that the reported transition was of interest. if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER | geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { // Get the geofences that were triggered. A single event can trigger // multiple geofences. val triggeringGeofences = geofencingEvent.triggeringGeofences // Get the transition details as a String. val geofenceTransitionDetails = getGeofenceTransitionDetails( this, geofenceTransition, triggeringGeofences ) // Send notification and log the transition details. sendNotification(geofenceTransitionDetails) Log.i(TAG, geofenceTransitionDetails) } else { // Log the error. Log.e(TAG, getString(R.string.geofence_transition_invalid_type, geofenceTransition)) } } }
Java
public class GeofenceBroadcastReceiver extends BroadcastReceiver { // ... protected void onReceive(Context context, Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); if (geofencingEvent.hasError()) { String errorMessage = GeofenceStatusCodes .getStatusCodeString(geofencingEvent.getErrorCode()); Log.e(TAG, errorMessage); return; } // Get the transition type. int geofenceTransition = geofencingEvent.getGeofenceTransition(); // Test that the reported transition was of interest. if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { // Get the geofences that were triggered. A single event can trigger // multiple geofences. List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences(); // Get the transition details as a String. String geofenceTransitionDetails = getGeofenceTransitionDetails( this, geofenceTransition, triggeringGeofences ); // Send notification and log the transition details. sendNotification(geofenceTransitionDetails); Log.i(TAG, geofenceTransitionDetails); } else { // Log the error. Log.e(TAG, getString(R.string.geofence_transition_invalid_type, geofenceTransition)); } } }
通过PendingIntent
检测到转换事件后,BroadcastReceiver
获取地理围栏转换类型并测试它是否为应用用于触发通知的事件之一——在本例中为GEOFENCE_TRANSITION_ENTER
或GEOFENCE_TRANSITION_EXIT
。然后,服务发送通知并记录转换详细信息。
停止地理围栏监控
在不再需要或希望时停止地理围栏监控可以帮助节省设备上的电池电量和 CPU 周期。您可以在用于添加和删除地理围栏的主 Activity 中停止地理围栏监控;删除地理围栏会立即停止它。API 提供了通过请求 ID 或通过删除与给定PendingIntent
关联的地理围栏来删除地理围栏的方法。
以下代码片段通过PendingIntent
删除地理围栏,停止设备在进入或退出先前添加的地理围栏时发出所有进一步的通知
Kotlin
geofencingClient?.removeGeofences(geofencePendingIntent)?.run { addOnSuccessListener { // Geofences removed // ... } addOnFailureListener { // Failed to remove geofences // ... } }
Java
geofencingClient.removeGeofences(getGeofencePendingIntent()) .addOnSuccessListener(this, new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Geofences removed // ... } }) .addOnFailureListener(this, new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Failed to remove geofences // ... } });
您可以将地理围栏与其他位置感知功能(例如定期位置更新)结合使用。有关更多信息,请参阅本课程中的其他课程。
使用地理围栏的最佳实践
本节概述了使用 Android 位置 API 的地理围栏的建议。
降低功耗
您可以使用以下技术来优化使用地理围栏的应用的功耗
将通知响应度设置为更高的值。这样做可以通过增加地理围栏警报的延迟来改善功耗。例如,如果将响应度值设置为五分钟,则您的应用仅每五分钟检查一次入口或出口警报。设置较低的值并不一定意味着用户会在该时间段内收到通知(例如,如果将值设置为 5 秒,则可能需要更长的时间才能收到警报)。
对于用户花费大量时间的位置(例如家或工作),使用更大的地理围栏半径。虽然更大的半径不会直接降低功耗,但它会降低应用检查入口或出口的频率,从而有效降低整体功耗。
选择地理围栏的最佳半径
为了获得最佳效果,地理围栏的最小半径应设置为 100 到 150 米之间。当 Wi-Fi 可用时,位置精度通常在 20 到 50 米之间。当室内定位可用时,精度范围可以小到 5 米。除非您知道地理围栏内有室内定位可用,否则假设 Wi-Fi 定位精度约为 50 米。
当 Wi-Fi 定位不可用时(例如,当您在农村地区开车时),位置精度会下降。精度范围可以大到几百米到几公里。在这种情况下,您应该使用更大的半径创建地理围栏。
向用户解释您的应用为何使用地理围栏
因为您的应用在使用地理围栏时会在后台访问位置,所以请考虑您的应用如何为用户提供好处。清楚地向他们解释您的应用为何需要此访问权限,以提高用户理解和透明度。
有关与位置访问(包括地理围栏)相关的最佳实践的更多信息,请参阅隐私最佳实践页面。
使用停留转换类型减少警报垃圾邮件
如果您在短暂经过地理围栏时收到大量警报,则减少警报的最佳方法是使用 GEOFENCE_TRANSITION_DWELL
类型的转换,而不是 GEOFENCE_TRANSITION_ENTER
。这样,只有当用户在某个地理围栏内停留一段时间后才会发送停留警报。您可以通过设置停留延迟来选择持续时间。
仅在需要时重新注册地理围栏
注册的地理围栏保存在由com.google.android.gms
包拥有的com.google.process.location
进程中。应用不需要执行任何操作来处理以下事件,因为系统会在这些事件后恢复地理围栏
- Google Play 服务已升级。
- 由于资源限制,系统终止并重新启动了 Google Play 服务。
- 位置进程崩溃。
在以下事件之后,如果仍然需要地理围栏,则应用必须重新注册地理围栏,因为系统无法在以下情况下恢复地理围栏
- 设备已重新启动。应用应侦听设备的启动完成操作,然后重新注册所需的地理围栏。
- 应用已卸载并重新安装。
- 应用数据已清除。
- Google Play 服务数据已清除。
- 应用已收到
GEOFENCE_NOT_AVAILABLE
警报。这通常发生在 NLP(Android 的网络位置提供程序)被禁用后。
对地理围栏进入事件进行故障排除
如果设备进入地理围栏时未触发地理围栏(未触发 GEOFENCE_TRANSITION_ENTER
警报),请首先确保您的地理围栏已按照本指南中所述正确注册。
以下是一些警报无法按预期工作的原因
- 地理围栏内或地理围栏太小,无法获得准确的位置。在大多数设备上,地理围栏服务仅使用网络位置来触发地理围栏。服务使用这种方法是因为网络位置消耗的电量少得多,获取离散位置所需的时间更少,最重要的是它可以在室内使用。
设备上的 Wi-Fi 已关闭。启用 Wi-Fi 可以显着提高位置精度,因此,如果 Wi-Fi 已关闭,则根据包括地理围栏半径、设备型号或 Android 版本在内的多个设置,您的应用可能永远不会收到地理围栏警报。从 Android 4.3(API 级别 18)开始,我们添加了“仅 Wi-Fi 扫描模式”的功能,允许用户禁用 Wi-Fi 但仍然获得良好的网络位置。如果两者都已禁用,则最好提示用户并为用户提供快捷方式以启用 Wi-Fi 或仅 Wi-Fi 扫描模式。使用 SettingsClient确保设备的系统设置已正确配置以进行最佳位置检测。
注意:如果您的应用面向 Android 10(API 级别 29)或更高版本,则除非您的应用是系统应用或设备策略控制器 (DPC),否则您不能直接调用
WifiManager.setEnabled()
。相反,请使用设置面板。- 地理围栏内没有可靠的网络连接。如果没有可靠的数据连接,则可能不会生成警报。这是因为地理围栏服务依赖于网络位置提供程序,而网络位置提供程序又需要数据连接。
- 警报可能会延迟。地理围栏服务不会持续查询位置,因此在接收警报时请注意一些延迟。通常延迟小于 2 分钟,即使设备一直在移动也是如此。如果后台位置限制生效,则平均延迟约为 2-3 分钟。如果设备已静止一段时间,则延迟可能会增加(最多 6 分钟)。
其他资源
要详细了解地理围栏,请查看以下资料