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 版权协议
技术支持: 欢迎在评论区交流讨论

相关推荐
沐欣工作室_lvyiyi4 小时前
基于腾讯云的物联网导盲助手设计与实现(论文+源码)
单片机·物联网·云计算·毕业设计·腾讯云·导盲杖
taxunjishu11 小时前
DeviceNet 转 Modbus TCP 协议转换在 S7-1200 PLC化工反应釜中的应用
运维·人工智能·物联网·自动化·区块链
千千道16 小时前
利用keil +RASC给瑞萨RA8D1编译烧写程序
单片机·嵌入式硬件·mcu·物联网
苏州知芯传感1 天前
物联网边缘节点中的MEMS传感器低功耗设计实战
物联网·mems
TensorTinker2 天前
基于FireBeetle 2 ESP32-C5的智能植物光照系统——物联网农业实践
物联网
电子科技圈2 天前
芯科科技第三代无线SoC现已全面供货
嵌入式硬件·mcu·物联网·网络安全·智能家居·智能硬件·iot
『往事』&白驹过隙;2 天前
浅谈内存DDR——DDR4性能优化技术
科技·物联网·学习·性能优化·内存·ddr
TDengine (老段)2 天前
TDengine 数学函数 ABS() 用户手册
大数据·数据库·sql·物联网·时序数据库·tdengine·涛思数据
lisw053 天前
AIoT(人工智能物联网):融合范式下的技术演进、系统架构与产业变革
大数据·人工智能·物联网·机器学习·软件工程