feat: Airbus-style PFD attitude indicator + map dropdown selector
Build APK / build (push) Failing after 7s
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-11
@@ -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("高德 卫星图")
|
||||
}
|
||||
|
||||
+383
-119
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user