鸿蒙App开发--雪痕App怎么做GPS轨迹记录?鸿蒙定位服务实战

鸿蒙App开发--雪痕App怎么做GPS轨迹记录?鸿蒙定位服务实战 滑雪App怎么做GPS轨迹记录鸿蒙定位服务实战如果你正在做一款滑雪App或者对运动类应用的定位开发感兴趣可以去鸿蒙应用市场搜一下**「雪痕」**下载下来滑一趟体验体验。实时速度、距离、滑行记录滑完还能看详细总结。体验完了再回来看这篇文章你会更清楚这些功能背后的定位服务是怎么工作的。写在前面大家好我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/VueCSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态用ArkTS开发App这一路踩了不少坑也积累了不少心得。很多人觉得前端转鸿蒙应该很容易——都是写UI嘛组件化、状态管理、生命周期概念都差不多。但真正上手之后你会发现相似的地方让你觉得亲切不同的地方让你抓狂。比如定位服务Web上有navigator.geolocation但浏览器里定位精度一般鸿蒙的ohos.location是系统级API精度更高、功耗更可控。后台定位Web页面切到后台定位就停了鸿蒙可以申请后台长时任务滑雪时切到其他App也能持续记录轨迹。速度计算GPS返回的速度单位是米/秒需要转换成公里/小时还要处理速度抖动。但别担心核心思想是一样的都是获取经纬度坐标都是把坐标转换成用户能看懂的信息。这篇文章聊什么雪痕这个App核心要解决的问题是GPS怎么定位— 用ohos.location获取GPS坐标轨迹怎么记录— 把一连串坐标存下来速度怎么计算— 根据坐标变化计算实时速度第一步权限申请使用定位服务之前需要在module.json5里声明权限{requestPermissions:[{name:ohos.permission.LOCATION,reason:用于获取GPS定位记录滑雪轨迹和速度,usedScene:{abilities:[EntryAbility],when:always}},{name:ohos.permission.APPROXIMATELY_LOCATION,reason:用于获取大致位置辅助定位,usedScene:{abilities:[EntryAbility],when:always}}]}然后在代码里动态申请import{abilityAccessCtrl}fromkit.AbilityKit;asyncfunctionrequestLocationPermission():Promiseboolean{try{constatManagerabilityAccessCtrl.createAtManager();constresultawaitatManager.requestPermissionsFromUser(getContext(),[ohos.permission.LOCATION,ohos.permission.APPROXIMATELY_LOCATION]);returnresult.authResults[0]0result.authResults[1]0;}catch(err){console.error(权限申请失败:,err);returnfalse;}}第二步封装定位服务实际开发中我们不会把定位逻辑直接写在页面组件里。封装一个独立的服务类// LocationService.etsimport{location}fromkit.LocationKit;exportclassLocationService{privatecallback:((location:LocationData)void)|nullnull;privateisRunning:booleanfalse;start(onLocation:(location:LocationData)void){this.callbackonLocation;this.isRunningtrue;location.enableLocation((err){if(err){console.error(启用定位失败:,err);return;}location.on(locationChange,{interval:1000,distanceInterval:0,locationScenario:location.LocationScenario.NAVIGATION},(err,data){if(err){console.error(定位失败:,err);return;}if(this.isRunningthis.callback){this.callback({latitude:data.latitude,longitude:data.longitude,speed:data.speed,altitude:data.altitude,accuracy:data.accuracy,timestamp:data.timeStamp});}});});}stop(){this.isRunningfalse;location.off(locationChange);location.disableLocation();this.callbacknull;}}interfaceLocationData{latitude:number;longitude:number;speed:number;altitude:number;accuracy:number;timestamp:number;}React对应版本模拟数据// React - 模拟定位服务 function useLocation() { const [location, setLocation] useState(null); const [isRunning, setIsRunning] useState(false); const intervalRef useRef(null); const baseLat useRef(46.5197); // 瑞士阿尔卑斯 const baseLng useRef(6.6323); const start () { setIsRunning(true); intervalRef.current setInterval(() { baseLat.current (Math.random() - 0.5) * 0.0002; baseLng.current (Math.random() - 0.5) * 0.0002; setLocation({ latitude: baseLat.current, longitude: baseLng.current, speed: 30 Math.random() * 40, // 30-70 km/h altitude: 1500 Math.random() * 500, accuracy: 5, timestamp: Date.now() }); }, 1000); }; const stop () { setIsRunning(false); clearInterval(intervalRef.current); }; return { location, isRunning, start, stop }; }第三步轨迹记录把GPS坐标存下来形成轨迹// ArkTS - 轨迹记录classTrackRecorder{privatepoints:LocationData[][];privatestartTime:number0;privateisRecording:booleanfalse;start(){this.points[];this.startTimeDate.now();this.isRecordingtrue;}addPoint(point:LocationData){if(!this.isRecording)return;if(point.accuracy50)return;if(this.points.length0){constlastPointthis.points[this.points.length-1];constdistancethis.calculateDistance(lastPoint,point);if(distance1)return;}this.points.push(point);}stop():Track{this.isRecordingfalse;consttotalDistancethis.calculateTotalDistance();consttotalDuration(Date.now()-this.startTime)/1000;return{id:Date.now().toString(),startTime:this.startTime,endTime:Date.now(),points:this.points,totalDistance,totalDuration,avgSpeed:totalDuration0?(totalDistance/totalDuration)*3.6:0,maxSpeed:this.calculateMaxSpeed()};}calculateDistance(p1:LocationData,p2:LocationData):number{constR6371000;constlat1p1.latitude*Math.PI/180;constlat2p2.latitude*Math.PI/180;constdeltaLat(p2.latitude-p1.latitude)*Math.PI/180;constdeltaLng(p2.longitude-p1.longitude)*Math.PI/180;constaMath.sin(deltaLat/2)*Math.sin(deltaLat/2)Math.cos(lat1)*Math.cos(lat2)*Math.sin(deltaLng/2)*Math.sin(deltaLng/2);constc2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));returnR*c;}calculateTotalDistance():number{lettotal0;for(leti1;ithis.points.length;i){totalthis.calculateDistance(this.points[i-1],this.points[i]);}returntotal;}calculateMaxSpeed():number{letmaxSpeed0;for(constpointofthis.points){constspeedpoint.speed*3.6;if(speedmaxSpeed)maxSpeedspeed;}returnmaxSpeed;}}React对应版本// React - 轨迹记录 Hook function useTrackRecorder() { const [points, setPoints] useState([]); const [isRecording, setIsRecording] useState(false); const startTimeRef useRef(null); const start useCallback(() { setPoints([]); startTimeRef.current Date.now(); setIsRecording(true); }, []); const addPoint useCallback((point) { if (!isRecording) return; if (point.accuracy 50) return; setPoints(prev { if (prev.length 0) { const lastPoint prev[prev.length - 1]; const distance calculateDistance(lastPoint, point); if (distance 1) return prev; } return [...prev, point]; }); }, [isRecording]); const stop useCallback(() { setIsRecording(false); const totalDistance calculateTotalDistance(points); const totalDuration (Date.now() - startTimeRef.current) / 1000; return { id: Date.now().toString(), startTime: startTimeRef.current, endTime: Date.now(), points, totalDistance, totalDuration, avgSpeed: totalDuration 0 ? (totalDistance / totalDuration) * 3.6 : 0, maxSpeed: calculateMaxSpeed(points) }; }, [points]); return { points, isRecording, start, addPoint, stop }; }第四步速度计算GPS返回的速度单位是米/秒需要转换成公里/小时// 速度单位转换functionmpsToKmh(mps:number):number{returnmps*3.6;}// 实时速度从GPS数据constspeedKmhmpsToKmh(location.speed);// 低通滤波平滑速度classSpeedFilter{privatefiltered:number0;privatealpha:number0.3;// 滤波系数update(raw:number):number{this.filteredthis.alpha*raw(1-this.alpha)*this.filtered;returnthis.filtered;}}React对应版本// React - 速度计算 const mpsToKmh (mps) mps * 3.6; function useSpeedFilter() { const filteredRef useRef(0); const alpha 0.3; const update useCallback((raw) { filteredRef.current alpha * raw (1 - alpha) * filteredRef.current; return filteredRef.current; }, []); return update; }第五步在滑雪页面集成把所有逻辑整合到滑雪页面// ArkTS - 滑雪页面Componentstruct SkiActive{Statespeed:number0;Statedistance:number0;Stateduration:number0;StateisRunning:booleanfalse;privatelocationService:LocationServicenewLocationService();privatetrackRecorder:TrackRecordernewTrackRecorder();privatespeedFilter:SpeedFilternewSpeedFilter();privatetimer:number0;startSki(){this.isRunningtrue;this.trackRecorder.start();this.locationService.start((loc){constfilteredSpeedthis.speedFilter.update(loc.speed);this.speedmpsToKmh(filteredSpeed);this.trackRecorder.addPoint(loc);consttrackthis.trackRecorder.stop();this.distancetrack.totalDistance/1000;});this.timersetInterval((){this.duration;},1000);}stopSki(){this.isRunningfalse;clearInterval(this.timer);this.locationService.stop();consttrackthis.trackRecorder.stop();router.pushUrl({url:pages/SkiSummary,params:track});}}React对应版本// React - 滑雪页面 function SkiActive() { const [speed, setSpeed] useState(0); const [distance, setDistance] useState(0); const [duration, setDuration] useState(0); const [isRunning, setIsRunning] useState(false); const { location, start, stop } useLocation(); const { points, isRecording, start: startTrack, addPoint, stop: stopTrack } useTrackRecorder(); const speedFilter useSpeedFilter(); const intervalRef useRef(null); useEffect(() { if (location isRunning) { const filteredSpeed speedFilter(location.speed); setSpeed(mpsToKmh(filteredSpeed)); addPoint(location); } }, [location, isRunning]); const startSki () { setIsRunning(true); start(); startTrack(); intervalRef.current setInterval(() { setDuration(prev prev 1); }, 1000); }; const stopSki () { setIsRunning(false); stop(); clearInterval(intervalRef.current); const track stopTrack(); navigate(/ski/summary, { state: track }); }; return ( div classNameflex flex-col items-center justify-center h-full p classNametext-6xl font-bold{speed.toFixed(1)}/p p classNametext-sm text-gray-500km/h/p p classNametext-2xl mt-4{distance.toFixed(2)}/p p classNametext-sm text-gray-500公里/p /div ); }踩坑提醒GPS精度GPS在室内、隧道、高楼密集区精度很差。建议加一个精度阈值如accuracy 50米时忽略数据。电量消耗GPS持续运行很耗电建议在页面不可见时降低更新频率或者暂停定位。后台运行鸿蒙默认会限制后台应用的GPS访问需要申请长时任务backgroundTaskManager才能在后台持续定位。速度抖动GPS返回的速度值可能有抖动建议加一个低通滤波平滑处理。存储空间轨迹点数据量很大一小时滑雪可能有3600个点。建议定期清理旧轨迹或者压缩存储。总结这篇文章带你走了一遍GPS轨迹记录的完整流程权限申请LOCATION和APPROXIMATELY_LOCATION权限定位服务用ohos.location获取GPS坐标轨迹记录把坐标存下来形成轨迹速度计算GPS返回米/秒转换为公里/小时页面集成把速度、距离、时长展示出来核心公式就一个Haversine公式计算两个经纬度之间的距离。其他的都是业务逻辑跟Web开发没太大区别。下一篇文章我会聊聊雪痕的传感器融合——怎么用加速度计和陀螺仪计算坡度。