Android开发实战:用GNSS API手把手教你画个卫星云图(附完整源码)

张开发
2026/5/25 16:24:39 15 分钟阅读
Android开发实战:用GNSS API手把手教你画个卫星云图(附完整源码)
Android开发实战用GNSS API手把手教你画个卫星云图附完整源码在移动应用开发中位置服务一直是核心功能之一。但大多数开发者只停留在获取经纬度坐标的层面很少深入挖掘设备GNSS模块提供的丰富数据。本文将带你探索一个有趣的应用场景——实时绘制卫星云图让你的应用不仅能告诉用户在哪里还能展示为什么能定位。想象一下当你打开地图应用时如果不仅能看见自己的位置标记还能直观地看到头顶上哪些卫星正在为你的设备提供定位服务它们的信号强度如何这该有多酷这种可视化不仅提升了用户体验还能帮助开发者调试定位相关问题。我们将使用Android的GNSS API从数据获取到图形渲染一步步构建这个功能。1. GNSS基础与API概览现代Android设备通常支持多种卫星定位系统包括GPS美国、GLONASS俄罗斯、北斗中国和Galileo欧盟等。这些系统统称为全球导航卫星系统GNSS。Android从7.0API 24开始提供了一套完整的GNSS状态监测接口让我们能够获取丰富的卫星原始数据。1.1 核心GNSS API介绍Android提供了三个主要的GNSS数据接口GnssStatusCallback最常用的接口提供卫星的基本状态信息方位角azimuth卫星相对于正北方向的角度俯仰角elevation卫星相对于地平线的角度信噪比CN0信号质量指标卫星系统类型GPS/GLONASS/北斗等GnssMeasurementsCallback提供更原始的测量数据载波频率伪距测量多普勒频移时间戳GnssNavigationMessageCallback提供导航电文数据星历数据年历数据系统时间对于卫星云图绘制我们主要使用GnssStatusCallback因为它提供了足够的位置和信号质量信息且实现相对简单。1.2 权限与兼容性检查在开始编码前我们需要确保应用有正确的权限并检查设备是否支持所需功能uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /在运行时检查val locationManager getSystemService(Context.LOCATION_SERVICE) as LocationManager // 检查GNSS状态回调支持 if (!locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) { Toast.makeText(this, 设备不支持GNSS, Toast.LENGTH_LONG).show() return } // 检查权限 if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) ! PackageManager.PERMISSION_GRANTED ) { // 请求权限 ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE_LOCATION ) return }2. 获取GNSS卫星数据有了权限和兼容性检查后我们可以开始注册GNSS状态回调获取实时的卫星数据。2.1 实现GnssStatusCallback创建一个自定义的GnssStatus.Callback实现类class SatelliteStatusCallback( private val onSatellitesUpdated: (ListSatelliteData) - Unit ) : GnssStatus.Callback() { override fun onSatelliteStatusChanged(status: GnssStatus) { val satellites mutableListOfSatelliteData() for (i in 0 until status.satelliteCount) { satellites.add( SatelliteData( svid status.getSvid(i), constellationType status.getConstellationType(i), azimuth status.getAzimuthDegrees(i), elevation status.getElevationDegrees(i), cn0 status.getCn0DbHz(i), hasAlmanac status.hasAlmanacData(i), hasEphemeris status.hasEphemerisData(i), usedInFix status.usedInFix(i) ) ) } onSatellitesUpdated(satellites) } } data class SatelliteData( val svid: Int, val constellationType: Int, val azimuth: Float, val elevation: Float, val cn0: Float, val hasAlmanac: Boolean, val hasEphemeris: Boolean, val usedInFix: Boolean )2.2 注册与注销回调在Activity或Fragment中管理回调的生命周期private lateinit var locationManager: LocationManager private lateinit var gnssCallback: GnssStatus.Callback override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) locationManager getSystemService(Context.LOCATION_SERVICE) as LocationManager gnssCallback SatelliteStatusCallback { satellites - // 更新UI或处理卫星数据 updateSatelliteData(satellites) } } override fun onResume() { super.onResume() locationManager.registerGnssStatusCallback(gnssCallback, Handler(mainLooper)) } override fun onPause() { super.onPause() locationManager.unregisterGnssStatusCallback(gnssCallback) }2.3 处理方位角补偿由于方位角是基于正北方向计算的而手机屏幕可能有任意朝向我们需要结合设备方向传感器数据来补偿这个角度差private var deviceOrientation 0f private fun setupOrientationSensor() { val sensorManager getSystemService(Context.SENSOR_SERVICE) as SensorManager val orientationSensor sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION) sensorManager.registerListener( object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { deviceOrientation event.values[0] // 方位角度 } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} }, orientationSensor, SensorManager.SENSOR_DELAY_UI ) }3. 设计卫星云图视图有了卫星数据后我们需要设计一个自定义View来可视化这些信息。卫星云图通常是一个圆形区域中心代表天顶头顶正上方边缘代表地平线。3.1 自定义View基础结构创建一个SatelliteView类class SatelliteView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { private val satellitePaint Paint(Paint.ANTI_ALIAS_FLAG).apply { style Paint.Style.FILL textSize 24f } private val circlePaint Paint(Paint.ANTI_ALIAS_FLAG).apply { style Paint.Style.STROKE strokeWidth 2f color Color.GRAY } private val textPaint Paint(Paint.ANTI_ALIAS_FLAG).apply { color Color.BLACK textSize 36f } private var satellites: ListSatelliteData emptyList() private var deviceOrientation: Float 0f fun updateData(satellites: ListSatelliteData, orientation: Float 0f) { this.satellites satellites this.deviceOrientation orientation invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val centerX width / 2f val centerY height / 2f val radius min(width, height) / 2f - 20f // 绘制基准圆 drawReferenceCircles(canvas, centerX, centerY, radius) // 绘制卫星 drawSatellites(canvas, centerX, centerY, radius) // 绘制指南针标记 drawCompass(canvas, centerX, centerY, radius) } // 其他绘制方法将在下面实现 }3.2 绘制参考圆和方位标记卫星云图通常包含几个同心圆代表不同的俯仰角private fun drawReferenceCircles(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) { // 地平线圆俯仰角0° canvas.drawCircle(centerX, centerY, radius, circlePaint) // 中间圆俯仰角30° canvas.drawCircle(centerX, centerY, radius * 2 / 3, circlePaint) // 天顶圆俯仰角60° canvas.drawCircle(centerX, centerY, radius / 3, circlePaint) // 绘制方位角标记每45°一个标记 for (angle in 0 until 360 step 45) { val rad Math.toRadians(angle.toDouble()) val startX centerX radius * cos(rad).toFloat() val startY centerY radius * sin(rad).toFloat() val endX centerX (radius 20) * cos(rad).toFloat() val endY centerY (radius 20) * sin(rad).toFloat() canvas.drawLine(startX, startY, endX, endY, circlePaint) // 绘制方位角文字 if (angle % 90 0) { val text when (angle) { 0 - N 90 - E 180 - S 270 - W else - angle.toString() } val textX centerX (radius 40) * cos(rad).toFloat() val textY centerY (radius 40) * sin(rad).toFloat() canvas.drawText(text, textX, textY, textPaint) } } }3.3 绘制卫星位置根据卫星的方位角和俯仰角计算其在云图中的位置private fun drawSatellites(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) { for (satellite in satellites) { // 补偿设备朝向 val adjustedAzimuth satellite.azimuth - deviceOrientation // 将方位角转换为弧度 val azimuthRad Math.toRadians(adjustedAzimuth.toDouble()) // 计算卫星距离中心的距离俯仰角0°在边缘90°在中心 val distance radius * (1 - satellite.elevation / 90f) // 计算卫星坐标 val x centerX distance * cos(azimuthRad).toFloat() val y centerY distance * sin(azimuthRad).toFloat() // 根据信号强度设置颜色 val color when { satellite.cn0 40 - Color.GREEN // 强信号 satellite.cn0 28 - Color.BLUE // 中等信号 else - Color.RED // 弱信号 } satellitePaint.color color canvas.drawCircle(x, y, 20f, satellitePaint) // 绘制卫星ID canvas.drawText(satellite.svid.toString(), x - 10f, y 5f, textPaint) } }3.4 绘制指南针标记为了帮助用户理解方向我们可以添加一个简单的指南针标记private fun drawCompass(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) { // 绘制中心点 circlePaint.color Color.RED canvas.drawCircle(centerX, centerY, 10f, circlePaint) // 绘制当前设备朝向标记 val orientationRad Math.toRadians(deviceOrientation.toDouble()) val endX centerX radius * cos(orientationRad).toFloat() val endY centerY radius * sin(orientationRad).toFloat() val path Path() path.moveTo(centerX, centerY) path.lineTo(endX, endY) val paint Paint(Paint.ANTI_ALIAS_FLAG).apply { color Color.RED style Paint.Style.STROKE strokeWidth 3f } canvas.drawPath(path, paint) // 绘制设备朝向文字 canvas.drawText( ↑, centerX (radius 60) * cos(orientationRad).toFloat(), centerY (radius 60) * sin(orientationRad).toFloat(), textPaint ) }4. 整合与优化现在我们已经有了所有关键组件是时候将它们整合起来并做一些优化提升了。4.1 主Activity整合在MainActivity中整合所有功能class MainActivity : AppCompatActivity() { private lateinit var satelliteView: SatelliteView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) satelliteView findViewById(R.id.satelliteView) if (checkPermissions()) { setupGnss() setupOrientationSensor() } } private fun checkPermissions(): Boolean { return if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) PackageManager.PERMISSION_GRANTED ) { true } else { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE_LOCATION ) false } } private fun setupGnss() { val locationManager getSystemService(Context.LOCATION_SERVICE) as LocationManager val gnssCallback SatelliteStatusCallback { satellites - runOnUiThread { satelliteView.updateData(satellites) } } locationManager.registerGnssStatusCallback(gnssCallback, Handler(mainLooper)) } private fun setupOrientationSensor() { val sensorManager getSystemService(Context.SENSOR_SERVICE) as SensorManager val orientationSensor sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION) sensorManager.registerListener( object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { satelliteView.updateData( satelliteView.satellites, event.values[0] ) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} }, orientationSensor, SensorManager.SENSOR_DELAY_UI ) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_CODE_LOCATION grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { setupGnss() setupOrientationSensor() } } }4.2 性能优化建议减少onDraw调用频率使用ValueAnimator控制刷新率避免过于频繁的重绘考虑只在卫星数据或设备方向有显著变化时才触发重绘内存优化重用Paint对象而不是在每次绘制时创建新的对于静态元素如参考圆可以考虑使用Bitmap缓存电池优化在应用进入后台时注销GNSS回调提供设置选项让用户调整更新频率用户体验增强添加卫星信息的详细Tooltip实现点击卫星显示详细信息的功能添加动画效果使卫星移动更平滑4.3 高级功能扩展多星座区分 可以根据getConstellationType()为不同卫星系统使用不同形状或颜色private fun getSatelliteIcon(constellationType: Int): PairInt, Path { return when (constellationType) { GnssStatus.CONSTELLATION_GPS - Color.BLUE to createTrianglePath() GnssStatus.CONSTELLATION_GLONASS - Color.RED to createSquarePath() GnssStatus.CONSTELLATION_BEIDOU - Color.YELLOW to createDiamondPath() GnssStatus.CONSTELLATION_GALILEO - Color.GREEN to createCirclePath() else - Color.GRAY to createCirclePath() } }历史轨迹显示 记录卫星位置变化绘制移动轨迹private val satelliteTrails mutableMapOfInt, MutableListPointF() fun updateTrail(satellite: SatelliteData, x: Float, y: Float) { val trail satelliteTrails.getOrPut(satellite.svid) { mutableListOf() } trail.add(PointF(x, y)) if (trail.size 20) { // 限制轨迹点数 trail.removeAt(0) } } private fun drawTrails(canvas: Canvas) { for ((_, trail) in satelliteTrails) { if (trail.size 1) { val path Path() path.moveTo(trail[0].x, trail[0].y) for (i in 1 until trail.size) { path.lineTo(trail[i].x, trail[i].y) } canvas.drawPath(path, trailPaint) } } }信号强度热力图 根据信号强度在云图上叠加热力效果private fun drawSignalHeatmap(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) { val shader RadialGradient( centerX, centerY, radius, intArrayOf( Color.argb(100, 255, 0, 0), Color.argb(50, 255, 255, 0), Color.argb(20, 0, 255, 0), Color.TRANSPARENT ), floatArrayOf(0f, 0.3f, 0.6f, 1f), Shader.TileMode.CLAMP ) heatmapPaint.shader shader canvas.drawCircle(centerX, centerY, radius, heatmapPaint) }5. 完整源码结构与使用说明5.1 项目结构/app /src/main /java/com/example/satelliteview MainActivity.kt SatelliteView.kt SatelliteStatusCallback.kt models/ SatelliteData.kt /res /layout activity_main.xml /values colors.xml strings.xml5.2 activity_main.xml?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools android:layout_widthmatch_parent android:layout_heightmatch_parent tools:context.MainActivity com.example.satelliteview.SatelliteView android:idid/satelliteView android:layout_widthmatch_parent android:layout_heightmatch_parent / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitybottom|center_horizontal android:text卫星云图 android:textSize24sp android:padding16dp android:background#80000000 android:textColor#FFFFFF/ /FrameLayout5.3 使用说明将上述代码文件添加到你的Android项目中确保在AndroidManifest.xml中添加了位置权限在支持GNSS的Android设备API 24上运行应用授予应用位置权限后即可看到实时卫星云图提示为了获得最佳效果请在户外开阔地带测试此应用室内可能无法接收到足够的卫星信号。5.4 已知问题与解决方案问题可能原因解决方案无卫星显示设备不支持GNSS或信号弱检查设备是否支持GNSS尝试在开阔地带使用方位角不准确设备指南针未校准按照设备制造商说明校准指南针更新延迟系统限制或性能问题降低更新频率或优化绘制代码不同设备显示不一致屏幕尺寸和密度差异使用dp单位而非像素添加多尺寸资源6. 测试与调试技巧6.1 模拟GNSS数据在开发过程中可以使用Android Studio的模拟器来模拟GNSS数据打开Android Studio的Extended Controls模拟器右侧三个点选择Location标签使用Manual选项卡手动设置位置使用GPX/KML选项卡导入预定义的轨迹文件6.2 调试卫星数据添加调试信息显示private fun drawDebugInfo(canvas: Canvas, centerX: Float, centerY: Float) { var yOffset 50f satellites.forEach { satellite - val info SV${satellite.svid}: Az${satellite.azimuth}° El${satellite.elevation}° CN0${satellite.cn0}dB canvas.drawText(info, 20f, yOffset, debugPaint) yOffset 30f } canvas.drawText(Orientation: $deviceOrientation°, 20f, yOffset 30f, debugPaint) canvas.drawText(Satellites: ${satellites.size}, 20f, yOffset 60f, debugPaint) }6.3 常见问题排查权限问题确保在运行时请求了ACCESS_FINE_LOCATION权限检查AndroidManifest.xml中的权限声明回调不触发确认设备是否支持GNSS检查是否在户外或有良好天空视野的位置测试验证是否正确地注册了回调性能问题使用Android Profiler监控CPU和内存使用优化onDraw方法避免不必要的对象创建方向传感器问题不同设备可能有不同的传感器实现考虑使用Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR作为替代7. 实际应用场景扩展卫星云图可视化不仅是一个炫酷的功能它还有多种实际应用场景7.1 定位质量诊断通过观察卫星云图可以直观地判断当前定位质量卫星数量可见卫星越多定位越精确卫星分布均匀分布的卫星比集中在一侧的卫星提供更好的几何精度信号强度高CN0值的卫星越多定位越稳定7.2 户外活动辅助对于登山、航海等户外活动卫星云图可以帮助用户预测定位精度变化如进入峡谷前判断何时能获得稳定的定位如等待足够数量的卫星识别可能的信号干扰源7.3 教育演示工具卫星云图是很好的STEM教育工具可以用于演示卫星导航系统工作原理几何精度因子GDOP概念不同卫星系统的特点和区别7.4 增强现实应用结合ARCore或ARKit可以创建增强现实视图将卫星位置叠加到真实天空获取设备姿态通过AR框架或IMU传感器将卫星方位角/俯仰角转换为AR空间坐标在相机画面上渲染卫星标记fun projectSatelliteToAR(azimuth: Float, elevation: Float): Vector3 { // 将球坐标转换为笛卡尔坐标 val azimuthRad Math.toRadians(azimuth.toDouble()) val elevationRad Math.toRadians(elevation.toDouble()) val x cos(elevationRad) * sin(azimuthRad) val y cos(elevationRad) * cos(azimuthRad) val z sin(elevationRad) return Vector3(x.toFloat(), y.toFloat(), z.toFloat()) }8. 进阶主题与资源8.1 GNSS原始测量数据对于需要更高精度的应用可以探索GnssMeasurementsCallback提供的原始测量数据伪距测量载波相位多普勒频移接收机钟差这些数据可用于实现RTK实时动态定位等高级定位技术。8.2 全球卫星系统比较系统国家/地区卫星数量精度特色GPS美国313-5m全球覆盖最成熟GLONASS俄罗斯245-10m高纬度性能好北斗中国353-5m亚太地区增强Galileo欧盟261-3m最高民用精度8.3 推荐学习资源官方文档Android GNSS API指南LocationManager文档开源项目GPSTest功能强大的GNSS测试工具Android GNSS LoggerGoogle官方的GNSS数据记录工具专业书籍Global Positioning System: Signals, Measurements, and PerformanceApplied GPS for Engineers and Project Managers在线课程Coursera的Global Positioning System (GPS)专项课程edX的Principles of GNSS Positioning课程9. 性能优化实战在实际项目中我们发现几个关键优化点可以显著提升卫星云图的流畅度和电池效率节流更新频率 GNSS数据更新可能非常频繁每秒1-10次但UI不需要如此高频刷新private var lastUpdateTime 0L private const val MIN_UPDATE_INTERVAL 500 // 毫秒 override fun onSatelliteStatusChanged(status: GnssStatus) { val now System.currentTimeMillis() if (now - lastUpdateTime MIN_UPDATE_INTERVAL) { lastUpdateTime now // 处理数据并更新UI } }离屏渲染缓存 对于静态元素参考圆、方位标记使用Bitmap缓存private var backgroundCache: Bitmap? null private fun drawBackgroundToCache(width: Int, height: Int): Bitmap { val bitmap Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) // 绘制所有静态元素到bitmap return bitmap } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) backgroundCache drawBackgroundToCache(w, h) } override fun onDraw(canvas: Canvas) { backgroundCache?.let { canvas.drawBitmap(it, 0f, 0f, null) } // 只动态绘制卫星和方向标记 }按需渲染 只在数据有显著变化时触发重绘private var lastSatellites emptyListSatelliteData() fun updateData(satellites: ListSatelliteData, orientation: Float 0f) { if (satellites ! lastSatellites || abs(orientation - deviceOrientation) 5) { lastSatellites satellites deviceOrientation orientation invalidate() } }传感器选择优化 方向传感器有多种实现方式选择最适合的private fun getBestOrientationSensor(sensorManager: SensorManager): Sensor? { // 优先使用游戏旋转矢量传感器更高效 return sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR) ?: sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION) }10. 跨平台兼容性处理不同Android设备和版本在GNSS支持上存在差异需要特别注意10.1 星座类型兼容性fun getConstellationName(type: Int): String { return when (type) { GnssStatus.CONSTELLATION_GPS - GPS GnssStatus.CONSTELLATION_GLONASS - GLONASS GnssStatus.CONSTELLATION_BEIDOU - 北斗 GnssStatus.CONSTELLATION_GALILEO - Galileo GnssStatus.CONSTELLATION_QZSS - QZSS GnssStatus.CONSTELLATION_SBAS - SBAS GnssStatus.CONSTELLATION_IRNSS - IRNSS else - UNKNOWN } }10.2 API级别兼容对于需要支持较旧Android版本的应用SuppressLint(MissingPermission) fun registerGnssCallback(manager: LocationManager, callback: GnssStatus.Callback): Boolean { return if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { manager.registerGnssStatusCallback(callback) true } else { // 旧版本使用GpsStatus API manager.addGpsStatusListener(object : GpsStatus.Listener { override fun onGpsStatusChanged(event: Int) { if (event GpsStatus.GPS_EVENT_SATELLITE_STATUS) { val status manager.getGpsStatus(null) // 转换为兼容的数据结构 } } }) true } }10.3 设备特定问题某些设备可能需要特殊处理华为设备EMUI系统可能有不同的权限处理方式需要考虑鸿蒙系统的兼容性小米设备可能需要额外检查电池优化设置某些型号需要手动开启高精度定位模式三星设备部分国际型号支持更多卫星系统传感器校准流程可能不同11. 用户体验优化技巧11.1 视觉反馈增强卫星状态动画private val animator ValueAnimator.ofFloat(0f, 1f).apply { duration 1000 repeatCount ValueAnimator.INFINITE repeatMode ValueAnimator.REVERSE addUpdateListener { pulseFactor it.animatedValue as Float invalidate() } } private fun drawPulsingSatellite(canvas: Canvas, x: Float, y: Float, color: Int) { satellitePaint.color color val radius 20f 5f * pulseFactor canvas.drawCircle(x, y, radius, satellitePaint) }信号强度可视化private fun drawSignalStrength(canvas: Canvas, x: Float, y: Float, cn0: Float) { val levels min(5, ((cn0 - 20) / 5).toInt()) for (i in 0 until levels) { val barWidth 4f val barHeight 8f i * 4f canvas.drawRect( x 25f i * (barWidth 2), y - barHeight / 2, x 25f i * (barWidth 2) barWidth, y barHeight / 2, signalPaint ) } }11.2 交互功能卫星详情点击override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action MotionEvent.ACTION_DOWN) { satellites.find { satellite - val (x, y) getSatellitePosition(satellite) sqrt((x - event.x).pow(2) (y - event.y).pow(2)) 30 }?.let { showSatelliteInfo(it) } } return super.onTouchEvent(event) }手势缩放private var scaleFactor 1f private val scaleListener object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { scaleFactor * detector.scaleFactor scaleFactor max(0.5f, min(scaleFactor, 2f)) invalidate() return true } }11.3 主题适配支持深色模式private fun setupThemeColors(context: Context) { val isDark (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) Configuration.UI_MODE_NIGHT_YES circlePaint.color if (isDark) Color.LTGRAY else Color.DKGRAY textPaint.color if (isDark) Color.WHITE else Color.BLACK // 其他颜色适配... }12. 测试策略与质量保证12.1 单元测试重点坐标转换验证Test fun testSatellitePositionCalculation() { val view SatelliteView(ApplicationProvider.getApplicationContext()) // 天顶卫星应该在中心 val (x1, y1) view.calculatePosition(0f, 90f, 100f, 100f, 50f, 0f) assertEquals(100f, x1, 0.1f) assertEquals(100f, y1, 0.1f) // 正东方向地平线卫星 val (x2, y2) view.calculatePosition(90f, 0f, 100f, 100f, 50f, 0f) assertEquals(150f, x2, 0.1f) assertEquals(100f, y2, 0.1f) }设备方向补偿测试Test fun testOrientationCompensation() { val view SatelliteView(ApplicationProvider.getApplicationContext()) // 设备转向东时北方卫星应显示在西方 val (x, y) view.calculate

更多文章