Initial commit: Компас Цель — Android приложение для навигации по GPS

This commit is contained in:
ua1zbe
2026-08-11 14:11:07 +03:00
commit e92e7b06a2
25 changed files with 1192 additions and 0 deletions

36
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,36 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.ua1zbe.compass"
compileSdk = 34
defaultConfig {
applicationId = "com.ua1zbe.compass"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Compass">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,169 @@
package com.ua1zbe.compass
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RadialGradient
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
class CompassView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
var heading = 0f
var bearing = 0f
var hasFix = false
private val dialPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val tickPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val majorTickPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val needlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val needleShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val centerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val headingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val needlePath = Path()
private val shadowPath = Path()
private val headingPath = Path()
private val cardinals = arrayOf("N", "E", "S", "W")
init {
dialPaint.color = Color.rgb(18, 28, 48)
dialPaint.style = Paint.Style.FILL
ringPaint.color = Color.rgb(41, 224, 255)
ringPaint.style = Paint.Style.STROKE
ringPaint.strokeWidth = 2f * resources.displayMetrics.density
tickPaint.color = Color.rgb(90, 110, 140)
tickPaint.strokeWidth = 1f * resources.displayMetrics.density
majorTickPaint.color = Color.rgb(41, 224, 255)
majorTickPaint.strokeWidth = 2f * resources.displayMetrics.density
textPaint.color = Color.rgb(138, 151, 168)
textPaint.textAlign = Paint.Align.CENTER
textPaint.textSize = 14f * resources.displayMetrics.density
textPaint.isFakeBoldText = true
needlePaint.color = Color.rgb(255, 90, 95)
needlePaint.style = Paint.Style.FILL
needleShadowPaint.color = Color.rgb(0, 0, 0)
needleShadowPaint.style = Paint.Style.FILL
centerPaint.color = Color.rgb(41, 224, 255)
centerPaint.style = Paint.Style.FILL
headingPaint.color = Color.rgb(233, 238, 247)
headingPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
val cx = w / 2f
val cy = h / 2f
val radius = min(w, h) / 2f - 24f * resources.displayMetrics.density
val density = resources.displayMetrics.density
// dial background with subtle glow (static, north at top)
val glow = RadialGradient(cx, cy, radius, Color.rgb(30, 45, 75), Color.rgb(18, 28, 48), Shader.TileMode.CLAMP)
dialPaint.shader = glow
canvas.drawCircle(cx, cy, radius, dialPaint)
dialPaint.shader = null
// outer ring
canvas.drawCircle(cx, cy, radius, ringPaint)
// ticks: fixed, north at top
for (deg in 0 until 360 step 5) {
val angle = Math.toRadians((deg - 90).toDouble())
val cosA = cos(angle).toFloat()
val sinA = sin(angle).toFloat()
val isMajor = deg % 30 == 0
val outer = radius - 10f * density
val inner = if (isMajor) radius - 26f * density else radius - 18f * density
canvas.drawLine(
cx + outer * cosA, cy + outer * sinA,
cx + inner * cosA, cy + inner * sinA,
if (isMajor) majorTickPaint else tickPaint
)
}
// cardinal letters: N top (red), E right, S bottom, W left
for (i in 0 until 4) {
val angle = Math.toRadians((i * 90 - 90).toDouble())
val cosA = cos(angle).toFloat()
val sinA = sin(angle).toFloat()
val r = radius - 42f * density
if (i == 0) {
textPaint.color = Color.rgb(255, 90, 95)
textPaint.textSize = 20f * density
canvas.drawText(cardinals[i], cx + r * cosA, cy + r * sinA + 7f * density, textPaint)
textPaint.textSize = 14f * density
} else {
textPaint.color = Color.rgb(138, 151, 168)
canvas.drawText(cardinals[i], cx + r * cosA, cy + r * sinA + 5f * density, textPaint)
}
}
// device heading marker (white triangle), rotated by heading
headingPath.reset()
val hr = radius - 58f * density
headingPath.moveTo(cx, cy - hr - 8f * density)
headingPath.lineTo(cx + 8f * density, cy - hr + 6f * density)
headingPath.lineTo(cx - 8f * density, cy - hr + 6f * density)
headingPath.close()
canvas.save()
canvas.rotate(heading, cx, cy)
canvas.drawPath(headingPath, headingPaint)
canvas.restore()
// needle pointing to target bearing (absolute, north at top = 0)
if (hasFix) {
needlePath.reset()
shadowPath.reset()
val needleLen = radius - 74f * density
val needleBase = radius - 95f * density
canvas.save()
canvas.rotate(bearing, cx, cy)
needlePath.moveTo(cx, cy - needleLen)
needlePath.lineTo(cx + 9f * density, cy - needleBase)
needlePath.lineTo(cx - 9f * density, cy - needleBase)
needlePath.close()
shadowPath.moveTo(cx, cy - needleLen + 4f * density)
shadowPath.lineTo(cx + 9f * density, cy - needleBase + 4f * density)
shadowPath.lineTo(cx - 9f * density, cy - needleBase + 4f * density)
shadowPath.close()
needleShadowPaint.alpha = 100
canvas.drawPath(shadowPath, needleShadowPaint)
needleShadowPaint.alpha = 255
canvas.drawPath(needlePath, needlePaint)
canvas.drawCircle(cx, cy, 10f * density, centerPaint)
canvas.restore()
}
}
fun setData(heading: Float, bearing: Float, hasFix: Boolean) {
this.heading = heading
this.bearing = bearing
this.hasFix = hasFix
invalidate()
}
}

View File

@@ -0,0 +1,394 @@
package com.ua1zbe.compass
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.GnssStatus
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import kotlin.math.roundToInt
class MainActivity : Activity(), SensorEventListener, LocationListener {
private lateinit var sensorManager: SensorManager
private lateinit var locationManager: LocationManager
private lateinit var compass: CompassView
private lateinit var tvTitle: TextView
private lateinit var tvDistance: TextView
private lateinit var tvUnit: TextView
private lateinit var tvBearing: TextView
private lateinit var tvAltitude: TextView
private lateinit var tvAccuracy: TextView
private lateinit var tvSatellites: TextView
private lateinit var tvStatus: TextView
private var lastGravity = FloatArray(3)
private var lastGeo = FloatArray(3)
private var heading = 0f
private var bearing = 0f
private var hasFix = false
private var lastLocation: Location? = null
private var targets: MutableList<Target> = mutableListOf()
private var activeTarget: Target = TargetsStore.defaultTargets[0]
private var satellitesUsed = 0
private var satellitesGps = 0
private var satellitesGlonass = 0
private var satellitesGalileo = 0
private var satellitesBeidou = 0
private var satellitesOther = 0
private val gnssCallback = object : GnssStatus.Callback() {
override fun onSatelliteStatusChanged(status: GnssStatus) {
var used = 0
var gps = 0
var glonass = 0
var galileo = 0
var beidou = 0
var other = 0
for (i in 0 until status.satelliteCount) {
when (status.getConstellationType(i)) {
GnssStatus.CONSTELLATION_GPS -> gps++
GnssStatus.CONSTELLATION_GLONASS -> glonass++
GnssStatus.CONSTELLATION_GALILEO -> galileo++
GnssStatus.CONSTELLATION_BEIDOU -> beidou++
else -> other++
}
if (status.usedInFix(i)) used++
}
satellitesUsed = used
satellitesGps = gps
satellitesGlonass = glonass
satellitesGalileo = galileo
satellitesBeidou = beidou
satellitesOther = other
runOnUiThread { updateSatellites() }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContentView(R.layout.activity_main)
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
tvTitle = findViewById(R.id.tvTitle)
tvDistance = findViewById(R.id.tvDistance)
tvUnit = findViewById(R.id.tvUnit)
tvBearing = findViewById(R.id.tvBearing)
tvAltitude = findViewById(R.id.tvAltitude)
tvAccuracy = findViewById(R.id.tvAccuracy)
tvSatellites = findViewById(R.id.tvSatellites)
tvStatus = findViewById(R.id.tvStatus)
compass = findViewById(R.id.compass)
targets = TargetsStore.load(this)
val activeIndex = TargetsStore.getActiveIndex(this)
activeTarget = targets[activeIndex]
updateTitle()
tvTitle.setOnClickListener { showTargetsMenu() }
checkPermissions()
}
private fun hasPermission(): Boolean {
return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
private fun checkPermissions() {
if (hasPermission()) {
startSensors()
} else if (Build.VERSION.SDK_INT >= 23) {
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startSensors()
} else {
tvStatus.text = "Нет разрешения на доступ к GPS"
}
}
}
private fun startSensors() {
try {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000L, 1f, this
)
} catch (e: Exception) {
tvStatus.text = "GPS недоступен: ${e.message}"
}
try {
if (Build.VERSION.SDK_INT >= 24) {
locationManager.registerGnssStatusCallback(gnssCallback)
}
} catch (e: Exception) {
tvStatus.text = "Спутники недоступны: ${e.message}"
}
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_UI
)
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_UI
)
}
// ---------- Цели ----------
private fun updateTitle() {
tvTitle.text = "ЦЕЛЬ: ${activeTarget.name}"
}
private fun refreshTargetData() {
val loc = lastLocation ?: return
val target = Location("target").apply {
latitude = activeTarget.latitude
longitude = activeTarget.longitude
}
val dist = loc.distanceTo(target)
bearing = (loc.bearingTo(target) + 360f) % 360f
compass.setData(heading, bearing, hasFix)
tvDistance.text = formatDistance(dist)
tvUnit.text = if (dist >= 1000f) "км до цели" else "метров до цели"
tvBearing.text = "${bearing.roundToInt()}° ${cardinal(bearing)}"
tvStatus.text = "GPS: ${loc.latitude.toString().take(8)}, ${loc.longitude.toString().take(8)} · Цель: ${activeTarget.latitude.toString().take(8)}, ${activeTarget.longitude.toString().take(8)}"
}
private fun showTargetsMenu() {
val names = targets.mapIndexed { i, t ->
"${i + 1}. ${t.name}${t.latitude.toString().take(9)}, ${t.longitude.toString().take(9)}"
}.toTypedArray()
val active = targets.indexOf(activeTarget)
AlertDialog.Builder(this)
.setTitle("Выбор цели")
.setSingleChoiceItems(names, active) { dialog, which ->
activeTarget = targets[which]
TargetsStore.setActiveIndex(this, which)
updateTitle()
refreshTargetData()
dialog.dismiss()
}
.setNegativeButton("Отмена", null)
.setNeutralButton("Редактировать…") { _, _ ->
showEditDialog(active)
}
.show()
}
private fun showEditDialog(index: Int) {
val target = targets[index]
val density = resources.displayMetrics.density
val pad = (16 * density).toInt()
val container = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(pad, pad, pad, pad)
}
fun makeLabel(text: String) = TextView(this).apply {
this.text = text
textSize = 13f
setTextColor(0xFF8A97A8.toInt())
setPadding(0, (8 * density).toInt(), 0, (4 * density).toInt())
}
val etName = EditText(this).apply { setText(target.name) }
val etLat = EditText(this).apply {
inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL or android.text.InputType.TYPE_NUMBER_FLAG_SIGNED
setText(target.latitude.toString())
}
val etLon = EditText(this).apply {
inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL or android.text.InputType.TYPE_NUMBER_FLAG_SIGNED
setText(target.longitude.toString())
}
container.addView(makeLabel("Название"))
container.addView(etName)
container.addView(makeLabel("Широта"))
container.addView(etLat)
container.addView(makeLabel("Долгота"))
container.addView(etLon)
val btnInsert = Button(this).apply {
text = "Вставить текущие координаты"
isAllCaps = false
setOnClickListener {
val loc = lastLocation ?: run {
tvStatus.text = "Нет GPS-фикса для вставки координат"
return@setOnClickListener
}
etLat.setText(loc.latitude.toString())
etLon.setText(loc.longitude.toString())
}
}
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = (12 * density).toInt() }
container.addView(btnInsert, lp)
val builder = AlertDialog.Builder(this)
.setTitle("Редактирование цели ${index + 1}")
.setView(container)
.setNegativeButton("Отмена", null)
.setPositiveButton("Сохранить") { _, _ ->
val lat = etLat.text.toString().toDoubleOrNull()
val lon = etLon.text.toString().toDoubleOrNull()
if (lat == null || lon == null || lat !in -90.0..90.0 || lon !in -180.0..180.0) {
tvStatus.text = "Некорректные координаты"
return@setPositiveButton
}
targets[index] = Target(etName.text.toString().ifBlank { "Цель ${index + 1}" }, lat, lon)
TargetsStore.save(this, targets)
if (targets[index] === activeTarget) {
activeTarget = targets[index]
updateTitle()
refreshTargetData()
}
}
builder.show()
}
// ---------- Сенсоры / GPS ----------
private fun updateSatellites() {
if (satellitesUsed == 0) {
tvSatellites.text = ""
return
}
val parts = mutableListOf("GPS $satellitesGps")
if (satellitesGlonass > 0) parts.add("ГЛОНАСС $satellitesGlonass")
if (satellitesGalileo > 0) parts.add("Galileo $satellitesGalileo")
if (satellitesBeidou > 0) parts.add("BeiDou $satellitesBeidou")
if (satellitesOther > 0) parts.add("прочие $satellitesOther")
tvSatellites.text = "исп. $satellitesUsed · ${parts.joinToString(" · ")}"
}
override fun onSensorChanged(event: SensorEvent) {
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> lowPass(event.values, lastGravity, 0.1f)
Sensor.TYPE_MAGNETIC_FIELD -> lowPass(event.values, lastGeo, 0.1f)
}
val R = FloatArray(9)
val I = FloatArray(9)
if (SensorManager.getRotationMatrix(R, I, lastGravity, lastGeo)) {
val orientation = FloatArray(3)
SensorManager.getOrientation(R, orientation)
var deg = Math.toDegrees(orientation[0].toDouble()).toFloat()
heading = (deg + 360f) % 360f
compass.setData(heading, bearing, hasFix)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
override fun onLocationChanged(location: Location) {
lastLocation = location
val target = Location("target").apply {
latitude = activeTarget.latitude
longitude = activeTarget.longitude
}
val dist = location.distanceTo(target)
bearing = (location.bearingTo(target) + 360f) % 360f
hasFix = true
compass.setData(heading, bearing, hasFix)
tvDistance.text = formatDistance(dist)
tvUnit.text = if (dist >= 1000f) "км до цели" else "метров до цели"
tvBearing.text = "${bearing.roundToInt()}° ${cardinal(bearing)}"
tvAltitude.text = "${location.altitude.roundToInt()} м"
tvAccuracy.text = if (location.accuracy > 0) "±${location.accuracy.roundToInt()} м" else ""
tvStatus.text = "GPS: ${location.latitude.toString().take(8)}, ${location.longitude.toString().take(8)} · Цель: ${activeTarget.latitude.toString().take(8)}, ${activeTarget.longitude.toString().take(8)}"
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
override fun onResume() {
super.onResume()
if (hasPermission()) {
try {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 1000L, 1f, this
)
} catch (e: Exception) {}
try {
if (Build.VERSION.SDK_INT >= 24) {
locationManager.registerGnssStatusCallback(gnssCallback)
}
} catch (e: Exception) {}
}
}
override fun onPause() {
super.onPause()
locationManager.removeUpdates(this)
if (Build.VERSION.SDK_INT >= 24) {
locationManager.unregisterGnssStatusCallback(gnssCallback)
}
sensorManager.unregisterListener(this)
}
override fun onDestroy() {
super.onDestroy()
locationManager.removeUpdates(this)
if (Build.VERSION.SDK_INT >= 24) {
locationManager.unregisterGnssStatusCallback(gnssCallback)
}
sensorManager.unregisterListener(this)
}
private fun lowPass(input: FloatArray, output: FloatArray, alpha: Float) {
for (i in input.indices) {
if (output[i] == 0f) output[i] = input[i]
else output[i] = alpha * input[i] + (1f - alpha) * output[i]
}
}
private fun formatDistance(meters: Float): String {
return if (meters >= 10000f) {
"%.1f".format(meters / 1000f)
} else if (meters >= 1000f) {
"%.2f".format(meters / 1000f)
} else {
meters.roundToInt().toString()
}
}
private fun cardinal(deg: Float): String {
val dirs = arrayOf("С", "СВ", "В", "ЮВ", "Ю", "ЮЗ", "З", "СЗ")
return dirs[((deg + 22.5f) / 45f).toInt() % 8]
}
}

View File

@@ -0,0 +1,85 @@
package com.ua1zbe.compass
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
data class Target(val name: String, val latitude: Double, val longitude: Double)
object TargetsStore {
private const val PREFS = "targets"
private const val KEY = "list"
private const val KEY_ACTIVE = "active"
private const val KEY_VERSION = "version"
private const val VERSION = 2
val defaultTargets: List<Target> = listOf(
Target("П-Зори DMR", 67.371, 32.48967),
Target("R1ZBF-SVX", 67.3685, 32.49),
Target("Лысая Гора", 67.43083, 32.4535),
Target("Апатиты DMR", 67.55917, 33.40967),
Target("Кировск DMR", 67.602, 33.72767),
Target("Мончегорск DMR", 67.93883, 32.9375),
Target("R1ZCU-SVX", 69.00283, 33.102),
Target("R1ZAAG-HE", 69.0735, 33.43167),
Target("Цель 9", 67.371, 32.48967),
Target("Цель 10", 67.371, 32.48967),
)
fun load(context: Context): MutableList<Target> {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val version = prefs.getInt(KEY_VERSION, 0)
if (version != VERSION) {
prefs.edit()
.putInt(KEY_VERSION, VERSION)
.putInt(KEY_ACTIVE, 0)
.remove(KEY)
.apply()
return defaultTargets.toMutableList()
}
val json = prefs.getString(KEY, null)
val list = defaultTargets.toMutableList()
if (!json.isNullOrEmpty()) {
try {
val arr = JSONArray(json)
for (i in 0 until arr.length()) {
val o = arr.getJSONObject(i)
if (i < 10) {
list[i] = Target(
o.optString("name", list[i].name),
o.optDouble("lat", list[i].latitude),
o.optDouble("lon", list[i].longitude)
)
}
}
} catch (e: Exception) {}
}
return list
}
fun save(context: Context, list: List<Target>) {
val arr = JSONArray()
for (i in list.indices) {
val o = JSONObject()
o.put("name", list[i].name)
o.put("lat", list[i].latitude)
o.put("lon", list[i].longitude)
arr.put(o)
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString(KEY, arr.toString()).apply()
}
fun getActiveIndex(context: Context): Int {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
return prefs.getInt(KEY_ACTIVE, 0).coerceIn(0, 9)
}
fun setActiveIndex(context: Context, index: Int) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putInt(KEY_ACTIVE, index.coerceIn(0, 9)).apply()
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="@color/bg_bottom"
android:startColor="@color/bg_top" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/card_bg" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="@color/card_stroke" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="#16233B" />
</vector>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="#16233B" />
<path
android:pathData="M54,24a30,30 0 1,0 0,60a30,30 0 1,0 0,-60z"
android:fillColor="#0B1020" />
<path
android:pathData="M54,34a20,20 0 1,0 0,40a20,20 0 1,0 0,-40z"
android:fillColor="#1B2A45" />
<path
android:pathData="M54,38 L61,54 L54,70 L47,54 Z"
android:fillColor="#29E0FF" />
</vector>

View File

@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_gradient"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:letterSpacing="0.15"
android:text="ЦЕЛЬ: 67.370898, 32.489668"
android:textAllCaps="true"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:minHeight="300dp">
<com.ua1zbe.compass.CompassView
android:id="@+id/compass"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tvDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="— — —"
android:textColor="@color/text_primary"
android:textSize="34sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvUnit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="км до цели"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/card_bg"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Азимут на цель"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvBearing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="—"
android:textColor="@color/accent"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/card_stroke" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Высота над уровнем моря"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAltitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="—"
android:textColor="@color/accent_green"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/card_stroke" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Спутники"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvSatellites"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="—"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@color/card_stroke" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Точность GPS"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAccuracy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="—"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="Поиск спутников GPS…"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg" />
<foreground android:drawable="@drawable/ic_launcher_fg" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="bg_top">#0B1020</color>
<color name="bg_bottom">#16233B</color>
<color name="card_bg">#1B2A45</color>
<color name="card_stroke">#2C3F63</color>
<color name="accent">#29E0FF</color>
<color name="accent_green">#3DDC97</color>
<color name="danger">#FF5A5F</color>
<color name="text_primary">#E8EEF7</color>
<color name="text_secondary">#8A97A8</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Компас Цель</string>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Compass" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/bg_top</item>
<item name="android:statusBarColor">@color/bg_top</item>
<item name="android:navigationBarColor">@color/bg_top</item>
<item name="android:colorAccent">@color/accent</item>
</style>
</resources>