1515 */
1616package org.greenstand.android.TreeTracker.map
1717
18- import androidx.compose.foundation.Image
18+ import androidx.compose.animation.core.animateDpAsState
19+ import androidx.compose.animation.core.tween
1920import androidx.compose.foundation.clickable
2021import androidx.compose.foundation.layout.Arrangement
2122import androidx.compose.foundation.layout.Box
2223import androidx.compose.foundation.layout.Column
2324import androidx.compose.foundation.layout.PaddingValues
2425import androidx.compose.foundation.layout.Spacer
26+ import androidx.compose.foundation.layout.fillMaxHeight
2527import androidx.compose.foundation.layout.fillMaxSize
2628import androidx.compose.foundation.layout.fillMaxWidth
2729import androidx.compose.foundation.layout.height
@@ -34,27 +36,30 @@ import androidx.compose.foundation.lazy.items
3436import androidx.compose.foundation.lazy.rememberLazyListState
3537import androidx.compose.foundation.shape.RoundedCornerShape
3638import androidx.compose.material.Card
37- import androidx.compose.material.Scaffold
39+ import androidx.compose.material.Icon
40+ import androidx.compose.material.IconButton
41+ import androidx.compose.material.Surface
3842import androidx.compose.material.Text
43+ import androidx.compose.material.icons.Icons
44+ import androidx.compose.material.icons.automirrored.filled.ArrowBack
3945import androidx.compose.runtime.Composable
40- import androidx.compose.runtime.DisposableEffect
4146import androidx.compose.runtime.LaunchedEffect
4247import androidx.compose.runtime.collectAsState
4348import androidx.compose.runtime.getValue
4449import androidx.compose.runtime.rememberCoroutineScope
4550import androidx.compose.ui.Alignment
4651import androidx.compose.ui.Modifier
52+ import androidx.compose.ui.graphics.Color
4753import androidx.compose.ui.layout.ContentScale
4854import androidx.compose.ui.platform.LocalConfiguration
4955import androidx.compose.ui.platform.LocalContext
5056import androidx.compose.ui.platform.LocalDensity
5157import androidx.compose.ui.res.painterResource
52- import androidx.compose.ui.res.stringResource
5358import androidx.compose.ui.text.font.FontWeight
54- import androidx.compose.ui.text.style.TextAlign
5559import androidx.compose.ui.text.style.TextOverflow
5660import androidx.compose.ui.unit.dp
5761import androidx.lifecycle.viewmodel.compose.viewModel
62+ import coil.compose.AsyncImage
5863import kotlinx.coroutines.launch
5964import kotlinx.datetime.Instant
6065import kotlinx.datetime.TimeZone
@@ -64,10 +69,9 @@ import org.greenstand.android.TreeTracker.R
6469import org.greenstand.android.TreeTracker.root.LocalNavHostController
6570import org.greenstand.android.TreeTracker.root.LocalViewModelFactory
6671import org.greenstand.android.TreeTracker.theme.CustomTheme
67- import org.greenstand.android.TreeTracker.view.ActionBar
6872import org.greenstand.android.TreeTracker.view.AppColors
69- import org.greenstand.android.TreeTracker.view.ArrowButton
7073import org.maplibre.android.MapLibre
74+ import java.io.File
7175import java.time.format.DateTimeFormatter
7276
7377@Composable
@@ -78,62 +82,53 @@ fun MapScreen(
7882 val context = LocalContext .current
7983 val state by viewModel.state.collectAsState()
8084
81- DisposableEffect (Unit ) {
85+ LaunchedEffect (Unit ) {
8286 MapLibre .getInstance(context)
83- onDispose { }
8487 }
8588
86- Scaffold (
87- topBar = {
88- ActionBar (
89- modifier = Modifier .statusBarsPadding(),
90- centerAction = {
91- Text (
92- modifier = Modifier .fillMaxWidth(),
93- text = stringResource(id = R .string.map_title),
94- color = AppColors .Green ,
95- fontWeight = FontWeight .Bold ,
96- style = CustomTheme .typography.large,
97- textAlign = TextAlign .Center ,
98- )
99- },
100- )
101- },
102- bottomBar = {
103- ActionBar (
104- modifier = Modifier .navigationBarsPadding(),
105- leftAction = {
106- ArrowButton (isLeft = true ) {
107- navController.popBackStack()
108- }
109- },
110- )
111- }
112- ) { paddingValues ->
113- Box (
89+ Box (
90+ modifier = Modifier .fillMaxSize()
91+ ) {
92+ LibreMap (
93+ markers = state.markers,
94+ selectedMarkerId = state.selectedMarkerId,
95+ styleUrl = " https://tiles.openfreemap.org/styles/liberty"
96+ )
97+
98+ // Back button in top left corner
99+ Surface (
114100 modifier = Modifier
115- .fillMaxSize()
116- .padding(paddingValues)
101+ .align(Alignment .TopStart )
102+ .padding(start = 16 .dp, top = 4 .dp)
103+ .statusBarsPadding(),
104+ elevation = 4 .dp,
105+ shape = RoundedCornerShape (24 .dp),
106+ color = Color .White
117107 ) {
118- LibreMap (
119- markers = state.markers,
120- selectedMarkerId = state.selectedMarkerId
121- )
122-
123- // Carousel at the bottom
124- if (state.markers.isNotEmpty()) {
125- TreeMarkerCarousel (
126- modifier = Modifier
127- .align(Alignment .BottomCenter )
128- .fillMaxWidth(),
129- markers = state.markers,
130- selectedMarkerId = state.selectedMarkerId,
131- onMarkerClick = { marker ->
132- viewModel.selectMarker(marker.id)
133- }
108+ IconButton (
109+ onClick = { navController.popBackStack() }
110+ ) {
111+ Icon (
112+ imageVector = Icons .AutoMirrored .Filled .ArrowBack ,
113+ contentDescription = " Back" ,
134114 )
135115 }
136116 }
117+
118+ // Carousel overlay at bottom
119+ if (state.markers.isNotEmpty()) {
120+ TreeMarkerCarousel (
121+ modifier = Modifier
122+ .align(Alignment .BottomCenter )
123+ .fillMaxWidth()
124+ .navigationBarsPadding(),
125+ markers = state.markers,
126+ selectedMarkerId = state.selectedMarkerId,
127+ onMarkerClick = { marker ->
128+ viewModel.selectMarker(marker.id)
129+ }
130+ )
131+ }
137132 }
138133}
139134
@@ -149,11 +144,10 @@ fun TreeMarkerCarousel(
149144 val configuration = LocalConfiguration .current
150145 val density = LocalDensity .current
151146
152- // Calculate centering offset
153- val cardWidth = 200 .dp
147+ // Calculate centering offset based on selected card size
154148 val screenWidth = configuration.screenWidthDp.dp
155149 val centerOffset = with (density) {
156- ((screenWidth - cardWidth ) / 2 ).toPx().toInt()
150+ ((screenWidth - CARD_WIDTH_SELECTED ) / 2 ).toPx().toInt()
157151 }
158152
159153 // Scroll to selected marker when it changes
@@ -173,10 +167,11 @@ fun TreeMarkerCarousel(
173167 }
174168
175169 LazyRow (
170+ modifier = modifier.height(CARD_HEIGHT_SELECTED ),
176171 state = listState,
177- modifier = modifier,
178172 contentPadding = PaddingValues (horizontal = 16 .dp, vertical = 8 .dp),
179- horizontalArrangement = Arrangement .spacedBy(12 .dp)
173+ horizontalArrangement = Arrangement .spacedBy(12 .dp),
174+ verticalAlignment = Alignment .CenterVertically ,
180175 ) {
181176 items(markers, key = { it.id }) { marker ->
182177 TreeMarkerCard (
@@ -195,31 +190,50 @@ fun TreeMarkerCard(
195190 onClick : () -> Unit ,
196191 modifier : Modifier = Modifier
197192) {
193+ // Animate width and height by 10% when selected
194+ val cardWidth by animateDpAsState(
195+ targetValue = if (isSelected) CARD_WIDTH_SELECTED else CARD_WIDTH_NORMAL ,
196+ animationSpec = tween(durationMillis = 300 ),
197+ label = " card_width"
198+ )
199+
200+ val cardHeight by animateDpAsState(
201+ targetValue = if (isSelected) CARD_HEIGHT_SELECTED else CARD_HEIGHT_NORMAL ,
202+ animationSpec = tween(durationMillis = 300 ),
203+ label = " card_height"
204+ )
205+
198206 Card (
199207 modifier = modifier
200- .width(200 .dp)
208+ .width(cardWidth)
209+ .height(cardHeight)
201210 .clickable { onClick() },
202211 elevation = if (isSelected) 8 .dp else 4 .dp,
203- backgroundColor = AppColors . LightGray ,
212+ backgroundColor = Color . White ,
204213 shape = RoundedCornerShape (8 .dp)
205214 ) {
206- Column {
207- // Placeholder image
208- Image (
209- painter = painterResource(id = R .drawable.yellow_leafs_placeholder),
210- contentDescription = " Tree placeholder" ,
215+ Column (
216+ modifier = Modifier .fillMaxHeight()
217+ ) {
218+ // Tree image loaded from file path - takes 2/3 of card space
219+ AsyncImage (
220+ model = marker.imagePath?.let { File (it) },
221+ contentDescription = " Tree image" ,
211222 modifier = Modifier
212223 .fillMaxWidth()
213- .height(120 .dp),
214- contentScale = ContentScale .Crop
224+ .weight(2f ),
225+ contentScale = ContentScale .Crop ,
226+ placeholder = painterResource(id = R .drawable.yellow_leafs_placeholder),
227+ error = painterResource(id = R .drawable.yellow_leafs_placeholder),
228+ fallback = painterResource(id = R .drawable.yellow_leafs_placeholder)
215229 )
216230
231+ // Content section - takes 1/3 of card space
217232 Column (
218- modifier = Modifier .padding(12 .dp)
233+ modifier = Modifier
234+ .weight(1f )
235+ .padding(12 .dp)
219236 ) {
220- Spacer (modifier = Modifier .height(8 .dp))
221-
222- // Latitude/Longitude
223237 Text (
224238 text = " Lat: ${String .format(" %.6f" , marker.latitude)} " ,
225239 style = CustomTheme .typography.small,
@@ -267,3 +281,9 @@ private fun formatPlantDate(instant: Instant): String {
267281 val formatter = DateTimeFormatter .ofPattern(" MMM dd, yyyy" )
268282 return javaDateTime.format(formatter)
269283}
284+
285+ // Card size constants
286+ private val CARD_WIDTH_NORMAL = 200 .dp
287+ private val CARD_WIDTH_SELECTED = 220 .dp
288+ private val CARD_HEIGHT_NORMAL = 280 .dp
289+ private val CARD_HEIGHT_SELECTED = 308 .dp
0 commit comments