@@ -17,15 +17,16 @@ import androidx.compose.material3.Icon
1717import androidx.compose.material3.SnackbarHostState
1818import androidx.compose.material3.Text
1919import androidx.compose.runtime.Composable
20- import androidx.compose.runtime.LaunchedEffect
2120import androidx.compose.runtime.getValue
21+ import androidx.compose.runtime.mutableStateOf
2222import androidx.compose.runtime.remember
2323import androidx.compose.runtime.rememberCoroutineScope
24+ import androidx.compose.runtime.setValue
25+ import kotlinx.coroutines.flow.MutableSharedFlow
2426import androidx.compose.ui.Alignment
2527import androidx.compose.ui.Modifier
2628import androidx.compose.ui.graphics.graphicsLayer
2729import androidx.compose.ui.platform.LocalContext
28- import androidx.compose.ui.platform.LocalDensity
2930import androidx.compose.ui.platform.LocalView
3031import androidx.compose.ui.tooling.preview.Preview
3132import androidx.compose.ui.unit.dp
@@ -36,19 +37,11 @@ import com.composables.core.BottomSheetState
3637import com.google.accompanist.permissions.ExperimentalPermissionsApi
3738import com.google.accompanist.permissions.PermissionState
3839import com.google.accompanist.permissions.isGranted
39- import com.google.android.gms.maps.CameraUpdate
40- import com.google.android.gms.maps.CameraUpdateFactory
41- import com.google.android.gms.maps.model.CameraPosition
42- import com.google.maps.android.compose.CameraMoveStartedReason
43- import com.google.maps.android.compose.CameraPositionState
44- import com.google.maps.android.compose.rememberCameraPositionState
4540import com.sloy.debugmenu.launcher.DebugMenuLauncher
46- import com.sloy.sevibus.Stubs
41+ import com.google.android.gms.maps.model.LatLng
4742import com.sloy.sevibus.domain.model.Stop
4843import com.sloy.sevibus.domain.model.isInsideSevilla
49- import com.sloy.sevibus.domain.model.toBounds
5044import com.sloy.sevibus.domain.model.toLatLng
51- import com.sloy.sevibus.domain.model.toLatLngBounds
5245import com.sloy.sevibus.feature.debug.SevDebugMenu
5346import com.sloy.sevibus.infrastructure.BuildVariant
5447import com.sloy.sevibus.infrastructure.EventCollector
@@ -66,9 +59,7 @@ import com.sloy.sevibus.ui.components.CustomFab
6659import com.sloy.sevibus.ui.preview.ScreenPreview
6760import com.sloy.sevibus.ui.snackbar.LocalSnackbarHostState
6861import com.sloy.sevibus.ui.theme.SevTheme
69- import kotlinx.coroutines.delay
7062import kotlinx.coroutines.launch
71- import okhttp3.internal.format
7263import org.koin.androidx.compose.koinViewModel
7364
7465
@@ -105,65 +96,13 @@ private fun MapScreen(
10596 onTrack : (SevEvent ) -> Unit ,
10697 snackbarHostState : SnackbarHostState ,
10798) {
108- val locationService: LocationService = koinInjectOnUI() ? : NoopLocationService
10999 val locationPermissionState = rememberPermissionStateOnUI(Manifest .permission.ACCESS_FINE_LOCATION )
110- val camera = rememberCameraPositionState {
111- position = CameraPosition .fromLatLngZoom(Stubs .locationInitial.toLatLng(), ZoomLevel .Far .minimumLevel.toFloat())
112- }
113- LaunchedEffect (locationPermissionState?.status?.isGranted) {
114- locationService.obtainCurrentLocation()?.toLatLng()?.let { userLocation ->
115- if (userLocation.isInsideSevilla()) {
116- camera.animate(CameraUpdateFactory .newLatLngZoom(userLocation, MY_LOCATION_ZOOM ))
117- }
118- }
119- }
120- LaunchedEffect (camera.isMoving, camera.cameraMoveStartedReason) {
121- if (camera.isMoving && camera.cameraMoveStartedReason == CameraMoveStartedReason .GESTURE ) {
122- onMapClick()
123- }
124- }
125-
126- suspend fun centerMapOn (cameraUpdate : CameraUpdate ? ) {
127- if (cameraUpdate == null ) return
128- // Wait until the sheet is in its final position
129- delay(20 )
130- while (! sheetState.isIdle) {
131- delay(10 )
132- }
133- camera.animate(cameraUpdate, MAP_CAMERA_ANIMATION_DURATION )
134- }
135-
136- val density = LocalDensity .current
137- val boundsPadding = with (density) { 40 .dp.toPx() }.toInt()
138-
139- when (state) {
140- is MapScreenState .StopSelected -> {
141- LaunchedEffect (state.selectedStop) {
142- centerMapOn(CameraUpdateFactory .newLatLngZoom(state.selectedStop.position.toLatLng(), ZoomLevel .Close .minimumLevel.toFloat()) )
143- }
144- }
145-
146- is MapScreenState .LineSelected -> {
147- LaunchedEffect (state.lineStops) {
148- centerMapOn(CameraUpdateFactory .newLatLngBounds(state.lineStops.toBounds().toLatLngBounds(), boundsPadding))
149- }
150- }
151-
152- is MapScreenState .StopAndLineSelected -> {
153- LaunchedEffect (state.selectedStop) {
154- centerMapOn(CameraUpdateFactory .newLatLngBounds(state.selectedStops().toBounds().toLatLngBounds(), boundsPadding))
155- }
156- }
157-
158- else -> {}
159- }
160100
161101 MapUI (
102+ sheetState,
162103 state,
163- camera,
164104 locationPermissionState,
165105 snackbarHostState,
166- locationService,
167106 contentPadding,
168107 onStopSelected,
169108 onMapClick,
@@ -174,25 +113,27 @@ private fun MapScreen(
174113@OptIn(ExperimentalPermissionsApi ::class )
175114@Composable
176115private fun MapUI (
116+ sheetState : BottomSheetState ,
177117 state : MapScreenState ,
178- cameraPositionState : CameraPositionState ,
179118 locationPermissionState : PermissionState ? ,
180119 snackbarHostState : SnackbarHostState ,
181- locationService : LocationService ,
182120 contentPadding : PaddingValues ,
183121 onStopSelected : (stop: Stop ) -> Unit ,
184122 onMapClick : () -> Unit ,
185123 onTrack : (SevEvent ) -> Unit ,
186124) {
125+ val locationService: LocationService = koinInjectOnUI() ? : NoopLocationService
187126 val shakeAnim = remember { Animatable (0f ) }
127+ val locationButtonClickFlow = remember { MutableSharedFlow <Unit >(extraBufferCapacity = 1 ) }
128+ var currentCameraPosition by remember { mutableStateOf<LatLng ?>(null ) }
188129
189130 Box (
190131 Modifier
191132 .fillMaxSize()
192133 .graphicsLayer { translationX = shakeAnim.value }) {
193134 if (FeatureFlags .showMapDebugInfo) {
194135 DebugInfo (
195- state, cameraPositionState,
136+ state,
196137 Modifier
197138 .zIndex(1f )
198139 .padding(contentPadding)
@@ -208,25 +149,30 @@ private fun MapUI(
208149 DebugButton ()
209150 LocationButton (
210151 locationPermissionState,
211- cameraPositionState,
212152 snackbarHostState,
213153 locationService,
154+ currentCameraPosition,
214155 onReject = {
215156 shakeAnim.animateShake()
216157 },
217158 onTrack = onTrack,
159+ onLocationButtonClicked = {
160+ locationButtonClickFlow.tryEmit(Unit )
161+ }
218162 )
219163 }
220164
221-
222-
223165 SevMap (
224166 state = state,
225- cameraPositionState = cameraPositionState,
226167 hasLocationPermission = locationPermissionState?.status?.isGranted ? : false ,
227168 onStopSelected = onStopSelected,
228169 onMapClick = onMapClick,
229170 contentPadding = contentPadding,
171+ sheetState = sheetState,
172+ locationButtonClickFlow = locationButtonClickFlow,
173+ onCameraPositionChanged = { position ->
174+ currentCameraPosition = position
175+ },
230176 modifier = Modifier .zIndex(0f )
231177 )
232178 }
@@ -253,19 +199,23 @@ private fun DebugButton(modifier: Modifier = Modifier) {
253199@Composable
254200private fun LocationButton (
255201 locationPermissionState : PermissionState ? ,
256- cameraPositionState : CameraPositionState ,
257202 snackbarState : SnackbarHostState ,
258203 locationService : LocationService ,
204+ currentCameraPosition : LatLng ? ,
259205 onReject : suspend () -> Unit ,
260206 onTrack : (SevEvent ) -> Unit ,
207+ onLocationButtonClicked : () -> Unit ,
261208 modifier : Modifier = Modifier ,
262209) {
263210 val hasPermission = locationPermissionState?.status?.isGranted ? : false
264211 val scope = rememberCoroutineScope()
265212 val view = LocalView .current
266213 val lifecycle = LocalLifecycleOwner .current.lifecycle
267214 val location by locationService.requestLocationUpdates().collectAsStateWithLifecycle(null , lifecycle)
268- val isCameraInCurrentPosition = location?.toLatLng()?.isApproximatelyEqualTo(cameraPositionState.position.target) == true
215+
216+ val isCameraInCurrentPosition = currentCameraPosition?.let { cameraPos ->
217+ location?.toLatLng()?.isApproximatelyEqualTo(cameraPos) == true
218+ } ? : false
269219
270220 val outsideSevillaMessenger = remember { OutsideSevillaMessenger () }
271221
@@ -279,7 +229,7 @@ private fun LocationButton(
279229 locationService.obtainCurrentLocation()?.toLatLng()?.let { userLocation ->
280230 if (userLocation.isInsideSevilla()) {
281231 onTrack(Clicks .LocationButtonClicked (" inside-sevilla" ))
282- cameraPositionState.animate( CameraUpdateFactory .newLatLngZoom(userLocation, MY_LOCATION_ZOOM ) )
232+ onLocationButtonClicked( )
283233 } else {
284234 onTrack(Clicks .LocationButtonClicked (" outside-sevilla" ))
285235 view.performHapticReject()
@@ -300,14 +250,11 @@ private fun LocationButton(
300250 ) {
301251 Icon (if (isCameraInCurrentPosition) Icons .Filled .MyLocation else Icons .Filled .LocationSearching , " Search Location" )
302252 }
303-
304-
305253}
306254
307255@Composable
308256private fun DebugInfo (
309257 state : MapScreenState ,
310- cameraPositionState : CameraPositionState ,
311258 modifier : Modifier = Modifier ,
312259) {
313260 Column (
@@ -316,31 +263,35 @@ private fun DebugInfo(
316263 .fillMaxSize()
317264 ) {
318265 Text (state::class .simpleName!! )
319- val zoom = cameraPositionState.position.zoom
320- Text (format(" %.2f" , zoom) + " - " + ZoomLevel (zoom.toInt()))
266+ Text (" Debug Info" )
321267 }
322268}
323269
324270
325- private const val MY_LOCATION_ZOOM = 17f
326- private const val MAP_CAMERA_ANIMATION_DURATION = 200
327-
328-
329271@OptIn(ExperimentalPermissionsApi ::class )
330272@Preview
331273@Composable
332274private fun MapScreenPreview () {
333275 ScreenPreview {
334- MapUI (
335- state = MapScreenState .Initial ,
276+ val scope = rememberCoroutineScope()
277+ val density = androidx.compose.ui.platform.LocalDensity .current
278+ MapScreen (
279+ sheetState = remember {
280+ com.composables.core.BottomSheetState (
281+ initialDetent = com.composables.core.SheetDetent .Hidden ,
282+ detents = listOf (com.composables.core.SheetDetent .Hidden ),
283+ coroutineScope = scope,
284+ animationSpec = androidx.compose.animation.core.spring(),
285+ velocityThreshold = { 125 .dp.value },
286+ positionalThreshold = { 56 .dp.value },
287+ decayAnimationSpec = androidx.compose.animation.core.exponentialDecay(),
288+ confirmDetentChange = { true },
289+ density = { density }
290+ )
291+ },
336292 contentPadding = PaddingValues (),
337- cameraPositionState = remember { CameraPositionState () },
338- snackbarHostState = SnackbarHostState (),
339- locationPermissionState = null ,
340- locationService = NoopLocationService ,
341293 onStopSelected = {},
342- onMapClick = {},
343- onTrack = {}
294+ onMapClick = {}
344295 )
345296 }
346297}
0 commit comments