Skip to content

Commit 59adacb

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

20 files changed

Lines changed: 645 additions & 1466 deletions

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

Lines changed: 45 additions & 232 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,8 @@ 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
2522
import androidx.compose.material3.HorizontalDivider
2623
import androidx.compose.material3.Icon
27-
import androidx.compose.material3.SecondaryTabRow
28-
import androidx.compose.material3.Tab
2924
import androidx.compose.material3.Text
3025
import androidx.compose.runtime.Composable
3126
import androidx.compose.runtime.Immutable
@@ -51,7 +46,6 @@ import androidx.compose.ui.res.stringResource
5146
import androidx.compose.ui.res.vectorResource
5247
import androidx.compose.ui.text.SpanStyle
5348
import androidx.compose.ui.text.buildAnnotatedString
54-
import androidx.compose.ui.text.style.TextAlign
5549
import androidx.compose.ui.text.style.TextOverflow
5650
import androidx.compose.ui.text.withStyle
5751
import androidx.compose.ui.tooling.preview.Preview
@@ -65,9 +59,10 @@ import com.yapp.ndgl.core.ui.designsystem.NDGLNonModalBottomSheet
6559
import com.yapp.ndgl.core.ui.designsystem.rememberNDGLNonModalBottomSheetState
6660
import com.yapp.ndgl.core.ui.theme.NDGLTheme
6761
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
7062
import 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
7166
import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
7267
import com.yapp.ndgl.feature.travel.model.PlaceInfo
7368
import 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
552331
private 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
570366
private 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

Comments
 (0)