Skip to content

Commit 6ec429c

Browse files
committed
Added current location feature Update dependencies and refactor code
1 parent 6535c65 commit 6ec429c

File tree

7 files changed

+191
-51
lines changed

7 files changed

+191
-51
lines changed

feature/map/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ dependencies {
2020
implementation(libs.maps.compose)
2121
implementation(libs.coil.compose)
2222
implementation(libs.lottie.compose)
23-
implementation(libs.androidx.activity.compose)
23+
implementation(libs.accompanist.permissions)
2424
}

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/DetailSheet.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,28 @@ import androidx.compose.ui.text.style.TextAlign
2727
import androidx.compose.ui.text.style.TextOverflow
2828
import androidx.compose.ui.unit.dp
2929
import androidx.compose.ui.unit.sp
30-
import com.espressodev.gptmap.core.designsystem.ext.clipPolygon
30+
3131
import com.espressodev.gptmap.core.designsystem.GmIcons
3232
import com.espressodev.gptmap.core.designsystem.IconType
33+
import com.espressodev.gptmap.core.designsystem.R.string as AppText
3334
import com.espressodev.gptmap.core.designsystem.component.ShimmerImage
3435
import com.espressodev.gptmap.core.designsystem.component.SquareButton
36+
import com.espressodev.gptmap.core.designsystem.ext.clipPolygon
3537
import com.espressodev.gptmap.core.model.Location
3638
import com.espressodev.gptmap.core.model.unsplash.LocationImage
37-
import com.espressodev.gptmap.core.designsystem.R.string as AppText
3839

3940
@Composable
4041
internal fun BoxScope.DetailSheet(
4142
location: Location,
4243
onEvent: (MapUiEvent) -> Unit,
44+
modifier: Modifier = Modifier
4345
) {
4446
BackHandler { onEvent(MapUiEvent.OnDetailSheetBackClick) }
4547
Box(
46-
modifier = Modifier
48+
modifier = modifier
4749
.fillMaxWidth()
4850
.clipPolygon(MaterialTheme.colorScheme.surface)
49-
5051
.align(Alignment.BottomCenter)
51-
5252
) {
5353
Column(
5454
horizontalAlignment = Alignment.CenterHorizontally,

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapNavigation.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import androidx.navigation.navArgument
99
import androidx.navigation.navOptions
1010

1111
const val MapRoute = "map_route"
12-
const val FAVOURITE_ID = "favId"
13-
const val MapRouteWithArg = "$MapRoute/{$FAVOURITE_ID}"
12+
const val FavouriteId = "favId"
13+
const val MapRouteWithArg = "$MapRoute/{$FavouriteId}"
1414
fun NavController.navigateToMap(
1515
favouriteId: String = "default",
1616
navOptions: NavOptions? = navOptions {
@@ -27,12 +27,14 @@ fun NavGraphBuilder.mapScreen(
2727
navigateToProfile: () -> Unit
2828
) {
2929
composable(
30-
route = "$MapRoute/{$FAVOURITE_ID}",
31-
arguments = listOf(navArgument(FAVOURITE_ID) { type = NavType.StringType })
30+
route = "$MapRoute/{$FavouriteId}",
31+
arguments = listOf(navArgument(FavouriteId) { type = NavType.StringType })
3232
) {
33-
val favouriteId = it.arguments?.getString(FAVOURITE_ID) ?: "default"
33+
val favouriteId = it.arguments?.getString(FavouriteId) ?: "default"
3434
MapRoute(
35-
navigateToStreetView = navigateToStreetView,
35+
navigateToStreetView = { locPair ->
36+
navigateToStreetView(locPair.first, locPair.second)
37+
},
3638
navigateToScreenshot = navigateToScreenshot,
3739
navigateToProfile = navigateToProfile,
3840
favouriteId = favouriteId

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapScreen.kt

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
@file:OptIn(ExperimentalPermissionsApi::class)
2+
13
package com.espressodev.gptmap.feature.map
24

35
import StreetView
46
import android.annotation.SuppressLint
7+
import android.util.Log
58
import androidx.activity.compose.BackHandler
69
import androidx.compose.animation.AnimatedVisibility
710
import androidx.compose.animation.core.LinearEasing
@@ -24,6 +27,8 @@ import androidx.compose.foundation.layout.width
2427
import androidx.compose.foundation.pager.HorizontalPager
2528
import androidx.compose.foundation.pager.rememberPagerState
2629
import androidx.compose.foundation.shape.RoundedCornerShape
30+
import androidx.compose.material3.FilledTonalIconButton
31+
import androidx.compose.material3.Icon
2732
import androidx.compose.material3.MaterialTheme
2833
import androidx.compose.material3.OutlinedButton
2934
import androidx.compose.material3.Scaffold
@@ -34,7 +39,6 @@ import androidx.compose.runtime.LaunchedEffect
3439
import androidx.compose.runtime.getValue
3540
import androidx.compose.runtime.mutableStateOf
3641
import androidx.compose.runtime.remember
37-
import androidx.compose.runtime.setValue
3842
import androidx.compose.ui.Alignment
3943
import androidx.compose.ui.Modifier
4044
import androidx.compose.ui.graphics.graphicsLayer
@@ -73,6 +77,8 @@ import com.espressodev.gptmap.feature.map.ComponentLoadingState.MAP
7377
import com.espressodev.gptmap.feature.map.MapBottomSheetState.BOTTOM_SHEET_HIDDEN
7478
import com.espressodev.gptmap.feature.map.MapBottomSheetState.DETAIL_CARD
7579
import com.espressodev.gptmap.feature.map.MapBottomSheetState.SMALL_INFORMATION_CARD
80+
import com.google.accompanist.permissions.ExperimentalPermissionsApi
81+
import com.google.accompanist.permissions.rememberMultiplePermissionsState
7682
import com.google.android.gms.maps.CameraUpdateFactory
7783
import com.google.android.gms.maps.model.CameraPosition
7884
import com.google.android.gms.maps.model.MapStyleOptions
@@ -88,7 +94,7 @@ import com.espressodev.gptmap.core.designsystem.R.string as AppText
8894
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
8995
@Composable
9096
fun MapRoute(
91-
navigateToStreetView: (Float, Float) -> Unit,
97+
navigateToStreetView: (Pair<Float, Float>) -> Unit,
9298
navigateToScreenshot: () -> Unit,
9399
navigateToProfile: () -> Unit,
94100
favouriteId: String,
@@ -102,9 +108,7 @@ fun MapRoute(
102108
onEvent = { event ->
103109
viewModel.onEvent(
104110
event = event,
105-
navigateToStreetView = { latLng ->
106-
navigateToStreetView(latLng.first.toFloat(), latLng.second.toFloat())
107-
}
111+
navigateToStreetView = navigateToStreetView
108112
)
109113
},
110114
onAvatarClick = navigateToProfile,
@@ -162,7 +166,7 @@ private fun MapScreen(
162166
DisplayBottomSheet(
163167
bottomSheetState = uiState.bottomSheetState,
164168
location = uiState.location,
165-
onEvent = onEvent
169+
onEvent = onEvent,
166170
)
167171
}
168172
}
@@ -186,25 +190,63 @@ private fun DisplayImageGallery(
186190
private fun BoxScope.DisplayBottomSheet(
187191
bottomSheetState: MapBottomSheetState,
188192
location: Location,
189-
onEvent: (MapUiEvent) -> Unit
193+
onEvent: (MapUiEvent) -> Unit,
194+
modifier: Modifier = Modifier
190195
) {
191196
when (bottomSheetState) {
192-
SMALL_INFORMATION_CARD -> {
193-
SmallInformationCard(
194-
content = location.content,
195-
onExploreWithAiClick = { onEvent(MapUiEvent.OnExploreWithAiClick) },
196-
onBackClick = { onEvent(MapUiEvent.OnBackClick) },
197-
)
198-
}
197+
SMALL_INFORMATION_CARD -> SmallInformationCard(
198+
content = location.content,
199+
onExploreWithAiClick = { onEvent(MapUiEvent.OnExploreWithAiClick) },
200+
onBackClick = { onEvent(MapUiEvent.OnBackClick) },
201+
modifier = modifier
202+
)
199203

200-
DETAIL_CARD -> {
201-
DetailSheet(location = location, onEvent = onEvent)
202-
}
204+
DETAIL_CARD -> DetailSheet(location = location, onEvent = onEvent, modifier = modifier)
203205

204206
BOTTOM_SHEET_HIDDEN -> {}
205207
}
206208
}
207209

210+
@Composable
211+
fun BoxScope.MyCurrentLocationButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
212+
val locationPermissionState = rememberMultiplePermissionsState(
213+
permissions = listOf(
214+
android.Manifest.permission.ACCESS_FINE_LOCATION,
215+
android.Manifest.permission.ACCESS_COARSE_LOCATION
216+
)
217+
)
218+
219+
val (shouldShowDialog, setShouldShowDialog) = remember { mutableStateOf(value = false) }
220+
221+
LaunchedEffect(shouldShowDialog) {
222+
if (shouldShowDialog) {
223+
if (!locationPermissionState.allPermissionsGranted) {
224+
locationPermissionState.launchMultiplePermissionRequest()
225+
} else {
226+
onClick()
227+
}
228+
}
229+
setShouldShowDialog(false)
230+
}
231+
232+
FilledTonalIconButton(
233+
onClick = {
234+
if (locationPermissionState.allPermissionsGranted) {
235+
onClick()
236+
} else {
237+
setShouldShowDialog(true)
238+
}
239+
},
240+
modifier = modifier
241+
.align(Alignment.BottomEnd)
242+
.padding(8.dp)
243+
) {
244+
Icon(
245+
imageVector = GmIcons.MyLocationOutlined,
246+
contentDescription = stringResource(id = AppText.my_location)
247+
)
248+
}
249+
}
208250

209251
@OptIn(ExperimentalFoundationApi::class)
210252
@Composable
@@ -266,7 +308,7 @@ private fun MapSearchBar(
266308
@Composable
267309
private fun MapSection(uiState: MapUiState, isPinVisible: Boolean, onEvent: (MapUiEvent) -> Unit) {
268310
val context = LocalContext.current
269-
var isMapLoaded by remember { mutableStateOf(value = false) }
311+
val (isMapLoaded, setMapLoaded) = remember { mutableStateOf(value = false) }
270312
val mapProperties = remember {
271313
MapProperties(
272314
mapStyleOptions = MapStyleOptions.loadRawResourceStyle(
@@ -280,7 +322,24 @@ private fun MapSection(uiState: MapUiState, isPinVisible: Boolean, onEvent: (Map
280322
}
281323

282324
LaunchedEffect(uiState.coordinatesLatLng) {
283-
cameraPositionState.animate(CameraUpdateFactory.newLatLngZoom(uiState.coordinatesLatLng, 14f))
325+
cameraPositionState.animate(
326+
CameraUpdateFactory.newLatLngZoom(
327+
uiState.coordinatesLatLng,
328+
14f
329+
)
330+
)
331+
}
332+
Log.d("MapScreen", "MapSection: ${uiState.myCurrentLocationState}")
333+
LaunchedEffect(uiState.myCurrentLocationState) {
334+
if (uiState.myCurrentLocationState.first) {
335+
cameraPositionState.animate(
336+
CameraUpdateFactory.newLatLngZoom(
337+
uiState.myCoordinatesLatLng,
338+
14f
339+
)
340+
)
341+
onEvent(MapUiEvent.OnUnsetMyCurrentLocationState)
342+
}
284343
}
285344

286345
Box(modifier = Modifier.fillMaxSize()) {
@@ -305,8 +364,13 @@ private fun MapSection(uiState: MapUiState, isPinVisible: Boolean, onEvent: (Map
305364
cameraPositionState = cameraPositionState,
306365
uiSettings = MapUiSettings(zoomControlsEnabled = false),
307366
properties = mapProperties,
308-
onMapLoaded = { isMapLoaded = true }
367+
onMapLoaded = { setMapLoaded(true) },
309368
)
369+
if (uiState.isMyLocationButtonVisible)
370+
MyCurrentLocationButton(
371+
onClick = { onEvent(MapUiEvent.OnMyCurrentLocationClick) },
372+
modifier = Modifier.zIndex(1f)
373+
)
310374
}
311375
}
312376

@@ -316,7 +380,7 @@ private fun BoxScope.LoadingDialog(
316380
modifier: Modifier = Modifier
317381
) {
318382
AnimatedVisibility(
319-
visible = loadingState == MAP,
383+
visible = loadingState != ComponentLoadingState.NOTHING,
320384
modifier = modifier
321385
.zIndex(1f)
322386
.align(Alignment.TopCenter)
@@ -325,6 +389,11 @@ private fun BoxScope.LoadingDialog(
325389
.padding(top = 72.dp)
326390
.padding(horizontal = 32.dp)
327391
) {
392+
val title = when (loadingState) {
393+
MAP -> AppText.discovering_your_dream_place
394+
ComponentLoadingState.MY_LOCATION -> AppText.loading_your_location
395+
ComponentLoadingState.NOTHING -> AppText.not_valid_name
396+
}
328397
Surface(
329398
shape = RoundedCornerShape(16.dp),
330399
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
@@ -335,7 +404,7 @@ private fun BoxScope.LoadingDialog(
335404
) {
336405
DefaultLoadingAnimation()
337406
Text(
338-
text = stringResource(AppText.discovering_your_dream_place),
407+
text = stringResource(title),
339408
textAlign = TextAlign.Center,
340409
style = MaterialTheme.typography.titleSmall,
341410
fontWeight = FontWeight.Medium,
@@ -380,7 +449,8 @@ private fun BoxScope.LocationPin(
380449
animationSpec = tween(
381450
durationMillis = if (isCameraMoving) 800 else 2000,
382451
easing = LinearEasing
383-
), label = "lottie animation progress"
452+
),
453+
label = "lottie animation progress"
384454
)
385455

386456
LottieAnimation(

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapUiState.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ enum class MapBottomSheetState {
88
}
99

1010
enum class ComponentLoadingState {
11-
STREET_VIEW, MAP, NOTHING
11+
MY_LOCATION, MAP, NOTHING
1212
}
1313

1414
enum class ScreenshotState {
@@ -28,11 +28,16 @@ data class MapUiState(
2828
val isLocationPinVisible: Boolean = true,
2929
val isStreetViewButtonVisible: Boolean = true,
3030
val isScreenshotButtonVisible: Boolean = true,
31+
val myCurrentLocationState: Pair<Boolean, Pair<Double, Double>> = Pair(false, Pair(0.0, 0.0)),
3132
val screenshotState: ScreenshotState = ScreenshotState.IDLE,
32-
val imageGalleryState: Pair<Int, Boolean> = Pair(0, false)
33+
val imageGalleryState: Pair<Int, Boolean> = Pair(0, false),
34+
val isMyLocationButtonVisible: Boolean = true,
3335
) {
3436
val coordinatesLatLng: LatLng
3537
get() = location.content.coordinates.let { LatLng(it.latitude, it.longitude) }
38+
39+
val myCoordinatesLatLng: LatLng
40+
get() = myCurrentLocationState.second.let { LatLng(it.first, it.second) }
3641
}
3742

3843
sealed class MapUiEvent {
@@ -46,4 +51,7 @@ sealed class MapUiEvent {
4651
data object OnExploreWithAiClick : MapUiEvent()
4752
data object OnScreenshotProcessStarted : MapUiEvent()
4853
data class OnStreetViewClick(val latLng: Pair<Double, Double>) : MapUiEvent()
54+
data object OnMyCurrentLocationClick : MapUiEvent()
55+
56+
data object OnUnsetMyCurrentLocationState : MapUiEvent()
4957
}

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapUtils.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ object MapUtils {
1717
* This function will check whether a location is available on StreetView or not.
1818
*
1919
* @param latLng The `LatLng` object representing the location for which you want to fetch Street View data.
20-
* @param source The source of the Street View panorama. It is optional parameter and default value is `Source.DEFAULT`
20+
* @param source It is optional parameter and default value is `Source.DEFAULT`
2121
* - `Source.DEFAULT`: Use the default Street View source.
2222
* - `Source.OUTDOOR`: Use the outdoor Street View source.
2323
* @return A Status value specifying if the location is available on Street View or not,
@@ -27,7 +27,6 @@ object MapUtils {
2727
latLng: LatLng,
2828
source: Source = Source.OUTDOOR
2929
): Status {
30-
3130
val urlString = buildString {
3231
append("https://maps.googleapis.com/maps/api/streetview/metadata")
3332
append("?location=${latLng.latitude},${latLng.longitude}")
@@ -85,4 +84,4 @@ enum class Source(var value: String) {
8584
}
8685

8786
fun CameraPositionState.toLatitudeLongitude(): Pair<Double, Double> =
88-
position.target.let { Pair(it.latitude, it.longitude) }
87+
position.target.let { Pair(it.latitude, it.longitude) }

0 commit comments

Comments
 (0)