Initial commit: APRS map for aprs.radiorubka.org

This commit is contained in:
ua1zbe
2026-09-23 08:42:20 +03:00
commit 306a867852
19 changed files with 808 additions and 0 deletions

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

@@ -0,0 +1,39 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "ru.ua1zbe.aprsmap"
compileSdk = 34
defaultConfig {
applicationId = "ru.ua1zbe.aprsmap"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
lint {
checkReleaseBuilds = false
abortOnError = false
}
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.APRS.Main">
<activity
android:name=".SplashActivity"
android:exported="true"
android:screenOrientation="unspecified"
android:theme="@style/Theme.APRS.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|uiMode"
android:theme="@style/Theme.APRS.Main" />
</application>
</manifest>

View File

@@ -0,0 +1,101 @@
package ru.ua1zbe.aprsmap
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ProgressBar
class MainActivity : Activity() {
private lateinit var webView: WebView
private lateinit var progress: ProgressBar
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = FrameLayout(this).apply {
setBackgroundColor(Color.rgb(6, 11, 20))
}
webView = WebView(this).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.rgb(6, 11, 20))
setInitialScale(0)
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.setSupportZoom(true)
settings.builtInZoomControls = true
settings.displayZoomControls = false
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
settings.mediaPlaybackRequiresUserGesture = false
settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith("http://") || url.startsWith("https://")) {
view.loadUrl(url)
}
return true
}
}
webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
val bar = this@MainActivity.progress
if (newProgress < 100) {
bar.visibility = ProgressBar.VISIBLE
bar.progress = newProgress
} else {
bar.visibility = ProgressBar.GONE
}
}
}
}
progress = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
max = 100
progress = 0
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
(2 * resources.displayMetrics.density).toInt()
).apply { gravity = Gravity.TOP }
val d = progressDrawable?.mutate()
d?.colorFilter = PorterDuffColorFilter(Color.rgb(0, 229, 255), PorterDuff.Mode.SRC_IN)
progressDrawable = d
visibility = ProgressBar.GONE
}
root.addView(webView)
root.addView(progress)
setContentView(root)
webView.loadUrl(getString(R.string.map_url))
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
finish()
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
}

View File

@@ -0,0 +1,31 @@
package ru.ua1zbe.aprsmap
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(SplashView(this))
if (savedInstanceState == null) {
Handler(Looper.getMainLooper()).postDelayed({
if (!isFinishing && !isDestroyed) {
startActivity(Intent(this, MainActivity::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
finish()
}
}, SPLASH_DURATION_MS)
}
}
companion object {
private const val SPLASH_DURATION_MS = 2600L
}
}

View File

@@ -0,0 +1,141 @@
package ru.ua1zbe.aprsmap
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import android.view.View
import android.view.animation.DecelerateInterpolator
import kotlin.math.sin
class SplashView(context: Context) : View(context) {
private val bgPaint = Paint()
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER }
private val subPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textAlign = Paint.Align.CENTER
color = Color.rgb(140, 180, 205)
}
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var progress = 0f
private val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 2400L
interpolator = DecelerateInterpolator()
addUpdateListener {
progress = it.animatedValue as Float
postInvalidateOnAnimation()
}
}
init {
glowPaint.maskFilter = android.graphics.BlurMaskFilter(
dp(28).toFloat(),
android.graphics.BlurMaskFilter.Blur.NORMAL
)
background = null
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
animator.start()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
animator.cancel()
}
override fun onDraw(canvas: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
val t = progress
if (t <= 0f) return
bgPaint.shader = LinearGradient(
0f, 0f, w, h,
intArrayOf(
Color.HSVToColor(floatArrayOf(206f + 18f * t, 0.45f, 0.05f)),
Color.HSVToColor(floatArrayOf(196f + 24f * t, 0.55f, 0.09f)),
Color.rgb(6, 11, 20)
),
floatArrayOf(0f, 0.55f, 1f),
Shader.TileMode.CLAMP
)
canvas.drawRect(0f, 0f, w, h, bgPaint)
val ease = 1f - (1f - t) * (1f - t)
val textDp = dp(64).toFloat()
textPaint.textSize = textDp * ease
val text = TEXT
val fullWidth = textPaint.measureText(text)
val spacing = fullWidth / text.length
var x = w / 2f - fullWidth / 2f + spacing / 2f
val baseY = h / 2f + textPaint.textSize * 0.36f - dp(8).toFloat()
var i = 0
glowPaint.color = Color.rgb(0, 229, 255)
val glowRadius = (dp(34).toFloat()) * (0.7f + 0.3f * sin(t * Math.PI.toFloat() * 2f))
canvas.drawCircle(w / 2f, h / 2f - dp(6).toFloat(), glowRadius, glowPaint)
for (ch in text) {
val wave = sin((i * 0.9f) + t * Math.PI.toFloat() * 2f) * dp(10).toFloat() * (1f - t * 0.7f)
val y = baseY - wave
textPaint.shader = LinearGradient(
0f, y - textPaint.textSize, 0f, y,
intArrayOf(Color.rgb(0, 229, 255), Color.rgb(0, 230, 118)),
null, Shader.TileMode.CLAMP
)
textPaint.setShadowLayer(dp(6).toFloat(), 0f, 0f, Color.rgb(0, 229, 255))
val cx = x
canvas.save()
canvas.translate(cx, y)
canvas.drawText(ch.toString(), 0f, 0f, textPaint)
canvas.restore()
textPaint.clearShadowLayer()
x += spacing
i++
}
subPaint.textSize = dp(14).toFloat()
subPaint.alpha = (255 * ease).toInt()
canvas.drawText(SUBTITLE, w / 2f, baseY + dp(46).toFloat(), subPaint)
val lineY = h - dp(10).toFloat()
val lineLen = w * 0.55f
progressPaint.color = Color.rgb(30, 60, 90)
canvas.drawRoundRect(
w / 2f - lineLen / 2f, lineY - dp(2).toFloat(),
w / 2f + lineLen / 2f, lineY + dp(2).toFloat(),
dp(2).toFloat(), dp(2).toFloat(), progressPaint
)
progressPaint.shader = LinearGradient(
w / 2f - lineLen / 2f, 0f, w / 2f + lineLen / 2f, 0f,
Color.rgb(0, 229, 255), Color.rgb(0, 230, 118), Shader.TileMode.CLAMP
)
canvas.drawRoundRect(
w / 2f - lineLen / 2f, lineY - dp(2).toFloat(),
w / 2f - lineLen / 2f + lineLen * t, lineY + dp(2).toFloat(),
dp(2).toFloat(), dp(2).toFloat(), progressPaint
)
}
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
companion object {
private const val TEXT = "APRS"
private const val SUBTITLE = "aprs.radiorubka.org"
}
}

View File

@@ -0,0 +1,36 @@
<?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">
<group
android:scaleX="0.72"
android:scaleY="0.72"
android:translateX="16"
android:translateY="14">
<path
android:fillColor="#00E676"
android:pathData="M54,22 C40.745,22 30,32.745 30,46 C30,64 54,86 54,86 C54,86 78,64 78,46 C78,32.745 67.255,22 54,22 Z" />
<path
android:fillColor="#0A1626"
android:pathData="M54,36 C46.268,36 40,42.268 40,50 C40,57.732 46.268,64 54,64 C61.732,64 68,57.732 68,50 C68,42.268 61.732,36 54,36 Z" />
<path
android:fillColor="#00E5FF"
android:pathData="M54,43 C50.134,43 47,46.134 47,50 C47,53.866 50.134,57 54,57 C57.866,57 61,53.866 61,50 C61,46.134 57.866,43 54,43 Z" />
</group>
<path
android:fillColor="#00000000"
android:pathData="M86,32 A30,30 0 0 1 92,52"
android:strokeColor="#00E5FF"
android:strokeLineCap="round"
android:strokeWidth="4" />
<path
android:fillColor="#00000000"
android:pathData="M82,26 A38,38 0 0 1 98,56"
android:strokeColor="#00E676"
android:strokeLineCap="round"
android:strokeWidth="4" />
</vector>

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="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

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="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0A1626</color>
<color name="splash_bg">#060B14</color>
<color name="aprs_line">#060B14</color>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APRS RadioRubka</string>
<string name="map_url">https://aprs.radiorubka.org/?center=67.5096,32.8271&amp;zoom=8</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.APRS.Splash" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/splash_bg</item>
<item name="android:windowFullscreen">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/splash_bg</item>
</style>
<style name="Theme.APRS.Main" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/splash_bg</item>
<item name="android:statusBarColor">@color/splash_bg</item>
<item name="android:navigationBarColor">@color/splash_bg</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>