step 11: fix edge-to-edge padding, map clipping, OSM attribution

AuthorKonata <konata@posteo.jp>
Date
Commitddd479a00c8e0275c3dbf0d1eae0ab8514d38949
Parent36d3e57
2 files changed, 42 insertions(+), 24 deletions(-)
▾Mandroid/app/src/main/java/net/lexcom/opentracker/MainActivity.kt
@@ -5,6 +5,9 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
@@ -12,6 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import net.lexcom.opentracker.store.Prefs
import net.lexcom.opentracker.ui.HomeScreen
import net.lexcom.opentracker.ui.LoginScreen
@@ -46,35 +50,46 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
missing = PermissionGate.missing(this)
// Captured once. Inside setContent every composable lambda brings its
// own receiver, so a bare `this` there means BoxScope, not the Activity.
val activity = this
setContent {
// One read per sign-in state change, not one per recomposition.
// Holding the Credentials rather than a boolean also gives
// HomeScreen its server address without a second load().
var credentials by remember { mutableStateOf(Prefs(this).load()) }
var credentials by remember { mutableStateOf(Prefs(activity).load()) }
// Local copy so the null check smart-casts; a delegated var cannot.
val current = credentials
MaterialTheme {
Surface {
if (current == null) {
LoginScreen(onLoggedIn = { credentials = Prefs(this).load() })
} else {
val state by TrackerState.state.collectAsState()
HomeScreen(
state = state,
missingPermissions = missing,
serverHost = "${current.udpHost}:${current.udpPort}",
onGrantPermissions = ::requestNextPermissions,
onStart = { TrackerService.start(this) },
onStop = { TrackerService.stop(this) },
onSignOut = {
// Stop first. Clearing the credentials under a
// running service leaves it sending with a token
// the user just gave up.
TrackerService.stop(this)
Prefs(this).clear()
credentials = null
},
)
// fillMaxSize so the theme's background covers the whole window
// and not just the height of the content. safeDrawingPadding on
// the inner Box rather than on the Surface: from targetSdk 35 the
// window is edge to edge by default, so without it the title sits
// under the status bar clock, and padding the Surface instead
// would leave the bars unpainted.
Surface(Modifier.fillMaxSize()) {
Box(Modifier.safeDrawingPadding()) {
if (current == null) {
LoginScreen(onLoggedIn = { credentials = Prefs(activity).load() })
} else {
val state by TrackerState.state.collectAsState()
HomeScreen(
state = state,
missingPermissions = missing,
serverHost = "${current.udpHost}:${current.udpPort}",
onGrantPermissions = ::requestNextPermissions,
onStart = { TrackerService.start(activity) },
onStop = { TrackerService.stop(activity) },
onSignOut = {
// Stop first. Clearing the credentials under a
// running service leaves it sending with a token
// the user just gave up.
TrackerService.stop(activity)
Prefs(activity).clear()
credentials = null
},
)
}
}
}
}
▾Mandroid/app/src/main/java/net/lexcom/opentracker/ui/HomeScreen.kt
@@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import net.lexcom.opentracker.TrackerState
@@ -58,8 +59,10 @@ fun HomeScreen(
if (missingPermissions.isNotEmpty()) {
PermissionBanner(missingPermissions, onGrantPermissions)
}
// The map takes whatever height the rest leaves.
MapView(state.last, Modifier.fillMaxWidth().weight(1f))
// The map takes whatever height the rest leaves. clipToBounds because
// osmdroid draws its tiles past its own bounds, over the banner above
// and the status card below.
MapView(state.last, Modifier.fillMaxWidth().weight(1f).clipToBounds())
StatusCard(state, serverHost)
Button(
onClick = if (state.running) onStop else onStart,