首页 / 资讯中心 / 文章详情

HarmonyOS定位服务与锁屏实况窗联动开发实践

HarmonyOS定位服务与锁屏实况窗联动开发实践 ★ FEATURED ARTICLE
做HarmonyOS应用开发定位服务和锁屏实况窗这两个能力基本绕不开。很多LBS类应用的第一道门槛就是位置获取而实况窗则是鸿蒙生态里非常有代表性的交互形态。把两者放在一起能做出很多实用的小功能比如打车时在锁屏上实时盯着司机距离点外卖后在锁屏看骑手到哪了或者做一个骑行回家的路线提醒卡片。这篇内容我从实际开发的角度把位置服务的权限配置、定位参数选择、实况窗的发布和更新流程以及两者联动时的状态切换思路都梳理一遍还会附上我在真机调试过程中踩过的坑。1. 定位和锁屏实况窗的结合点在哪里1.1 实况窗和普通通知的本质区别很多开发者第一次接触实况窗时会把它理解为高级一点的通知。这个理解不算错但会限制你的设计思路。普通通知是一次性的发完就完事了用户在锁屏上看到的是静态摘要想获取更多信息还得点击通知、解锁手机、跳进应用。实况窗的核心价值在于常驻和实时它可以在锁屏界面上保留一个卡片形态内容可以持续更新用户不用做任何操作就能看到最新状态。这种体验差异在具体场景里非常明显。外卖场景里普通通知只能在骑手接单、取餐、送达时各推一条消息中间那段漫长的配送过程完全靠用户自己反复打开App猜进度。实况窗则是把配送进度、剩余距离、预计到达时间直接钉在锁屏上用户抬起手腕或点亮屏幕就能看到。更关键的是实况窗在系统层面有优先级保障不会被后续的其他通知挤出通知栏这个特性是普通通知完全做不到的。从开发角度看两者的底层实现其实共用一套通知服务框架但实况窗在通知接口上有独立的模板和生命周期管理方式。它支持在卡片上放按钮、进度条、文本甚至可以让用户直接点击卡片拉起应用。我习惯把它比作一块可以远程遥控的小号广告牌内容是活的交互是直达的展示位置是系统给你保住的。1.2 这套组合最适合做什么场景定位加实况窗本质上是在解决用户不想频繁打开App但又需要持续知道位置状态的问题。我实际做下来感觉这类场景有几个共同特点状态变化频率适中、数据可以被量化展示、结果有明确的完成任务节点。出行类是最典型的。骑行或步行导航时实时显示距离目的地还有多远、进度条走到哪儿了车队管理场景里调度员在锁屏上看司机位置趋势宠物定位器、老人防走丢手环这类设备配套App也能用实况窗把位置更新推送到锁屏。还有自营配送、同城闪送等业务锁屏上的距离数字每刷新一次用户的焦虑感就降低一分。我这次做的Demo就设定为一个骑行回家的场景应用获取当前位置实时计算与家庭地址的距离通过锁屏实况窗展示剩余距离、进度百分比和即将到达的状态切换。这个Demo麻雀虽小但位置服务、实况窗生命周期、数据联动这些关键点全部覆盖到了你要做复杂场景时只要在这个骨架上往里面填业务逻辑就行。2. 位置服务开发权限、参数和连续定位2.1 先把权限配置写对HarmonyOS的位置权限有两级一个是ohos.permission.LOCATION精确位置一个是ohos.permission.APPROXIMATELY_LOCATION模糊位置。从HarmonyOS NEXT开始系统在权限管理上更强调最小化授权如果你只申请了精确位置权限而没申请模糊位置某些版本下反而会被系统判定为不合理申请弹窗拒绝。稳妥的做法是两个一起申请。模块配置文件里要这样声明权限{ module: { requestPermissions: [ { name: ohos.permission.LOCATION, reason: $string:location_reason, usedScene: { abilities: [EntryAbility], when: inuse } }, { name: ohos.permission.APPROXIMATELY_LOCATION, reason: $string:location_reason, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }注意reason字段是给用户在权限弹窗里看的说明文字不要写为了更好体验这种模糊话术系统审核和用户授权率都会受影响。比较务实的写法是用于在骑行回家时展示实时距离和到达提醒越具体越容易获得用户信任。配置完模块文件还需要在代码里动态申请权限。有两点容易被新手忽略一是动态申请必须在onWindowStageCreate之后执行太早发起会因为窗口尚未就绪而失败二是用户在系统设置里手动关闭权限后应用不会收到主动回调需要在重新回到前台时用abilityAccessCtrl.checkAccessToken检查一遍授权状态。import { abilityAccessCtrl, Permissions } from kit.AbilityKit; async function checkAndRequestLocationPermission(): Promiseboolean { const atManager abilityAccessCtrl.createAtManager(); const grantStatus await atManager.checkAccessToken( { tokenId: 0, permissionName: ohos.permission.LOCATION } ); if (grantStatus abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { return true; } const permissions: ArrayPermissions [ ohos.permission.LOCATION, ohos.permission.APPROXIMATELY_LOCATION ]; try { await atManager.requestPermissionsFromUser(permissions); return true; } catch (err) { console.error(Permission request failed: JSON.stringify(err)); return false; } }2.2 定位参数不是随便填的HarmonyOS的定位服务通过geoLocationManager提供调用getCurrentLocation或订阅on(locationChange)之前都要先构造一个LocationRequest对象。这个对象的参数设置直接决定定位成功率、精度和功耗。最关键的是priority和scenario两个字段很多人把它们当成冗余配置实际它们会在底层影响GPS芯片和网络模块的调度策略。import { geoLocationManager } from kit.LocationKit; import { BusinessError } from kit.BasicServicesKit; let locationRequest: geoLocationManager.LocationRequest { priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, scenario: geoLocationManager.LocationRequestScenario.NAVIGATION, maxAccuracy: 50, timeoutMs: 10000, maxScale: 1, }; try { const location await geoLocationManager.getCurrentLocation(locationRequest); console.info(Latitude: ${location.latitude}, Longitude: ${location.longitude}); } catch (err) { const e err as BusinessError; console.error(getCurrentLocation failed: code${e.code}, message${e.message}); }FIRST_FIX的含义是尽快拿到首次定位适合骑行、步行等需要快速展示起始位置的场景。NAVIGATION场景会让系统优先启用GPS、提高定位频率代价是功耗更高。如果你的场景只是每隔几分钟上报一次位置用DAILY_LIFE_SERVICE配LOW_POWER会更合适。maxAccuracy是期望精度单位是米。这里要特别提醒它表达的是一个期望值不是保证值。系统会尽量满足但如果当前环境信号差返回的定位点精度劣于50米也是正常的代码里要做兜底判断。timeoutMs建议设置10秒左右太短容易在室内定位失败太长又会让用户等得烦躁。2.3 单次定位还是连续定位不同的业务要选不同的定位模式。Navigation场景需要持续关注位置变化应该订阅locationChange而打卡报平安这类只需要一个点的场景用getCurrentLocation就够了。我这次做骑行回家显然需要连续定位。订阅方式很简单import { geoLocationManager } from kit.LocationKit; let subscriptionId -1; function startLocationTracking(callback: (lat: number, lng: number) void) { let request: geoLocationManager.LocationRequest { priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, scenario: geoLocationManager.LocationRequestScenario.NAVIGATION, maxAccuracy: 50, timeoutMs: 10000, maxScale: 1, }; subscriptionId geoLocationManager.on(locationChange, request, (location) { callback(location.latitude, location.longitude); }); } function stopLocationTracking() { if (subscriptionId 0) { geoLocationManager.off(locationChange, subscriptionId); subscriptionId -1; } }订阅后有一个容易被忽视的点off要传入订阅时返回的subscriptionId否则解绑可能不生效导致定位一直在后台跑、耗电直线上升。这个坑我在真机上踩过一次应用都退出了日志里还能看到位置回调在打印。后来查了社区资料才发现是off参数没带对。此外连续定位的回调频率通常比业务需要的频率高。实况窗的卡片刷新也不宜过于频繁我建议在回调层做一次节流比如距离变化超过50米或者时间间隔超过3秒才触发UI更新。这样既能保证体验又避免无谓的性能消耗。3. 锁屏沉浸实况窗开发要点3.1 实况窗的工作机制概述实况窗在HarmonyOS里走的是NotificationKit这套框架但它和普通通知是不同类型。发布时指定NotificationContentType为NOTIFICATION_CONTENT_LIVE_VIEW系统就会把它当作实况窗处理。实况窗内容由LiveViewContent描述支持标题、文本、图标、进度条和按钮这些元素。沉浸体现在锁屏场景下系统会用专门的模板来渲染实况窗卡片层级高于普通通知并且可以展示进度条等动态元素。你可以把它理解成系统给应用划出了一块锁屏画布应用只需要上传内容和更新指令绘制工作由系统统一完成。这套机制的好处是不同应用的实况窗在视觉和交互上有统一的规范不会出现某个应用霸屏锁屏的情况。系统层面做了严格的限制锁屏上同时展示的实况窗数量有限超过限制后新的实况窗要排队所以不要指望靠多发几条实况窗来刷存在感把一条做好做精才是正道。3.2 用代码发布一张实况窗卡片发布实况窗的核心是构造NotificationRequest把content.notificationContentType设为NOTIFICATION_CONTENT_LIVE_VIEW再把liveView属性赋上内容对象。import { notificationManager } from kit.NotificationKit; import { wantAgent } from kit.AbilityKit; async function publishLiveView() { // 先构造一个WantAgent用于用户点击卡片时拉起应用 let wantAgentInfo: wantAgent.WantAgentInfo { wants: [ { bundleName: com.example.locationlive, abilityName: EntryAbility } ], actionType: wantAgent.OperationType.START_ABILITY, requestCode: 0 }; let wantAgentObj await wantAgent.getWantAgent(wantAgentInfo); let liveViewContent: notificationManager.LiveViewContent { title: 回家路线, text: 正在定位当前进度, icon: $r(app.media.app_icon), progress: { value: 0, maxValue: 100 }, button: { names: [查看详情], wantAgent: wantAgentObj } }; let notificationRequest: notificationManager.NotificationRequest { id: 1001, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_LIVE_VIEW, liveView: liveViewContent } }; await notificationManager.publish(notificationRequest); }id字段在这个流程里很重要。后续更新、删除实况窗都要靠它定位到具体的那条通知所以建议用一个专门的常量来管理别随便写个魔法数字。代码里的icon我直接用了应用图标实际业务中可以换成语义更明确的图片比如一辆自行车或者一个家的图标。button是可选的如果你希望用户点击卡片上的按钮跳到特定页面就用WantAgent去拉起对应Ability。不传按钮整个卡片区域也可以响应点击。3.3 更新和结束实况窗的生命周期管理实况窗的活主要体现在它支持原地更新。方法很简单把publish换成update传入同样的id内容替换成新状态即可。async function updateLiveView(newText: string, progressValue: number) { let updatedContent: notificationManager.LiveViewContent { title: 回家路线, text: newText, icon: $r(app.media.app_icon), progress: { value: progressValue, maxValue: 100 }, button: { names: [查看详情], wantAgent: wantAgentObj } }; let updateRequest: notificationManager.NotificationRequest { id: 1001, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_LIVE_VIEW, liveView: updatedContent } }; await notificationManager.update(updateRequest); }更新时有个细节值得注意LiveViewContent里的title每次都要带上如果更新时只传了变化的字段某些版本的系统模板可能会把未传字段重置成默认值导致卡片上的标题消失。我在模拟器上验证过这个现象稳妥做法是每次更新都把完整内容对象构造好。任务结束时要记得把实况窗从通知中心清掉避免用户完成行程后在锁屏上还挂着一个已完成的卡片。删除同样走notificationManagerawait notificationManager.cancel(1001);到这里实况窗的完整生命周期就闭环了发布、更新、结束。这三个阶段对应到业务上就是开始行程、行程进行中、行程结束。4. 定位数据驱动实况窗的联动实现4.1 从定位回调到界面刷新的数据链路定位和实况窗两者单独看都不算复杂真正有意思的是联动部分。我把整条链路设计成这样的单向数据流定位回调产生坐标点坐标点经过距离计算变成剩余距离和进度百分比这两个数值再包装成LiveViewContent推送出去。这个链路里有个设计原则实况窗的更新函数应该是纯展示逻辑不要在里面去获取定位、不要做耗时计算。原因很简单实况窗更新走的是系统通知通道如果每次更新前还要等定位回调消息就变成异步串联了卡片上的数字会滞后。正确做法是先把距离算好然后立刻更新卡片。代码结构上我用一个updateLoop统一处理function onLocationUpdate(lat: number, lng: number) { const distance calculateDistance(lat, lng, HOME_LAT, HOME_LNG); const progress Math.min(100, Math.round((1 - distance / TOTAL_DISTANCE) * 100)); const displayText formatDistanceText(distance); if (distance ARRIVE_THRESHOLD) { updateLiveView(你已经到达目的地附近, 100); stopLocationTracking(); return; } updateLiveView(displayText, progress); }这里把所有的业务判断都放在定位回调里做实况窗更新就变成了一个无脑执行的动作。调试时思路特别清晰看到定位日志就猜到会更新什么UI。4.2 距离计算与到达判断距离这个值看似简单实际实现时有个关键选择是调用系统提供的计算接口还是自己写球面距离公式。我选择了自己写Haversine公式。原因是传统定位SDK里经常封装好的距离计算函数在HarmonyOS的geoLocationManager里没有直接暴露与其到处找工具类不如自己写一个顺便还能加深对球面几何的理解。function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { const R 6371000; // 地球平均半径单位米 const toRad (deg: number) deg * Math.PI / 180; const dLat toRad(lat2 - lat1); const dLng toRad(lng2 - lng1); const a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); const c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }对于骑行回家这种几百米到几公里的尺度Haversine公式的误差完全可以接受。如果你做的场景是跨城市的长距离运输或者对精度要求达到米级以下建议换成更适合的算法但实际业务中很少遇到这种需求。到达判断的阈值我设为100米。这个值不是拍脑袋定的参考了系统通知和地图导航场景的常见值太小的阈值如20米GPS漂移很容易造成误判太大的阈值如500米用户还没真正到达就触发提醒体验反而奇怪。100米配合上是否处于移动状态的判断基本能做到准确。const ARRIVE_THRESHOLD 100; // 米 const DISTANCE_CHANGE_THRESHOLD 50; // 米触发UI更新的最小距离变化 function formatDistanceText(distance: number): string { if (distance 1000) { return 距目的地还有 ${(distance / 1000).toFixed(1)} 公里; } return 距目的地还有 ${Math.round(distance)} 米; }4.3 状态切换与防抖实况窗最忌讳的就是内容反复横跳。定位点天然有噪声同一个地方停着不动连续两次定位的坐标也可能相差30到50米。如果不做处理卡片上的距离数字会像呼吸灯一样一高一低地跳。我做了两个层面的防抖。第一层是距离变化阈值。连续定位回调到达时先计算一下新距离和当前展示距离的差值小于50米就跳过UI更新。这样屏幕上显示的数字是阶梯式下降的符合用户预期。第二层是时间间隔限制。即使距离变化超过阈值也要求距离上次UI更新至少间隔2秒。这个限制对功耗特别有用因为实况窗更新本身会触发系统通知的重新渲染太频繁会让蓝牙和显示模块都处于高负荷状态。状态切换我用一个简单的有限状态机来管理enum TripState { IDLE, MOVING, NEAR_ARRIVE, ARRIVED }MOVING状态下距离大于1000米时显示公里数小于1000米时显示米数进入NEAR_ARRIVE时卡片文案切换为即将到达进度条跳到90以上到达后直接走cancel逻辑清理实况窗。状态机的好处是整个流程的可预测性很强不会出现已经提示到达了又因为一次定位抖动变回配送中的尴尬情况。切到终态后后续即使还有定位回调进来也会被状态机拦住不再更新UI。5. 实战中的常见问题与处理经验5.1 位置权限明明申请了却一直拿不到定位这个现象最常见的原因是应用在onWindowStageCreate之前发起了权限请求。在HarmonyOS里用户授权弹窗必须依赖窗口才能展示太早调用requestPermissionsFromUser会直接走异常分支。你检查一下代码就会发现catch里确实捕获到了错误但很多时候日志不够详细容易被忽略。另一个原因是权限声明和实际使用场景不符。如果只在module.json5里声明了LOCATIONAPPROXIMATELY_LOCATION没加部分版本在requestPermissionsFromUser时会因为权限组不完整而拒绝。把两个权限都配上同时在真机上检查系统设置里的位置信息开关是否打开基本能解决90%的问题。注意模拟器上定位行为与真机差异极大权限弹窗和GPS信号在模拟器里都是模拟的。我建议定位相关功能一律在真机上调试。5.2 实况窗在锁屏上不出现代码没有任何报错publish也返回了成功但锁屏上就是看不到实况窗卡片。这个问题我排查了很久最后定位到是通知渠道的原因。NotificationRequest里如果没有显式指定渠道系统会使用默认渠道而默认渠道的锁屏展示属性可能是关闭的。解决办法是在发布前创建或获取一个锁屏可见的通知渠道并确认渠道的allowBanner和锁屏权限都被正确设置。另外还要检查用户在系统设置里是不是对当前应用关闭了锁屏通知这个开关在HarmonyOS的设置路径比较深普通用户很容易误触。还有一种情况是实况窗确实发布了但被其他应用的实况窗挤掉了。系统对同时展示的实况窗数量有限制测试时建议先把不相关的实况窗清掉再观察。5.3 定位点抖动导致距离数字乱跳如前文所述这是GPS噪声的必然结果不是代码bug但处理不好体验会很差。我的方案是距离变化阈值加时间节流双重过滤。如果做了这两层处理还跳那就要看定位请求参数了——maxAccuracy设得过大、scenario选了低功耗场景都会放大抖动幅度。调试这类问题时有个实用技巧把每次定位回调的经纬度和计算出的距离打印到日志里画一条距离变化曲线。看到原始数据后你就能分清抖动是GPS信号问题还是代码逻辑问题。实测室外开阔地带的定位误差大约在5到15米室内或高架桥下误差可能超过100米UI层面的防抖阈值建议按这个量级设定。5.4 实况窗更新太频繁耗电量明显上涨实况窗每次更新系统都要重新渲染锁屏卡片这本身就比普通通知的展示更耗电。加上连续定位在后台运行两个因素叠加耗电翻倍都不奇怪。优化思路有两个方向。一是定位侧降低定位频率、选择更省电的scenario当任务结束或应用退到后台超过一定时间后主动调用off(locationChange)。二是实况窗侧更新前做距离阈值判断100米或500米才更新一次而不是每次定位回调都触发。我实际测试下来把更新频率从无限刷新降到2秒一次、距离变化50米才刷新后整机功耗下降了大概三分之一。如果你的业务允许进一步放宽阈值也没有问题实况窗展示的本质是趋势和关键节点不是秒级刷新的仪表盘。最后分享一点我自己的体会这套联动方案跑顺畅之后我发现最有价值的不只是最终效果而是过程中培养的状态管理思维。定位数据和UI展示之间不是简单的一对一关系中间隔着精度误差、功耗约束和用户预期只有把状态机、防抖、生命周期这些细节都考虑进去做出来的功能才扛得住真机考验。后来我又把同样的思路扩展到了电池电量提醒和音乐播放控制场景核心框架基本不用改改改数据源和卡片文案就能复用。
阅读完成 · 觉得有帮助?
咨询建站