Android GPS定位与行车轨迹追踪完整实战
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
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
plugins {id 'com.android.application'id 'kotlin-android'id 'kotlin-kapt'
}android {namespace 'com.example.gpstracker'compileSdk 34defaultConfig {applicationId "com.example.gpstracker"minSdk 24targetSdk 34versionCode 1versionName "1.0"}buildFeatures {viewBinding truedataBinding true}compileOptions {sourceCompatibility JavaVersion.VERSION_17targetCompatibility 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'// ViewModelimplementation '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 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" /><applicationandroid: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-dataandroid:name="com.amap.api.v2.apikey"android:value="YOUR_AMAP_KEY_HERE" /><activityandroid: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" /><!-- 定位服务 --><serviceandroid:name=".service.LocationTrackingService"android:enabled="true"android:exported="false"android:foregroundServiceType="location" /></application></manifest>
五、数据层实现
5.1 完整数据模型
// 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.6ffun 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/sval avgSpeed: Float, // m/sval 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层
// LocationDao.kt
@Dao
interface LocationDao {@Insertsuspend fun insertLocation(location: LocationRecord): Long@Insertsuspend 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 {@Insertsuspend fun insertTrack(track: TrackRecord): Long@Updatesuspend fun updateTrack(track: TrackRecord)@Deletesuspend 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 数据库类
@Database(entities = [TrackRecord::class, LocationRecord::class],version = 1,exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {abstract fun trackDao(): TrackDaoabstract fun locationDao(): LocationDaocompanion object {@Volatileprivate var INSTANCE: AppDatabase? = nullfun getDatabase(context: Context): AppDatabase {return INSTANCE ?: synchronized(this) {val instance = Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"gps_tracker_database").fallbackToDestructiveMigration().build()INSTANCE = instanceinstance}}}
}
5.4 数据仓库层
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) ?: returnval 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.0val 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 完整定位服务
class LocationTrackingService : Service() {private lateinit var fusedLocationClient: FusedLocationProviderClientprivate lateinit var locationCallback: LocationCallbackprivate lateinit var repository: LocationRepositoryprivate var currentTrackId: Long = -1private var isTracking = falseprivate var lastLocation: LocationPoint? = nullprivate var totalDistance = 0.0private 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 = binderoverride 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) returnisTracking = truestartForeground(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) returnisTracking = falsefusedLocationClient.removeLocationUpdates(locationCallback)serviceScope.launch {val statistics = calculateStatistics()repository.completeTrack(currentTrackId, statistics)// 清理数据locationHistory.clear()lastLocation = nulltotalDistance = 0.0currentTrackId = -1}sendBroadcast(Intent(ACTION_TRACKING_STATE_CHANGED).apply {putExtra(EXTRA_IS_TRACKING, false)})stopForeground(STOP_FOREGROUND_REMOVE)stopSelf()}private fun pauseTracking() {if (!isTracking) returnfusedLocationClient.removeLocationUpdates(locationCallback)updateNotification("追踪已暂停")}@SuppressLint("MissingPermission")private fun resumeTracking() {if (!isTracking) returnval 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.0val speedKmh = if (timeDiff > 0) (distance / timeDiff) * 3.6 else 0.0if (speedKmh < 200) {totalDistance += distance}}lastLocation = filteredPointlocationHistory.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.timestampif (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 } ?: 0fval avgSpeed = locationHistory.map { it.speed }.average().toFloat()val duration = locationHistory.last().timestamp - locationHistory.first().timestampreturn 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 NotificationManagernotificationManager.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 = 1001private const val LOCATION_UPDATE_INTERVAL = 2000Lprivate const val FASTEST_UPDATE_INTERVAL = 1000Lprivate const val MAX_UPDATE_DELAY = 5000Lprivate const val MAX_ACCURACY_THRESHOLD = 50fprivate const val MAX_SPEED_THRESHOLD = 55.5f // 200 km/hprivate const val MAX_LOCATION_AGE = 10000Lconst 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 位置计算工具类
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.00001val measurementNoise = current.accuracy / 100.0val 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 pointsval 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.0var maxIndex = 0for (i in start + 1 until end) {val distance = perpendicularDistance(points[i],points[start],points[end])if (distance > maxDistance) {maxDistance = distancemaxIndex = 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.latitudeval y0 = point.longitudeval x1 = lineStart.latitudeval y1 = lineStart.longitudeval x2 = lineEnd.latitudeval y2 = lineEnd.longitudeval 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.0var total = 0.0for (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 = 0for (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) / 1000if (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 主界面布局
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayoutxmlns: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.MapViewandroid:id="@+id/mapView"android:layout_width="match_parent"android:layout_height="match_parent" /><LinearLayoutandroid: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"><TextViewandroid: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" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:orientation="horizontal"><TextViewandroid:id="@+id/tvSpeed"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="速度: 0 km/h"android:textSize="14sp" /><TextViewandroid: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><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="4dp"android:orientation="horizontal"><TextViewandroid: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" /><TextViewandroid: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><LinearLayoutandroid: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"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center"><com.google.android.material.button.MaterialButtonandroid: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.MaterialButtonandroid: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><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:orientation="horizontal"android:gravity="center"><ImageButtonandroid: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" /><ImageButtonandroid:id="@+id/btnLayers"android:layout_width="48dp"android:layout_height="48dp"android:layout_marginEnd="8dp"android:src="@drawable/ic_layers"android:background="?attr/selectableItemBackgroundBorderless" /><ImageButtonandroid: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完整实现
class MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate lateinit var viewModel: MainViewModelprivate var aMap: AMap? = nullprivate var currentPolyline: Polyline? = nullprivate var currentMarker: Marker? = nullprivate var isTracking = falseprivate var startTime = 0Lprivate 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 = falseuiSettings.isCompassEnabled = trueuiSettings.isScaleControlsEnabled = true// 地图类型mapType = AMap.MAP_TYPE_NORMAL// 显示定位蓝点isMyLocationEnabled = truemyLocationStyle = 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 = falseupdateTrackingUI()}.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() - startTimebinding.tvDuration.text = formatDuration(duration)// 添加到ViewModelviewModel.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 = latLngcurrentMarker?.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_NORMAL1 -> AMap.MAP_TYPE_SATELLITE2 -> AMap.MAP_TYPE_NIGHTelse -> 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 / 1000val hours = seconds / 3600val minutes = (seconds % 3600) / 60val secs = seconds % 60return 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实现
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>> = _currentTrackPointsval 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
class TrackPlaybackActivity : AppCompatActivity() {private lateinit var binding: ActivityPlaybackBindingprivate var aMap: AMap? = nullprivate var trackPoints: List<LocationPoint> = emptyList()private var currentIndex = 0private var isPlaying = falseprivate val playbackHandler = Handler(Looper.getMainLooper())private var playbackRunnable: Runnable? = nullprivate var playbackPolyline: Polyline? = nullprivate var playbackMarker: Marker? = nulloverride 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_NORMALuiSettings.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 - 1binding.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 = progressupdatePlaybackPosition()}}override fun onStartTrackingTouch(seekBar: SeekBar) {pausePlayback()}override fun onStopTrackingTouch(seekBar: SeekBar) {}})// 速度控制binding.rgSpeed.setOnCheckedChangeListener { _, checkedId ->// 更新播放速度}}private fun startPlayback() {if (trackPoints.isEmpty()) returnisPlaying = truebinding.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 = falsebinding.btnPlay.setImageResource(R.drawable.ic_play)playbackRunnable?.let { playbackHandler.removeCallbacks(it) }}private fun resetPlayback() {pausePlayback()currentIndex = 0binding.seekBar.progress = 0updatePlaybackPosition()}private fun updatePlaybackPosition() {if (trackPoints.isEmpty() || currentIndex >= trackPoints.size) returnval 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 -> 1000LR.id.rbSpeed2x -> 500LR.id.rbSpeed5x -> 200LR.id.rbSpeed10x -> 100Lelse -> 1000L}}override fun onDestroy() {super.onDestroy()binding.mapView.onDestroy()playbackHandler.removeCallbacksAndMessages(null)}
}
九、GPX导出功能
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 Location API
- 高德地图Android SDK
- Room Persistence Library
- GPX格式规范
作者: Android高级开发工程师
完稿日期: 2025年10月11日
代码仓库: github.com/yourname/gps-tracker
版权声明: 本文为原创技术文章,遵循 CC 4.0 BY-SA 版权协议
技术支持: 欢迎在评论区交流讨论