鸿蒙App开发--雪痕App怎么做GPS轨迹记录?鸿蒙定位服务实战
滑雪App怎么做GPS轨迹记录?鸿蒙定位服务实战
如果你正在做一款滑雪App,或者对运动类应用的定位开发感兴趣,可以去鸿蒙应用市场搜一下**「雪痕」**,下载下来滑一趟体验体验。实时速度、距离、滑行记录,滑完还能看详细总结。体验完了再回来看这篇文章,你会更清楚这些功能背后的定位服务是怎么工作的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、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 } from '@kit.AbilityKit';
async function requestLocationPermission(): Promise<boolean> {
try {
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(
getContext(),
['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']
);
return result.authResults[0] === 0 && result.authResults[1] === 0;
} catch (err) {
console.error('权限申请失败:', err);
return false;
}
}
第二步:封装定位服务
实际开发中,我们不会把定位逻辑直接写在页面组件里。封装一个独立的服务类:
// LocationService.ets
import { location } from '@kit.LocationKit';
export class LocationService {
private callback: ((location: LocationData) => void) | null = null;
private isRunning: boolean = false;
start(onLocation: (location: LocationData) => void) {
this.callback = onLocation;
this.isRunning = true;
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.isRunning && this.callback) {
this.callback({
latitude: data.latitude,
longitude: data.longitude,
speed: data.speed,
altitude: data.altitude,
accuracy: data.accuracy,
timestamp: data.timeStamp
});
}
});
});
}
stop() {
this.isRunning = false;
location.off('locationChange');
location.disableLocation();
this.callback = null;
}
}
interface LocationData {
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 - 轨迹记录
class TrackRecorder {
private points: LocationData[] = [];
private startTime: number = 0;
private isRecording: boolean = false;
start() {
this.points = [];
this.startTime = Date.now();
this.isRecording = true;
}
addPoint(point: LocationData) {
if (!this.isRecording) return;
if (point.accuracy > 50) return;
if (this.points.length > 0) {
const lastPoint = this.points[this.points.length - 1];
const distance = this.calculateDistance(lastPoint, point);
if (distance < 1) return;
}
this.points.push(point);
}
stop(): Track {
this.isRecording = false;
const totalDistance = this.calculateTotalDistance();
const totalDuration = (Date.now() - this.startTime) / 1000;
return {
id: Date.now().toString(),
startTime: this.startTime,
endTime: Date.now(),
points: this.points,
totalDistance,
totalDuration,
avgSpeed: totalDuration > 0 ? (totalDistance / totalDuration) * 3.6 : 0,
maxSpeed: this.calculateMaxSpeed()
};
}
calculateDistance(p1: LocationData, p2: LocationData): number {
const R = 6371000;
const lat1 = p1.latitude * Math.PI / 180;
const lat2 = p2.latitude * Math.PI / 180;
const deltaLat = (p2.latitude - p1.latitude) * Math.PI / 180;
const deltaLng = (p2.longitude - p1.longitude) * Math.PI / 180;
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
calculateTotalDistance(): number {
let total = 0;
for (let i = 1; i < this.points.length; i++) {
total += this.calculateDistance(this.points[i - 1], this.points[i]);
}
return total;
}
calculateMaxSpeed(): number {
let maxSpeed = 0;
for (const point of this.points) {
const speed = point.speed * 3.6;
if (speed > maxSpeed) maxSpeed = speed;
}
return maxSpeed;
}
}
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返回的速度单位是米/秒,需要转换成公里/小时:
// 速度单位转换
function mpsToKmh(mps: number): number {
return mps * 3.6;
}
// 实时速度(从GPS数据)
const speedKmh = mpsToKmh(location.speed);
// 低通滤波(平滑速度)
class SpeedFilter {
private filtered: number = 0;
private alpha: number = 0.3; // 滤波系数
update(raw: number): number {
this.filtered = this.alpha * raw + (1 - this.alpha) * this.filtered;
return this.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 - 滑雪页面
@Component
struct SkiActive {
@State speed: number = 0;
@State distance: number = 0;
@State duration: number = 0;
@State isRunning: boolean = false;
private locationService: LocationService = new LocationService();
private trackRecorder: TrackRecorder = new TrackRecorder();
private speedFilter: SpeedFilter = new SpeedFilter();
private timer: number = 0;
startSki() {
this.isRunning = true;
this.trackRecorder.start();
this.locationService.start((loc) => {
const filteredSpeed = this.speedFilter.update(loc.speed);
this.speed = mpsToKmh(filteredSpeed);
this.trackRecorder.addPoint(loc);
const track = this.trackRecorder.stop();
this.distance = track.totalDistance / 1000;
});
this.timer = setInterval(() => {
this.duration++;
}, 1000);
}
stopSki() {
this.isRunning = false;
clearInterval(this.timer);
this.locationService.stop();
const track = this.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 className="flex flex-col items-center justify-center h-full">
<p className="text-6xl font-bold">{speed.toFixed(1)}</p>
<p className="text-sm text-gray-500">km/h</p>
<p className="text-2xl mt-4">{distance.toFixed(2)}</p>
<p className="text-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开发没太大区别。
下一篇文章,我会聊聊雪痕的传感器融合——怎么用加速度计和陀螺仪计算坡度。
更多推荐




所有评论(0)