Skip to content

Commit e19fe26

Browse files
committed
[NDGL-49] feature: followTravelScreen 제작 및 관련 컴포넌트 추가
1 parent 098aa93 commit e19fe26

5 files changed

Lines changed: 777 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package com.yapp.ndgl.feature.travel.followtravel
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.navigationBarsPadding
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.statusBarsPadding
13+
import androidx.compose.foundation.lazy.LazyColumn
14+
import androidx.compose.foundation.lazy.rememberLazyListState
15+
import androidx.compose.foundation.shape.RoundedCornerShape
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.derivedStateOf
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.mutableStateOf
20+
import androidx.compose.runtime.remember
21+
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
25+
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.tooling.preview.Preview
28+
import androidx.compose.ui.unit.dp
29+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
30+
import com.yapp.ndgl.core.ui.R
31+
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButton
32+
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButtonAttr
33+
import com.yapp.ndgl.core.ui.designsystem.NDGLChipTab
34+
import com.yapp.ndgl.core.ui.designsystem.NDGLChipTabAttr
35+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
36+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr
37+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
38+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
39+
import com.yapp.ndgl.core.ui.util.dropShadow
40+
import com.yapp.ndgl.feature.travel.followtravel.component.BudgetBar
41+
import com.yapp.ndgl.feature.travel.followtravel.component.ContentCard
42+
import com.yapp.ndgl.feature.travel.followtravel.component.PlaceItem
43+
import com.yapp.ndgl.feature.travel.followtravel.component.TransportSegment
44+
import com.yapp.ndgl.feature.travel.followtravel.component.TravelMap
45+
import kotlinx.collections.immutable.persistentListOf
46+
47+
@Composable
48+
internal fun FollowTravelRoute(
49+
viewModel: FollowTravelViewModel = hiltViewModel(),
50+
navigateBack: () -> Unit = {},
51+
) {
52+
val state by viewModel.collectAsState()
53+
54+
// TODO viewModel.collectSideEffect { sideEffect -> }
55+
56+
FollowTravelScreen(
57+
state = state,
58+
clickBackButton = navigateBack,
59+
selectDay = { viewModel.onIntent(FollowTravelIntent.OnDaySelected(it)) },
60+
clickFollowTravel = { viewModel.onIntent(FollowTravelIntent.OnFollowClick) },
61+
)
62+
}
63+
64+
@Composable
65+
private fun FollowTravelScreen(
66+
state: FollowTravelState,
67+
clickBackButton: () -> Unit,
68+
selectDay: (Int) -> Unit,
69+
clickFollowTravel: () -> Unit,
70+
) {
71+
val tabs = (1..state.contentInfo.days).map { day ->
72+
NDGLChipTabAttr.Tab(
73+
tag = "d$day",
74+
name = stringResource(R.string.day_format, day),
75+
)
76+
}.let { persistentListOf(*it.toTypedArray()) }
77+
78+
val listState = rememberLazyListState()
79+
80+
// FIXME("임시 sticky 판단 로직")
81+
val isHeaderSticky by remember {
82+
derivedStateOf {
83+
val firstItem = listState.layoutInfo.visibleItemsInfo.firstOrNull()
84+
val result = firstItem?.index == 1 && firstItem.offset <= 0
85+
result
86+
}
87+
}
88+
89+
var columnScrollingEnabled by remember { mutableStateOf(true) }
90+
91+
Box(
92+
modifier = Modifier
93+
.fillMaxSize()
94+
.navigationBarsPadding(),
95+
) {
96+
LazyColumn(
97+
state = listState,
98+
userScrollEnabled = columnScrollingEnabled,
99+
modifier = Modifier
100+
.fillMaxSize()
101+
.padding(bottom = 60.dp),
102+
) {
103+
item {
104+
Column(
105+
modifier = Modifier
106+
.fillMaxWidth()
107+
.clip(shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
108+
.background(NDGLTheme.colors.black50)
109+
.statusBarsPadding(),
110+
) {
111+
NDGLNavigationBar(
112+
textAlignType = NDGLNavigationBarAttr.TextAlignType.START,
113+
leadingIcon = R.drawable.ic_28_chevron_left,
114+
onLeadingIconClick = clickBackButton,
115+
trailingContents = {
116+
NDGLNavigationIcon(
117+
icon = R.drawable.ic_28_share,
118+
onClick = {},
119+
)
120+
},
121+
)
122+
ContentCard(contentInfo = state.contentInfo)
123+
}
124+
}
125+
126+
stickyHeader {
127+
Column(
128+
modifier = Modifier
129+
.fillMaxWidth()
130+
.background(NDGLTheme.colors.white)
131+
.then(
132+
if (isHeaderSticky) {
133+
Modifier
134+
.statusBarsPadding()
135+
.padding(top = 10.dp)
136+
} else {
137+
Modifier.padding(top = 22.dp)
138+
},
139+
)
140+
.padding(horizontal = 24.dp),
141+
) {
142+
NDGLChipTab(
143+
tabs = tabs,
144+
selectedIndex = state.selectedDay - 1,
145+
onTabSelected = { index -> selectDay(index + 1) },
146+
)
147+
Spacer(Modifier.height(16.dp))
148+
}
149+
}
150+
151+
item(key = "map_${state.selectedDay}") {
152+
Column(
153+
modifier = Modifier
154+
.fillMaxWidth()
155+
.padding(horizontal = 24.dp),
156+
) {
157+
state.itineraries.getOrNull(state.selectedDay - 1)?.let { itinerary ->
158+
BudgetBar(day = state.selectedDay, budget = itinerary.budget)
159+
Spacer(Modifier.height(16.dp))
160+
TravelMap(
161+
places = itinerary.places,
162+
onScrollEnabledChange = { columnScrollingEnabled = it },
163+
)
164+
}
165+
Spacer(Modifier.height(23.5.dp))
166+
}
167+
}
168+
169+
val currentItinerary = state.itineraries.getOrNull(state.selectedDay - 1)
170+
val currentPlaces = currentItinerary?.places.orEmpty()
171+
val currentTransportSegments = currentItinerary?.transportSegments.orEmpty()
172+
173+
currentPlaces.forEachIndexed { index, place ->
174+
item(key = "place_${place.id}") {
175+
Box(modifier = Modifier.padding(horizontal = 24.dp)) {
176+
PlaceItem(place = place)
177+
}
178+
}
179+
180+
if (index < currentPlaces.size - 1) {
181+
item(key = "transport_${place.id}_$index") {
182+
Spacer(Modifier.height(17.5.dp))
183+
Box(modifier = Modifier.padding(horizontal = 24.dp)) {
184+
currentTransportSegments.getOrNull(index)?.let { segment ->
185+
TransportSegment(segment = segment)
186+
}
187+
}
188+
Spacer(Modifier.height(17.5.dp))
189+
}
190+
}
191+
}
192+
193+
item {
194+
Spacer(Modifier.height(60.dp))
195+
}
196+
}
197+
198+
Column(
199+
modifier = Modifier
200+
.fillMaxWidth()
201+
.dropShadow(
202+
shape = RoundedCornerShape(16.dp),
203+
color = Color.Black.copy(alpha = 0.06f),
204+
blur = 20.dp,
205+
offsetY = (-10).dp,
206+
)
207+
.align(Alignment.BottomCenter)
208+
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
209+
.background(NDGLTheme.colors.white)
210+
.padding(top = 20.dp, bottom = 16.dp)
211+
.padding(horizontal = 24.dp),
212+
) {
213+
NDGLCTAButton(
214+
modifier = Modifier.fillMaxWidth(),
215+
type = NDGLCTAButtonAttr.Type.PRIMARY,
216+
size = NDGLCTAButtonAttr.Size.LARGE,
217+
status = NDGLCTAButtonAttr.Status.ACTIVE,
218+
label = stringResource(R.string.follow_travel_button),
219+
onClick = clickFollowTravel,
220+
)
221+
}
222+
}
223+
}
224+
225+
@Preview(showBackground = true)
226+
@Composable
227+
private fun FollowTravelScreenPreview() {
228+
FollowTravelScreen(
229+
state = FollowTravelState(),
230+
clickBackButton = {},
231+
selectDay = {},
232+
clickFollowTravel = {},
233+
)
234+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.yapp.ndgl.feature.travel.followtravel.component
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.width
9+
import androidx.compose.foundation.shape.RoundedCornerShape
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.draw.clip
16+
import androidx.compose.ui.graphics.vector.ImageVector
17+
import androidx.compose.ui.res.stringResource
18+
import androidx.compose.ui.res.vectorResource
19+
import androidx.compose.ui.tooling.preview.Preview
20+
import androidx.compose.ui.unit.dp
21+
import com.yapp.ndgl.core.ui.R
22+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
23+
import com.yapp.ndgl.feature.travel.followtravel.Budget
24+
25+
@Composable
26+
fun BudgetBar(
27+
day: Int,
28+
budget: Budget,
29+
) {
30+
Row(
31+
modifier = Modifier
32+
.fillMaxWidth()
33+
.clip(RoundedCornerShape(4.dp))
34+
.background(NDGLTheme.colors.black50)
35+
.padding(vertical = 10.dp)
36+
.padding(start = 12.dp),
37+
verticalAlignment = Alignment.CenterVertically,
38+
) {
39+
Icon(
40+
imageVector = ImageVector.vectorResource(R.drawable.ic_20_piggybank),
41+
contentDescription = null,
42+
)
43+
Spacer(Modifier.width(8.dp))
44+
Text(
45+
text = stringResource(R.string.budget_bar_format, day, budget.formatString()),
46+
color = NDGLTheme.colors.black700,
47+
style = NDGLTheme.typography.bodyMdSemiBold,
48+
)
49+
}
50+
}
51+
52+
@Preview
53+
@Composable
54+
private fun BudgetBarPreview() {
55+
NDGLTheme {
56+
BudgetBar(
57+
day = 1,
58+
budget = Budget(300000),
59+
)
60+
}
61+
}

0 commit comments

Comments
 (0)