From e2c825ed6f6d87cc8c240dc8dde08395129a7157 Mon Sep 17 00:00:00 2001 From: Redsandy <34872843+Redsandyg@users.noreply.github.com> Date: Sun, 15 Mar 2026 01:49:37 +0300 Subject: [PATCH] Enhance MapScreen with location permission handling and camera positioning - Added functionality to request location permissions and navigate to the user's current location on the map. - Integrated camera state management to animate the map view to the user's location. - Updated UI to include a floating action button for accessing the user's location and refreshing zone data. --- ...kotlin-compiler-2129342199983897734.salive | 0 .../java/com/geozoner/app/ui/map/MapScreen.kt | 115 ++++++++++++++++-- 2 files changed, 107 insertions(+), 8 deletions(-) delete mode 100644 .kotlin/sessions/kotlin-compiler-2129342199983897734.salive diff --git a/.kotlin/sessions/kotlin-compiler-2129342199983897734.salive b/.kotlin/sessions/kotlin-compiler-2129342199983897734.salive deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/geozoner/app/ui/map/MapScreen.kt b/app/src/main/java/com/geozoner/app/ui/map/MapScreen.kt index 7872c40..692cfd2 100644 --- a/app/src/main/java/com/geozoner/app/ui/map/MapScreen.kt +++ b/app/src/main/java/com/geozoner/app/ui/map/MapScreen.kt @@ -1,9 +1,17 @@ package com.geozoner.app.ui.map +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MyLocation import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -13,21 +21,32 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.graphics.Color -import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import com.geozoner.app.data.remote.dto.ZoneBriefResponse -import com.geozoner.app.ui.theme.ZoneColors +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationTokenSource +import kotlinx.coroutines.launch +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.spatialk.geojson.Position +import kotlin.time.Duration.Companion.seconds private const val OPENFREEMAP_STYLE = "https://tiles.openfreemap.org/styles/liberty" @@ -36,6 +55,31 @@ fun MapScreen( viewModel: MapViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsState() + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val cameraState = rememberCameraState( + firstPosition = CameraPosition( + target = Position(latitude = 55.7558, longitude = 37.6173), // Moscow default + zoom = 12.0, + ), + ) + + var hasLocationPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + ) + } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + hasLocationPermission = permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true + if (hasLocationPermission) { + goToMyLocation(context, scope, cameraState) + } + } // Build GeoJSON strings for own and friend zones val myZoneGeoJson = remember(state.myZones) { @@ -50,6 +94,7 @@ fun MapScreen( MaplibreMap( modifier = Modifier.fillMaxSize(), baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE), + cameraState = cameraState, ) { // Friend zones layer (rendered below own zones) if (friendZoneGeoJson.isNotEmpty()) { @@ -115,22 +160,76 @@ fun MapScreen( ) } - // Refresh button - SmallFloatingActionButton( - onClick = { viewModel.loadZones() }, + // FABs column (top-end) + Column( modifier = Modifier .align(Alignment.TopEnd) .padding(top = 16.dp, end = 16.dp), - containerColor = MaterialTheme.colorScheme.surface, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Icon(Icons.Default.Refresh, contentDescription = "Refresh zones") + // My Location button + SmallFloatingActionButton( + onClick = { + if (hasLocationPermission) { + goToMyLocation(context, scope, cameraState) + } else { + permissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + ) + } + }, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Icon(Icons.Default.MyLocation, contentDescription = "My location") + } + + // Refresh zones button + SmallFloatingActionButton( + onClick = { viewModel.loadZones() }, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Icon(Icons.Default.Refresh, contentDescription = "Refresh zones") + } } } } +/** + * Gets the current location and animates the camera to it. + */ +@SuppressLint("MissingPermission") +private fun goToMyLocation( + context: android.content.Context, + scope: kotlinx.coroutines.CoroutineScope, + cameraState: org.maplibre.compose.camera.CameraState, +) { + val fusedClient = LocationServices.getFusedLocationProviderClient(context) + val cancellationToken = CancellationTokenSource() + + fusedClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, cancellationToken.token) + .addOnSuccessListener { location -> + if (location != null) { + scope.launch { + cameraState.animateTo( + finalPosition = CameraPosition( + target = Position( + latitude = location.latitude, + longitude = location.longitude, + ), + zoom = 15.0, + ), + duration = 1.seconds, + ) + } + } + } +} + /** * Builds a GeoJSON FeatureCollection string from zone responses. - * Each zone becomes a Feature with a Polygon geometry. */ private fun buildZoneGeoJson(zones: List): String { if (zones.isEmpty()) return ""