HarmonyOS APP开发:地理围栏与区域触发

举报
Jack20 发表于 2026/06/21 16:14:59 2026/06/21
【摘要】 HarmonyOS APP开发:地理围栏与区域触发核心要点:深入掌握HarmonyOS地理围栏(Geofence)机制,实现基于位置的区域进出检测与智能事件触发,构建考勤打卡、智能提醒、区域营销等LBS场景应用。项目说明核心模块@ohos.geoLocationManager关键APIaddGeofence / removeGeofence 一、背景与动机 1.1 什么是地理围栏地理围栏(...

HarmonyOS APP开发:地理围栏与区域触发

核心要点:深入掌握HarmonyOS地理围栏(Geofence)机制,实现基于位置的区域进出检测与智能事件触发,构建考勤打卡、智能提醒、区域营销等LBS场景应用。

项目 说明
核心模块 @ohos.geoLocationManager
关键API addGeofence / removeGeofence

一、背景与动机

1.1 什么是地理围栏

地理围栏(Geofence)是一种虚拟边界技术,通过在地图上划定一个地理区域(圆形或多边形),当设备进入、离开或停留在该区域时,系统自动触发预设的事件。与主动定位不同,地理围栏是被动触发机制——应用无需持续轮询位置,系统在围栏事件发生时主动通知应用。

1.2 地理围栏的典型应用场景

场景 围栏类型 触发行为 应用示例
考勤打卡 公司区域 进入/离开 企业微信、钉钉
智能提醒 家/公司 到达提醒 提醒事项、Todo
区域营销 商圈 进入推送 大众点评、美团
车辆管理 停车场/禁区 进出记录 车联网、共享汽车
儿童安全 学校/家 离开告警 亲情守护
智能家居 到达开灯 智慧生活

1.3 地理围栏 vs 持续定位

flowchart LR
    subgraph A[持续定位方案]
        A1[持续获取位置] --> A2[应用端计算距离]
        A2 --> A3[判断是否在区域内]
        A3 --> A4[触发事件]
    end
    
    subgraph B[地理围栏方案]
        B1[注册围栏] --> B2[系统后台监控]
        B2 --> B3[围栏事件触发]
        B3 --> B4[系统回调通知]
    end
    
    A -.->|高功耗| C[持续耗电]
    B -.->|低功耗| D[事件驱动省电]
    
    classDef approachA fill:#FF6B6B,stroke:#C0392B,color:#FFF,font-weight:bold
    classDef approachB fill:#4ECDC4,stroke:#1ABC9C,color:#FFF,font-weight:bold
    classDef resultA fill:#D63031,stroke:#C0392B,color:#FFF,font-weight:bold
    classDef resultB fill:#00B894,stroke:#1ABC9C,color:#FFF,font-weight:bold
    
    class A1,A2,A3,A4 approachA
    class B1,B2,B3,B4 approachB
    class C resultA
    class D resultB
对比维度 持续定位 地理围栏
功耗 高(持续运行GPS) 低(系统优化调度)
实时性 高(秒级) 中(分钟级)
精确度 高(可控精度) 中(依赖系统调度)
开发复杂度 低(自己计算) 中(需理解围栏API)
适用场景 导航、轨迹 区域触发、提醒

二、核心原理

2.1 地理围栏工作原理

HarmonyOS地理围栏的工作流程分为三个阶段:

flowchart TB
    A[1. 注册阶段] --> B[2. 监控阶段] --> C[3. 触发阶段]
    
    A --> A1[创建GeofenceRequest]
    A1 --> A2[设置围栏参数<br/>经纬度/半径/触发类型]
    A2 --> A3[调用addGeofence注册]
    
    B --> B1[系统位置服务后台运行]
    B1 --> B2[周期性获取位置]
    B2 --> B3[判断设备与围栏关系]
    B3 --> B4{是否满足触发条件}
    B4 -->|| B2
    B4 -->|| C
    
    C --> C1[触发GeofenceEvent]
    C1 --> C2[回调通知应用]
    C2 --> C3[应用执行业务逻辑]
    
    classDef phaseStyle fill:#6C5CE7,stroke:#5B4CDB,color:#FFF,font-weight:bold
    classDef stepStyle fill:#0984E3,stroke:#0770C2,color:#FFF,font-weight:bold
    classDef decisionStyle fill:#FDCB6E,stroke:#F0B429,color:#333,font-weight:bold
    classDef triggerStyle fill:#00B894,stroke:#1ABC9C,color:#FFF,font-weight:bold
    
    class A,B,C phaseStyle
    class A1,A2,A3,B1,B2,B3 stepStyle
    class B4 decisionStyle
    class C1,C2,C3 triggerStyle

2.2 围栏参数详解

HarmonyOS地理围栏通过GeofenceRequest定义:

// 地理围栏请求结构
interface GeofenceRequest {
  priority: LocationRequestPriority;  // 定位优先级
  scenario: LocationRequestScenario;  // 使用场景
  geofence: Geofence;                 // 围栏定义
}

// 围栏定义
interface Geofence {
  latitude: number;    // 圆心纬度
  longitude: number;   // 圆心经度
  radius: number;      // 半径(米)
  expiration: number;  // 过期时间(毫秒),0表示永不过期
}

围栏触发类型(通过GeoEvent枚举定义):

事件类型 常量 说明
GEOFENCE_EVENT_ENTER 1 进入围栏
GEOFENCE_EVENT_EXIT 2 离开围栏
GEOFENCE_EVENT_STAY 4 停留在围栏内

2.3 围栏精度与半径选择

围栏半径的选择直接影响触发准确性和误报率:

半径范围 适用场景 误报风险 建议触发类型
50~100m 办公室、店铺 较高 ENTER+STAY
100~300m 小区、学校 中等 ENTER+EXIT
300~1000m 商圈、公园 较低 ENTER
1000m+ 城市、行政区 极低 ENTER

关键原则

  • 半径不宜太小(<50m),GPS精度波动可能导致频繁误触发
  • 半径不宜太大(>5km),会失去围栏的精确性意义
  • 建议最小半径 = 预期GPS精度 × 2~3倍

2.4 围栏数量限制

限制项 说明
单应用最大围栏数 100个
系统总围栏数 取决于设备,通常300~500个
同时激活围栏数 建议不超过20个
围栏过期 支持设置过期时间,到期自动移除

三、代码实战

3.1 地理围栏管理器

// GeofenceManager.ets
// 功能:地理围栏完整管理,支持添加/移除/查询围栏

import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = '[GeofenceManager]';

// 围栏配置
export interface GeofenceConfig {
  id: string;                  // 围栏唯一标识
  name: string;                // 围栏名称
  latitude: number;            // 圆心纬度
  longitude: number;           // 圆心经度
  radius: number;              // 半径(米)
  expirationMs: number;        // 过期时间(毫秒),0=永不过期
  triggerEvents: number;       // 触发事件类型(位掩码)
  onEnter?: () => void;        // 进入回调
  onExit?: () => void;         // 离开回调
  onStay?: () => void;         // 停留回调
}

// 围栏状态
export interface GeofenceState {
  config: GeofenceConfig;
  fenceId: number;              // 系统分配的围栏ID
  isActive: boolean;            // 是否激活
  lastTriggerTime: number;      // 最后触发时间
  lastTriggerEvent: string;     // 最后触发事件
}

export class GeofenceManager {
  private fenceStates: Map<string, GeofenceState> = new Map();
  private callbackId: number = -1;

  /**
   * 初始化围栏监听
   */
  initGeofenceListener(): void {
    try {
      this.callbackId = geoLocationManager.on('geofenceEvent',
        (geofenceEvent: geoLocationManager.GeofenceEvent) => {
          this.handleGeofenceEvent(geofenceEvent);
        }
      );
      console.info(TAG, `围栏监听已注册, callbackId=${this.callbackId}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(TAG, `注册围栏监听失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 添加地理围栏
   */
  addGeofence(config: GeofenceConfig): boolean {
    if (this.fenceStates.has(config.id)) {
      console.warn(TAG, `围栏 ${config.id} 已存在`);
      return false;
    }

    try {
      // 构建围栏请求
      const geofenceRequest: geoLocationManager.GeofenceRequest = {
        priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
        scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
        geofence: {
          latitude: config.latitude,
          longitude: config.longitude,
          radius: config.radius,
          expiration: config.expirationMs
        }
      };

      // 添加围栏,获取系统分配的围栏ID
      const fenceId = geoLocationManager.addGeofence(geofenceRequest);

      // 保存围栏状态
      const state: GeofenceState = {
        config: config,
        fenceId: fenceId,
        isActive: true,
        lastTriggerTime: 0,
        lastTriggerEvent: 'none'
      };
      this.fenceStates.set(config.id, state);

      console.info(TAG, `围栏已添加: id=${config.id}, name=${config.name}, ` +
        `fenceId=${fenceId}, center=(${config.latitude}, ${config.longitude}), ` +
        `radius=${config.radius}m`);
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(TAG, `添加围栏失败: ${err.code} - ${err.message}`);
      return false;
    }
  }

  /**
   * 移除地理围栏
   */
  removeGeofence(fenceConfigId: string): boolean {
    const state = this.fenceStates.get(fenceConfigId);
    if (!state) {
      console.warn(TAG, `围栏 ${fenceConfigId} 不存在`);
      return false;
    }

    try {
      geoLocationManager.removeGeofence(state.fenceId);
      this.fenceStates.delete(fenceConfigId);
      console.info(TAG, `围栏已移除: id=${fenceConfigId}`);
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(TAG, `移除围栏失败: ${err.code} - ${err.message}`);
      return false;
    }
  }

  /**
   * 处理围栏事件
   */
  private handleGeofenceEvent(geofenceEvent: geoLocationManager.GeofenceEvent): void {
    console.info(TAG, `收到围栏事件: fenceId=${geofenceEvent.fenceId}, ` +
      `event=${geofenceEvent.event}`);

    // 查找对应的围栏配置
    for (const [configId, state] of this.fenceStates) {
      if (state.fenceId === geofenceEvent.fenceId) {
        // 更新围栏状态
        state.lastTriggerTime = Date.now();

        // 根据事件类型触发回调
        switch (geofenceEvent.event) {
          case geoLocationManager.GeoEvent.GEOFENCE_EVENT_ENTER:
            state.lastTriggerEvent = 'enter';
            console.info(TAG, `进入围栏: ${state.config.name}`);
            state.config.onEnter?.();
            break;

          case geoLocationManager.GeoEvent.GEOFENCE_EVENT_EXIT:
            state.lastTriggerEvent = 'exit';
            console.info(TAG, `离开围栏: ${state.config.name}`);
            state.config.onExit?.();
            break;

          case geoLocationManager.GeoEvent.GEOFENCE_EVENT_STAY:
            state.lastTriggerEvent = 'stay';
            console.info(TAG, `停留在围栏: ${state.config.name}`);
            state.config.onStay?.();
            break;
        }
        break;
      }
    }
  }

  /**
   * 获取所有围栏状态
   */
  getAllFenceStates(): GeofenceState[] {
    return Array.from(this.fenceStates.values());
  }

  /**
   * 获取围栏数量
   */
  getFenceCount(): number {
    return this.fenceStates.size;
  }

  /**
   * 移除所有围栏
   */
  removeAllFences(): void {
    for (const [configId, state] of this.fenceStates) {
      try {
        geoLocationManager.removeGeofence(state.fenceId);
      } catch (error) {
        console.error(TAG, `移除围栏 ${configId} 失败`);
      }
    }
    this.fenceStates.clear();
    console.info(TAG, '所有围栏已移除');
  }

  /**
   * 销毁管理器
   */
  destroy(): void {
    this.removeAllFences();
    if (this.callbackId !== -1) {
      try {
        geoLocationManager.off('geofenceEvent', this.callbackId);
      } catch (e) {
        // 忽略
      }
      this.callbackId = -1;
    }
  }
}

3.2 考勤打卡场景实现

// AttendanceGeofence.ets
// 功能:基于地理围栏的考勤打卡系统

import { GeofenceManager, GeofenceConfig } from './GeofenceManager';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = '[AttendanceGeofence]';

// 考勤记录
export interface AttendanceRecord {
  date: string;           // 日期 YYYY-MM-DD
  checkInTime: number;    // 签到时间戳
  checkOutTime: number;   // 签退时间戳
  status: 'normal' | 'late' | 'early_leave' | 'absent';
  location: string;       // 位置描述
}

// 考勤配置
export interface AttendanceConfig {
  companyLat: number;     // 公司纬度
  companyLng: number;     // 公司经度
  radius: number;         // 围栏半径(米)
  workStartTime: string;  // 上班时间 HH:mm
  workEndTime: string;    // 下班时间 HH:mm
  lateThreshold: number;  // 迟到阈值(分钟)
}

export class AttendanceGeofence {
  private geofenceManager = new GeofenceManager();
  private config: AttendanceConfig;
  private todayRecord: AttendanceRecord | null = null;
  private isCheckedIn: boolean = false;

  constructor(config: AttendanceConfig) {
    this.config = config;
  }

  /**
   * 启动考勤围栏
   */
  startAttendance(): void {
    // 初始化围栏监听
    this.geofenceManager.initGeofenceListener();

    // 添加公司围栏
    const companyFence: GeofenceConfig = {
      id: 'company_attendance',
      name: '公司考勤区域',
      latitude: this.config.companyLat,
      longitude: this.config.companyLng,
      radius: this.config.radius,
      expirationMs: 0, // 永不过期
      triggerEvents: 3, // ENTER + EXIT
      onEnter: () => {
        this.handleEnterCompany();
      },
      onExit: () => {
        this.handleLeaveCompany();
      }
    };

    this.geofenceManager.addGeofence(companyFence);
    console.info(TAG, '考勤围栏已启动');
  }

  /**
   * 进入公司区域
   */
  private handleEnterCompany(): void {
    const now = new Date();
    const today = this.formatDate(now);

    // 检查是否已签到
    if (this.isCheckedIn && this.todayRecord?.date === today) {
      console.info(TAG, '今日已签到,忽略重复触发');
      return;
    }

    // 签到
    const workStartMinutes = this.parseTimeToMinutes(this.config.workStartTime);
    const currentMinutes = now.getHours() * 60 + now.getMinutes();
    const isLate = currentMinutes > workStartMinutes + this.config.lateThreshold;

    this.todayRecord = {
      date: today,
      checkInTime: Date.now(),
      checkOutTime: 0,
      status: isLate ? 'late' : 'normal',
      location: '公司考勤区域'
    };
    this.isCheckedIn = true;

    console.info(TAG, `签到成功: ${isLate ? '迟到' : '正常'}, 时间=${now.toLocaleTimeString()}`);

    // 发送通知
    this.sendNotification(
      isLate ? '迟到签到' : '正常签到',
      `签到时间: ${now.toLocaleTimeString()}`
    );
  }

  /**
   * 离开公司区域
   */
  private handleLeaveCompany(): void {
    if (!this.isCheckedIn || !this.todayRecord) {
      return;
    }

    const now = new Date();
    const workEndMinutes = this.parseTimeToMinutes(this.config.workEndTime);
    const currentMinutes = now.getHours() * 60 + now.getMinutes();
    const isEarlyLeave = currentMinutes < workEndMinutes;

    // 更新签退时间
    this.todayRecord.checkOutTime = Date.now();
    if (isEarlyLeave && this.todayRecord.status === 'normal') {
      this.todayRecord.status = 'early_leave';
    }

    console.info(TAG, `签退: ${isEarlyLeave ? '早退' : '正常'}, 时间=${now.toLocaleTimeString()}`);

    this.sendNotification(
      isEarlyLeave ? '早退签退' : '正常签退',
      `签退时间: ${now.toLocaleTimeString()}`
    );

    this.isCheckedIn = false;
  }

  /**
   * 停止考勤
   */
  stopAttendance(): void {
    this.geofenceManager.destroy();
    console.info(TAG, '考勤围栏已停止');
  }

  /**
   * 获取今日考勤记录
   */
  getTodayRecord(): AttendanceRecord | null {
    return this.todayRecord;
  }

  private formatDate(date: Date): string {
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
  }

  private parseTimeToMinutes(time: string): number {
    const [h, m] = time.split(':').map(Number);
    return h * 60 + m;
  }

  private sendNotification(title: string, content: string): void {
    // 实际项目中使用NotificationManager发送通知
    console.info(TAG, `[通知] ${title}: ${content}`);
  }
}

3.3 智能提醒围栏

// SmartReminderGeofence.ets
// 功能:基于地理围栏的智能提醒系统

import { GeofenceManager, GeofenceConfig } from './GeofenceManager';

const TAG = '[SmartReminderGeofence]';

// 提醒配置
export interface ReminderConfig {
  id: string;
  name: string;
  latitude: number;
  longitude: number;
  radius: number;
  message: string;
  triggerOnEnter: boolean;
  triggerOnExit: boolean;
  repeat: boolean;          // 是否重复触发
  cooldownMs: number;       // 冷却时间(毫秒)
}

export class SmartReminderGeofence {
  private geofenceManager = new GeofenceManager();
  private reminders: Map<string, ReminderConfig> = new Map();
  private lastTriggerTime: Map<string, number> = new Map();

  /**
   * 初始化提醒系统
   */
  init(): void {
    this.geofenceManager.initGeofenceListener();
  }

  /**
   * 添加位置提醒
   */
  addReminder(config: ReminderConfig): boolean {
    if (this.reminders.has(config.id)) {
      console.warn(TAG, `提醒 ${config.id} 已存在`);
      return false;
    }

    this.reminders.set(config.id, config);

    // 构建围栏配置
    const fenceConfig: GeofenceConfig = {
      id: `reminder_${config.id}`,
      name: config.name,
      latitude: config.latitude,
      longitude: config.longitude,
      radius: config.radius,
      expirationMs: 0,
      triggerEvents: (config.triggerOnEnter ? 1 : 0) | (config.triggerOnExit ? 2 : 0),
      onEnter: config.triggerOnEnter ? () => {
        this.triggerReminder(config.id, 'enter');
      } : undefined,
      onExit: config.triggerOnExit ? () => {
        this.triggerReminder(config.id, 'exit');
      } : undefined
    };

    return this.geofenceManager.addGeofence(fenceConfig);
  }

  /**
   * 触发提醒
   */
  private triggerReminder(reminderId: string, eventType: string): void {
    const config = this.reminders.get(reminderId);
    if (!config) return;

    // 冷却检查
    const lastTime = this.lastTriggerTime.get(reminderId) ?? 0;
    if (Date.now() - lastTime < config.cooldownMs) {
      console.info(TAG, `提醒 ${config.name} 冷却中,跳过`);
      return;
    }

    this.lastTriggerTime.set(reminderId, Date.now());

    const action = eventType === 'enter' ? '到达' : '离开';
    console.info(TAG, `📍 ${action}${config.name}: ${config.message}`);

    // 发送通知
    this.sendReminderNotification(
      `${action}${config.name}`,
      config.message
    );

    // 非重复提醒,触发后移除
    if (!config.repeat) {
      this.removeReminder(reminderId);
    }
  }

  /**
   * 移除提醒
   */
  removeReminder(reminderId: string): boolean {
    this.reminders.delete(reminderId);
    this.lastTriggerTime.delete(reminderId);
    return this.geofenceManager.removeGeofence(`reminder_${reminderId}`);
  }

  /**
   * 获取所有提醒
   */
  getAllReminders(): ReminderConfig[] {
    return Array.from(this.reminders.values());
  }

  /**
   * 销毁
   */
  destroy(): void {
    this.geofenceManager.destroy();
    this.reminders.clear();
    this.lastTriggerTime.clear();
  }

  private sendReminderNotification(title: string, content: string): void {
    console.info(TAG, `[提醒通知] ${title}: ${content}`);
  }
}

3.4 完整围栏管理UI

// GeofenceManagementPage.ets
// 功能:地理围栏管理界面,支持添加/查看/删除围栏

import { GeofenceManager, GeofenceConfig, GeofenceState } from './GeofenceManager';
import { SmartReminderGeofence, ReminderConfig } from './SmartReminderGeofence';

@Entry
@Component
struct GeofenceManagementPage {
  private geofenceManager = new GeofenceManager();
  private reminderManager = new SmartReminderGeofence();

  @State fenceList: GeofenceState[] = [];
  @State reminderList: ReminderConfig[] = [];
  @State showAddDialog: boolean = false;
  @State newFenceName: string = '';
  @State newFenceLat: string = '39.9042';
  @State newFenceLng: string = '116.4074';
  @State newFenceRadius: string = '200';
  @State eventLog: string[] = [];

  aboutToAppear(): void {
    this.geofenceManager.initGeofenceListener();
    this.reminderManager.init();
    this.loadFenceList();

    // 添加预设围栏
    this.addPresetFences();
  }

  aboutToDisappear(): void {
    this.geofenceManager.destroy();
    this.reminderManager.destroy();
  }

  /**
   * 添加预设围栏
   */
  private addPresetFences(): void {
    // 公司围栏
    this.geofenceManager.addGeofence({
      id: 'company',
      name: '公司',
      latitude: 39.9042,
      longitude: 116.4074,
      radius: 200,
      expirationMs: 0,
      triggerEvents: 3,
      onEnter: () => {
        this.addLog('📍 进入公司区域');
      },
      onExit: () => {
        this.addLog('👋 离开公司区域');
      }
    });

    // 家围栏
    this.geofenceManager.addGeofence({
      id: 'home',
      name: '家',
      latitude: 39.9219,
      longitude: 116.4435,
      radius: 300,
      expirationMs: 0,
      triggerEvents: 3,
      onEnter: () => {
        this.addLog('🏠 到家了');
      },
      onExit: () => {
        this.addLog('🚶 离开家');
      }
    });

    this.loadFenceList();
  }

  /**
   * 加载围栏列表
   */
  private loadFenceList(): void {
    this.fenceList = this.geofenceManager.getAllFenceStates();
  }

  /**
   * 添加日志
   */
  private addLog(message: string): void {
    const time = new Date().toLocaleTimeString();
    this.eventLog.unshift(`[${time}] ${message}`);
    if (this.eventLog.length > 50) {
      this.eventLog.pop();
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('地理围栏管理')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        
        Text(`${this.fenceList.length} 个围栏`)
          .fontSize(12)
          .fontColor('#AAAAAA')
          .margin({ left: 8 })
      }
      .width('90%')
      .margin({ top: 20, bottom: 16 })

      // 围栏列表
      if (this.fenceList.length > 0) {
        Text('活跃围栏')
          .fontSize(14)
          .fontColor('#AAAAAA')
          .width('90%')
          .margin({ bottom: 8 })

        List() {
          ForEach(this.fenceList, (state: GeofenceState) => {
            ListItem() {
              this.FenceCard(state)
            }
            .margin({ bottom: 8 })
          })
        }
        .width('90%')
        .height('30%')
      }

      // 添加围栏按钮
      Button('+ 添加围栏')
        .width('90%')
        .height(44)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .backgroundColor('#6C5CE7')
        .borderRadius(22)
        .margin({ top: 12 })
        .onClick(() => {
          this.showAddDialog = true;
        })

      // 事件日志
      if (this.eventLog.length > 0) {
        Text('围栏事件')
          .fontSize(14)
          .fontColor('#AAAAAA')
          .width('90%')
          .margin({ top: 20, bottom: 8 })

        List() {
          ForEach(this.eventLog, (log: string, index: number) => {
            ListItem() {
              Text(log)
                .fontSize(12)
                .fontColor(index === 0 ? '#4ECDC4' : '#999999')
                .width('100%')
                .padding(8)
            }
            .borderRadius(8)
            .backgroundColor(index === 0 ? 'rgba(78,205,196,0.1)' : 'transparent')
            .margin({ bottom: 2 })
          })
        }
        .width('90%')
        .height('30%')
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  FenceCard(state: GeofenceState) {
    Column() {
      Row() {
        // 围栏状态指示灯
        Circle({ width: 8, height: 8 })
          .fill(state.isActive ? '#00B894' : '#FF6B6B')
          .margin({ right: 8 })

        Text(state.config.name)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Medium)
          .layoutWeight(1)

        // 删除按钮
        Text('✕')
          .fontSize(16)
          .fontColor('#FF6B6B')
          .onClick(() => {
            this.geofenceManager.removeGeofence(state.config.id);
            this.loadFenceList();
          })
      }
      .width('100%')

      Row() {
        Text(`📍 (${state.config.latitude.toFixed(4)}, ${state.config.longitude.toFixed(4)})`)
          .fontSize(11)
          .fontColor('#888888')
        
        Text(`${state.config.radius}m`)
          .fontSize(11)
          .fontColor('#888888')
          .margin({ left: 12 })
      }
      .width('100%')
      .margin({ top: 4 })

      if (state.lastTriggerTime > 0) {
        Text(`最近触发: ${state.lastTriggerEvent} @ ${new Date(state.lastTriggerTime).toLocaleTimeString()}`)
          .fontSize(10)
          .fontColor('#666666')
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .padding(14)
    .borderRadius(12)
    .backgroundColor('rgba(255,255,255,0.06)')
    .backdropBlur(10)
  }
}

四、踩坑与注意事项

4.1 围栏触发延迟

地理围栏不是实时触发的,存在一定的延迟:

因素 延迟范围 说明
系统调度 1~5分钟 系统周期性检查位置
GPS精度 ±10~50m 精度波动可能导致边界误判
网络延迟 0.5~3秒 网络定位结果传输延迟
设备休眠 5~15分钟 休眠时位置更新频率降低

应对策略

// 对于时间敏感的场景,结合持续定位使用
// 围栏作为粗筛,持续定位作为精确认
async function preciseGeofenceDetection(
  fenceLat: number, fenceLng: number, fenceRadius: number
): Promise<void> {
  // 1. 先用围栏做粗略检测
  // 2. 围栏触发后,启动持续定位做精确判断
  const location = await geoLocationManager.getCurrentLocation({
    priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
    timeoutMs: 10000
  });
  
  const distance = calculateDistance(
    location.latitude, location.longitude,
    fenceLat, fenceLng
  );
  
  if (distance <= fenceRadius) {
    console.info('确认在围栏内');
  }
}

// Haversine公式计算两点距离
function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const R = 6371000; // 地球半径(米)
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

4.2 围栏ID管理

// ⚠️ 注意:addGeofence返回的fenceId是系统分配的,与自定义id不同
// 必须保存映射关系

// ❌ 错误:不保存系统fenceId,无法移除围栏
const fenceId = geoLocationManager.addGeofence(request);
// fenceId丢失了,无法调用removeGeofence

// ✅ 正确:保存映射关系
const fenceIdMap = new Map<string, number>();
function addFenceWithTracking(customId: string, request: geoLocationManager.GeofenceRequest): void {
  const systemFenceId = geoLocationManager.addGeofence(request);
  fenceIdMap.set(customId, systemFenceId);
}

function removeFenceByCustomId(customId: string): void {
  const systemFenceId = fenceIdMap.get(customId);
  if (systemFenceId !== undefined) {
    geoLocationManager.removeGeofence(systemFenceId);
    fenceIdMap.delete(customId);
  }
}

4.3 围栏与省电模式冲突

当设备进入省电模式时,系统可能降低位置更新频率,导致围栏触发延迟增大甚至不触发。

解决方案

  • 关键围栏设置较大的半径(>200m)
  • 使用ACCURACY_PRIORITY优先级
  • 在应用内提示用户关闭省电模式

4.4 常见错误码

错误码 含义 解决方案
3301000 位置服务不可用 检查系统定位开关
3301600 围栏数量超限 移除不需要的围栏
3301601 添加围栏失败 检查参数是否合法
3301602 移除围栏失败 检查fenceId是否正确

五、HarmonyOS 6适配

5.1 多边形围栏

HarmonyOS 6扩展了围栏形状,支持多边形围栏

// HarmonyOS 6 多边形围栏(预期API)
interface PolygonGeofence {
  vertices: Array<{ latitude: number; longitude: number }>; // 顶点列表
  expiration: number;
}

多边形围栏适用于不规则区域,如校区、园区、商圈等。

5.2 围栏优先级

HarmonyOS 6新增围栏优先级机制:

  • 高优先级围栏:系统保证及时触发,适用于安全类场景
  • 普通优先级围栏:系统根据功耗策略调度,适用于提醒类场景

5.3 围栏与智慧感知融合

HarmonyOS 6将地理围栏与智慧感知能力融合:

  • 结合时间围栏:工作日9:00-18:00 + 公司区域
  • 结合活动状态:步行状态 + 商圈区域
  • 结合设备状态:连接车载蓝牙 + 离家区域

六、总结

本文深入探讨了HarmonyOS地理围栏与区域触发的技术原理与开发实践:

知识点 关键内容
围栏原理 虚拟边界、被动触发、系统后台监控
API体系 addGeofence、removeGeofence、geofenceEvent
触发类型 ENTER进入、EXIT离开、STAY停留
参数配置 经纬度、半径、过期时间、触发事件
实战场景 考勤打卡、智能提醒、区域营销
性能优化 半径选择、冷却机制、ID映射管理
HarmonyOS 6 多边形围栏、优先级、智慧感知融合

核心建议

  1. 围栏半径不宜过小(建议≥100m),避免GPS精度波动导致误触发
  2. 关键场景结合围栏+持续定位实现粗筛+确认的双重检测
  3. 务必保存系统fenceId与自定义id的映射关系
  4. 实现冷却机制,防止同一围栏短时间内重复触发
  5. 页面销毁时及时移除围栏和监听,避免资源泄漏

下一篇我们将深入位置订阅与轨迹追踪,探讨如何实现持续位置订阅、轨迹记录与可视化展示。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。