Android GPS定位与行车轨迹追踪完整实战教程
一、前言
随着移动互联网的快速发展,基于位置的服务(LBS)已经成为移动应用的重要组成部分。本文将提供一套完整的Android GPS定位解决方案,包括实时定位、轨迹记录、地图展示、轨迹回放、数据分析等全套功能。
二、项目架构设计
2.1 技术栈
- 定位服务: FusedLocationProviderClient
- 地图SDK: 高德地图 (可替换为百度/腾讯地图)
- 数据库: Room Persistence Library
- 异步处理: Kotlin Coroutines / RxJava
- 依赖注入: Hilt (可选)
- 架构模式: MVVM
2.2 模块划分
app/
├── data/
│ ├── database/ # 数据库相关
│ ├── model/ # 数据模型
│ └── repository/ # 数据仓库
├── service/ # 后台服务
├── ui/ # UI界面
│ ├── main/ # 主界面
│ ├── history/ # 历史记录
│ └── playback/ # 轨迹回放
├── utils/ # 工具类
└── widget/ # 自定义控件
三、Gradle配置
3.1 项目级 build.gradle
groovy
buildscript {
ext.kotlin_version = "1.9.0"
ext.room_version = "2.6.0"
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
3.2 应用级 build.gradle
groovy
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
namespace 'com.example.gpstracker'
compileSdk 34
defaultConfig {
applicationId "com.example.gpstracker"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
buildFeatures {
viewBinding true
dataBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
// Android核心库
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// 定位服务
implementation 'com.google.android.gms:play-services-location:21.0.1'
// 高德地图
implementation 'com.amap.api:map2d:6.0.0'
implementation 'com.amap.api:location:6.3.0'
// Room数据库
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// 协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
// 权限处理
implementation 'com.permissionx.guolindev:permissionx:1.7.1'
// JSON解析
implementation 'com.google.code.gson:gson:2.10.1'
// 图表库
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}
四、完整权限配置
4.1 AndroidManifest.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- 电源管理 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".GPSTrackerApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.GPSTracker"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 高德地图Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="YOUR_AMAP_KEY_HERE" />
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.history.TrackHistoryActivity" />
<activity android:name=".ui.playback.TrackPlaybackActivity" />
<!-- 定位服务 -->
<service
android:name=".service.LocationTrackingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
</application>
</manifest>
五、数据层实现
5.1 完整数据模型
kotlin
// LocationPoint.kt - 位置点数据类
data class LocationPoint(
val latitude: Double,
val longitude: Double,
val altitude: Double = 0.0,
val speed: Float = 0f,
val bearing: Float = 0f,
val accuracy: Float = 0f,
val timestamp: Long = System.currentTimeMillis()
) : Parcelable {
fun toLatLng(): LatLng = LatLng(latitude, longitude)
fun getSpeedKmh(): Float = speed * 3.6f
fun getFormattedTime(): String {
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
return sdf.format(Date(timestamp))
}
}
// TrackRecord.kt - 行程记录实体
@Entity(tableName = "track_records")
data class TrackRecord(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val startTime: Long,
val endTime: Long,
val totalDistance: Double, // 米
val totalDuration: Long, // 毫秒
val maxSpeed: Float, // m/s
val avgSpeed: Float, // m/s
val pointCount: Int,
val trackName: String = "",
val isCompleted: Boolean = false
)
// LocationRecord.kt - 位置记录实体
@Entity(
tableName = "location_records",
foreignKeys = [ForeignKey(
entity = TrackRecord::class,
parentColumns = ["id"],
childColumns = ["trackId"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index("trackId"), Index("timestamp")]
)
data class LocationRecord(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val trackId: Long,
val latitude: Double,
val longitude: Double,
val altitude: Double,
val speed: Float,
val bearing: Float,
val accuracy: Float,
val timestamp: Long
) {
fun toLocationPoint(): LocationPoint {
return LocationPoint(
latitude, longitude, altitude,
speed, bearing, accuracy, timestamp
)
}
}
5.2 数据库DAO层
kotlin
// LocationDao.kt
@Dao
interface LocationDao {
@Insert
suspend fun insertLocation(location: LocationRecord): Long
@Insert
suspend fun insertLocations(locations: List<LocationRecord>)
@Query("SELECT * FROM location_records WHERE trackId = :trackId ORDER BY timestamp ASC")
suspend fun getLocationsByTrackId(trackId: Long): List<LocationRecord>
@Query("SELECT * FROM location_records WHERE trackId = :trackId ORDER BY timestamp ASC")
fun getLocationsByTrackIdFlow(trackId: Long): Flow<List<LocationRecord>>
@Query("DELETE FROM location_records WHERE trackId = :trackId")
suspend fun deleteLocationsByTrackId(trackId: Long)
@Query("SELECT COUNT(*) FROM location_records WHERE trackId = :trackId")
suspend fun getLocationCount(trackId: Long): Int
}
// TrackDao.kt
@Dao
interface TrackDao {
@Insert
suspend fun insertTrack(track: TrackRecord): Long
@Update
suspend fun updateTrack(track: TrackRecord)
@Delete
suspend fun deleteTrack(track: TrackRecord)
@Query("SELECT * FROM track_records ORDER BY startTime DESC")
fun getAllTracks(): Flow<List<TrackRecord>>
@Query("SELECT * FROM track_records WHERE id = :trackId")
suspend fun getTrackById(trackId: Long): TrackRecord?
@Query("SELECT * FROM track_records WHERE isCompleted = 0 ORDER BY startTime DESC LIMIT 1")
suspend fun getCurrentTrack(): TrackRecord?
@Query("SELECT * FROM track_records WHERE startTime >= :startTime AND endTime <= :endTime")
suspend fun getTracksByDateRange(startTime: Long, endTime: Long): List<TrackRecord>
@Query("SELECT SUM(totalDistance) FROM track_records WHERE isCompleted = 1")
suspend fun getTotalDistance(): Double?
@Query("SELECT COUNT(*) FROM track_records WHERE isCompleted = 1")
suspend fun getCompletedTrackCount(): Int
}
5.3 数据库类
kotlin
@Database(
entities = [TrackRecord::class, LocationRecord::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun trackDao(): TrackDao
abstract fun locationDao(): LocationDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"gps_tracker_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
5.4 数据仓库层
kotlin
class LocationRepository(private val database: AppDatabase) {
private val trackDao = database.trackDao()
private val locationDao = database.locationDao()
// 创建新行程
suspend fun createNewTrack(): Long {
val track = TrackRecord(
startTime = System.currentTimeMillis(),
endTime = 0,
totalDistance = 0.0,
totalDuration = 0,
maxSpeed = 0f,
avgSpeed = 0f,
pointCount = 0,
isCompleted = false
)
return trackDao.insertTrack(track)
}
// 保存位置点
suspend fun saveLocation(trackId: Long, point: LocationPoint) {
val record = LocationRecord(
trackId = trackId,
latitude = point.latitude,
longitude = point.longitude,
altitude = point.altitude,
speed = point.speed,
bearing = point.bearing,
accuracy = point.accuracy,
timestamp = point.timestamp
)
locationDao.insertLocation(record)
}
// 完成行程
suspend fun completeTrack(trackId: Long, statistics: TrackStatistics) {
val track = trackDao.getTrackById(trackId) ?: return
val updatedTrack = track.copy(
endTime = System.currentTimeMillis(),
totalDistance = statistics.totalDistance,
totalDuration = statistics.duration,
maxSpeed = statistics.maxSpeed,
avgSpeed = statistics.avgSpeed,
pointCount = statistics.pointCount,
isCompleted = true
)
trackDao.updateTrack(updatedTrack)
}
// 获取所有行程
fun getAllTracks(): Flow<List<TrackRecord>> = trackDao.getAllTracks()
// 获取行程轨迹点
suspend fun getTrackLocations(trackId: Long): List<LocationPoint> {
return locationDao.getLocationsByTrackId(trackId)
.map { it.toLocationPoint() }
}
// 删除行程
suspend fun deleteTrack(track: TrackRecord) {
locationDao.deleteLocationsByTrackId(track.id)
trackDao.deleteTrack(track)
}
// 获取统计数据
suspend fun getOverallStatistics(): OverallStatistics {
val totalDistance = trackDao.getTotalDistance() ?: 0.0
val trackCount = trackDao.getCompletedTrackCount()
return OverallStatistics(
totalDistance = totalDistance,
trackCount = trackCount
)
}
}
data class TrackStatistics(
val totalDistance: Double,
val duration: Long,
val maxSpeed: Float,
val avgSpeed: Float,
val pointCount: Int
)
data class OverallStatistics(
val totalDistance: Double,
val trackCount: Int
)
六、核心服务实现
6.1 完整定位服务
kotlin
class LocationTrackingService : Service() {
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback
private lateinit var repository: LocationRepository
private var currentTrackId: Long = -1
private var isTracking = false
private var lastLocation: LocationPoint? = null
private var totalDistance = 0.0
private val locationHistory = mutableListOf<LocationPoint>()
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val binder = LocationBinder()
inner class LocationBinder : Binder() {
fun getService(): LocationTrackingService = this@LocationTrackingService
}
override fun onCreate() {
super.onCreate()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
repository = LocationRepository(AppDatabase.getDatabase(this))
initLocationCallback()
}
override fun onBind(intent: Intent): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START_TRACKING -> startTracking()
ACTION_STOP_TRACKING -> stopTracking()
ACTION_PAUSE_TRACKING -> pauseTracking()
ACTION_RESUME_TRACKING -> resumeTracking()
}
return START_STICKY
}
private fun initLocationCallback() {
locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { location ->
handleNewLocation(location)
}
}
}
}
@SuppressLint("MissingPermission")
fun startTracking() {
if (isTracking) return
isTracking = true
startForeground(NOTIFICATION_ID, createNotification())
serviceScope.launch {
currentTrackId = repository.createNewTrack()
withContext(Dispatchers.Main) {
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
LOCATION_UPDATE_INTERVAL
).apply {
setMinUpdateIntervalMillis(FASTEST_UPDATE_INTERVAL)
setMaxUpdateDelayMillis(MAX_UPDATE_DELAY)
setWaitForAccurateLocation(true)
}.build()
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
}
sendBroadcast(Intent(ACTION_TRACKING_STATE_CHANGED).apply {
putExtra(EXTRA_IS_TRACKING, true)
})
}
fun stopTracking() {
if (!isTracking) return
isTracking = false
fusedLocationClient.removeLocationUpdates(locationCallback)
serviceScope.launch {
val statistics = calculateStatistics()
repository.completeTrack(currentTrackId, statistics)
// 清理数据
locationHistory.clear()
lastLocation = null
totalDistance = 0.0
currentTrackId = -1
}
sendBroadcast(Intent(ACTION_TRACKING_STATE_CHANGED).apply {
putExtra(EXTRA_IS_TRACKING, false)
})
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
private fun pauseTracking() {
if (!isTracking) return
fusedLocationClient.removeLocationUpdates(locationCallback)
updateNotification("追踪已暂停")
}
@SuppressLint("MissingPermission")
private fun resumeTracking() {
if (!isTracking) return
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
LOCATION_UPDATE_INTERVAL
).build()
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
updateNotification("正在追踪")
}
private fun handleNewLocation(location: Location) {
val point = LocationPoint(
latitude = location.latitude,
longitude = location.longitude,
altitude = location.altitude,
speed = location.speed,
bearing = location.bearing,
accuracy = location.accuracy,
timestamp = location.time
)
// 数据过滤
if (!isValidLocation(point)) {
Log.w(TAG, "Invalid location filtered: accuracy=${point.accuracy}")
return
}
// 卡尔曼滤波
val filteredPoint = lastLocation?.let {
LocationCalculator.kalmanFilter(point, it)
} ?: point
// 计算距离
lastLocation?.let { last ->
val distance = LocationCalculator.calculateDistance(last, filteredPoint)
// 过滤异常距离(速度超过200km/h)
val timeDiff = (filteredPoint.timestamp - last.timestamp) / 1000.0
val speedKmh = if (timeDiff > 0) (distance / timeDiff) * 3.6 else 0.0
if (speedKmh < 200) {
totalDistance += distance
}
}
lastLocation = filteredPoint
locationHistory.add(filteredPoint)
// 保存到数据库
serviceScope.launch {
repository.saveLocation(currentTrackId, filteredPoint)
}
// 广播位置更新
broadcastLocationUpdate(filteredPoint)
// 更新通知
updateNotification("距离: ${String.format("%.2f", totalDistance / 1000)} km")
}
private fun isValidLocation(point: LocationPoint): Boolean {
// 精度过滤
if (point.accuracy > MAX_ACCURACY_THRESHOLD) {
return false
}
// 速度过滤
if (point.speed > MAX_SPEED_THRESHOLD) {
return false
}
// 时间过滤(排除过时数据)
val age = System.currentTimeMillis() - point.timestamp
if (age > MAX_LOCATION_AGE) {
return false
}
return true
}
private fun calculateStatistics(): TrackStatistics {
if (locationHistory.isEmpty()) {
return TrackStatistics(0.0, 0, 0f, 0f, 0)
}
val maxSpeed = locationHistory.maxOfOrNull { it.speed } ?: 0f
val avgSpeed = locationHistory.map { it.speed }.average().toFloat()
val duration = locationHistory.last().timestamp - locationHistory.first().timestamp
return TrackStatistics(
totalDistance = totalDistance,
duration = duration,
maxSpeed = maxSpeed,
avgSpeed = avgSpeed,
pointCount = locationHistory.size
)
}
private fun broadcastLocationUpdate(point: LocationPoint) {
val intent = Intent(ACTION_LOCATION_UPDATE).apply {
putExtra(EXTRA_LOCATION, point)
putExtra(EXTRA_DISTANCE, totalDistance)
}
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
private fun createNotification(): Notification {
createNotificationChannel()
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("GPS追踪服务")
.setContentText("正在记录位置...")
.setSmallIcon(R.drawable.ic_location)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(R.drawable.ic_stop, "停止", createStopIntent())
.build()
}
private fun updateNotification(content: String) {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("GPS追踪服务")
.setContentText(content)
.setSmallIcon(R.drawable.ic_location)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"位置追踪",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "GPS位置追踪服务"
setShowBadge(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
private fun createStopIntent(): PendingIntent {
val intent = Intent(this, LocationTrackingService::class.java).apply {
action = ACTION_STOP_TRACKING
}
return PendingIntent.getService(
this, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
companion object {
private const val TAG = "LocationTrackingService"
private const val CHANNEL_ID = "location_tracking_channel"
private const val NOTIFICATION_ID = 1001
private const val LOCATION_UPDATE_INTERVAL = 2000L
private const val FASTEST_UPDATE_INTERVAL = 1000L
private const val MAX_UPDATE_DELAY = 5000L
private const val MAX_ACCURACY_THRESHOLD = 50f
private const val MAX_SPEED_THRESHOLD = 55.5f // 200 km/h
private const val MAX_LOCATION_AGE = 10000L
const val ACTION_START_TRACKING = "action_start_tracking"
const val ACTION_STOP_TRACKING = "action_stop_tracking"
const val ACTION_PAUSE_TRACKING = "action_pause_tracking"
const val ACTION_RESUME_TRACKING = "action_resume_tracking"
const val ACTION_LOCATION_UPDATE = "action_location_update"
const val ACTION_TRACKING_STATE_CHANGED = "action_tracking_state_changed"
const val EXTRA_LOCATION = "extra_location"
const val EXTRA_DISTANCE = "extra_distance"
const val EXTRA_IS_TRACKING = "extra_is_tracking"
}
}
6.2 位置计算工具类
kotlin
object LocationCalculator {
private const val EARTH_RADIUS = 6371000.0 // 地球半径(米)
/**
* 使用Haversine公式计算两点间距离
*/
fun calculateDistance(point1: LocationPoint, point2: LocationPoint): Double {
val lat1Rad = Math.toRadians(point1.latitude)
val lat2Rad = Math.toRadians(point2.latitude)
val deltaLat = Math.toRadians(point2.latitude - point1.latitude)
val deltaLng = Math.toRadians(point2.longitude - point1.longitude)
val a = sin(deltaLat / 2).pow(2) +
cos(lat1Rad) * cos(lat2Rad) *
sin(deltaLng / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return EARTH_RADIUS * c
}
/**
* 计算方位角
*/
fun calculateBearing(from: LocationPoint, to: LocationPoint): Double {
val lat1 = Math.toRadians(from.latitude)
val lat2 = Math.toRadians(to.latitude)
val deltaLng = Math.toRadians(to.longitude - from.longitude)
val y = sin(deltaLng) * cos(lat2)
val x = cos(lat1) * sin(lat2) -
sin(lat1) * cos(lat2) * cos(deltaLng)
val bearing = Math.toDegrees(atan2(y, x))
return (bearing + 360) % 360
}
/**
* 简化的卡尔曼滤波
*/
fun kalmanFilter(current: LocationPoint, previous: LocationPoint): LocationPoint {
val processNoise = 0.00001
val measurementNoise = current.accuracy / 100.0
val gain = processNoise / (processNoise + measurementNoise)
val latitude = previous.latitude + gain * (current.latitude - previous.latitude)
val longitude = previous.longitude + gain * (current.longitude - previous.longitude)
return current.copy(
latitude = latitude,
longitude = longitude
)
}
/**
* Douglas-Peucker算法简化轨迹
*/
fun simplifyTrack(points: List<LocationPoint>, tolerance: Double): List<LocationPoint> {
if (points.size <= 2) return points
val result = mutableListOf<LocationPoint>()
douglasPeucker(points, 0, points.size - 1, tolerance, result)
return result.sortedBy { it.timestamp }
}
private fun douglasPeucker(
points: List<LocationPoint>,
start: Int,
end: Int,
tolerance: Double,
result: MutableList<LocationPoint>
) {
var maxDistance = 0.0
var maxIndex = 0
for (i in start + 1 until end) {
val distance = perpendicularDistance(
points[i],
points[start],
points[end]
)
if (distance > maxDistance) {
maxDistance = distance
maxIndex = i
}
}
if (maxDistance > tolerance) {
douglasPeucker(points, start, maxIndex, tolerance, result)
douglasPeucker(points, maxIndex, end, tolerance, result)
} else {
result.add(points[start])
result.add(points[end])
}
}
private fun perpendicularDistance(
point: LocationPoint,
lineStart: LocationPoint,
lineEnd: LocationPoint
): Double {
val x0 = point.latitude
val y0 = point.longitude
val x1 = lineStart.latitude
val y1 = lineStart.longitude
val x2 = lineEnd.latitude
val y2 = lineEnd.longitude
val numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
val denominator = sqrt((y2 - y1).pow(2) + (x2 - x1).pow(2))
return if (denominator == 0.0) 0.0 else numerator / denominator
}
/**
* 计算轨迹总长度
*/
fun calculateTotalDistance(points: List<LocationPoint>): Double {
if (points.size < 2) return 0.0
var total = 0.0
for (i in 0 until points.size - 1) {
total += calculateDistance(points[i], points[i + 1])
}
return total
}
/**
* 检测停车点
*/
fun detectStopPoints(
points: List<LocationPoint>,
radiusMeters: Double = 50.0,
minDurationSeconds: Int = 60
): List<StopPoint> {
if (points.isEmpty()) return emptyList()
val stops = mutableListOf<StopPoint>()
var clusterStart = 0
for (i in 1 until points.size) {
val distance = calculateDistance(points[clusterStart], points[i])
if (distance > radiusMeters) {
// 检查停留时长
val duration = (points[i - 1].timestamp - points[clusterStart].timestamp) / 1000
if (duration >= minDurationSeconds) {
val centerPoint = calculateCenterPoint(
points.subList(clusterStart, i)
)
stops.add(
StopPoint(
location = centerPoint,
duration = duration,
startTime = points[clusterStart].timestamp,
endTime = points[i - 1].timestamp
)
)
}
clusterStart = i
}
}
return stops
}
private fun calculateCenterPoint(points: List<LocationPoint>): LocationPoint {
val avgLat = points.map { it.latitude }.average()
val avgLng = points.map { it.longitude }.average()
return points.first().copy(latitude = avgLat, longitude = avgLng)
}
}
data class StopPoint(
val location: LocationPoint,
val duration: Long,
val startTime: Long,
val endTime: Long
)
七、UI层实现
7.1 主界面布局
xml
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.amap.api.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/bg_stats_panel">
<TextView
android:id="@+id/tvDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="距离: 0.00 km"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/colorPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvSpeed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="速度: 0 km/h"
android:textSize="14sp" />
<TextView
android:id="@+id/tvDuration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="时长: 00:00:00"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvAccuracy"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="精度: -- m"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
<TextView
android:id="@+id/tvPoints"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="点数: 0"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/bg_control_panel">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStartStop"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="开始追踪"
app:icon="@drawable/ic_play"
app:iconGravity="textStart"
app:cornerRadius="28dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnHistory"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:text="历史"
app:icon="@drawable/ic_history"
app:cornerRadius="28dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:gravity="center">
<ImageButton
android:id="@+id/btnCenter"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_my_location"
android:background="?attr/selectableItemBackgroundBorderless" />
<ImageButton
android:id="@+id/btnLayers"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_layers"
android:background="?attr/selectableItemBackgroundBorderless" />
<ImageButton
android:id="@+id/btnExport"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_export"
android:background="?attr/selectableItemBackgroundBorderless" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
7.2 主Activity完整实现
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
private var aMap: AMap? = null
private var currentPolyline: Polyline? = null
private var currentMarker: Marker? = null
private var isTracking = false
private var startTime = 0L
private val locationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
LocationTrackingService.ACTION_LOCATION_UPDATE -> {
val location = intent.getParcelableExtra<LocationPoint>(
LocationTrackingService.EXTRA_LOCATION
)
val distance = intent.getDoubleExtra(
LocationTrackingService.EXTRA_DISTANCE, 0.0
)
location?.let { updateLocationUI(it, distance) }
}
LocationTrackingService.ACTION_TRACKING_STATE_CHANGED -> {
isTracking = intent.getBooleanExtra(
LocationTrackingService.EXTRA_IS_TRACKING, false
)
updateTrackingUI()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.mapView.onCreate(savedInstanceState)
initMap()
initViews()
checkPermissions()
registerReceivers()
observeViewModel()
}
private fun initMap() {
aMap = binding.mapView.map?.apply {
// 地图UI设置
uiSettings.isZoomControlsEnabled = false
uiSettings.isCompassEnabled = true
uiSettings.isScaleControlsEnabled = true
// 地图类型
mapType = AMap.MAP_TYPE_NORMAL
// 显示定位蓝点
isMyLocationEnabled = true
myLocationStyle = MyLocationStyle().apply {
myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER)
interval(2000)
strokeColor(Color.argb(180, 3, 145, 255))
radiusFillColor(Color.argb(50, 0, 0, 180))
}
// 设置初始位置
moveCamera(CameraUpdateFactory.newLatLngZoom(
LatLng(39.9, 116.4), 15f
))
}
}
private fun initViews() {
binding.btnStartStop.setOnClickListener {
if (isTracking) {
stopTracking()
} else {
startTracking()
}
}
binding.btnHistory.setOnClickListener {
startActivity(Intent(this, TrackHistoryActivity::class.java))
}
binding.btnCenter.setOnClickListener {
centerToMyLocation()
}
binding.btnLayers.setOnClickListener {
showMapTypeDialog()
}
binding.btnExport.setOnClickListener {
exportCurrentTrack()
}
}
private fun checkPermissions() {
PermissionX.init(this)
.permissions(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(
deniedList,
"需要位置权限来追踪您的行程",
"确定",
"取消"
)
}
.request { allGranted, _, _ ->
if (!allGranted) {
Toast.makeText(this, "需要位置权限才能使用", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun registerReceivers() {
val filter = IntentFilter().apply {
addAction(LocationTrackingService.ACTION_LOCATION_UPDATE)
addAction(LocationTrackingService.ACTION_TRACKING_STATE_CHANGED)
}
LocalBroadcastManager.getInstance(this)
.registerReceiver(locationReceiver, filter)
}
private fun observeViewModel() {
viewModel.currentTrack.observe(this) { track ->
// 更新UI
}
viewModel.statistics.observe(this) { stats ->
updateStatisticsUI(stats)
}
}
private fun startTracking() {
val intent = Intent(this, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_START_TRACKING
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
startTime = System.currentTimeMillis()
isTracking = true
// 清空之前的轨迹
currentPolyline?.remove()
viewModel.clearCurrentTrack()
updateTrackingUI()
}
private fun stopTracking() {
AlertDialog.Builder(this)
.setTitle("停止追踪")
.setMessage("确定要停止当前的轨迹追踪吗?")
.setPositiveButton("确定") { _, _ ->
val intent = Intent(this, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_STOP_TRACKING
}
startService(intent)
isTracking = false
updateTrackingUI()
}
.setNegativeButton("取消", null)
.show()
}
private fun updateLocationUI(location: LocationPoint, distance: Double) {
// 更新统计数据
binding.tvDistance.text = String.format("距离: %.2f km", distance / 1000)
binding.tvSpeed.text = String.format("速度: %.1f km/h", location.getSpeedKmh())
binding.tvAccuracy.text = String.format("精度: %.1f m", location.accuracy)
// 更新时长
val duration = System.currentTimeMillis() - startTime
binding.tvDuration.text = formatDuration(duration)
// 添加到ViewModel
viewModel.addLocationPoint(location)
// 更新地图
updateMapWithLocation(location)
}
private fun updateMapWithLocation(location: LocationPoint) {
val latLng = location.toLatLng()
// 更新标记
if (currentMarker == null) {
currentMarker = aMap?.addMarker(MarkerOptions().apply {
position(latLng)
icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_car))
anchor(0.5f, 0.5f)
})
} else {
currentMarker?.position = latLng
currentMarker?.rotateAngle = location.bearing
}
// 更新轨迹线
viewModel.currentTrackPoints.value?.let { points ->
currentPolyline?.remove()
if (points.size >= 2) {
currentPolyline = aMap?.addPolyline(PolylineOptions().apply {
addAll(points.map { it.toLatLng() })
width(15f)
color(Color.parseColor("#4285F4"))
geodesic(true)
useGradient(true)
})
}
}
// 更新点数
binding.tvPoints.text = "点数: ${viewModel.currentTrackPoints.value?.size ?: 0}"
}
private fun updateTrackingUI() {
if (isTracking) {
binding.btnStartStop.apply {
text = "停止追踪"
setIconResource(R.drawable.ic_stop)
setBackgroundColor(Color.parseColor("#F44336"))
}
} else {
binding.btnStartStop.apply {
text = "开始追踪"
setIconResource(R.drawable.ic_play)
setBackgroundColor(Color.parseColor("#4CAF50"))
}
}
binding.btnExport.isEnabled = !isTracking
}
private fun updateStatisticsUI(stats: OverallStatistics) {
// 可以显示总体统计
}
private fun centerToMyLocation() {
aMap?.myLocation?.let { location ->
aMap?.animateCamera(
CameraUpdateFactory.newLatLngZoom(
LatLng(location.latitude, location.longitude),
17f
)
)
}
}
private fun showMapTypeDialog() {
val types = arrayOf("标准地图", "卫星地图", "夜间模式")
AlertDialog.Builder(this)
.setTitle("选择地图类型")
.setItems(types) { _, which ->
aMap?.mapType = when (which) {
0 -> AMap.MAP_TYPE_NORMAL
1 -> AMap.MAP_TYPE_SATELLITE
2 -> AMap.MAP_TYPE_NIGHT
else -> AMap.MAP_TYPE_NORMAL
}
}
.show()
}
private fun exportCurrentTrack() {
viewModel.currentTrackPoints.value?.let { points ->
if (points.isEmpty()) {
Toast.makeText(this, "没有可导出的轨迹", Toast.LENGTH_SHORT).show()
return
}
lifecycleScope.launch {
try {
val gpxFile = GPXExporter.export(points, this@MainActivity)
Toast.makeText(
this@MainActivity,
"轨迹已导出: ${gpxFile.name}",
Toast.LENGTH_LONG
).show()
// 分享文件
shareGPXFile(gpxFile)
} catch (e: Exception) {
Toast.makeText(
this@MainActivity,
"导出失败: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun shareGPXFile(file: File) {
val uri = FileProvider.getUriForFile(
this,
"${packageName}.fileprovider",
file
)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "application/gpx+xml"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(intent, "分享轨迹"))
}
private fun formatDuration(millis: Long): String {
val seconds = millis / 1000
val hours = seconds / 3600
val minutes = (seconds % 3600) / 60
val secs = seconds % 60
return String.format("%02d:%02d:%02d", hours, minutes, secs)
}
override fun onResume() {
super.onResume()
binding.mapView.onResume()
}
override fun onPause() {
super.onPause()
binding.mapView.onPause()
}
override fun onDestroy() {
super.onDestroy()
binding.mapView.onDestroy()
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(locationReceiver)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.mapView.onSaveInstanceState(outState)
}
}
7.3 ViewModel实现
kotlin
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val repository = LocationRepository(
AppDatabase.getDatabase(application)
)
private val _currentTrackPoints = MutableLiveData<List<LocationPoint>>(emptyList())
val currentTrackPoints: LiveData<List<LocationPoint>> = _currentTrackPoints
val currentTrack = liveData {
repository.getAllTracks().collect { tracks ->
emit(tracks.firstOrNull { !it.isCompleted })
}
}
val statistics = liveData {
val stats = repository.getOverallStatistics()
emit(stats)
}
fun addLocationPoint(point: LocationPoint) {
val currentList = _currentTrackPoints.value.orEmpty().toMutableList()
currentList.add(point)
_currentTrackPoints.value = currentList
}
fun clearCurrentTrack() {
_currentTrackPoints.value = emptyList()
}
fun loadTrack(trackId: Long) = liveData {
val points = repository.getTrackLocations(trackId)
emit(points)
}
}
八、轨迹回放功能
8.1 回放Activity
kotlin
class TrackPlaybackActivity : AppCompatActivity() {
private lateinit var binding: ActivityPlaybackBinding
private var aMap: AMap? = null
private var trackPoints: List<LocationPoint> = emptyList()
private var currentIndex = 0
private var isPlaying = false
private val playbackHandler = Handler(Looper.getMainLooper())
private var playbackRunnable: Runnable? = null
private var playbackPolyline: Polyline? = null
private var playbackMarker: Marker? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPlaybackBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.mapView.onCreate(savedInstanceState)
initMap()
val trackId = intent.getLongExtra("track_id", -1)
if (trackId != -1L) {
loadTrack(trackId)
}
initControls()
}
private fun initMap() {
aMap = binding.mapView.map?.apply {
mapType = AMap.MAP_TYPE_NORMAL
uiSettings.isZoomControlsEnabled = false
}
}
private fun loadTrack(trackId: Long) {
lifecycleScope.launch {
val repository = LocationRepository(AppDatabase.getDatabase(this@TrackPlaybackActivity))
trackPoints = repository.getTrackLocations(trackId)
if (trackPoints.isNotEmpty()) {
setupTrack()
}
}
}
private fun setupTrack() {
// 绘制完整轨迹
playbackPolyline = aMap?.addPolyline(PolylineOptions().apply {
addAll(trackPoints.map { it.toLatLng() })
width(10f)
color(Color.GRAY)
geodesic(true)
})
// 添加起点标记
aMap?.addMarker(MarkerOptions().apply {
position(trackPoints.first().toLatLng())
icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN))
title("起点")
})
// 添加终点标记
aMap?.addMarker(MarkerOptions().apply {
position(trackPoints.last().toLatLng())
icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
title("终点")
})
// 移动到起点
aMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(
trackPoints.first().toLatLng(), 15f
))
// 设置进度条
binding.seekBar.max = trackPoints.size - 1
binding.tvPointCount.text = "总点数: ${trackPoints.size}"
}
private fun initControls() {
binding.btnPlay.setOnClickListener {
if (isPlaying) {
pausePlayback()
} else {
startPlayback()
}
}
binding.btnReset.setOnClickListener {
resetPlayback()
}
binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
currentIndex = progress
updatePlaybackPosition()
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
pausePlayback()
}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
// 速度控制
binding.rgSpeed.setOnCheckedChangeListener { _, checkedId ->
// 更新播放速度
}
}
private fun startPlayback() {
if (trackPoints.isEmpty()) return
isPlaying = true
binding.btnPlay.setImageResource(R.drawable.ic_pause)
playbackRunnable = object : Runnable {
override fun run() {
if (currentIndex < trackPoints.size - 1) {
currentIndex++
updatePlaybackPosition()
binding.seekBar.progress = currentIndex
// 根据速度调整延迟
val delay = getPlaybackDelay()
playbackHandler.postDelayed(this, delay)
} else {
pausePlayback()
}
}
}
playbackHandler.post(playbackRunnable!!)
}
private fun pausePlayback() {
isPlaying = false
binding.btnPlay.setImageResource(R.drawable.ic_play)
playbackRunnable?.let { playbackHandler.removeCallbacks(it) }
}
private fun resetPlayback() {
pausePlayback()
currentIndex = 0
binding.seekBar.progress = 0
updatePlaybackPosition()
}
private fun updatePlaybackPosition() {
if (trackPoints.isEmpty() || currentIndex >= trackPoints.size) return
val point = trackPoints[currentIndex]
// 更新标记位置
if (playbackMarker == null) {
playbackMarker = aMap?.addMarker(MarkerOptions().apply {
position(point.toLatLng())
icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_car))
anchor(0.5f, 0.5f)
})
} else {
playbackMarker?.position = point.toLatLng()
playbackMarker?.rotateAngle = point.bearing
}
// 移动相机跟随
aMap?.animateCamera(CameraUpdateFactory.newLatLng(point.toLatLng()))
// 更新信息
binding.tvCurrentPoint.text = "当前点: ${currentIndex + 1}/${trackPoints.size}"
binding.tvSpeed.text = String.format("速度: %.1f km/h", point.getSpeedKmh())
binding.tvTime.text = "时间: ${point.getFormattedTime()}"
}
private fun getPlaybackDelay(): Long {
return when (binding.rgSpeed.checkedRadioButtonId) {
R.id.rbSpeed1x -> 1000L
R.id.rbSpeed2x -> 500L
R.id.rbSpeed5x -> 200L
R.id.rbSpeed10x -> 100L
else -> 1000L
}
}
override fun onDestroy() {
super.onDestroy()
binding.mapView.onDestroy()
playbackHandler.removeCallbacksAndMessages(null)
}
}
九、GPX导出功能
kotlin
object GPXExporter {
fun export(points: List<LocationPoint>, context: Context): File {
val gpx = generateGPX(points)
val fileName = "track_${System.currentTimeMillis()}.gpx"
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName)
file.writeText(gpx)
return file
}
private fun generateGPX(points: List<LocationPoint>): String {
val builder = StringBuilder()
builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
builder.append("<gpx version=\"1.1\" creator=\"GPS Tracker\"\n")
builder.append(" xmlns=\"http://www.topografix.com/GPX/1/1\"\n")
builder.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n")
builder.append(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\n")
builder.append(" <metadata>\n")
builder.append(" <name>GPS Track</name>\n")
builder.append(" <time>${formatGPXTime(points.first().timestamp)}</time>\n")
builder.append(" </metadata>\n")
builder.append(" <trk>\n")
builder.append(" <name>Track ${formatDate(points.first().timestamp)}</name>\n")
builder.append(" <trkseg>\n")
points.forEach { point ->
builder.append(" <trkpt lat=\"${point.latitude}\" lon=\"${point.longitude}\">\n")
builder.append(" <ele>${point.altitude}</ele>\n")
builder.append(" <time>${formatGPXTime(point.timestamp)}</time>\n")
builder.append(" <extensions>\n")
builder.append(" <speed>${point.speed}</speed>\n")
builder.append(" <course>${point.bearing}</course>\n")
builder.append(" </extensions>\n")
builder.append(" </trkpt>\n")
}
builder.append(" </trkseg>\n")
builder.append(" </trk>\n")
builder.append("</gpx>")
return builder.toString()
}
private fun formatGPXTime(timestamp: Long): String {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
return sdf.format(Date(timestamp))
}
private fun formatDate(timestamp: Long): String {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return sdf.format(Date(timestamp))
}
}
十、性能优化总结
10.1 电池优化策略
- 动态调整更新频率: 根据速度自动调整GPS更新间隔
- 使用批量位置更新: 减少唤醒设备的次数
- 后台限制: Android 8.0+限制后台定位频率
- 电池优化白名单: 引导用户将应用加入白名单
10.2 数据优化
- 卡尔曼滤波: 平滑GPS数据,减少漂移
- Douglas-Peucker算法: 简化轨迹点,减少存储
- 异常值过滤: 过滤不合理的位置数据
- 数据压缩: 定期清理过期数据
10.3 内存优化
- 分页加载: 历史轨迹使用分页加载
- 及时释放: 不使用的地图资源及时释放
- 图片优化: 使用合适尺寸的标记图标
十一、总结与展望
本文提供了一套完整的Android GPS定位和轨迹追踪解决方案,涵盖了从数据采集、存储、展示到导出的全流程。
核心功能:
- ✅ 实时GPS定位与轨迹记录
- ✅ 高德地图集成与可视化
- ✅ 轨迹回放功能
- ✅ 数据持久化(Room数据库)
- ✅ GPX格式导出
- ✅ 性能优化与电池管理
扩展方向:
- 云端同步(Firebase/自建服务器)
- 社交分享功能
- 运动数据分析(配速、热力图)
- 离线地图支持
- 语音导航提示
希望本教程能帮助您快速构建专业的GPS追踪应用!
十二、参考资料
作者 : Android高级开发工程师
完稿日期 : 2025年10月11日
代码仓库 : github.com/yourname/gps-tracker
版权声明 : 本文为原创技术文章,遵循 CC 4.0 BY-SA 版权协议
技术支持: 欢迎在评论区交流讨论