13、地理围栏:Geofencing概念、Geofence.Builder构建、GeofencingRequest与广播接收

地理围栏,说白了就是在地图上画个虚拟的圈。手机一旦进入或离开这个圈,你的App就能收到通知。这功能在LBS应用里太常见了——比如你靠近星巴克,手机弹个优惠券;或者孩子离开学校区域,家长收到提醒。

我最早接触Geofencing是在做一个商场导购App时。当时需求是用户走进商场200米范围内,自动推送当天的促销活动。嗯,那时候我还不太熟悉这套API,踩了不少坑。今天咱们就把这块彻底讲透。

13.1 地理围栏的核心概念

先理清几个关键点:

  • 围栏(Geofence):一个圆形区域,由经纬度+半径定义。半径最小可以到100米左右,再小精度就不太靠谱了。
  • 触发事件:进入(ENTER)、离开(EXIT)、停留(DWELL)。停留需要设置一个超时时间,比如用户在里面待了5分钟才算停留。
  • 响应延迟:系统不是实时检测的,而是根据设备位置更新频率来触发。省电模式下延迟可能高达几分钟。

我个人习惯把地理围栏理解成「虚拟围栏+事件监听器」的组合。你画好圈,告诉系统你想监听什么事件,剩下的交给Location Service去轮询。

关键限制: Android设备同时注册的地理围栏数量有限制,通常是100个。每个围栏的半径不能小于100米(实际测试中,200米以下误差较大)。

13.2 Geofence.Builder:构建围栏对象

每个围栏都是一个Geofence对象。我们用Builder模式来创建它:

Geofence geofence = new Geofence.Builder()
    .setRequestId("fence_home")          // 唯一标识,用于区分不同围栏
    .setCircularRegion(
        39.9042,                         // 纬度
        116.4074,                        // 经度
        300.0f                           // 半径(米)
    )
    .setExpirationDuration(Geofence.NEVER_EXPIRE)  // 永不过期
    .setTransitionTypes(
        Geofence.GEOFENCE_TRANSITION_ENTER |
        Geofence.GEOFENCE_TRANSITION_EXIT
    )
    .setLoiteringDelay(60000)            // 停留检测延迟(毫秒)
    .build();

这里有几个参数需要特别留意:

参数 说明 我的建议
setRequestId 围栏的唯一ID 用业务含义命名,比如"store_1001"
setExpirationDuration 过期时间(毫秒) 临时围栏设6小时,长期围栏用NEVER_EXPIRE
setLoiteringDelay 停留检测延迟 至少设30秒以上,太短容易误触
setTransitionTypes 监听的事件类型 按需组合,不要全选
小技巧: 如果你只需要进入事件,就别监听退出。减少事件类型可以降低系统开销。我在项目中就见过有人把ENTER、EXIT、DWELL全勾上,结果回调满天飞。

13.3 GeofencingRequest:注册请求

围栏建好了,怎么告诉系统去监听呢?通过GeofencingRequest。它把多个围栏打包成一个请求,同时指定初始触发行为:

GeofencingRequest request = new GeofencingRequest.Builder()
    .addGeofence(geofence)               // 可以添加多个围栏
    .setInitialTrigger(
        GeofencingRequest.INITIAL_TRIGGER_ENTER
    )
    .build();

这个setInitialTrigger是什么意思?说白了就是:如果用户当前已经在围栏内,注册成功后要不要立刻触发一次进入事件?

  • INITIAL_TRIGGER_ENTER:立即触发进入事件(适合「你已经在区域内」的场景)
  • INITIAL_TRIGGER_EXIT:立即触发离开事件(很少用)
  • INITIAL_TRIGGER_DWELL:立即触发停留事件(需要配合loiteringDelay)
  • 不设置:只监听后续变化

我记得有一次做签到功能,用户打开App时如果已经在门店附近,应该自动签到。当时忘了设INITIAL_TRIGGER_ENTER,结果用户站在店里死活触发不了。嗯,这个坑我替你们踩过了。

13.4 广播接收:处理围栏事件

围栏事件是通过广播发送的。你需要创建一个BroadcastReceiver来接收:

public class GeofenceReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        GeofencingEvent event = GeofencingEvent.fromIntent(intent);
        if (event == null) return;

        int transitionType = event.getGeofenceTransition();
        List<Geofence> triggeredFences = event.getTriggeringGeofences();

        switch (transitionType) {
            case Geofence.GEOFENCE_TRANSITION_ENTER:
                // 处理进入事件
                break;
            case Geofence.GEOFENCE_TRANSITION_EXIT:
                // 处理离开事件
                break;
            case Geofence.GEOFENCE_TRANSITION_DWELL:
                // 处理停留事件
                break;
        }
    }
}

别忘了在AndroidManifest里注册:

<receiver android:name=".GeofenceReceiver" />
注意: Android 8.0以上,隐式广播会被限制。如果你的Receiver是静态注册的,必须指定exported=false,并且用显式Intent发送。不过Geofencing API内部已经处理好了,你只需要保证Receiver是公开的(exported=true)即可。

13.5 完整注册流程

把上面几块串起来,完整的注册代码大概是这样的:

// 1. 检查权限
if (ContextCompat.checkSelfPermission(this, 
        Manifest.permission.ACCESS_FINE_LOCATION) 
        != PackageManager.PERMISSION_GRANTED) {
    return;
}

// 2. 构建围栏
Geofence fence = new Geofence.Builder()
    .setRequestId("fence_office")
    .setCircularRegion(39.9042, 116.4074, 300)
    .setExpirationDuration(12 * 60 * 60 * 1000) // 12小时
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
    .build();

// 3. 构建请求
GeofencingRequest request = new GeofencingRequest.Builder()
    .addGeofence(fence)
    .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
    .build();

// 4. 注册
GeofencingClient client = LocationServices.getGeofencingClient(this);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
    this, 0, new Intent(this, GeofenceReceiver.class),
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

client.addGeofences(request, pendingIntent)
    .addOnSuccessListener(aVoid -> {
        // 注册成功
    })
    .addOnFailureListener(e -> {
        // 注册失败,检查错误码
    });

13.6 知识体系总览

下面这张图帮你理清整个流程:

地理围栏核心流程 Geofence.Builder 设置ID、圆心、半径、事件 GeofencingRequest 添加围栏、设置初始触发 GeofencingClient addGeofences() Location Service 后台检测 根据设备位置变化触发事件 发送广播 Intent 携带GeofencingEvent BroadcastReceiver 解析事件类型、处理业务 业务逻辑(推送、签到等)

13.7 避坑指南

最后分享几个我实际项目中遇到的坑:

  • 权限问题:Android 10以上,后台位置权限需要单独申请。如果App退到后台还想检测围栏,必须声明ACCESS_BACKGROUND_LOCATION
  • PendingIntent复用:同一个PendingIntent如果被多个围栏使用,回调时一定要通过getTriggeringGeofences()区分是哪个围栏触发的。
  • 电量消耗:围栏检测本身是低功耗的,但如果你注册太多围栏(超过20个),系统会合并检测间隔,导致响应变慢。
  • 测试技巧:在模拟器上测试时,可以用geo fix命令模拟位置变化。我一般写个自动化脚本,让坐标沿着围栏边界来回移动。
我的习惯: 每次注册围栏前,先调用removeGeofences()清理旧的,防止重复注册导致内存泄漏。尤其是Activity重建时,别忘了在onResume里重新注册。

地理围栏这块,说白了就是「画圈+监听」的组合。Builder负责画圈,Request负责打包,Client负责注册,Receiver负责收信。把这四步理清楚,基本就掌握了。剩下的就是根据业务场景灵活组合事件类型和触发条件了。


公众号:蓝海资料掘金营,微信deep3321