@@ -9,9 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
99import androidx.compose.foundation.layout.Box
1010import androidx.compose.foundation.layout.Column
1111import androidx.compose.foundation.layout.Row
12- import androidx.compose.foundation.layout.RowScope
1312import androidx.compose.foundation.layout.Spacer
14- import androidx.compose.foundation.layout.aspectRatio
1513import androidx.compose.foundation.layout.fillMaxSize
1614import androidx.compose.foundation.layout.fillMaxWidth
1715import androidx.compose.foundation.layout.height
@@ -21,11 +19,8 @@ import androidx.compose.foundation.layout.statusBarsPadding
2119import androidx.compose.foundation.lazy.LazyColumn
2220import androidx.compose.foundation.shape.CircleShape
2321import androidx.compose.foundation.shape.RoundedCornerShape
24- import androidx.compose.material3.ExperimentalMaterial3Api
2522import androidx.compose.material3.HorizontalDivider
2623import androidx.compose.material3.Icon
27- import androidx.compose.material3.SecondaryTabRow
28- import androidx.compose.material3.Tab
2924import androidx.compose.material3.Text
3025import androidx.compose.runtime.Composable
3126import androidx.compose.runtime.Immutable
@@ -51,7 +46,6 @@ import androidx.compose.ui.res.stringResource
5146import androidx.compose.ui.res.vectorResource
5247import androidx.compose.ui.text.SpanStyle
5348import androidx.compose.ui.text.buildAnnotatedString
54- import androidx.compose.ui.text.style.TextAlign
5549import androidx.compose.ui.text.style.TextOverflow
5650import androidx.compose.ui.text.withStyle
5751import androidx.compose.ui.tooling.preview.Preview
@@ -65,9 +59,10 @@ import com.yapp.ndgl.core.ui.designsystem.NDGLNonModalBottomSheet
6559import com.yapp.ndgl.core.ui.designsystem.rememberNDGLNonModalBottomSheetState
6660import com.yapp.ndgl.core.ui.theme.NDGLTheme
6761import com.yapp.ndgl.core.ui.util.dropShadow
68- import com.yapp.ndgl.core.ui.util.noRippleClickable
69- import com.yapp.ndgl.core.util.formatString
7062import com.yapp.ndgl.feature.travel.additinerary.SelectedPlaceDetail
63+ import com.yapp.ndgl.feature.travel.component.PlaceDetailTabRow
64+ import com.yapp.ndgl.feature.travel.component.PlaceInfoTab
65+ import com.yapp.ndgl.feature.travel.component.PlacePhotoTab
7166import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
7267import com.yapp.ndgl.feature.travel.model.PlaceInfo
7368import com.yapp.ndgl.feature.travel.model.PlacePhoto
@@ -160,11 +155,14 @@ internal fun SearchedPlaceBottomSheet(
160155 NDGLBottomSheetDragHandle ()
161156 }
162157 }
158+
163159 Box (modifier = Modifier .weight(1f )) {
164160 val density = LocalDensity .current
165161 val thumbnailHeight = 230 .dp
166162 val navBarSectionHeight = 48 .dp
167- val maxCollapseHeightPx = with (density) { (navBarSectionHeight + thumbnailHeight).toPx() }
163+ val thumbnailHeightPx = with (density) { thumbnailHeight.toPx() }
164+ val navBarSectionHeightPx = with (density) { navBarSectionHeight.toPx() }
165+ val maxCollapseHeightPx = navBarSectionHeightPx + thumbnailHeightPx
168166
169167 var collapseOffset by remember { mutableFloatStateOf(0f ) }
170168 var selectedTab by remember { mutableStateOf(PlaceDetailTab .INFO ) }
@@ -191,10 +189,7 @@ internal fun SearchedPlaceBottomSheet(
191189 }
192190 }
193191
194- val thumbnailProgress = (
195- 1f - (collapseOffset - with (density) { navBarSectionHeight.toPx() })
196- .coerceAtLeast(0f ) / with (density) { thumbnailHeight.toPx() }
197- ).coerceIn(0f , 1f )
192+ val thumbnailProgress = (1f - (collapseOffset - navBarSectionHeightPx).coerceAtLeast(0f ) / thumbnailHeightPx).coerceIn(0f , 1f )
198193
199194 Column (
200195 modifier = Modifier
@@ -221,14 +216,14 @@ internal fun SearchedPlaceBottomSheet(
221216 )
222217 }
223218
224- SearchedPlaceDetailTabRow (selectedTab = selectedTab, onTabSelected = { selectedTab = it })
219+ PlaceDetailTabRow (selectedTab = selectedTab, onTabSelected = { selectedTab = it })
225220 HorizontalDivider (thickness = 1 .dp, color = NDGLTheme .colors.black200)
226221
227222 LazyColumn (modifier = Modifier .weight(1f )) {
228223 when (selectedTab) {
229224 PlaceDetailTab .INFO -> item {
230225 Spacer (Modifier .height(24 .dp))
231- SearchedPlaceInfoTab (placeInfo, clickAddress, clickMenu)
226+ PlaceInfoTab (placeInfo, clickAddress, clickMenu)
232227 }
233228
234229 PlaceDetailTab .PHOTO -> {
@@ -245,7 +240,7 @@ internal fun SearchedPlaceBottomSheet(
245240
246241 item {
247242 Spacer (Modifier .height(20 .dp))
248- SearchedPlacePhotoTab (leftPhotos = leftPhotos, rightPhotos = rightPhotos)
243+ PlacePhotoTab (leftPhotos = leftPhotos, rightPhotos = rightPhotos)
249244 }
250245 }
251246 }
@@ -331,225 +326,26 @@ private fun SearchedPlaceInfoHeader(isExpanded: Boolean, placeInfo: PlaceInfo, b
331326 }
332327}
333328
334- @OptIn(ExperimentalMaterial3Api ::class )
335- @Composable
336- private fun SearchedPlaceDetailTabRow (
337- selectedTab : PlaceDetailTab ,
338- onTabSelected : (PlaceDetailTab ) -> Unit ,
339- ) {
340- val tabs = PlaceDetailTab .entries
341- val selectedIndex = tabs.indexOf(selectedTab)
342-
343- Box (
344- modifier = Modifier
345- .fillMaxWidth()
346- .height(48 .dp),
347- ) {
348- SecondaryTabRow (
349- selectedTabIndex = selectedIndex,
350- modifier = Modifier .fillMaxWidth(),
351- containerColor = NDGLTheme .colors.white,
352- contentColor = NDGLTheme .colors.black900,
353- indicator = {},
354- divider = {},
355- ) {
356- tabs.forEachIndexed { index, tab ->
357- val isSelected = index == selectedIndex
358- Tab (
359- selected = isSelected,
360- onClick = { onTabSelected(tab) },
361- modifier = Modifier .background(
362- if (isSelected) NDGLTheme .colors.black100 else NDGLTheme .colors.white,
363- ),
364- text = {
365- Text (
366- stringResource(tab.titleRes),
367- color = NDGLTheme .colors.black600,
368- style = NDGLTheme .typography.bodyMdSemiBold,
369- textAlign = TextAlign .Center ,
370- )
371- },
372- )
373- }
374- }
375- }
376- }
377-
378- @Composable
379- private fun SearchedPlaceInfoTab (
380- placeInfo : PlaceInfo ,
381- clickAddress : () -> Unit ,
382- clickMenu : () -> Unit ,
383- ) {
384- Column (
385- modifier = Modifier
386- .fillMaxWidth()
387- .padding(horizontal = 24 .dp)
388- .padding(top = 24 .dp, bottom = 16 .dp),
389- verticalArrangement = Arrangement .spacedBy(8 .dp),
390- ) {
391- if (placeInfo.address != null ) {
392- SearchedPlaceInfoRow (
393- iconRes = R .drawable.ic_24_pin,
394- onClick = clickAddress,
395- ) {
396- Text (
397- text = placeInfo.address,
398- style = NDGLTheme .typography.bodyMdMedium,
399- color = NDGLTheme .colors.black700,
400- modifier = Modifier .weight(1f ),
401- )
402- }
403- }
404-
405- if (placeInfo.websiteUrl != null ) {
406- SearchedPlaceInfoRow (
407- iconRes = R .drawable.ic_24_book,
408- onClick = clickMenu,
409- ) {
410- Column (
411- modifier = Modifier .weight(1f ),
412- verticalArrangement = Arrangement .spacedBy(4 .dp),
413- ) {
414- Text (
415- text = stringResource(R .string.place_detail_menu),
416- style = NDGLTheme .typography.bodyMdMedium,
417- color = NDGLTheme .colors.black700,
418- )
419- Text (
420- text = placeInfo.websiteUrl,
421- style = NDGLTheme .typography.bodyMdMedium,
422- color = NDGLTheme .colors.black500,
423- maxLines = 1 ,
424- )
425- }
426- }
427- }
428-
429- if (placeInfo.phoneNumber != null ) {
430- SearchedPlaceInfoRow (iconRes = R .drawable.ic_24_phone) {
431- Text (
432- text = placeInfo.phoneNumber,
433- style = NDGLTheme .typography.bodyMdMedium,
434- color = NDGLTheme .colors.black700,
435- modifier = Modifier .weight(1f ),
436- )
437- }
438- }
439-
440- SearchedPlaceInfoRow (iconRes = R .drawable.ic_24_clock) {
441- Text (
442- text = stringResource(
443- R .string.estimated_duration_format,
444- placeInfo.estimatedDuration.formatString(),
445- ),
446- style = NDGLTheme .typography.bodyMdMedium,
447- color = NDGLTheme .colors.black700,
448- modifier = Modifier .weight(1f ),
449- )
450- }
451- }
452- }
453-
454- @Composable
455- private fun SearchedPlaceInfoRow (
456- iconRes : Int ,
457- onClick : (() -> Unit )? = null,
458- content : @Composable RowScope .() -> Unit ,
459- ) {
460- Row (
461- modifier = Modifier
462- .fillMaxWidth()
463- .then(if (onClick != null ) Modifier .noRippleClickable { onClick() } else Modifier ),
464- horizontalArrangement = Arrangement .spacedBy(8 .dp),
465- verticalAlignment = Alignment .CenterVertically ,
466- ) {
467- Icon (
468- imageVector = ImageVector .vectorResource(iconRes),
469- contentDescription = null ,
470- tint = NDGLTheme .colors.green500,
471- modifier = Modifier .size(24 .dp),
472- )
473- content()
474- if (onClick != null ) {
475- Icon (
476- modifier = Modifier
477- .size(24 .dp)
478- .clip(CircleShape )
479- .clickable { onClick() },
480- imageVector = ImageVector .vectorResource(R .drawable.ic_24_chevron_right),
481- tint = NDGLTheme .colors.black600,
482- contentDescription = null ,
483- )
484- }
485- }
486- }
487-
488- @Composable
489- private fun SearchedPlacePhotoTab (leftPhotos : List <PlacePhoto >, rightPhotos : List <PlacePhoto >) {
490- Row (
491- modifier = Modifier
492- .fillMaxWidth()
493- .padding(horizontal = 24 .dp),
494- horizontalArrangement = Arrangement .spacedBy(8 .dp),
495- ) {
496- Column (
497- modifier = Modifier .weight(1f ),
498- verticalArrangement = Arrangement .spacedBy(8 .dp),
499- ) {
500- leftPhotos.forEach { photo ->
501- AsyncImage (
502- model = photo.url,
503- contentDescription = null ,
504- modifier = Modifier
505- .fillMaxWidth()
506- .aspectRatio(photo.aspectRatio)
507- .clip(RoundedCornerShape (8 .dp))
508- .background(Color .LightGray ),
509- contentScale = ContentScale .Crop ,
510- )
511- }
512- }
513- Column (
514- modifier = Modifier .weight(1f ),
515- verticalArrangement = Arrangement .spacedBy(8 .dp),
516- ) {
517- rightPhotos.forEach { photo ->
518- AsyncImage (
519- model = photo.url,
520- contentDescription = null ,
521- modifier = Modifier
522- .fillMaxWidth()
523- .aspectRatio(photo.aspectRatio)
524- .clip(RoundedCornerShape (8 .dp))
525- .background(Color .LightGray ),
526- contentScale = ContentScale .Crop ,
527- )
528- }
529- }
530- }
531- }
532-
533- private val previewPlaceDetail = SelectedPlaceDetail (
534- PlaceInfo (
535- name = " 콜로세움" ,
536- placeType = PlaceType .ATTRACTION ,
537- rating = 4.8 ,
538- userRatingCount = 12450 ,
539- address = " Piazza del Colosseo, 1, 00184 Roma RM, Italy" ,
540- phoneNumber = " +39 06 3996 7700" ,
541- websiteUrl = " https://www.colosseo.it" ,
542- estimatedDuration = 2 .hours,
543- priceRange = PriceRange (
544- startPrice = Price (currencyCode = " EUR" , units = " 5" , symbol = " €" ),
545- endPrice = Price (currencyCode = " EUR" , units = " 15" , symbol = " €" ),
546- ),
547- ),
548- )
549-
550329@Preview(name = " SearchedPlace - Expanded" , showBackground = true )
551330@Composable
552331private fun SearchedPlaceBottomSheet_Expanded_Preview () {
332+ val previewPlaceDetail = SelectedPlaceDetail (
333+ PlaceInfo (
334+ name = " 콜로세움" ,
335+ placeType = PlaceType .ATTRACTION ,
336+ rating = 4.8 ,
337+ userRatingCount = 12450 ,
338+ address = " Piazza del Colosseo, 1, 00184 Roma RM, Italy" ,
339+ phoneNumber = " +39 06 3996 7700" ,
340+ websiteUrl = " https://www.colosseo.it" ,
341+ estimatedDuration = 2 .hours,
342+ priceRange = PriceRange (
343+ startPrice = Price (currencyCode = " EUR" , units = " 5" , symbol = " €" ),
344+ endPrice = Price (currencyCode = " EUR" , units = " 15" , symbol = " €" ),
345+ ),
346+ ),
347+ )
348+
553349 NDGLTheme {
554350 Box (modifier = Modifier .fillMaxSize()) {
555351 SearchedPlaceBottomSheet (
@@ -568,6 +364,23 @@ private fun SearchedPlaceBottomSheet_Expanded_Preview() {
568364@Preview(name = " SearchedPlace - PartiallyExpanded" , showBackground = true )
569365@Composable
570366private fun SearchedPlaceBottomSheet_PartiallyExpanded_Preview () {
367+ val previewPlaceDetail = SelectedPlaceDetail (
368+ PlaceInfo (
369+ name = " 콜로세움" ,
370+ placeType = PlaceType .ATTRACTION ,
371+ rating = 4.8 ,
372+ userRatingCount = 12450 ,
373+ address = " Piazza del Colosseo, 1, 00184 Roma RM, Italy" ,
374+ phoneNumber = " +39 06 3996 7700" ,
375+ websiteUrl = " https://www.colosseo.it" ,
376+ estimatedDuration = 2 .hours,
377+ priceRange = PriceRange (
378+ startPrice = Price (currencyCode = " EUR" , units = " 5" , symbol = " €" ),
379+ endPrice = Price (currencyCode = " EUR" , units = " 15" , symbol = " €" ),
380+ ),
381+ ),
382+ )
383+
571384 NDGLTheme {
572385 Box (modifier = Modifier .fillMaxSize()) {
573386 SearchedPlaceBottomSheet (
0 commit comments