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

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 电池优化策略

  1. 动态调整更新频率: 根据速度自动调整GPS更新间隔
  2. 使用批量位置更新: 减少唤醒设备的次数
  3. 后台限制: Android 8.0+限制后台定位频率
  4. 电池优化白名单: 引导用户将应用加入白名单

10.2 数据优化

  1. 卡尔曼滤波: 平滑GPS数据,减少漂移
  2. Douglas-Peucker算法: 简化轨迹点,减少存储
  3. 异常值过滤: 过滤不合理的位置数据
  4. 数据压缩: 定期清理过期数据

10.3 内存优化

  1. 分页加载: 历史轨迹使用分页加载
  2. 及时释放: 不使用的地图资源及时释放
  3. 图片优化: 使用合适尺寸的标记图标

十一、总结与展望

本文提供了一套完整的Android GPS定位和轨迹追踪解决方案,涵盖了从数据采集、存储、展示到导出的全流程。

核心功能:

  • ✅ 实时GPS定位与轨迹记录
  • ✅ 高德地图集成与可视化
  • ✅ 轨迹回放功能
  • ✅ 数据持久化(Room数据库)
  • ✅ GPX格式导出
  • ✅ 性能优化与电池管理

扩展方向:

  • 云端同步(Firebase/自建服务器)
  • 社交分享功能
  • 运动数据分析(配速、热力图)
  • 离线地图支持
  • 语音导航提示

希望本教程能帮助您快速构建专业的GPS追踪应用!

十二、参考资料


作者 : Android高级开发工程师
完稿日期 : 2025年10月11日
代码仓库 : github.com/yourname/gps-tracker
版权声明 : 本文为原创技术文章,遵循 CC 4.0 BY-SA 版权协议
技术支持: 欢迎在评论区交流讨论

相关推荐
我先去打把游戏先1 天前
ESP32开发指南(基于IDF):连接AWS,乐鑫官方esp-aws-iot-master例程实验、跑通
开发语言·笔记·单片机·物联网·学习·云计算·aws
QQ12958455041 天前
ThingsBoard部件数据结构解析
数据结构·数据库·物联网·iot
TDengine (老段)1 天前
TDengine 数学函数 ASCII 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
TDengine (老段)2 天前
TDengine 数学函数 TRUNCATE 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
TDengine (老段)2 天前
TDengine 数据函数 CORR 用户手册
大数据·数据库·物联网·时序数据库·tdengine·1024程序员节
搜移IT科技2 天前
2025广州国际物联网产业生态博览会(物联网展)最新技术与亮点揭秘!
物联网
卍郝凝卍2 天前
NVR(网络视频录像机)和视频网关的工作方式
网络·图像处理·物联网·音视频·视频解决方案
kaka❷❷2 天前
STM32 单片机 ESP8266 联网 和 MQTT协议
stm32·单片机·嵌入式硬件·物联网·mqtt·esp8266
塔能物联运维2 天前
物联网运维中基于数字孪生的实时设备状态同步与仿真验证技术
运维·物联网
紫金桥软件2 天前
组态软件和实时数据库区别大吗?
数据库·物联网·软件工程·scada·监控组态软件