feat: location button, and abstracting extInsets to common code

This commit is contained in:
LavaDesu 2025-04-14 23:40:54 +10:00
parent cdbf1970ec
commit e428883d01
Signed by: cilly
GPG key ID: 6500251E087653C9
9 changed files with 178 additions and 38 deletions

View file

@ -35,6 +35,7 @@ kotlin {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.play.services.location)
implementation(libs.play.services.maps)
implementation(libs.maps.compose)
}
@ -49,6 +50,8 @@ kotlin {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.moko.geo)
implementation(libs.moko.geo.compose)
implementation(projects.shared)
}
@ -89,3 +92,8 @@ dependencies {
secrets {
propertiesFileName = "secrets.properties"
}
compose.resources {
publicResClass = true
packageOfResClass = "moe.lava.banksia.resources"
}

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"

View file

@ -1,68 +1,73 @@
package moe.lava.banksia.native.maps
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
import android.view.WindowManager
import android.Manifest
import android.content.pm.PackageManager
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MapStyleOptions
import com.google.maps.android.compose.ComposeMapColorScheme
import com.google.maps.android.compose.DefaultMapProperties
import com.google.maps.android.compose.DefaultMapUiSettings
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.Polyline
import com.google.maps.android.compose.rememberCameraPositionState
import kotlin.math.roundToInt
import moe.lava.banksia.R
fun Point.toLatLng(): LatLng = LatLng(this.lat, this.lng)
@Composable
private fun checkLocationPermission() =
ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
@Composable
actual fun getScreenHeight(): Int {
val dp = LocalConfiguration.current.screenHeightDp.dp
return with(LocalDensity.current) {
dp.roundToPx()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
actual fun Maps(
modifier: Modifier,
markers: List<Marker>,
polylines: List<Polyline>,
cameraPosition: Point,
sheetState: SheetState,
newCameraPosition: Point?,
cameraPositionUpdated: () -> Unit,
extInsets: Int,
) {
val extInsets = if (
sheetState.currentValue != SheetValue.Hidden ||
sheetState.targetValue != SheetValue.Hidden
) {
val context = LocalContext.current
val windowManager = remember { context.getSystemService(Context.WINDOW_SERVICE) as WindowManager }
val screenHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
windowManager.currentWindowMetrics.bounds.height()
else {
var outMetrics = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(outMetrics)
outMetrics.heightPixels
}
val scaffoldOffset = sheetState.requireOffset().roundToInt()
(screenHeight - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
} else 0
var camPos = rememberCameraPositionState()
LaunchedEffect(cameraPosition) {
camPos.position = CameraPosition(cameraPosition.toLatLng(), 16.0f, 0.0f, 0.0f)
val ctx = LocalContext.current
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
LaunchedEffect(Unit) {
fusedLocation.lastLocation.addOnSuccessListener {
if (it != null)
camPos.position = CameraPosition(LatLng(it.latitude, it.longitude), 16.0f, 0.0f, 0.0f)
}
}
LaunchedEffect(newCameraPosition) {
if (newCameraPosition != null) {
camPos.position = CameraPosition(newCameraPosition.toLatLng(), 16.0f, 0.0f, 0.0f)
cameraPositionUpdated()
}
}
GoogleMap(
@ -70,8 +75,8 @@ actual fun Maps(
cameraPositionState = camPos,
mapColorScheme = ComposeMapColorScheme.FOLLOW_SYSTEM,
properties = DefaultMapProperties.copy(
//mapStyleOptions = MapStyleOptions.loadRawResourceStyle(LocalContext.current, R.raw.def_mapstyle),
//isMyLocationEnabled = checkLocationPermission(),
mapStyleOptions = MapStyleOptions.loadRawResourceStyle(LocalContext.current, R.raw.def_mapstyle),
isMyLocationEnabled = checkLocationPermission(),
),
uiSettings = DefaultMapUiSettings.copy(
zoomControlsEnabled = false,

View file

@ -0,0 +1,37 @@
[
{
"featureType": "poi.business",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "transit",
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
}
]

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View file

@ -1,9 +1,18 @@
package moe.lava.banksia
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetScaffoldState
@ -12,12 +21,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import moe.lava.banksia.api.ptv.PtvService
import moe.lava.banksia.native.maps.Maps
import moe.lava.banksia.native.maps.Point
import moe.lava.banksia.native.maps.getScreenHeight
import moe.lava.banksia.resources.Res
import moe.lava.banksia.resources.my_location_24
import moe.lava.banksia.ui.Searcher
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -30,17 +53,43 @@ fun App() {
)
)
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
val locationTracker = remember { locationFactory.createLocationTracker() }
BindLocationTrackerEffect(locationTracker)
var lastLocation by remember { mutableStateOf(Point(-37.8136, 144.9631)) }
var newCameraPosition by remember { mutableStateOf<Point?>(Point(-37.8136, 144.9631)) }
var searchTextState by remember { mutableStateOf("") }
var searchExpandedState by remember { mutableStateOf(false) }
val sheetState = scaffoldState.bottomSheetState
val extInsets = if (
sheetState.currentValue != SheetValue.Hidden ||
sheetState.targetValue != SheetValue.Hidden
) {
val scaffoldOffset = sheetState.requireOffset().roundToInt()
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
} else 0
var scope = rememberCoroutineScope()
scope.launch {
val flow = locationTracker.getLocationsFlow()
locationTracker.startTracking()
flow.distinctUntilChanged().collect {
lastLocation = Point(it.latitude, it.longitude)
}
}
MaterialTheme {
BottomSheetScaffold(
scaffoldState = scaffoldState,
modifier = Modifier.fillMaxSize(),
sheetContent = { Box(modifier = Modifier) },
) {
Maps(
modifier = Modifier.fillMaxSize(),
sheetState = scaffoldState.bottomSheetState,
newCameraPosition = newCameraPosition,
cameraPositionUpdated = { newCameraPosition = null },
extInsets = extInsets,
)
Searcher(
ptvService = PtvService(),
@ -50,6 +99,20 @@ fun App() {
onTextChange = { searchTextState = it },
onRouteChange = {}
)
Box(
Modifier.windowInsetsPadding(WindowInsets.safeContent.add(WindowInsets(bottom = extInsets))),
contentAlignment = Alignment.BottomEnd
) {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
onClick = {
newCameraPosition = lastLocation
},
) {
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
}
}
}
}
}

View file

@ -5,17 +5,22 @@ import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
data class Marker(val name: String, val onClick: () -> Boolean)
data class Point(val lat: Double, val lng: Double)
data class Polyline(val points: List<Point>, val colour: Color)
@Composable
expect fun getScreenHeight(): Int
@OptIn(ExperimentalMaterial3Api::class)
@Composable
expect fun Maps(
modifier: Modifier = Modifier,
markers: List<Marker> = listOf(),
polylines: List<Polyline> = listOf(),
cameraPosition: Point = Point(-37.8136, 144.9631),
sheetState: SheetState,
newCameraPosition: Point? = Point(-37.8136, 144.9631),
cameraPositionUpdated: () -> Unit,
extInsets: Int,
)

View file

@ -3,7 +3,16 @@ package moe.lava.banksia.native.maps
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.Dp
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun getScreenHeight(): Int {
return LocalWindowInfo.current.containerSize.height
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -11,8 +20,9 @@ actual fun Maps(
modifier: Modifier,
markers: List<Marker>,
polylines: List<Polyline>,
cameraPosition: Point,
sheetState: SheetState,
newCameraPosition: Point?,
cameraPositionUpdated: () -> Unit,
extInsets: Int,
) {
TODO("Not yet implemented")
}

View file

@ -13,6 +13,7 @@ androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
compose-multiplatform = "1.7.3"
coroutines = "1.9.0"
geo = "0.8.0"
junit = "4.13.2"
kotlin = "2.1.10"
kotlinxSerializationJson = "1.8.1"
@ -20,10 +21,13 @@ ktor = "3.1.1"
logback = "1.5.17"
mapsCompose = "6.4.1"
okio = "3.11.0"
playServicesLocation = "21.3.0"
playServicesMaps = "19.1.0"
secretsGradlePlugin = "2.0.1"
[libraries]
moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -50,6 +54,7 @@ ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "k
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" }
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }