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.
This commit is contained in:
Redsandy
2026-03-15 01:49:37 +03:00
parent 02171118ec
commit e2c825ed6f
2 changed files with 107 additions and 8 deletions

View File

@@ -1,9 +1,17 @@
package com.geozoner.app.ui.map 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -13,21 +21,32 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.geozoner.app.data.remote.dto.ZoneBriefResponse 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.expressions.dsl.const
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.rememberGeoJsonSource 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" private const val OPENFREEMAP_STYLE = "https://tiles.openfreemap.org/styles/liberty"
@@ -36,6 +55,31 @@ fun MapScreen(
viewModel: MapViewModel = hiltViewModel(), viewModel: MapViewModel = hiltViewModel(),
) { ) {
val state by viewModel.uiState.collectAsState() 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 // Build GeoJSON strings for own and friend zones
val myZoneGeoJson = remember(state.myZones) { val myZoneGeoJson = remember(state.myZones) {
@@ -50,6 +94,7 @@ fun MapScreen(
MaplibreMap( MaplibreMap(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE), baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
cameraState = cameraState,
) { ) {
// Friend zones layer (rendered below own zones) // Friend zones layer (rendered below own zones)
if (friendZoneGeoJson.isNotEmpty()) { if (friendZoneGeoJson.isNotEmpty()) {
@@ -115,22 +160,76 @@ fun MapScreen(
) )
} }
// Refresh button // FABs column (top-end)
SmallFloatingActionButton( Column(
onClick = { viewModel.loadZones() },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(top = 16.dp, end = 16.dp), .padding(top = 16.dp, end = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
// 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, containerColor = MaterialTheme.colorScheme.surface,
) { ) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh zones") 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. * Builds a GeoJSON FeatureCollection string from zone responses.
* Each zone becomes a Feature with a Polygon geometry.
*/ */
private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String { private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String {
if (zones.isEmpty()) return "" if (zones.isEmpty()) return ""