feat: Airbus-style PFD attitude indicator + map dropdown selector
Build APK / build (push) Failing after 7s

- Rewrite ArtificialHorizonView as Airbus A320 PFD style:
  - Central artificial horizon with pitch scale
  - Fixed yellow aircraft symbol on moving sky/ground
  - Roll scale arc with triangular indicator at top
  - Speed tape (knots) on left side
  - Altitude tape (meters) on right side
  - Heading compass rose at bottom (N/E/S/W)
  - Supports additional flight data display

- Map selection:
  - Replace TabLayout with Spinner dropdown
  - Options: 百度卫星图/普通/混合, 高德卫星图/普通, Google卫星/普通/地形/混合
  - Default: 百度卫星图
  - Each option includes provider + map type in one selection

- Fix Chinese map type names (was garbled in repo)
This commit is contained in:
茂之钳
2026-05-25 15:24:58 +00:00
parent 2ac5c75cf3
commit 27873c2544
4 changed files with 447 additions and 237 deletions
@@ -4,9 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayout
import com.senstools.R
import com.senstools.databinding.FragmentMapBinding
import com.senstools.presentation.ui.map.provider.*
import com.senstools.presentation.viewmodel.MapViewModel
@@ -19,22 +22,21 @@ class MapFragment : Fragment() {
private val viewModel: MapViewModel by viewModels()
private var currentProvider: MapProvider? = null
private val providers = mutableMapOf<String, MapProvider>()
private val googleTypes = listOf(
MapType.GOOGLE_NORMAL,
MapType.GOOGLE_SATELLITE,
MapType.GOOGLE_TERRAIN,
MapType.GOOGLE_HYBRID
)
private val baiduTypes = listOf(
MapType.BAIDU_NORMAL,
MapType.BAIDU_SATELLITE,
MapType.BAIDU_HYBRID
)
private val gaodeTypes = listOf(
MapType.GAODE_NORMAL,
MapType.GAODE_SATELLITE
// 地图选项:显示名称 -> (provider类型, map类型)
private data class MapOption(val providerName: String, val mapType: MapType)
private val mapOptions = listOf(
"百度 卫星图" to MapOption("Baidu", MapType.BAIDU_SATELLITE),
"百度 普通地图" to MapOption("Baidu", MapType.BAIDU_NORMAL),
"百度 混合图" to MapOption("Baidu", MapType.BAIDU_HYBRID),
"高德 卫星图" to MapOption("Gaode", MapType.GAODE_SATELLITE),
"高德 普通地图" to MapOption("Gaode", MapType.GAODE_NORMAL),
"Google 卫星图" to MapOption("Google", MapType.GOOGLE_SATELLITE),
"Google 普通地图" to MapOption("Google", MapType.GOOGLE_NORMAL),
"Google 地形图" to MapOption("Google", MapType.GOOGLE_TERRAIN),
"Google 混合图" to MapOption("Google", MapType.GOOGLE_HYBRID)
)
private val optionNames = mapOptions.map { it.first }
private val defaultIndex = 0 // 百度卫星图
override fun onCreateView(
inflater: LayoutInflater,
@@ -47,86 +49,51 @@ class MapFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupProviders()
setupTabs()
setupMapSelector()
setupObservers()
}
private fun setupProviders() {
providers["Google"] = GoogleMapProvider()
providers["ٶ"] = BaiduMapProvider()
providers["ߵ"] = GaodeMapProvider()
}
private fun setupMapSelector() {
val spinner: Spinner = binding.mapSelector
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, optionNames)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.setSelection(defaultIndex)
private fun setupTabs() {
binding.tabMapProvider.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val providerName = tab?.text.toString()
switchProvider(providerName)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (position in mapOptions.indices) {
switchToMap(mapOptions[position].second)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
binding.tabMapType.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val position = tab?.position ?: 0
switchMapType(position)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
providers.keys.forEach { providerName ->
binding.tabMapProvider.addTab(binding.tabMapProvider.newTab().setText(providerName))
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
providers.keys.firstOrNull()?.let { switchProvider(it) }
// 初始化默认地图(百度卫星图)
switchToMap(mapOptions[defaultIndex].second)
}
private fun switchProvider(providerName: String) {
private fun switchToMap(option: MapOption) {
currentProvider?.onPause()
currentProvider?.onDestroy()
val provider = providers[providerName] ?: return
val provider = createProvider(option.providerName) ?: return
provider.initialize(requireContext())
provider.setMapType(option.mapType)
binding.mapContainer.removeAllViews()
binding.mapContainer.addView(provider.view)
currentProvider = provider
provider.onResume()
updateMapTypeTabs()
viewModel.currentLocation.value?.let {
provider.setLocation(it.latitude, it.longitude)
}
}
private fun typesForProvider(providerName: String): List<MapType> {
return when (providerName) {
"Google" -> googleTypes
"ٶ" -> baiduTypes
"ߵ" -> gaodeTypes
else -> googleTypes
}
}
private fun updateMapTypeTabs() {
binding.tabMapType.removeAllTabs()
val types = typesForProvider(currentProvider?.name ?: "Google")
types.forEach { type ->
binding.tabMapType.addTab(binding.tabMapType.newTab().setText(type.displayName))
}
}
private fun switchMapType(position: Int) {
val types = typesForProvider(currentProvider?.name ?: "Google")
if (position in types.indices) {
currentProvider?.setMapType(types[position])
private fun createProvider(name: String): MapProvider? {
return when (name) {
"Google" -> GoogleMapProvider()
"Baidu" -> BaiduMapProvider()
"Gaode" -> GaodeMapProvider()
else -> null
}
}
@@ -1,9 +1,7 @@
package com.senstools.presentation.ui.map.provider
import android.content.Context
import android.location.Location
import android.view.View
import androidx.annotation.ColorInt
interface MapProvider {
val name: String
@@ -21,13 +19,13 @@ interface MapProvider {
}
enum class MapType(val displayName: String) {
GOOGLE_NORMAL("Google ׼"),
GOOGLE_SATELLITE("Google "),
GOOGLE_TERRAIN("Google "),
GOOGLE_HYBRID("Google "),
BAIDU_NORMAL("ٶ ׼"),
BAIDU_SATELLITE("ٶ "),
BAIDU_HYBRID("ٶ "),
GAODE_NORMAL("ߵ ׼"),
GAODE_SATELLITE("ߵ ")
GOOGLE_NORMAL("Google 普通地图"),
GOOGLE_SATELLITE("Google 卫星图"),
GOOGLE_TERRAIN("Google 地形图"),
GOOGLE_HYBRID("Google 混合图"),
BAIDU_NORMAL("百度 普通地图"),
BAIDU_SATELLITE("百度 卫星图"),
BAIDU_HYBRID("百度 混合图"),
GAODE_NORMAL("高德 普通地图"),
GAODE_SATELLITE("高德 卫星图")
}
@@ -5,180 +5,444 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.atan2
import kotlin.math.sqrt
import kotlin.math.PI
/**
* 空客风格 PFD (Primary Flight Display) 姿态仪表
*
* 参考空客 A320 的 PFD 布局:
* - 中央: 人工地平仪(橙色飞机符号固定,天地线随姿态运动)
* - 左侧: 空速带(白色刻度)
* - 右侧: 高度带(白色刻度)
* - 底部: 航向罗盘(0-360度)
* - 上方: 横滚指示弧 + 三角指示器
*/
class ArtificialHorizonView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var pitch: Float = 0f // degrees
private var roll: Float = 0f // degrees
// 姿态数据
private var pitch: Float = 0f
private var roll: Float = 0f
private var yaw: Float = 0f
// 辅助数据(可选)
private var airspeed: Float = 0f
private var altitude: Float = 0f
private var heading: Float = 0f
// 垂直速度(英尺/分钟)
private var verticalSpeed: Float = 0f
private val skyPaint = Paint().apply {
color = Color.parseColor("#1E90FF")
style = Paint.Style.FILL
// 颜色常量(空客 PFD 典型配色)
companion object {
private val SKY_BLUE = Color.parseColor("#1E90FF")
private val GROUND_BROWN = Color.parseColor("#8B4513")
private val HORIZON_WHITE = Color.WHITE
private val PFD_BG = Color.parseColor("#0A1628")
private val ORANGE = Color.parseColor("#FF6B00")
private val LIGHT_BLUE = Color.parseColor("#00BFFF")
private val YELLOW = Color.parseColor("#FFD700")
private val RED = Color.parseColor("#FF4444")
private val GREEN = Color.parseColor("#00C853")
private val GRAY = Color.parseColor("#4A5568")
private val DARK_GRAY = Color.parseColor("#2D3748")
private val DEG_TO_RAD = PI.toFloat() / 180f
}
private val groundPaint = Paint().apply {
color = Color.parseColor("#8B4513")
private val skyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = SKY_BLUE
style = Paint.Style.FILL
}
private val horizonPaint = Paint().apply {
color = Color.WHITE
private val groundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = GROUND_BROWN
style = Paint.Style.FILL
}
private val horizonLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
strokeWidth = 3f
style = Paint.Style.STROKE
}
private val aircraftPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 4f
private val pitchMarkPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(180, 255, 255, 255)
strokeWidth = 2f
style = Paint.Style.STROKE
}
private val pitchMarksPaint = Paint().apply {
color = Color.WHITE
textSize = 24f
private val pitchTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(180, 255, 255, 255)
textSize = 28f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
private val rollIndicatorPaint = Paint().apply {
color = Color.parseColor("#FF6B00")
private val aircraftPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = YELLOW
strokeWidth = 4f
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
private val aircraftFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = YELLOW
style = Paint.Style.FILL
}
private val rollArcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
strokeWidth = 2f
style = Paint.Style.STROKE
}
private val rollTrianglePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = GREEN
style = Paint.Style.FILL
}
private val rollPointerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
strokeWidth = 2f
style = Paint.Style.STROKE
}
// 刻度带用
private val scaleTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
textSize = 26f
isFakeBoldText = true
}
private val scaleLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
strokeWidth = 2f
}
private val scaleBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(30, 255, 255, 255)
style = Paint.Style.FILL
}
private val rollArcPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 3f
style = Paint.Style.STROKE
}
fun setAttitude(pitch: Float, roll: Float) {
this.pitch = pitch
fun setAttitude(pitch: Float, roll: Float, yaw: Float = this.yaw) {
this.pitch = pitch.coerceIn(-90f, 90f)
this.roll = roll
this.yaw = yaw
invalidate()
}
fun setAttitude(pitch: Float, roll: Float) {
setAttitude(pitch, roll, this.yaw)
}
fun setFlightData(airspeed: Float, altitude: Float, heading: Float, verticalSpeed: Float) {
this.airspeed = airspeed
this.altitude = altitude
this.heading = heading
this.verticalSpeed = verticalSpeed
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
val cx = w / 2f
val cy = h / 2f
val centerX = width / 2f
val centerY = height / 2f
val radius = minOf(width, height) / 2f - 20f
// 背景
canvas.drawColor(PFD_BG)
// === 1. 人工地平仪(中央主区) ===
val horizonRadius = minOf(w, h) * 0.35f
// Save canvas state
canvas.save()
canvas.clipRect(cx - horizonRadius - 20, cy - horizonRadius - 20,
cx + horizonRadius + 20, cy + horizonRadius + 20)
// Rotate canvas for roll
canvas.rotate(-roll, centerX, centerY)
canvas.save()
canvas.rotate(-roll, cx, cy)
// Calculate horizon offset based on pitch
val pitchOffset = (pitch / 90f) * radius
val pitchOffset = (pitch / 90f) * horizonRadius * 0.7f
val horizonY = cy + pitchOffset
// Draw sky (top half)
val horizonY = centerY + pitchOffset
canvas.drawRect(0f, 0f, width.toFloat(), horizonY, skyPaint)
// 天 + 地
canvas.drawRect(0f, 0f, w, horizonY, skyPaint)
canvas.drawRect(0f, horizonY, w, h, groundPaint)
// Draw ground (bottom half)
canvas.drawRect(0f, horizonY, width.toFloat(), height.toFloat(), groundPaint)
// 地平线
canvas.drawLine(cx - horizonRadius * 1.5f, horizonY, cx + horizonRadius * 1.5f, horizonY, horizonLinePaint)
// Draw horizon line
canvas.drawLine(0f, horizonY, width.toFloat(), horizonY, horizonPaint)
// Draw pitch marks
drawPitchMarks(canvas, centerX, centerY, pitchOffset)
// Restore canvas
canvas.restore()
// Draw roll indicators (fixed, not affected by pitch/roll)
drawRollIndicators(canvas, centerX, centerY, radius)
// Draw aircraft reference
drawAircraftReference(canvas, centerX, centerY)
}
private fun drawPitchMarks(canvas: Canvas, centerX: Float, centerY: Float, pitchOffset: Float) {
val pitchStep = 10
val maxPitch = 90
for (deg in -maxPitch..maxPitch step pitchStep) {
// 俯仰刻度线
for (deg in -90..90 step 5) {
if (deg == 0) continue
val y = cy + pitchOffset - (deg / 90f) * horizonRadius * 0.7f
if (y < cy - horizonRadius || y > cy + horizonRadius) continue
val y = centerY + pitchOffset - (deg / 90f) * (height / 2f)
val lineLen = when {
deg % 10 == 0 -> horizonRadius * 0.3f
else -> horizonRadius * 0.15f
}
canvas.drawLine(cx - lineLen, y, cx + lineLen, y, pitchMarkPaint)
if (y < 0 || y > height) continue
val lineWidth = if (deg % 30 == 0) 60f else 30f
canvas.drawLine(centerX - lineWidth, y, centerX + lineWidth, y, horizonPaint)
if (deg % 30 == 0) {
val text = if (deg > 0) "+$deg" else "$deg"
canvas.drawText(text, centerX - lineWidth - 40, y + 8, pitchMarksPaint)
canvas.drawText(text, centerX + lineWidth + 40, y + 8, pitchMarksPaint)
if (deg % 10 == 0) {
val label = if (deg > 0) "+$deg" else "$deg"
canvas.drawText(label, cx - lineLen - 35f, y + 10f, pitchTextPaint)
canvas.drawText(label, cx + lineLen + 35f, y + 10f, pitchTextPaint)
}
}
canvas.restore() // roll transform
// 地平仪圆形遮罩边框
canvas.drawCircle(cx, cy, horizonRadius, Paint().apply {
color = HORIZON_WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
})
canvas.restore() // clip
// 飞机符号(固定居中,黄色)
drawAircraftSymbol(canvas, cx, cy, horizonRadius)
// === 2. 横滚指示弧(顶部) ===
drawRollScale(canvas, cx, cy, horizonRadius)
// === 3. 横滚三角指示器 ===
drawRollIndicator(canvas, cx, cy, horizonRadius)
// === 4. 左侧:空速标尺(简化版) ===
drawSpeedTape(canvas, cx, cy, horizonRadius)
// === 5. 右侧:高度标尺(简化版) ===
drawAltitudeTape(canvas, cx, cy, horizonRadius)
// === 6. 底部:航向罗盘 ===
drawCompass(canvas, cx, cy, horizonRadius)
}
private fun drawRollIndicators(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) {
// Draw roll arc
val arcRadius = radius * 0.8f
canvas.drawArc(
centerX - arcRadius,
centerY - arcRadius,
centerX + arcRadius,
centerY + arcRadius,
-150f, 120f, false, rollArcPaint
)
// Draw roll tick marks
for (angle in listOf(-60, -45, -30, -15, 0, 15, 30, 45, 60)) {
val rad = Math.toRadians((angle - 90).toDouble())
val innerRadius = arcRadius - 15
val outerRadius = arcRadius
val startX = centerX + (innerRadius * cos(rad)).toFloat()
val startY = centerY + (innerRadius * sin(rad)).toFloat()
val endX = centerX + (outerRadius * cos(rad)).toFloat()
val endY = centerY + (outerRadius * sin(rad)).toFloat()
canvas.drawLine(startX, startY, endX, endY, rollArcPaint)
}
// Draw roll indicator triangle
val rollRad = Math.toRadians((-roll - 90).toDouble())
val indicatorRadius = arcRadius - 5
val indicatorX = centerX + (indicatorRadius * cos(rollRad)).toFloat()
val indicatorY = centerY + (indicatorRadius * sin(rollRad)).toFloat()
private fun drawAircraftSymbol(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val wingHalf = r * 0.35f
val bodyLen = r * 0.18f
val noseLen = r * 0.12f
val path = Path()
path.moveTo(indicatorX, indicatorY)
path.lineTo(indicatorX - 10, indicatorY - 20)
path.lineTo(indicatorX + 10, indicatorY - 20)
// 飞机符号:V形翅膀 + 中央竖线 + 顶部小三角
// 左翼
path.moveTo(cx - wingHalf, cy)
path.lineTo(cx - bodyLen, cy - bodyLen * 0.3f)
// 中央
path.lineTo(cx, cy - noseLen)
// 右翼
path.lineTo(cx + bodyLen, cy - bodyLen * 0.3f)
path.lineTo(cx + wingHalf, cy)
// 回到底部
path.lineTo(cx + bodyLen, cy + bodyLen * 0.2f)
path.lineTo(cx, cy + bodyLen * 0.1f)
path.lineTo(cx - bodyLen, cy + bodyLen * 0.2f)
path.close()
canvas.drawPath(path, rollIndicatorPaint)
canvas.drawPath(path, aircraftPaint)
canvas.drawPath(path, aircraftFillPaint.apply { alpha = 40 })
// 机头指示小三角
val triPath = Path()
triPath.moveTo(cx - 6f, cy - noseLen - 8f)
triPath.lineTo(cx, cy - noseLen - 18f)
triPath.lineTo(cx + 6f, cy - noseLen - 8f)
triPath.close()
canvas.drawPath(triPath, aircraftFillPaint)
}
private fun drawAircraftReference(canvas: Canvas, centerX: Float, centerY: Float) {
// Draw fixed aircraft reference marks
val wingSpan = 100f
val wingHeight = 8f
private fun drawRollScale(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val arcR = r + 24f
val arcRect = RectF(cx - arcR, cy - arcR, cx + arcR, cy + arcR)
// Left wing
canvas.drawLine(centerX - wingSpan, centerY, centerX - 30, centerY, aircraftPaint)
canvas.drawLine(centerX - 30, centerY, centerX - 30, centerY + wingHeight, aircraftPaint)
// 白色弧线
canvas.drawArc(arcRect, 210f, 120f, false, rollArcPaint)
// Right wing
canvas.drawLine(centerX + wingSpan, centerY, centerX + 30, centerY, aircraftPaint)
canvas.drawLine(centerX + 30, centerY, centerX + 30, centerY + wingHeight, aircraftPaint)
// 刻度
for (angle in listOf(-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60)) {
val rad = (angle - 90) * DEG_TO_RAD
val innerR = arcR - 12f
val outerR = arcR
val sx = cx + innerR * cos(rad)
val sy = cy + innerR * sin(rad)
val ex = cx + outerR * cos(rad)
val ey = cy + outerR * sin(rad)
canvas.drawLine(sx, sy, ex, ey, rollPointerPaint)
}
}
// Center dot
canvas.drawCircle(centerX, centerY, 5f, aircraftPaint)
private fun drawRollIndicator(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val arcR = r + 24f
val rollRad = (-roll - 90) * DEG_TO_RAD
val ix = cx + (arcR - 4f) * cos(rollRad)
val iy = cy + (arcR - 4f) * sin(rollRad)
val path = Path()
path.moveTo(ix, iy)
path.lineTo(ix - 8f, iy - 16f)
path.lineTo(ix + 8f, iy - 16f)
path.close()
canvas.drawPath(path, rollTrianglePaint)
}
private fun drawSpeedTape(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val tapeX = cx - r - 40f
val tapeW = 36f
val tapeH = r * 1.2f
val tapeTop = cy - tapeH / 2f
val tapeBottom = cy + tapeH / 2f
// 半透明背景
canvas.drawRoundRect(tapeX, tapeTop, tapeX + tapeW, tapeBottom, 4f, 4f, scaleBarPaint)
// 刻度(0-600 knots, 简化为 0-60 显示)
val speed = airspeed.coerceIn(0f, 600f)
val range = 60f
val startSpeed = (speed - range / 2f).coerceAtLeast(0f)
val endSpeed = (speed + range / 2f).coerceAtMost(600f)
val pixelsPerUnit = tapeH / range
// 刻度线
var s = (startSpeed / 10).toInt() * 10
while (s <= endSpeed.toInt()) {
val y = tapeBottom - (s - startSpeed) * pixelsPerUnit
if (y < tapeTop || y > tapeBottom) { s += 10; continue }
val isMajor = s % 50 == 0
canvas.drawLine(tapeX + tapeW - (if (isMajor) 18f else 10f), y,
tapeX + tapeW, y, scaleLinePaint)
if (isMajor) {
canvas.drawText("$s", tapeX + 4f, y + 10f, scaleTextPaint)
}
s += 10
}
// 当前速度指示框
val boxH = 28f
val boxW = 50f
val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(200, 0, 0, 0)
style = Paint.Style.FILL
}
val boxStroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
}
val speedTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = LIGHT_BLUE
textSize = 24f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
// 速度框在标尺右侧
val boxCenterX = tapeX + tapeW + 24f
canvas.drawRoundRect(boxCenterX - boxW / 2f, cy - boxH / 2f,
boxCenterX + boxW / 2f, cy + boxH / 2f, 4f, 4f, boxPaint)
canvas.drawRoundRect(boxCenterX - boxW / 2f, cy - boxH / 2f,
boxCenterX + boxW / 2f, cy + boxH / 2f, 4f, 4f, boxStroke)
canvas.drawText("${speed.toInt()}", boxCenterX, cy + 9f, speedTextPaint)
}
private fun drawAltitudeTape(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val tapeX = cx + r + 8f
val tapeW = 36f
val tapeH = r * 1.2f
val tapeTop = cy - tapeH / 2f
val tapeBottom = cy + tapeH / 2f
canvas.drawRoundRect(tapeX, tapeTop, tapeX + tapeW, tapeBottom, 4f, 4f, scaleBarPaint)
val alt = altitude.coerceIn(0f, 50000f)
val range = 500f
val startAlt = (alt - range / 2f).coerceAtLeast(0f)
val endAlt = (alt + range / 2f).coerceAtMost(50000f)
val pixelsPerUnit = tapeH / range
var a = (startAlt / 50).toInt() * 50
while (a <= endAlt.toInt()) {
val y = tapeBottom - (a - startAlt) * pixelsPerUnit
if (y < tapeTop || y > tapeBottom) { a += 50; continue }
val isMajor = a % 100 == 0
canvas.drawLine(tapeX, y, tapeX + (if (isMajor) 18f else 10f), y, scaleLinePaint)
if (isMajor) {
canvas.drawText("${a / 10 * 10}", tapeX + tapeW - 4f, y + 10f, scaleTextPaint)
}
a += 50
}
val boxH = 28f
val boxW = 50f
val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(200, 0, 0, 0)
style = Paint.Style.FILL
}
val boxStroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = HORIZON_WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
}
val altTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = LIGHT_BLUE
textSize = 24f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val boxCenterX = tapeX - 24f
canvas.drawRoundRect(boxCenterX - boxW / 2f, cy - boxH / 2f,
boxCenterX + boxW / 2f, cy + boxH / 2f, 4f, 4f, boxPaint)
canvas.drawRoundRect(boxCenterX - boxW / 2f, cy - boxH / 2f,
boxCenterX + boxW / 2f, cy + boxH / 2f, 4f, 4f, boxStroke)
canvas.drawText("${alt.toInt()}", boxCenterX, cy + 9f, altTextPaint)
}
private fun drawCompass(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val compassY = cy + r + 28f
val compassWidth = r * 1.8f
val left = cx - compassWidth / 2f
val right = cx + compassWidth / 2f
// 背景条
canvas.drawRoundRect(left, compassY - 14f, right, compassY + 14f, 4f, 4f, scaleBarPaint)
// 航向值
val hdg = heading.coerceIn(0f, 360f)
val range = 60f
val pixelsPerDeg = compassWidth / range
val startHdg = hdg - range / 2f
val endHdg = hdg + range / 2f
// 刻度
var d = ((startHdg) / 10).toInt() * 10
while (d <= endHdg.toInt()) {
val normD = ((d % 360) + 360) % 360
val x = left + (d - startHdg) * pixelsPerDeg
if (x < left || x > right) { d += 10; continue }
val isMajor = normD % 30 == 0
canvas.drawLine(x, compassY - 10f, x, compassY + 10f, scaleLinePaint)
if (isMajor) {
val label = when (normD) {
0 -> "N"
90 -> "E"
180 -> "S"
270 -> "W"
else -> "${normD / 10}"
}
canvas.drawText(label, x, compassY + 22f, scaleTextPaint)
}
d += 10
}
// 当前航向三角指示
val triPath = Path()
triPath.moveTo(cx, compassY - 14f)
triPath.lineTo(cx - 6f, compassY - 22f)
triPath.lineTo(cx + 6f, compassY - 22f)
triPath.close()
canvas.drawPath(triPath, rollTrianglePaint)
// 航向数值
val hdgTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = GREEN
textSize = 24f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
canvas.drawText("${hdg.toInt()}°", cx, compassY - 24f, hdgTextPaint)
}
}
@@ -4,6 +4,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<Spinner
android:id="@+id/mapSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@android:color/white"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<FrameLayout
android:id="@+id/mapContainer"
android:layout_width="0dp"
@@ -11,37 +22,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mapTypeSelector" />
<LinearLayout
android:id="@+id/mapTypeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@android:color/white"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabMapProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:tabMode="scrollable"
app:tabGravity="start" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabMapType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:tabMode="scrollable"
app:tabGravity="start" />
</LinearLayout>
app:layout_constraintTop_toBottomOf="@id/mapSelector" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabMyLocation"