Skip to content

Commit ec521e0

Browse files
committed
[NDGL-100] design: 공통되는 컴포넌트 정리 - AlternativePlaceContent, PlaceDetailTabRow, PlaceInfoTab, PlacePhotoTab, PlaceTipCard
1 parent 426161d commit ec521e0

20 files changed

Lines changed: 645 additions & 1468 deletions

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchedPlaceBottomSheet.kt

Lines changed: 45 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
99
import androidx.compose.foundation.layout.Box
1010
import androidx.compose.foundation.layout.Column
1111
import androidx.compose.foundation.layout.Row
12-
import androidx.compose.foundation.layout.RowScope
1312
import androidx.compose.foundation.layout.Spacer
14-
import androidx.compose.foundation.layout.aspectRatio
1513
import androidx.compose.foundation.layout.fillMaxSize
1614
import androidx.compose.foundation.layout.fillMaxWidth
1715
import androidx.compose.foundation.layout.height
@@ -21,11 +19,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
2119
import androidx.compose.foundation.lazy.LazyColumn
2220
import androidx.compose.foundation.shape.CircleShape
2321
import androidx.compose.foundation.shape.RoundedCornerShape
24-
import androidx.compose.material3.ExperimentalMaterial3Api
25-
import androidx.compose.material3.HorizontalDivider
2622
import androidx.compose.material3.Icon
27-
import androidx.compose.material3.SecondaryTabRow
28-
import androidx.compose.material3.Tab
2923
import androidx.compose.material3.Text
3024
import androidx.compose.runtime.Composable
3125
import androidx.compose.runtime.Immutable
@@ -51,7 +45,6 @@ import androidx.compose.ui.res.stringResource
5145
import androidx.compose.ui.res.vectorResource
5246
import androidx.compose.ui.text.SpanStyle
5347
import androidx.compose.ui.text.buildAnnotatedString
54-
import androidx.compose.ui.text.style.TextAlign
5548
import androidx.compose.ui.text.style.TextOverflow
5649
import androidx.compose.ui.text.withStyle
5750
import androidx.compose.ui.tooling.preview.Preview
@@ -65,9 +58,10 @@ import com.yapp.ndgl.core.ui.designsystem.NDGLNonModalBottomSheet
6558
import com.yapp.ndgl.core.ui.designsystem.rememberNDGLNonModalBottomSheetState
6659
import com.yapp.ndgl.core.ui.theme.NDGLTheme
6760
import com.yapp.ndgl.core.ui.util.dropShadow
68-
import com.yapp.ndgl.core.ui.util.noRippleClickable
69-
import com.yapp.ndgl.core.util.formatString
7061
import com.yapp.ndgl.feature.travel.additinerary.SelectedPlaceDetail
62+
import com.yapp.ndgl.feature.travel.component.PlaceDetailTabRow
63+
import com.yapp.ndgl.feature.travel.component.PlaceInfoTab
64+
import com.yapp.ndgl.feature.travel.component.PlacePhotoTab
7165
import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
7266
import com.yapp.ndgl.feature.travel.model.PlaceInfo
7367
import com.yapp.ndgl.feature.travel.model.PlacePhoto
@@ -160,11 +154,14 @@ internal fun SearchedPlaceBottomSheet(
160154
NDGLBottomSheetDragHandle()
161155
}
162156
}
157+
163158
Box(modifier = Modifier.weight(1f)) {
164159
val density = LocalDensity.current
165160
val thumbnailHeight = 230.dp
166161
val navBarSectionHeight = 48.dp
167-
val maxCollapseHeightPx = with(density) { (navBarSectionHeight + thumbnailHeight).toPx() }
162+
val thumbnailHeightPx = with(density) { thumbnailHeight.toPx() }
163+
val navBarSectionHeightPx = with(density) { navBarSectionHeight.toPx() }
164+
val maxCollapseHeightPx = navBarSectionHeightPx + thumbnailHeightPx
168165

169166
var collapseOffset by remember { mutableFloatStateOf(0f) }
170167
var selectedTab by remember { mutableStateOf(PlaceDetailTab.INFO) }
@@ -191,10 +188,7 @@ internal fun SearchedPlaceBottomSheet(
191188
}
192189
}
193190

194-
val thumbnailProgress = (
195-
1f - (collapseOffset - with(density) { navBarSectionHeight.toPx() })
196-
.coerceAtLeast(0f) / with(density) { thumbnailHeight.toPx() }
197-
).coerceIn(0f, 1f)
191+
val thumbnailProgress = (1f - (collapseOffset - navBarSectionHeightPx).coerceAtLeast(0f) / thumbnailHeightPx).coerceIn(0f, 1f)
198192

199193
Column(
200194
modifier = Modifier
@@ -221,14 +215,13 @@ internal fun SearchedPlaceBottomSheet(
221215
)
222216
}
223217

224-
SearchedPlaceDetailTabRow(selectedTab = selectedTab, onTabSelected = { selectedTab = it })
225-
HorizontalDivider(thickness = 1.dp, color = NDGLTheme.colors.black200)
218+
PlaceDetailTabRow(selectedTab = selectedTab, onTabSelected = { selectedTab = it })
226219

227220
LazyColumn(modifier = Modifier.weight(1f)) {
228221
when (selectedTab) {
229222
PlaceDetailTab.INFO -> item {
230223
Spacer(Modifier.height(24.dp))
231-
SearchedPlaceInfoTab(placeInfo, clickAddress, clickMenu)
224+
PlaceInfoTab(placeInfo, clickAddress, clickMenu)
232225
}
233226

234227
PlaceDetailTab.PHOTO -> {
@@ -245,7 +238,7 @@ internal fun SearchedPlaceBottomSheet(
245238

246239
item {
247240
Spacer(Modifier.height(20.dp))
248-
SearchedPlacePhotoTab(leftPhotos = leftPhotos, rightPhotos = rightPhotos)
241+
PlacePhotoTab(leftPhotos = leftPhotos, rightPhotos = rightPhotos)
249242
}
250243
}
251244
}
@@ -331,225 +324,26 @@ private fun SearchedPlaceInfoHeader(isExpanded: Boolean, placeInfo: PlaceInfo, b
331324
}
332325
}
333326

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-
550327
@Preview(name = "SearchedPlace - Expanded", showBackground = true)
551328
@Composable
552329
private fun SearchedPlaceBottomSheet_Expanded_Preview() {
330+
val previewPlaceDetail = SelectedPlaceDetail(
331+
PlaceInfo(
332+
name = "콜로세움",
333+
placeType = PlaceType.ATTRACTION,
334+
rating = 4.8,
335+
userRatingCount = 12450,
336+
address = "Piazza del Colosseo, 1, 00184 Roma RM, Italy",
337+
phoneNumber = "+39 06 3996 7700",
338+
websiteUrl = "https://www.colosseo.it",
339+
estimatedDuration = 2.hours,
340+
priceRange = PriceRange(
341+
startPrice = Price(currencyCode = "EUR", units = "5", symbol = ""),
342+
endPrice = Price(currencyCode = "EUR", units = "15", symbol = ""),
343+
),
344+
),
345+
)
346+
553347
NDGLTheme {
554348
Box(modifier = Modifier.fillMaxSize()) {
555349
SearchedPlaceBottomSheet(
@@ -568,6 +362,23 @@ private fun SearchedPlaceBottomSheet_Expanded_Preview() {
568362
@Preview(name = "SearchedPlace - PartiallyExpanded", showBackground = true)
569363
@Composable
570364
private fun SearchedPlaceBottomSheet_PartiallyExpanded_Preview() {
365+
val previewPlaceDetail = SelectedPlaceDetail(
366+
PlaceInfo(
367+
name = "콜로세움",
368+
placeType = PlaceType.ATTRACTION,
369+
rating = 4.8,
370+
userRatingCount = 12450,
371+
address = "Piazza del Colosseo, 1, 00184 Roma RM, Italy",
372+
phoneNumber = "+39 06 3996 7700",
373+
websiteUrl = "https://www.colosseo.it",
374+
estimatedDuration = 2.hours,
375+
priceRange = PriceRange(
376+
startPrice = Price(currencyCode = "EUR", units = "5", symbol = ""),
377+
endPrice = Price(currencyCode = "EUR", units = "15", symbol = ""),
378+
),
379+
),
380+
)
381+
571382
NDGLTheme {
572383
Box(modifier = Modifier.fillMaxSize()) {
573384
SearchedPlaceBottomSheet(

0 commit comments

Comments
 (0)